Browse Source

增加u24自动优化

Gogs 6 ngày trước cách đây
mục cha
commit
a87c44d77b

+ 36 - 1
README.md

@@ -95,7 +95,7 @@ cp /home/mynetspeeder/demo-config.json /home/mynetspeeder/config.json
 启动透明接管:
 
 ```bash
-sudo /home/mynetspeeder/scripts/start-transparent.sh --capture-uid <sing-box运行UID> /home/mynetspeeder/config.json
+sudo /home/mynetspeeder/scripts/start-transparent.sh --kernel auto --capture-uid <sing-box运行UID> /home/mynetspeeder/config.json
 ```
 
 不指定 `--capture-uid` 时,默认接管所有用户发起的流量:
@@ -122,6 +122,41 @@ python3 -m mynetspeeder probe --config /home/mynetspeeder/config.json --once
 python3 -m mynetspeeder summary --log-file /var/log/mynetspeeder-edge.log
 ```
 
+
+## Ubuntu 20 / 24 内核模式
+
+当前版本新增 `--kernel auto|20|24`:
+
+- `auto`:默认,根据系统自动判断
+- `20`:更保守的兼容模式
+- `24`:更积极的 Ubuntu 24 优化模式
+
+Ubuntu 24 模式会额外:
+
+- 打印 `iptables` 后端类型(`nf_tables` / `legacy`)
+- 启动后执行规则自检
+- 自动缩短 direct / relay 建连超时
+- 启用更积极的 `happy eyeballs` 建连参数
+
+如需手动指定:
+
+```bash
+sudo /home/mynetspeeder/scripts/start-transparent.sh --kernel 24 --capture-uid $(id -u singbox) /home/mynetspeeder/config.json
+```
+
+配置文件还支持:
+
+```json
+{
+  "kernel_mode": "auto",
+  "direct_open_timeout": 6.0,
+  "relay_open_timeout": 6.0,
+  "tcp_connect_happy_eyeballs_delay": 0.25,
+  "relay_reconnect_delay": 3.0,
+  "relay_tcp_nodelay": true
+}
+```
+
 ## 工作方式
 
 透明模式启动后:

BIN
__pycache__/cli.cpython-313.pyc


BIN
__pycache__/config.cpython-313.pyc


BIN
__pycache__/relay_client.cpython-313.pyc


BIN
__pycache__/transparent_edge.cpython-313.pyc


+ 2 - 1
cli.py

@@ -38,6 +38,7 @@ def build_parser() -> argparse.ArgumentParser:
     edge.add_argument("--listen-port", type=int, default=19080)
     edge.add_argument("--config", required=True)
     edge.add_argument("--enable-udp", action="store_true")
+    edge.add_argument("--kernel", choices=("auto", "20", "24"), default="auto")
     edge.set_defaults(handler=handle_edge)
 
     probe = sub.add_parser("probe", help="查看子节点探测与在线状态")
@@ -59,7 +60,7 @@ def handle_relay(args: argparse.Namespace) -> int:
 
 
 def handle_edge(args: argparse.Namespace) -> int:
-    asyncio.run(TransparentEdge(args.listen_host, args.listen_port, Config.load(args.config), enable_udp=args.enable_udp).start())
+    asyncio.run(TransparentEdge(args.listen_host, args.listen_port, Config.load(args.config), enable_udp=args.enable_udp, kernel_mode=args.kernel).start())
     return 0
 
 

+ 1 - 0
config.json

@@ -5,6 +5,7 @@
   "tcp_loser_grace_ms": 1500,
   "probe_interval": 3,
   "relays": [
+    {"name": "hk1", "host": "23.95.134.159", "port": 9009, "token": "130", "weight": 100}
   ]
   
 }

+ 14 - 1
config.py

@@ -6,6 +6,7 @@ from pathlib import Path
 from typing import Literal
 
 Strategy = Literal["broadcast", "top2", "top3", "top4", "backup"]
+KernelMode = Literal["auto", "20", "24"]
 
 
 @dataclass
@@ -21,20 +22,32 @@ class RelayNode:
 class Config:
     relays: list[RelayNode]
     strategy: Strategy = "top3"
+    kernel_mode: KernelMode = "auto"
     redundancy: int = 3
     tcp_warmup_bytes: int = 1048576
     probe_interval: float = 15.0
     tcp_loser_grace_ms: int = 1500
+    direct_open_timeout: float = 10.0
+    relay_open_timeout: float = 10.0
+    tcp_connect_happy_eyeballs_delay: float | None = None
+    relay_reconnect_delay: float = 3.0
+    relay_tcp_nodelay: bool = True
 
     @classmethod
     def load(cls, path: str) -> "Config":
         raw = json.loads(Path(path).read_text())
-        relays = [RelayNode(**item) for item in raw["relays"]]
+        relays = [RelayNode(**item) for item in raw.get("relays", [])]
         return cls(
             relays=relays,
             strategy=raw.get("strategy", "top3"),
+            kernel_mode=raw.get("kernel_mode", "auto"),
             redundancy=raw.get("redundancy", 3),
             tcp_warmup_bytes=raw.get("tcp_warmup_bytes", 1048576),
             probe_interval=raw.get("probe_interval", 15.0),
             tcp_loser_grace_ms=raw.get("tcp_loser_grace_ms", 1500),
+            direct_open_timeout=raw.get("direct_open_timeout", 10.0),
+            relay_open_timeout=raw.get("relay_open_timeout", 10.0),
+            tcp_connect_happy_eyeballs_delay=raw.get("tcp_connect_happy_eyeballs_delay"),
+            relay_reconnect_delay=raw.get("relay_reconnect_delay", 3.0),
+            relay_tcp_nodelay=raw.get("relay_tcp_nodelay", True),
         )

+ 7 - 2
relay_client.py

@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import asyncio
 import contextlib
+import socket
 from dataclasses import dataclass
 from typing import Awaitable, Callable, Dict
 
@@ -88,14 +89,18 @@ class RelayManager:
                 await asyncio.sleep(2)
                 continue
             try:
-                reader, writer = await asyncio.open_connection(node.host, node.port)
+                reader, writer = await asyncio.wait_for(asyncio.open_connection(node.host, node.port), timeout=self.config.relay_open_timeout)
                 connection = RelayConnection(node=node, manager=self, reader=reader, writer=writer)
+                sock = writer.get_extra_info("socket")
+                if sock is not None and self.config.relay_tcp_nodelay:
+                    with contextlib.suppress(OSError):
+                        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                 await connection.start()
                 self.connections[node.name] = connection
                 await connection.pump_task
             except Exception as exc:
                 print(f"[edge] relay connect failed name={node.name} addr={node.host}:{node.port} error={exc!r}")
-                await asyncio.sleep(3)
+                await asyncio.sleep(self.config.relay_reconnect_delay)
 
     def on_closed(self, connection: RelayConnection) -> None:
         current = self.connections.get(connection.node.name)

+ 130 - 46
scripts/start-transparent.sh

@@ -2,19 +2,21 @@
 set -euo pipefail
 
 usage() {
-  cat <<'EOF'
-Usage: start-transparent.sh [-v|--verbose] [--enable-udp] [--capture-uid UID] [config_path]
+  cat <<'EOH'
+Usage: start-transparent.sh [-v|--verbose] [--enable-udp] [--capture-uid UID] [--kernel auto|20|24] [config_path]
 
 Options:
   -v, --verbose        启动后实时输出 mynetspeeder 日志
   --capture-uid UID    指定时只接管该 UID;不指定时接管所有用户流量
   --enable-udp         额外启用 UDP 透明接管(实验性,默认关闭)
+  --kernel MODE        指定内核优化模式:auto|20|24,默认 auto
   -h, --help           显示帮助
-EOF
+EOH
 }
 
 VERBOSE=0
 ENABLE_UDP=0
+KERNEL_MODE="${MYNETSPEEDER_KERNEL_MODE:-auto}"
 CONFIG_PATH="/home/mynetspeeder/config.json"
 CAPTURE_UID="${MYNETSPEEDER_CAPTURE_UID:-}"
 
@@ -28,6 +30,10 @@ while [[ $# -gt 0 ]]; do
       CAPTURE_UID="${2:-}"
       [[ -n "$CAPTURE_UID" ]] || { echo "missing value for --capture-uid"; exit 1; }
       shift 2 ;;
+    --kernel)
+      KERNEL_MODE="${2:-}"
+      [[ -n "$KERNEL_MODE" ]] || { echo "missing value for --kernel"; exit 1; }
+      shift 2 ;;
     -h|--help)
       usage; exit 0 ;;
     *)
@@ -45,10 +51,24 @@ LOG_BACKUPS="${MYNETSPEEDER_LOG_BACKUPS:-3}"
 CHAIN4="MYNETSPEEDER"
 CHAIN6="MYNETSPEEDER6"
 SSH_PORTS="${MYNETSPEEDER_SSH_PORTS:-}"
+SELF_EXCLUDE_V4="127.0.0.0/8 169.254.0.0/16"
+SELF_EXCLUDE_V6="::1/128 fe80::/10"
 
 if [[ $EUID -ne 0 ]]; then echo "need root"; exit 1; fi
 if [[ ! -f "$CONFIG_PATH" ]]; then echo "config not found: $CONFIG_PATH"; exit 1; fi
 if [[ -n "$CAPTURE_UID" ]] && ! [[ "$CAPTURE_UID" =~ ^[0-9]+$ ]]; then echo "capture uid must be numeric"; exit 1; fi
+case "$KERNEL_MODE" in
+  auto|20|24) ;;
+  *) echo "invalid kernel mode: $KERNEL_MODE"; exit 1 ;;
+esac
+
+if [[ "$KERNEL_MODE" == "auto" ]]; then
+  if [[ -f /etc/os-release ]] && grep -q '^VERSION_ID="24' /etc/os-release; then
+    KERNEL_MODE="24"
+  else
+    KERNEL_MODE="20"
+  fi
+fi
 
 if [[ -z "$SSH_PORTS" && -n "${SSH_CONNECTION:-}" ]]; then
   SSH_PORTS="${SSH_CONNECTION##* }"
@@ -69,12 +89,67 @@ chown "$RUNTIME_USER":"$RUNTIME_USER" "$LOG_FILE"
 if ! [[ "$LOG_MAX_MB" =~ ^[0-9]+$ ]] || ! [[ "$LOG_BACKUPS" =~ ^[0-9]+$ ]]; then echo "log limits must be numeric"; exit 1; fi
 LOG_MAX_BYTES=$((LOG_MAX_MB * 1024 * 1024))
 
+IPTABLES_BACKEND="unknown"
+if iptables --version 2>/dev/null | grep -qi 'nf_tables'; then
+  IPTABLES_BACKEND="nf_tables"
+else
+  IPTABLES_BACKEND="legacy"
+fi
+
+ensure_rule() {
+  local cmd="$1"
+  local table="$2"
+  local chain="$3"
+  shift 3
+  if ! "$cmd" -t "$table" -C "$chain" "$@" >/dev/null 2>&1; then
+    "$cmd" -t "$table" -A "$chain" "$@"
+  fi
+}
+
+add_exclusions_v4() {
+  for cidr in $SELF_EXCLUDE_V4; do
+    iptables -t nat -A "$CHAIN4" -d "$cidr" -j RETURN
+  done
+  iptables -t nat -A "$CHAIN4" -m owner --uid-owner "$RUNTIME_USER" -j RETURN
+  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"
+import json, sys
+cfg = json.load(open(sys.argv[1]))
+for relay in cfg.get('relays', []):
+    print(relay['host'])
+PY
+)
+}
+
+add_exclusions_v6() {
+  for cidr in $SELF_EXCLUDE_V6; do
+    ip6tables -t nat -A "$CHAIN6" -d "$cidr" -j RETURN
+  done
+  ip6tables -t nat -A "$CHAIN6" -m owner --uid-owner "$RUNTIME_USER" -j RETURN
+  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"
+import json, sys
+cfg = json.load(open(sys.argv[1]))
+for relay in cfg.get('relays', []):
+    print(relay['host'])
+PY
+)
+}
+
 pkill -f 'python3 -m mynetspeeder edge' || true
 EDGE_UDP_FLAG=""
 if [[ "$ENABLE_UDP" == "1" ]]; then
   EDGE_UDP_FLAG="--enable-udp"
 fi
-runuser -u "$RUNTIME_USER" -- bash -lc "export PYTHONUNBUFFERED=1; export PYTHONPATH=/home; cd /home && exec nohup python3 -m mynetspeeder edge --listen-host ${LISTEN_HOST} --listen-port ${LISTEN_PORT} --config ${CONFIG_PATH} ${EDGE_UDP_FLAG} 2>&1 | python3 /home/mynetspeeder/scripts/rotate-log.py ${LOG_FILE} ${LOG_MAX_BYTES} ${LOG_BACKUPS}" &
+runuser -u "$RUNTIME_USER" -- bash -lc "export PYTHONUNBUFFERED=1; export PYTHONPATH=/home; cd /home && exec nohup python3 -m mynetspeeder edge --listen-host ${LISTEN_HOST} --listen-port ${LISTEN_PORT} --kernel ${KERNEL_MODE} --config ${CONFIG_PATH} ${EDGE_UDP_FLAG} 2>&1 | python3 /home/mynetspeeder/scripts/rotate-log.py ${LOG_FILE} ${LOG_MAX_BYTES} ${LOG_BACKUPS}" &
 EDGE_PID=$!
 echo "$EDGE_PID" > "$PID_FILE"
 sleep 1
@@ -82,77 +157,86 @@ ss -ln | grep -qE "[:.]${LISTEN_PORT}( |$)" || { echo "edge failed to listen"; t
 
 iptables -t nat -N "$CHAIN4" 2>/dev/null || true
 iptables -t nat -F "$CHAIN4"
-iptables -t nat -A "$CHAIN4" -d 127.0.0.0/8 -j RETURN
-iptables -t nat -A "$CHAIN4" -m owner --uid-owner "$RUNTIME_USER" -j RETURN
-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"
-import json, sys
-cfg = json.load(open(sys.argv[1]))
-for relay in cfg.get('relays', []):
-    print(relay['host'])
-PY
-)
+add_exclusions_v4
 if [[ -n "$CAPTURE_UID" ]]; then
   iptables -t nat -A "$CHAIN4" -p tcp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
 else
   iptables -t nat -A "$CHAIN4" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
 fi
-iptables -t nat -C OUTPUT -p tcp -j "$CHAIN4" 2>/dev/null || iptables -t nat -A OUTPUT -p tcp -j "$CHAIN4"
+ensure_rule iptables nat OUTPUT -p tcp -j "$CHAIN4"
 if [[ "$ENABLE_UDP" == "1" ]]; then
   if [[ -n "$CAPTURE_UID" ]]; then
     iptables -t nat -A "$CHAIN4" -p udp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
   else
     iptables -t nat -A "$CHAIN4" -p udp -j REDIRECT --to-ports "$LISTEN_PORT"
   fi
-  iptables -t nat -C OUTPUT -p udp -j "$CHAIN4" 2>/dev/null || iptables -t nat -A OUTPUT -p udp -j "$CHAIN4"
+  ensure_rule iptables nat OUTPUT -p udp -j "$CHAIN4"
 fi
 
+IP6_ENABLED=0
+IP6_NAT_SUPPORTED=0
 if command -v ip6tables >/dev/null 2>&1; then
-  ip6tables -t nat -N "$CHAIN6" 2>/dev/null || true
-  ip6tables -t nat -F "$CHAIN6"
-  ip6tables -t nat -A "$CHAIN6" -d ::1/128 -j RETURN
-  ip6tables -t nat -A "$CHAIN6" -m owner --uid-owner "$RUNTIME_USER" -j RETURN
-  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"
-import json, sys
-cfg = json.load(open(sys.argv[1]))
-for relay in cfg.get('relays', []):
-    print(relay['host'])
-PY
-)
-  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
-  ip6tables -t nat -C OUTPUT -p tcp -j "$CHAIN6" 2>/dev/null || ip6tables -t nat -A OUTPUT -p tcp -j "$CHAIN6"
-  if [[ "$ENABLE_UDP" == "1" ]]; then
+  if ip6tables -t nat -S >/dev/null 2>&1; then
+    IP6_ENABLED=1
+    IP6_NAT_SUPPORTED=1
+    ip6tables -t nat -N "$CHAIN6" 2>/dev/null || true
+    ip6tables -t nat -F "$CHAIN6"
+    add_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"
+      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 udp -j REDIRECT --to-ports "$LISTEN_PORT"
+      ip6tables -t nat -A "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
+    fi
+    ensure_rule ip6tables nat OUTPUT -p tcp -j "$CHAIN6"
+    if [[ "$ENABLE_UDP" == "1" ]]; then
+      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_rule ip6tables nat OUTPUT -p udp -j "$CHAIN6"
     fi
-    ip6tables -t nat -C OUTPUT -p udp -j "$CHAIN6" 2>/dev/null || ip6tables -t nat -A OUTPUT -p udp -j "$CHAIN6"
+  else
+    echo "ipv6 nat unavailable: ip6tables nat table not supported, skip ipv6 transparent rules"
+  fi
+fi
+
+RULES_V4=$(iptables -t nat -S "$CHAIN4" 2>/dev/null | wc -l)
+RULES_V6=0
+if [[ "$IP6_ENABLED" == "1" && "$IP6_NAT_SUPPORTED" == "1" ]]; then
+  RULES_V6=$(ip6tables -t nat -S "$CHAIN6" 2>/dev/null | wc -l)
+fi
+iptables -t nat -C OUTPUT -p tcp -j "$CHAIN4" >/dev/null 2>&1 || { echo "self-check failed: ipv4 tcp output hook missing"; exit 1; }
+if [[ "$ENABLE_UDP" == "1" ]]; then
+  iptables -t nat -C OUTPUT -p udp -j "$CHAIN4" >/dev/null 2>&1 || { echo "self-check failed: ipv4 udp output hook missing"; exit 1; }
+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; }
+  if [[ "$ENABLE_UDP" == "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; }
   fi
 fi
 
 echo "mynetspeeder transparent mode started on ${LISTEN_HOST}:${LISTEN_PORT}"
+echo "kernel mode: $KERNEL_MODE"
+echo "iptables backend: $IPTABLES_BACKEND"
 if [[ -n "$CAPTURE_UID" ]]; then
   echo "capture uid: $CAPTURE_UID"
 else
   echo "capture uid: all users"
 fi
+if [[ ${#SSH_PORT_ARRAY[@]} -gt 0 ]]; then
+  echo "ssh exempt ports: ${SSH_PORT_ARRAY[*]}"
+else
+  echo "ssh exempt ports: none"
+fi
 echo "udp capture: $ENABLE_UDP"
 echo "log file: $LOG_FILE"
 echo "log max: ${LOG_MAX_MB}MB x ${LOG_BACKUPS}"
+echo "ipv4 chain rules: $RULES_V4"
+echo "ipv6 chain rules: $RULES_V6"
+
+echo "self-check: ok"
 
 if [[ "$VERBOSE" == "1" ]]; then
   echo "verbose mode: press Ctrl+C to stop viewing logs, service keeps running"

+ 3 - 1
scripts/start_udp.sh

@@ -1 +1,3 @@
-./start-transparent.sh  --capture-uid 996 --enable-udp
+#!/usr/bin/env bash
+set -euo pipefail
+exec "$(dirname "$0")/start-transparent.sh" --enable-udp "$@"

+ 14 - 8
scripts/stop-transparent.sh

@@ -16,16 +16,22 @@ if [[ -f "$PID_FILE" ]]; then
 fi
 pkill -f 'python3 -m mynetspeeder edge' || true
 
-iptables -t nat -D OUTPUT -p tcp -j "$CHAIN4" 2>/dev/null || true
-iptables -t nat -D OUTPUT -p udp -j "$CHAIN4" 2>/dev/null || true
-iptables -t nat -F "$CHAIN4" 2>/dev/null || true
-iptables -t nat -X "$CHAIN4" 2>/dev/null || true
+if iptables -t nat -S >/dev/null 2>&1; then
+  iptables -t nat -D OUTPUT -p tcp -j "$CHAIN4" 2>/dev/null || true
+  iptables -t nat -D OUTPUT -p udp -j "$CHAIN4" 2>/dev/null || true
+  iptables -t nat -F "$CHAIN4" 2>/dev/null || true
+  iptables -t nat -X "$CHAIN4" 2>/dev/null || true
+fi
 
 if command -v ip6tables >/dev/null 2>&1; then
-  ip6tables -t nat -D OUTPUT -p tcp -j "$CHAIN6" 2>/dev/null || true
-  ip6tables -t nat -D OUTPUT -p udp -j "$CHAIN6" 2>/dev/null || true
-  ip6tables -t nat -F "$CHAIN6" 2>/dev/null || true
-  ip6tables -t nat -X "$CHAIN6" 2>/dev/null || true
+  if ip6tables -t nat -S >/dev/null 2>&1; then
+    ip6tables -t nat -D OUTPUT -p tcp -j "$CHAIN6" 2>/dev/null || true
+    ip6tables -t nat -D OUTPUT -p udp -j "$CHAIN6" 2>/dev/null || true
+    ip6tables -t nat -F "$CHAIN6" 2>/dev/null || true
+    ip6tables -t nat -X "$CHAIN6" 2>/dev/null || true
+  else
+    echo "ipv6 nat unavailable: skip ipv6 transparent cleanup"
+  fi
 fi
 
 echo "mynetspeeder transparent mode stopped"

+ 44 - 6
transparent_edge.py

@@ -1,8 +1,11 @@
 from __future__ import annotations
 
+from pathlib import Path
+
 import asyncio
 import contextlib
 import itertools
+import os
 import socket
 import struct
 from dataclasses import dataclass, field
@@ -66,16 +69,26 @@ class BasePath:
 
 
 class DirectTcpPath(BasePath):
-    def __init__(self, name: str, on_frame: Callable[[BasePath, str, bytes | None], Awaitable[None]]) -> None:
+    def __init__(self, name: str, on_frame: Callable[[BasePath, str, bytes | None], Awaitable[None]], open_timeout: float, happy_eyeballs_delay: float | None, tcp_nodelay: bool = True) -> None:
         super().__init__(name, on_frame)
         self.reader: asyncio.StreamReader | None = None
         self.writer: asyncio.StreamWriter | None = None
         self.pump_task: asyncio.Task | None = None
+        self.open_timeout = open_timeout
+        self.happy_eyeballs_delay = happy_eyeballs_delay
+        self.tcp_nodelay = tcp_nodelay
 
     async def open(self, target: TargetAddress) -> None:
         try:
             family = socket.AF_INET6 if target.family == socket.AF_INET6 else socket.AF_INET
-            self.reader, self.writer = await asyncio.open_connection(host=target.host, port=target.port, family=family)
+            kwargs = {"host": target.host, "port": target.port, "family": family}
+            if self.happy_eyeballs_delay is not None:
+                kwargs["happy_eyeballs_delay"] = self.happy_eyeballs_delay
+            self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**kwargs), timeout=self.open_timeout)
+            sock = self.writer.get_extra_info("socket")
+            if sock is not None and self.tcp_nodelay:
+                with contextlib.suppress(OSError):
+                    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
             self.opened = True
             self.pump_task = asyncio.create_task(self._pump())
             await self.on_frame(self, "status", b"ok")
@@ -199,7 +212,7 @@ class TransparentSession:
 
     async def start(self) -> None:
         await asyncio.gather(*(path.open(self.target) for path in self.paths), return_exceptions=True)
-        await asyncio.wait_for(self.open_event.wait(), timeout=10)
+        await asyncio.wait_for(self.open_event.wait(), timeout=15)
         if self.opened_count == 0:
             raise ConnectionError(self.errors[0] if self.errors else "all paths failed")
         self.pump_task = asyncio.create_task(self._pump_local())
@@ -475,11 +488,12 @@ class TransparentUdpListener:
 
 
 class TransparentEdge:
-    def __init__(self, listen_host: str, listen_port: int, config: Config, enable_udp: bool = False) -> None:
+    def __init__(self, listen_host: str, listen_port: int, config: Config, enable_udp: bool = False, kernel_mode: str = "auto") -> None:
         self.listen_host = listen_host
         self.listen_port = listen_port
         self.config = config
         self.enable_udp = enable_udp
+        self.kernel_mode = self._resolve_kernel_mode(kernel_mode, config.kernel_mode)
         self.manager = RelayManager(config)
         self.session_ids = itertools.count(1)
         self.stream_ids = itertools.count(1)
@@ -490,9 +504,33 @@ class TransparentEdge:
         self.tcp_win_counts: dict[str, int] = {}
         self.tcp_target_wins: dict[tuple[str, int], dict[str, int]] = {}
 
+    def _resolve_kernel_mode(self, cli_kernel_mode: str, config_kernel_mode: str) -> str:
+        mode = cli_kernel_mode if cli_kernel_mode != "auto" else config_kernel_mode
+        if mode != "auto":
+            return mode
+        try:
+            if Path("/etc/os-release").exists() and 'VERSION_ID="24' in Path("/etc/os-release").read_text(errors="ignore"):
+                return "24"
+        except Exception:
+            pass
+        try:
+            release = os.uname().release
+            if release.startswith("6."):
+                return "24"
+        except Exception:
+            pass
+        return "20"
+
     async def start(self) -> None:
+        if self.kernel_mode == "24":
+            if self.config.direct_open_timeout == 10.0:
+                self.config.direct_open_timeout = 6.0
+            if self.config.relay_open_timeout == 10.0:
+                self.config.relay_open_timeout = 6.0
+            if self.config.tcp_connect_happy_eyeballs_delay is None:
+                self.config.tcp_connect_happy_eyeballs_delay = 0.25
         await self.manager.start()
-        print(f"[edge] relay snapshot: {self.manager.snapshot()}")
+        print(f"[edge] kernel_mode={self.kernel_mode} relay snapshot: {self.manager.snapshot()}")
         server4 = await asyncio.start_server(self._accept, self.listen_host, self.listen_port, family=socket.AF_INET)
         sockets = [str(sock.getsockname()) for sock in server4.sockets or []]
         server6 = None
@@ -537,7 +575,7 @@ class TransparentEdge:
             target = self._get_original_dst(writer)
             session_id = next(self.session_ids)
             session = TransparentSession(session_id=session_id, target=target, reader=reader, writer=writer, paths=[], warmup_bytes=self.config.tcp_warmup_bytes, loser_grace_ms=self.config.tcp_loser_grace_ms, stats=self.tcp_win_counts, target_stats=self.tcp_target_wins)
-            paths: list[BasePath] = [DirectTcpPath(name="direct", on_frame=lambda path, event, payload, s=session: self._handle_tcp_session(s, path, event, payload))]
+            paths: list[BasePath] = [DirectTcpPath(name="direct", on_frame=lambda path, event, payload, s=session: self._handle_tcp_session(s, path, event, payload), open_timeout=self.config.direct_open_timeout, happy_eyeballs_delay=self.config.tcp_connect_happy_eyeballs_delay, tcp_nodelay=self.config.relay_tcp_nodelay)]
             for connection in self.manager.available():
                 stream_id = next(self.stream_ids)
                 paths.append(RelayTcpPath(name=connection.node.name, on_frame=lambda path, event, payload, s=session: self._handle_tcp_session(s, path, event, payload), connection=connection, session_id=session_id, stream_id=stream_id))