Gogs 2 settimane fa
parent
commit
68c8fed695
3 ha cambiato i file con 73 aggiunte e 17 eliminazioni
  1. 48 8
      cli.py
  2. 8 1
      relay_client.py
  3. 17 8
      scripts/start-transparent.sh

+ 48 - 8
cli.py

@@ -16,10 +16,13 @@ from .transparent_edge import TransparentEdge
 
 
 TCP_WIN_RE = re.compile(
-    r"tcp win session=(?P<session>\d+) target=(?P<host>[^:]+):(?P<port>\d+) winner=(?P<winner>\S+) .*?direct=(?P<direct>\d+) .*?relay=(?P<relay>\d+)"
+    r"tcp win session=(?P<session>\d+) target=(?P<target>\S+) winner=(?P<winner>\S+) .*?direct=(?P<direct>\d+) .*?relay=(?P<relay>\d+) .*?family=(?P<family>ipv4|ipv6)"
+)
+RELAY_STATUS_SUMMARY_RE = re.compile(
+    r"relay status summary name=(?P<name>\S+) errors=(?P<errors>\d+) last_detail=(?P<detail>.*)"
 )
 UDP_WIN_RE = re.compile(
-    r"udp flow=(?P<flow>\d+) winner=(?P<winner>\S+) target=(?P<host>[^:]+):(?P<port>\d+)"
+    r"udp flow=(?P<flow>\d+) winner=(?P<winner>\S+) target=(?P<target>\S+)"
 )
 SOCKS_UDP_SUMMARY_RE = re.compile(
     r"udp summary .*?winner_detail=(?P<winner_detail>.*?) (?:target_detail=(?P<target_detail>.*?) )?packets_sent="
@@ -30,6 +33,15 @@ def normalize_winner(name: str) -> str:
     return "direct" if name.startswith("direct") else name
 
 
+def split_target(target: str) -> tuple[str, str] | None:
+    if ":" not in target:
+        return None
+    host, port = target.rsplit(":", 1)
+    if not port.isdigit():
+        return None
+    return host, port
+
+
 def build_parser() -> argparse.ArgumentParser:
     parser = argparse.ArgumentParser(prog="mynetspeeder")
     parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
@@ -109,6 +121,9 @@ def handle_summary(args: argparse.Namespace) -> int:
     tcp_relay = 0
     tcp_winners: dict[str, int] = defaultdict(int)
     tcp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
+    tcp_families: dict[str, int] = defaultdict(int)
+    relay_status_errors: dict[str, int] = defaultdict(int)
+    relay_status_details: dict[str, str] = {}
 
     udp_total = 0
     udp_direct = 0
@@ -117,15 +132,25 @@ def handle_summary(args: argparse.Namespace) -> int:
     udp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
 
     for line in log_path.read_text(errors="replace").splitlines():
+        relay_status_match = RELAY_STATUS_SUMMARY_RE.search(line)
+        if relay_status_match:
+            name = relay_status_match.group("name")
+            relay_status_errors[name] += int(relay_status_match.group("errors"))
+            relay_status_details[name] = relay_status_match.group("detail")
+            continue
+
         tcp_match = TCP_WIN_RE.search(line)
         if tcp_match:
-            tcp_total += 1
             winner = normalize_winner(tcp_match.group("winner"))
-            host = tcp_match.group("host")
-            port = tcp_match.group("port")
+            target_parts = split_target(tcp_match.group("target"))
+            if target_parts is None:
+                continue
+            host, port = target_parts
             key = f"{host}:{port}"
+            tcp_total += 1
             tcp_winners[winner] += 1
             tcp_targets[key][winner] += 1
+            tcp_families[tcp_match.group("family")] += 1
             if winner == "direct":
                 tcp_direct += 1
             else:
@@ -134,11 +159,13 @@ def handle_summary(args: argparse.Namespace) -> int:
 
         udp_match = UDP_WIN_RE.search(line)
         if udp_match:
-            udp_total += 1
             winner = normalize_winner(udp_match.group("winner"))
-            host = udp_match.group("host")
-            port = udp_match.group("port")
+            target_parts = split_target(udp_match.group("target"))
+            if target_parts is None:
+                continue
+            host, port = target_parts
             key = f"{host}:{port}"
+            udp_total += 1
             udp_winners[winner] += 1
             udp_targets[key][winner] += 1
             if winner == "direct":
@@ -205,6 +232,7 @@ def handle_summary(args: argparse.Namespace) -> int:
             "total": tcp_total,
             "direct": tcp_direct,
             "relay": tcp_relay,
+            "families": dict(sorted(tcp_families.items(), key=lambda item: item[0])),
             "winners": dict(sorted(tcp_winners.items(), key=lambda item: (-item[1], item[0]))),
             "targets": [
                 {
@@ -227,6 +255,10 @@ def handle_summary(args: argparse.Namespace) -> int:
                 for target, counts in udp_ordered_targets
             ],
         },
+        "relay_status": {
+            "errors": dict(sorted(relay_status_errors.items(), key=lambda item: (-item[1], item[0]))),
+            "last_detail": dict(sorted(relay_status_details.items(), key=lambda item: item[0])),
+        },
     }
 
     if args.json:
@@ -239,6 +271,9 @@ def handle_summary(args: argparse.Namespace) -> int:
     for protocol in ("tcp", "udp"):
         section = result[protocol]
         print(f"{protocol}: total={section['total']} direct={section['direct']} relay={section['relay']}")
+        if protocol == "tcp":
+            families = ", ".join(f"{name}={count}" for name, count in section.get("families", {}).items()) or "none"
+            print(f"families: {families}")
         print("winners:")
         for name, count in section["winners"].items():
             print(f"  {name}: {count}")
@@ -246,6 +281,11 @@ def handle_summary(args: argparse.Namespace) -> int:
         for item in section["targets"]:
             wins = ", ".join(f"{name}={count}" for name, count in item["wins"].items())
             print(f"  {item['target']}: {wins}")
+    if result["relay_status"]["errors"]:
+        print("relay_status:")
+        for name, count in result["relay_status"]["errors"].items():
+            detail = result["relay_status"]["last_detail"].get(name, "unknown")
+            print(f"  {name}: errors={count} last_detail={detail}")
     return 0
 
 

+ 8 - 1
relay_client.py

@@ -31,6 +31,8 @@ class RelayConnection:
     closed_event: asyncio.Event | None = None
     dropped_frames: Dict[int, int] = None
     dropped_report_task: asyncio.Task | None = None
+    status_error_count: int = 0
+    status_error_last_detail: str = ""
 
     def __post_init__(self) -> None:
         if self.handlers is None:
@@ -80,7 +82,8 @@ class RelayConnection:
                     continue
                 if frame.kind == TCP_STATUS and frame.packet_id != STATUS_OK:
                     detail = frame.payload.decode("utf-8", errors="replace") if frame.payload else "unknown"
-                    print(f"[edge] relay status error name={self.node.name} session={frame.session_id} stream={frame.stream_id} detail={detail}")
+                    self.status_error_count += 1
+                    self.status_error_last_detail = detail
                 handler = self.handlers.get((frame.session_id, frame.stream_id))
                 if handler:
                     self._dispatch_frame(frame, handler)
@@ -249,4 +252,8 @@ class RelayManager:
         online = {name for name, conn in self.connections.items() if not conn.closed}
         for item in data:
             item["online"] = item["name"] in online
+            conn = self.connections.get(item["name"])
+            if conn is not None and conn.status_error_count:
+                item["status_errors"] = conn.status_error_count
+                item["status_last_detail"] = conn.status_error_last_detail or "unknown"
         return data

+ 17 - 8
scripts/start-transparent.sh

@@ -150,15 +150,16 @@ add_exclusions_v4() {
   for ssh_port in "${SSH_PORT_ARRAY[@]}"; do
     iptables -t nat -A "$CHAIN4" -p tcp --sport "$ssh_port" -j RETURN
   done
-  while read -r host; do
-    [[ -n "$host" && "$host" != *:* ]] && iptables -t nat -A "$CHAIN4" -d "$host" -j RETURN
-  done < <(python3 - <<'PY' "$CONFIG_PATH"
+  mapfile -t RELAY_HOSTS_V4 < <(python3 - <<'PY' "$CONFIG_PATH"
 import json, sys
 cfg = json.load(open(sys.argv[1]))
 for relay in cfg.get('relays', []):
     print(relay['host'])
 PY
 )
+  for host in "${RELAY_HOSTS_V4[@]}"; do
+    [[ -n "$host" && "$host" != *:* ]] && iptables -t nat -A "$CHAIN4" -d "$host" -j RETURN
+  done
 }
 
 add_exclusions_v6() {
@@ -170,15 +171,21 @@ add_exclusions_v6() {
   for ssh_port in "${SSH_PORT_ARRAY[@]}"; do
     ip6tables -t nat -A "$CHAIN6" -p tcp --sport "$ssh_port" -j RETURN
   done
-  while read -r host; do
-    [[ -n "$host" && "$host" == *:* ]] && ip6tables -t nat -A "$CHAIN6" -d "$host" -j RETURN
-  done < <(python3 - <<'PY' "$CONFIG_PATH"
+  mapfile -t RELAY_HOSTS_V6 < <(python3 - <<'PY' "$CONFIG_PATH"
 import json, sys
 cfg = json.load(open(sys.argv[1]))
 for relay in cfg.get('relays', []):
     print(relay['host'])
 PY
 )
+  for host in "${RELAY_HOSTS_V6[@]}"; do
+    [[ -n "$host" && "$host" == *:* ]] && ip6tables -t nat -A "$CHAIN6" -d "$host" -j RETURN
+  done
+  if [[ -n "$CAPTURE_UID" ]]; then
+    ip6tables -t nat -A "$CHAIN6" -p tcp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
+  else
+    ip6tables -t nat -A "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
+  fi
 }
 
 add_udp_exclusions_v4() {
@@ -256,21 +263,21 @@ if command -v ip6tables >/dev/null 2>&1; then
     IP6_NAT_SUPPORTED=1
     ip6tables -t nat -N "$CHAIN6" 2>/dev/null || true
     ip6tables -t nat -F "$CHAIN6"
-    ensure_first_rule ip6tables nat OUTPUT -p tcp -j "$CHAIN6"
     add_exclusions_v6
     if [[ -n "$CAPTURE_UID" ]]; then
       ip6tables -t nat -A "$CHAIN6" -p tcp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
     else
       ip6tables -t nat -A "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
     fi
+    ensure_first_rule ip6tables nat OUTPUT -p tcp -j "$CHAIN6"
     if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
-      ensure_first_rule ip6tables nat OUTPUT -p udp -j "$CHAIN6"
       add_udp_exclusions_v6
       if [[ -n "$CAPTURE_UID" ]]; then
         ip6tables -t nat -A "$CHAIN6" -p udp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
       else
         ip6tables -t nat -A "$CHAIN6" -p udp -j REDIRECT --to-ports "$LISTEN_PORT"
       fi
+      ensure_first_rule ip6tables nat OUTPUT -p udp -j "$CHAIN6"
     fi
   else
     echo "ipv6 nat unavailable: ip6tables nat table not supported, skip ipv6 transparent rules"
@@ -288,8 +295,10 @@ if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
 fi
 if [[ "$IP6_ENABLED" == "1" && "$IP6_NAT_SUPPORTED" == "1" ]]; then
   ip6tables -t nat -C OUTPUT -p tcp -j "$CHAIN6" >/dev/null 2>&1 || { echo "self-check failed: ipv6 tcp output hook missing"; exit 1; }
+  ip6tables -t nat -C "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT" >/dev/null 2>&1 || { echo "self-check failed: ipv6 tcp redirect missing"; exit 1; }
   if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
     ip6tables -t nat -C OUTPUT -p udp -j "$CHAIN6" >/dev/null 2>&1 || { echo "self-check failed: ipv6 udp output hook missing"; exit 1; }
+    ip6tables -t nat -C "$CHAIN6" -p udp -j REDIRECT --to-ports "$LISTEN_PORT" >/dev/null 2>&1 || { echo "self-check failed: ipv6 udp redirect missing"; exit 1; }
   fi
 fi