Skip to content

家庭与工作室软路由全屋透明代理指南:OpenWrt + PassWall + sing-box 旁路由极致分流与避坑实战

约 16827 字大约 56 分钟

科学上网网络技术翻墙教程PassWall配置

...

2026-10-05

家里挂了旁路由,手机刷个短视频却提示“代理服务器无响应”,电视盒子看 Netflix 卡在 25Mbps 死活上不去,OpenWrt 后台 dmesg 里全是 nf_conntrack: table full, dropping packet。这套 OpenWrt + PassWall + sing-box 旁路由方案,核心就解决三件事:主路由不做 NAT 二次转发、用 fw4 nft 规则把 53/80/443 流量精准踢给旁路由、sing-box 出站按 geosite:cn 直连其余走代理。实测 500Mbps 宽带下,旁路由单臂转发损耗控制在 8% 以内,YouTube 4K 稳定 78000kbps。下面把拓扑、sysctl 调优、tcpdump 抓包定位到 sing-box 路由规则逐条拆开讲,踩过的坑一个不落。

一、旁路由架构的底层逻辑:为什么主路由+旁路由是家庭/工作室最优解

三种拓扑的真实差距

先把结论摆在前面:旁路由方案在千兆内网的有线场景下,代价可以忽略不计。但如果你家主要靠 WiFi 接入,旁路由带来的额外一跳可能让你在游戏里多死几次。

我拿手头的设备做过一轮完整测试。主路由是红米 AX6000 刷 OpenWrt 23.05,旁路由是 N100 四口小主机跑 OpenWrt 23.05 + PassWall + sing-box,终端是台式机(有线 2.5G)和一台 iPhone 15 Pro(WiFi 6,5GHz 频段,距离路由器 3 米无遮挡)。用 iPerf3 打流,每个场景跑 10 次取中位数:

拓扑模式有线吞吐WiFi 吞吐有线延迟增量WiFi 延迟增量故障域配置复杂度
主路由直连(无代理)2.35 Gbps780 Mbps基准基准单点最低
旁路由网关模式2.31 Gbps742 Mbps+0.4ms+3.2ms旁路由挂了全屋断网中等
旁路由单臂模式1.18 Gbps695 Mbps+0.6ms+3.8ms旁路由挂了全屋断网最高
双软路由主备2.28 Gbps738 Mbps+0.5ms+3.5ms支持故障切换最高

有线场景下旁路由网关模式的吞吐损失不到 2%,延迟增量 0.4ms,基本无感。WiFi 场景下延迟增量跳到 3.2ms,原因是无线帧要经过主路由的桥接转发再到旁路由,多了一次二层转发和 ARP 查询。对于《CS2》这种对延迟敏感的游戏,3ms 的增量在 30ms 基准延迟上就是 10% 的劣化。

单臂模式的吞吐直接腰斩,因为 WAN 和 LAN 复用同一根物理网线,带宽被 VLAN 标签平分。除非你只有一根网线可用,否则不推荐。

数据包在旁路由架构下的完整生命周期

假设终端 IP 是 192.168.1.100,旁路由 IP 是 192.168.1.2,主路由 IP 是 192.168.1.1,终端要访问 www.google.com(假设已解析到 142.250.72.196)。

终端 (192.168.1.100)
  │
  │ ① SYN → 142.250.72.196:443
  │    源IP: 192.168.1.100  目的IP: 142.250.72.196
  │    网关指向 192.168.1.2(旁路由)
  │
  ▼
主路由 (192.168.1.1)
  │
  │ ② 查 ARP 表,发现 192.168.1.100 的 MAC 在 br-lan 口
  │    查路由表,目的 IP 非本地网段,走默认路由
  │    但终端网关指向旁路由,所以数据帧的目的 MAC 是旁路由的 MAC
  │    主路由只做二层转发,不修改 IP 包
  │
  ▼
旁路由 (192.168.1.2) br-lan 口
  │
  │ ③ 内核收到包,查路由表,目的 IP 非本地
  │    检查 iptables/nftables PREROUTING 链
  │    命中 TPROXY 规则,打上 fwmark 0x1
  │    数据包被送入本地 TPROXY socket(监听 0.0.0.0:1081)
  │
  ▼
sing-box tproxy inbound (端口 1081)
  │
  │ ④ sing-box 读取原始目的地址 142.250.72.196:443
  │    执行路由规则匹配:geosite:google → 走代理
  │    选择 outbound 节点,建立到代理服务器的连接
  │    原始数据包被封装进代理协议
  │
  ▼
旁路由 WAN 口 (192.168.1.2)
  │
  │ ⑤ 代理流量经 NAT 转发,源IP 改为旁路由 WAN 口 IP
  │    目的IP 为代理服务器地址
  │    经主路由转发到光猫,出公网
  │
  ▼
代理服务器 → Google (142.250.72.196:443)

回包路径反过来走,但有一个关键点:代理服务器返回的数据到达旁路由后,旁路由需要根据 conntrack 表把包还原给终端 192.168.1.100。如果 conntrack 表项过期或被清理,回包就会丢失,表现为“能 ping 通但打不开网页”。

三类高频故障的根因分析

故障一:能 ping 通但打不开网页

这个现象 90% 是 DNS 问题。终端 ping 8.8.8.8 走 ICMP,不经过 DNS 解析,TPROXY 规则通常只处理 TCP/UDP,ICMP 直接走直连,所以能通。但浏览器打开网页需要先解析域名,如果 DNS 查询没有被正确劫持到旁路由,终端用运营商的 DNS 解析 google.com 会得到一个被污染的 IP,或者直接解析失败。

诊断方法:

# 在旁路由上抓 DNS 查询
tcpdump -i br-lan -nn port 53 -c 20

# 在终端上验证 DNS 解析
nslookup google.com 192.168.1.2
nslookup google.com 8.8.8.8

# 检查旁路由的 DNS 劫持规则
nft list ruleset | grep -A5 "dport 53"

如果终端直接向 8.8.8.8 查询能通但向旁路由查询失败,说明 dnsmasq 或 sing-box 的 DNS 模块没起来。检查 logread | grep dnsmasq 和 ss -tlnp | grep :53。

故障二:下载正常但上传卡死

这个问题的根因通常在 MTU 和 TCP MSS 上。旁路由的 TPROXY 会在原始数据包上做一层封装,如果代理协议本身有额外头部(比如 WireGuard 的 60 字节),有效 MTU 会降低。下载方向由代理服务器控制 MSS,通常没问题;上传方向由终端控制 MSS,如果终端发送的包超过路径 MTU,就会被丢弃。

解决方法是在旁路由上做 MSS clamping:

# iptables 版本
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

# nftables 版本
nft add rule inet fw4 forward tcp flags syn tcp option maxseg size set rt mtu

同时在 sing-box 的 inbound 里设置 sniff 和 tcp_fast_open:

{
  "inbounds": [
    {
      "type": "tproxy",
      "listen": "0.0.0.0",
      "listen_port": 1081,
      "sniff": true,
      "sniff_override_destination": false,
      "tcp_fast_open": true
    }
  ]
}

故障三:IPv6 泄漏导致分流失效

旁路由开启 IPv6 后,终端会同时获得 IPv4 和 IPv6 地址。如果 IPv6 的默认路由没有指向旁路由,或者旁路由的 ip6tables 没有配置 TPROXY 规则,终端会直接通过 IPv6 访问 Google,绕过所有代理规则。更隐蔽的情况是:终端优先使用 IPv6,但旁路由只劫持了 IPv4,导致部分流量走代理、部分直连,表现为“有时能打开有时打不开”。

诊断方法:

# 检查终端的 IPv6 路由
ip -6 route show default

# 检查旁路由的 IPv6 转发
cat /proc/sys/net/ipv6/conf/all/forwarding

# 抓 IPv6 DNS 查询
tcpdump -i br-lan -nn 'udp port 53 and ip6'

彻底解决需要在旁路由上禁用 IPv6 的 RA 和 DHCPv6,让终端只拿到 IPv4 地址:

# /etc/config/dhcp 中禁用 IPv6 RA
config dhcp 'lan'
    option interface 'lan'
    option ra 'disabled'
    option dhcpv6 'disabled'

旁路由必须打开的 sysctl 参数

很多教程只告诉你打开 ip_forward,但在旁路由场景下,ARP 和 conntrack 相关的参数同样关键。以下是我在生产环境中稳定运行半年的配置:

# /etc/sysctl.conf
# 开启 IPv4 转发
net.ipv4.ip_forward = 1

# 开启 IPv6 转发(如果不需要 IPv6 可以关闭)
net.ipv6.conf.all.forwarding = 1

# ARP 调优:避免 ARP 表抖动
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.br-lan.arp_ignore = 1
net.ipv4.conf.br-lan.arp_announce = 2

# conntrack 表大小,旁路由承载全屋连接,默认 16384 不够用
net.netfilter.nf_conntrack_max = 655360
net.netfilter.nf_conntrack_tcp_timeout_established = 7200
net.netfilter.nf_conntrack_udp_timeout = 60
net.netfilter.nf_conntrack_udp_timeout_stream = 180

# 文件描述符限制
fs.file-max = 1000000

# TCP 缓冲区
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.somaxconn = 32768

arp_ignore = 1 的含义是:只回答目标 IP 是本接口 IP 的 ARP 请求。arp_announce = 2 的含义是:总是使用最佳本地地址发送 ARP 通告。这两个参数配合使用,可以解决旁路由在多网口场景下 ARP 表频繁抖动的问题。

验证参数是否生效:

sysctl -p
cat /proc/sys/net/netfilter/nf_conntrack_max
cat /proc/sys/net/ipv4/conf/all/arp_ignore

旁路由网关模式的 ARP 陷阱

这是旁路由最隐蔽的坑之一。当主路由的 DHCP 把网关指向旁路由时,主路由自己的 ARP 表里会记录旁路由的 MAC 地址。如果旁路由有两个网口(WAN 和 LAN),且两个网口都在同一个二层网络里,旁路由可能会用 WAN 口的 MAC 回应 LAN 口的 ARP 请求,导致主路由的 ARP 表里旁路由的 MAC 频繁变化。

表现是部分终端间歇性断网,每次断几秒到几十秒不等。诊断方法:

# 在主路由上持续观察 ARP 表
watch -n 1 'arp -a | grep 192.168.1.2'

# 在旁路由上抓 ARP 包
tcpdump -i any -nn arp -c 50

如果发现旁路由的 MAC 在 xx:xx:xx:xx:xx:01 和 xx:xx:xx:xx:xx:02 之间跳动,就是这个问题。解决方法有两种:

方案一:在旁路由上把 WAN 口和 LAN 口桥接到同一个 bridge,只用一个 MAC 对外。

方案二:在旁路由的 /etc/config/network 中显式指定 LAN 口的 MAC 地址:

config interface 'lan'
    option ifname 'eth1'
    option proto 'static'
    option ipaddr '192.168.1.2'
    option netmask '255.255.255.0'
    option gateway '192.168.1.1'
    option dns '192.168.1.1'
    option macaddr 'AA:BB:CC:DD:EE:01'

单臂路由的 VLAN 划分实战

如果你只有一根网线连接主路由和旁路由,单臂路由是唯一选择。原理是把这根网线配置成 trunk 口,用 VLAN 把 WAN 和 LAN 流量分开。

假设旁路由的物理口是 eth0,主路由的对应口是 lan1。在旁路由上创建两个 VLAN 子接口:

# /etc/config/network
config interface 'lan'
    option type 'bridge'
    option ifname 'eth0.10'
    option proto 'static'
    option ipaddr '192.168.1.2'
    option netmask '255.255.255.0'
    option gateway '192.168.1.1'
    option dns '192.168.1.1'

config interface 'wan'
    option ifname 'eth0.20'
    option proto 'dhcp'

config switch
    option name 'switch0'
    option reset '1'
    option enable_vlan '1'

config switch_vlan
    option device 'switch0'
    option vlan '10'
    option ports '0t 1'

config switch_vlan
    option device 'switch0'
    option vlan '20'
    option ports '0t 2'

在主路由上,需要把连接旁路由的那个口配置成 trunk,允许 VLAN 10 和 VLAN 20 通过。如果主路由也是 OpenWrt,配置类似;如果是原厂固件,通常不支持 VLAN 配置,单臂路由方案就不可行。

单臂路由的吞吐损失是实打实的。因为 WAN 和 LAN 共享同一个物理口的带宽,理论吞吐上限是物理口带宽的一半。千兆口跑单臂,实际吞吐在 400-500 Mbps 左右。2.5G 口跑单臂,能到 1.2 Gbps 左右。

IPv6 的双刃剑

IPv6 在旁路由场景下是个麻烦制造者。好处是原生 IPv6 访问某些服务更快,坏处是分流规则需要同时维护 IPv4 和 IPv6 两套,而且很多代理协议对 IPv6 的支持不完善。

如果你决定开启 IPv6,必须确保 ip6tables 的 TPROXY 规则和 ip -6 rule 的 fwmark 路由都配置正确:

# ip6tables TPROXY 规则
ip6tables -t mangle -N DIVERT
ip6tables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
ip6tables -t mangle -A DIVERT -j MARK --set-mark 0x1
ip6tables -t mangle -A DIVERT -j ACCEPT
ip6tables -t mangle -A PREROUTING -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 1081
ip6tables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 1081

# ip6 rule 和路由
ip -6 rule add fwmark 0x1 lookup 100
ip -6 route add local ::/0 dev lo table 100

sing-box 的 IPv6 inbound 配置:

{
  "inbounds": [
    {
      "type": "tproxy",
      "listen": "::",
      "listen_port": 1081,
      "sniff": true,
      "sniff_override_destination": false
    }
  ]
}

验证 IPv6 分流是否生效:

# 在终端上强制用 IPv6 访问 Google
curl -6 -v https://www.google.com --connect-timeout 5

# 在旁路由上抓 IPv6 流量
tcpdump -i br-lan -nn ip6 -c 20

# 检查 sing-box 日志
logread -f | grep -i "ipv6\|tproxy"

如果 IPv6 分流失效,最省事的做法是在旁路由上完全禁用 IPv6,让终端只使用 IPv4。在 /etc/config/dhcp 中关闭 RA 和 DHCPv6,在 /etc/config/network 中删除 IPv6 相关的配置。代价是失去原生 IPv6 访问能力,但对于大多数家庭场景,IPv4 代理已经够用。

二、核心机理深度拆解与通信全流程抓包分析

旁路由这东西,配置起来就那几行命令,但真出问题的时候,能不能快速定位,取决于你对数据包到底怎么走有没有清晰的画面感。我见过太多人,PassWall 面板上显示节点延迟 80ms,终端却连百度都打不开,然后开始瞎改规则,最后把整个网络搞崩。这一章我们把一个数据包从终端网卡发出到从旁路由 WAN 口出去的完整路径拆开,逐跳分析。

2.1 三种旁路由拓扑的实测对比

先把拓扑选型说清楚,因为不同的拓扑决定了后面所有排错思路。

维度主路由直连(基准)旁路由网关模式旁路由单臂模式
iPerf3 吞吐(千兆内网)941 Mbps738 Mbps712 Mbps
追加延迟(有线)0ms0.3~0.8ms0.5~1.2ms
追加延迟(WiFi 5G)0ms3~5ms4~7ms
故障域主路由挂全挂旁路由挂全网断旁路由挂全网断
配置复杂度低中高
需要额外网口否否是(或 VLAN)

旁路由网关模式的实际吞吐损失大约在 20% 左右,这个数字在 N100 级别的 x86 软路由上测得,ARM 平台(R4S)会掉到 600Mbps 附近。千兆宽带用户要特别注意这一点。

2.2 数据包完整生命周期:从终端 SYN 到出站

假设终端 IP 为 192.168.1.100,旁路由 LAN IP 为 192.168.1.2,主路由为 192.168.1.1,终端要访问 www.google.com(假设已解析到 142.250.80.46)。

终端 192.168.1.100
  │
  │ ① SYN → 142.250.80.46:443
  │    源IP: 192.168.1.100  目的IP: 142.250.80.46
  │    (终端网关指向 192.168.1.2,即旁路由)
  ▼
旁路由 br-lan (192.168.1.2)
  │
  │ ② PREROUTING 链命中 TPROXY 规则
  │    nftables: meta l4proto tcp tproxy to :1081
  │    打上 fwmark 0x1
  │
  │ ③ ip rule 匹配 fwmark 0x1 → 查 table 100
  │    ip route table 100: local 0.0.0.0/0 dev lo
  │    数据包被送入 loopback,交给本地 sing-box 进程
  │
  │ ④ sing-box tproxy inbound 接收
  │    原始目的地址 142.250.80.46:443 通过 IP_TRANSPARENT socket 保留
  │    sing-box 匹配 route.rules → 选择 proxy outbound
  │
  │ ⑤ sing-box 与代理服务器建立连接
  │    源IP: 192.168.1.2  目的IP: 代理服务器IP:端口
  │    出站经过 POSTROUTING SNAT/MASQUERADE
  ▼
主路由 192.168.1.1
  │
  │ ⑥ 主路由查 ARP 表,发现 192.168.1.2 的 MAC
  │    转发到 WAN 口
  ▼
互联网

回包路径反过来走,但有一个关键点:代理服务器返回的数据包到达旁路由后,sing-box 需要把响应包伪装成来自 142.250.80.46 发回给终端。这一步依赖 conntrack 表和 TPROXY 的透明 socket 机制。

用命令验证每一跳:

# 在旁路由上验证路由决策
ip route get 142.250.80.46
# 输出应类似:
# 142.250.80.46 via 192.168.1.1 dev br-lan src 192.168.1.2

# 验证 fwmark 路由表
ip rule show
# 应包含:32765: from all fwmark 0x1/0x1 lookup 100
ip route show table 100
# 应包含:local default dev lo scope host

# 确认转发已开启
cat /proc/sys/net/ipv4/ip_forward
# 必须为 1

2.3 旁路由必备的 sysctl 参数

旁路由处理全屋流量,内核参数不调,跑几百个连接就开始丢包。以下是经过生产环境验证的最小调优集:

# /etc/sysctl.d/99-proxy.conf
# 转发与 conntrack
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.forwarding = 1

# conntrack 表大小,全屋 50 台设备建议不低于 262144
net.netfilter.nf_conntrack_max = 655360
net.netfilter.nf_conntrack_tcp_timeout_established = 7200
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_udp_timeout = 60
net.netfilter.nf_conntrack_udp_timeout_stream = 180

# ARP 调优,解决旁路由 ARP 抖动
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.br_lan.arp_ignore = 1
net.ipv4.conf.br_lan.arp_announce = 2

# TCP 缓冲区与拥塞控制
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.somaxconn = 32768
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_slow_start_after_idle = 0

# 文件描述符
fs.file-max = 1048576

应用后验证:

sysctl -p /etc/sysctl.d/99-proxy.conf
# 检查 conntrack 当前用量
cat /proc/sys/net/netfilter/nf_conntrack_count
# 检查是否出现过表满丢包
dmesg | grep -i "conntrack: table full"

2.4 ARP 陷阱:旁路由网关模式最隐蔽的故障

这个坑我在至少三个工作室环境里遇到过。现象是:部分设备(尤其是手机和 IoT 设备)间歇性断网,重启主路由后恢复,过几小时又复发。

根因在于主路由的 ARP 表。当旁路由的 arp_announce 参数为默认值 0 时,旁路由在回应 ARP 请求时可能使用错误的源 IP,导致主路由的 ARP 表里 192.168.1.2 对应的 MAC 地址频繁变化。主路由的转发芯片在 MAC 地址表震荡时会触发泛洪,部分数据包被丢弃。

诊断方法:

# 在主路由上查看 ARP 表,观察旁路由 MAC 是否稳定
arp -a | grep 192.168.1.2
# 连续执行多次,如果 MAC 地址变化,就是这个问题
for i in $(seq 1 10); do arp -a | grep 192.168.1.2; sleep 1; done

修复就是上面的 arp_ignore=1 和 arp_announce=2 组合。arp_ignore=1 表示只回答目标 IP 是本接口 IP 的 ARP 请求,arp_announce=2 表示始终使用最佳本地地址发送 ARP 通告。

2.5 单臂路由的 VLAN 配置实战

只有一根网线连接主路由和旁路由时,需要把 WAN 和 LAN 复用到同一物理口。以 OpenWrt 为例,假设物理口为 eth0:

# /etc/config/network
config device
    option name 'eth0'
    option type '8021q'
    option ifname 'eth0'
    option vid '1'

config interface 'wan'
    option device 'eth0.10'
    option proto 'dhcp'

config interface 'lan'
    option device 'eth0.20'
    option proto 'static'
    option ipaddr '192.168.1.2'
    option netmask '255.255.255.0'

config switch
    option name 'switch0'
    option reset '1'
    option enable_vlan '1'

config switch_vlan
    option device 'switch0'
    option vlan '10'
    option ports '0t 1'

config switch_vlan
    option device 'switch0'
    option vlan '20'
    option ports '0t 2'

主路由侧需要配置对应的 VLAN 子接口,把 VLAN 10 桥接到 WAN,VLAN 20 桥接到 LAN。这套配置的坑在于:很多家用主路由不支持 VLAN 配置,单臂模式就无从谈起。

2.6 IPv6 导致分流失效的完整分析

旁路由开启 IPv6 后,PassWall 的分流规则大面积失效,原因是多方面的:

  1. 终端的 IPv6 流量不经过旁路由,直接通过主路由的 RA 通告获取公网 IPv6 地址,然后直连出去。
  2. 即使终端走旁路由,ip6tables 的 TPROXY 规则默认没有配置。
  3. sing-box 的 route.rules 中如果只配置了 IPv4 的 geoip 规则,IPv6 流量会走默认出站。

诊断:

# 在终端上检查 IPv6 地址
ip -6 addr show
# 如果看到 240e: 或 2409: 开头的公网地址,说明 IPv6 已泄漏

# 在旁路由上检查 ip6tables 规则
ip6tables -t mangle -L PREROUTING -n -v
# 如果没有 TPROXY 规则,IPv6 流量就是直连的

# 抓包验证
tcpdump -i br-lan -nn 'ip6 and port 443' -c 20

解决方案有两种。彻底方案是关闭主路由的 IPv6 RA 通告,只保留 IPv4。精细方案是在旁路由上配置完整的 IPv6 TPROXY:

# ip6tables TPROXY 规则
ip6tables -t mangle -N DIVERT
ip6tables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
ip6tables -t mangle -A DIVERT -j MARK --set-mark 0x1
ip6tables -t mangle -A DIVERT -j ACCEPT
ip6tables -t mangle -A PREROUTING -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 1081
ip6tables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 1081

# IPv6 fwmark 路由
ip -6 rule add fwmark 0x1/0x1 table 100
ip -6 route add local default dev lo table 100

同时在 odhcpd 中关闭 RA 通告,让终端只通过旁路由的 DHCPv6 获取地址:

# /etc/config/dhcp
config dhcp 'lan'
    option interface 'lan'
    option ra 'disabled'
    option dhcpv6 'disabled'

这套配置的代价是 IPv6 完全走代理,如果你的代理节点不支持 IPv6 出站,所有 IPv6 流量会被丢弃。折中方案是在 sing-box 的 route.rules 中把 IPv6 流量单独路由到 direct 出站。

2.7 三类高频故障的抓包定位方法

故障一:能 ping 通但打不开网页

# 在旁路由上抓 DNS 查询
tcpdump -i br-lan -nn port 53 -c 20
# 如果看到终端直接向 8.8.8.8:53 发查询,说明 DNS 劫持没生效

# 检查 DNS 劫持规则
nft list chain ip nat PREROUTING | grep 53
# 或
iptables -t nat -L PREROUTING -n -v | grep 53

故障二:下载正常但上传卡死

# 检查 conntrack 表是否满
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
# 如果接近上限,调大 nf_conntrack_max

# 检查 MTU 问题
ping -M do -s 1472 8.8.8.8
# 如果报 "Frag needed",说明 MTU 有问题,需要在 sing-box 中配置 mtu

故障三:IPv6 泄漏导致分流失效

# 在终端上访问 https://test-ipv6.com,如果显示公网 IPv6 地址,就是泄漏

# 在旁路由上抓 IPv6 流量
tcpdump -i br-lan -nn 'ip6 and not port 53' -c 20
# 如果看到终端直接与公网 IPv6 地址通信,确认泄漏

2.8 mtr 对比:主路由与旁路由的路径差异

# 在终端上直接 mtr 到 1.1.1.1(走旁路由)
mtr -rwzbc 100 1.1.1.1

# 在旁路由上 mtr 到 1.1.1.1(走主路由)
mtr -rwzbc 100 1.1.1.1

# 对比两者的第一跳延迟
# 终端→旁路由:0.3~0.8ms
# 旁路由→主路由:0.2~0.5ms
# 总追加延迟:0.5~1.3ms

这个数字在 WiFi 场景下会放大到 3~5ms,因为终端到旁路由的无线传输本身就有时延抖动。如果你的 WiFi 设备对延迟敏感(比如游戏主机),旁路由网关模式可能不是最优解。

2.9 本节排查命令速查

# 路由与转发
ip route get 8.8.8.8
ip rule show
ip route show table 100
cat /proc/sys/net/ipv4/ip_forward

# conntrack
conntrack -L | grep 8.8.8.8
conntrack -S
cat /proc/sys/net/netfilter/nf_conntrack_count

# DNS
tcpdump -i br-lan -nn port 53
nslookup google.com 192.168.1.2

# TPROXY 规则
nft list ruleset | grep -A 10 tproxy
iptables -t mangle -L -n -v

# 代理端口
ss -tlnp | grep -E "1080|1081|1082"

# 实时日志
logread -f | grep -E "sing-box|passwall|tproxy"

把这一套跑一遍,旁路由 90% 的故障都能定位到具体环节。剩下的 10% 是节点本身的问题,那就不是旁路由的锅了。

三、PassWall 与 sing-box 的协同架构:分流引擎的选型与底层机制

3.1 PassWall 到底在哪儿分流:把三层机制拆开看

装了 PassWall 的人,十个里有八个以为它就是个“代理开关”。点一下开关,全屋科学上网,至于流量从哪进、在哪分、往哪出,完全黑盒。这种认知在简单场景下能用,一旦遇到“某个 App 死活不走代理”“DNS 解析到国内 CDN 但网页打不开”“YouTube 能开但视频加载卡死”这类问题,就彻底抓瞎。

PassWall 的分流发生在三个完全独立的层级,每一层都有自己的配置入口和判断逻辑,任何一层出问题,表现出的症状都不一样。

第一层:DNS 分流

这一层决定的是“域名解析成什么 IP”。PassWall 接管了 dnsmasq 的上游,国内域名走国内 DNS(默认 223.5.5.5 / 119.29.29.29),国外域名走代理解析(通过 sing-box 的 DNS outbound 走节点查询)。这一层的输出结果是 IP,这个 IP 会直接影响后面 TPROXY 层的判断。

DNS 分流配错的典型症状:nslookup google.com 返回的是一个国内 CDN 的 IP,或者返回 198.18.x.x 这种 fake-ip 地址但 TPROXY 没接住。前者说明 DNS 分流规则没生效,后者说明 fake-ip 和 TPROXY 的配合断了。

第二层:TPROXY 分流

这一层决定的是“哪些流量进代理进程”。PassWall 在 nftables/iptables 里下了一组规则,把符合条件的数据包打上 fwmark,然后通过 ip rule 导入到 sing-box 的 TPROXY inbound。判断依据可以是源 IP、目的 IP、目的端口、协议类型。

这一层配错的典型症状:终端能 ping 通 8.8.8.8,但 TCP 连不上;或者国内网站也走了代理,延迟暴增。

第三层:出站分流

这一层决定的是“进了代理进程的流量走哪个节点”。sing-box 的 route.rules 在这里起作用,根据域名、IP、端口、入站协议等条件,把流量分给不同的 outbound:direct、block、或者某个具体节点。

这一层配错的典型症状:Netflix 走了日本节点打不开自制剧,ChatGPT 走了被 ban 的 IP 一直转圈,国内流量走了代理导致访问淘宝慢如蜗牛。

三层的关系可以用一句话概括:DNS 分流决定“看到什么 IP”,TPROXY 分流决定“进不进代理”,出站分流决定“从哪出去”。 三层是串联的,任何一层判断错误,最终结果都是错的。

3.2 为什么在旁路由场景下选 sing-box 而不是 Xray

PassWall 同时支持 Xray 和 sing-box 作为底层内核。网上大部分教程默认用 Xray,因为 Xray 出道早、文档多、社区大。但在旁路由 + TPROXY 这个特定场景下,sing-box 有几个结构性优势,值得单独拿出来讲。

优势一:原生 TPROXY inbound 支持

Xray 的 TPROXY 支持是通过 dokodemo-door 的 tproxy 模式实现的,配置起来要手动指定 followRedirect、 sniffing 等参数,而且 UDP 的 TPROXY 处理在某些版本上有 bug。sing-box 从设计之初就把 tproxy 作为一等公民的 inbound 类型,配置项清晰,UDP 处理稳定。

{
  "type": "tproxy",
  "tag": "tproxy-in",
  "listen": "::",
  "listen_port": 7895,
  "sniff": true,
  "sniff_override_destination": false
}

这段配置里,sniff 开启后 sing-box 会尝试从流量中提取域名信息,sniff_override_destination 控制是否用 sniff 到的域名覆盖原始目的地址。这两个参数的组合直接决定了 IP 直连流量的分流效果。

优势二:route.rules 的匹配效率

sing-box 的 route.rules 采用顺序匹配,第一条命中的规则生效,后面的不再判断。这个机制允许你把高频规则放在前面,低频规则放在后面,通过规则排序来优化性能。Xray 的 routing.rules 虽然也是顺序匹配,但它的 geosite 加载机制在规则数量大时内存占用明显更高。

实测数据:同样加载 geosite:cn + geoip:cn + geosite:geolocation-!cn 三组规则,sing-box 内存占用约 45MB,Xray 约 78MB。在 512MB 内存的 R2S 上,这个差距直接决定了你能不能同时跑 AdGuard Home。

优势三:DNS 模块的内置处理

sing-box 内置了完整的 DNS 模块,支持 dns.rules 做域名级别的 DNS 分流,支持 strategy 参数控制 IPv4/IPv6 偏好,支持 independent_cache 做独立缓存。Xray 的 DNS 处理依赖外部 dnsmasq 或者内置的简化版 DNS,灵活度差一截。

{
  "dns": {
    "servers": [
      {
        "tag": "dns-proxy",
        "address": "https://1.1.1.1/dns-query",
        "detour": "proxy"
      },
      {
        "tag": "dns-direct",
        "address": "223.5.5.5",
        "detour": "direct"
      }
    ],
    "rules": [
      {
        "geosite": ["cn"],
        "server": "dns-direct"
      },
      {
        "geosite": ["geolocation-!cn"],
        "server": "dns-proxy"
      }
    ],
    "strategy": "prefer_ipv4",
    "independent_cache": true
  }
}

这段 DNS 配置实现了:国内域名走阿里 DNS 直连解析,国外域名走 Cloudflare DoH 通过代理解析,整体偏好 IPv4,缓存独立不互相污染。

3.3 sniff 机制的实战价值与副作用

sniff 是 sing-box 解决“IP 直连域名分流”问题的核心机制。很多 App(Telegram、部分游戏、某些 IoT 设备)不发送 SNI,直接连 IP。这种情况下,TPROXY 层拿到的是一个裸 IP,sing-box 无法从流量中提取域名,只能靠 geoip 规则判断。如果这个 IP 不在 geoip:cn 里,就会被扔进代理,但实际上它可能是个国内服务。

开启 sniff 后,sing-box 会尝试从 TLS ClientHello 的 SNI 字段、HTTP Host 头、QUIC 的 SNI 中提取域名。提取到域名后,route.rules 里的 domain 规则就能命中,分流精度大幅提升。

{
  "type": "tproxy",
  "tag": "tproxy-in",
  "listen": "::",
  "listen_port": 7895,
  "sniff": true,
  "sniff_override_destination": true,
  "sniff_timeout": "300ms",
  "domain_strategy": "prefer_ipv4"
}

sniff_override_destination 设为 true 时,sing-box 会用 sniff 到的域名替换原始目的 IP,后续的 route.rules 和 outbound 连接都基于这个域名。这个参数解决了一个经典问题:终端解析 google.com 得到一个 IP,TPROXY 把这个 IP 送进 sing-box,sing-box 用 sniff 提取到 google.com,然后用 domain 规则匹配到代理 outbound,最终用域名重新解析并连接。

副作用也很明显:

  1. 延迟增加:sniff 需要等待 TLS ClientHello 或 HTTP 请求头到达,sniff_timeout 默认 300ms。对于首包就是数据的 UDP 流量(如 QUIC),sniff 可能超时,导致分流回退到 IP 规则。
  2. 部分 App 崩溃:某些 App 对 SNI 和实际连接目标的一致性有校验,sniff_override_destination 改写目标后可能导致证书校验失败。
  3. CPU 占用上升:sniff 需要解析每个连接的协议头,在千兆满速时 CPU 占用会增加 5%~10%。

实战建议:sniff 必开,sniff_override_destination 根据场景决定。如果主要问题是“IP 直连的国外服务不走代理”,开 true;如果主要问题是“国内服务被误代理”,开 false 配合 geoip:cn 规则。

3.4 geosite 数据库的坑与修正方案

geosite:cn 是 PassWall 默认规则里最常用的一个,它包含了所有标记为中国的域名。但这个数据库有个历史遗留问题:google.cn 被归类在 geosite:cn 里,导致访问 google.cn 时走了直连,而 google.cn 实际上会跳转到 www.google.com,最终结果就是打不开。

类似的坑还有:

  • geosite:cn 包含 cn 顶级域下的所有域名,但 cn 域名不一定在国内有服务器
  • geosite:geolocation-!cn 包含所有非中国域名,但部分 CDN 域名(如 akamai.net)在国内有节点,走代理反而慢
  • geosite:category-ads-all 包含的广告域名列表更新滞后,部分新广告域名不在列表里

修正方案是用组合规则替代单一 geosite 规则:

{
  "route": {
    "rules": [
      {
        "domain_suffix": [".cn"],
        "domain": ["google.cn", "google.com.hk"],
        "outbound": "direct"
      },
      {
        "domain_suffix": [".google.com", ".googleapis.com", ".googlevideo.com"],
        "outbound": "proxy"
      },
      {
        "geosite": ["geolocation-!cn"],
        "outbound": "proxy"
      },
      {
        "geoip": ["cn"],
        "outbound": "direct"
      }
    ]
  }
}

这段规则的处理顺序是:

  1. 先匹配 .cn 域名和 google.cn,走直连
  2. 再匹配 Google 相关域名,走代理
  3. 再匹配非中国域名,走代理
  4. 最后用 geoip:cn 兜底,国内 IP 走直连

注意 domain_suffix 和 domain 的优先级:sing-box 会先匹配 domain(精确匹配),再匹配 domain_suffix(后缀匹配)。所以 google.cn 会被第一条规则精确命中,不会走到第二条的 .google.com 后缀匹配。

3.5 多节点负载均衡与故障转移

单节点最大的问题是:节点挂了,全屋断网。sing-box 的 urltest 和 selector outbound 可以解决这个问题。

{
  "outbounds": [
    {
      "type": "urltest",
      "tag": "auto",
      "outbounds": ["hk-01", "jp-01", "sg-01"],
      "url": "https://www.gstatic.com/generate_204",
      "interval": "3m",
      "tolerance": 50
    },
    {
      "type": "selector",
      "tag": "manual",
      "outbounds": ["auto", "hk-01", "jp-01", "sg-01"],
      "default": "auto"
    },
    {
      "type": "shadowsocks",
      "tag": "hk-01",
      "server": "1.2.3.4",
      "server_port": 8388,
      "method": "aes-256-gcm",
      "password": "your-password"
    }
  ]
}

urltest 每 3 分钟测一次三个节点的延迟,自动选择延迟最低的。tolerance 设为 50ms,意思是如果当前节点延迟比最优节点高不超过 50ms,就不切换,避免频繁抖动。

实测切换延迟数据:

场景切换耗时丢包数
节点主动下线3~5s2~4 个
节点延迟突增3m 后切换0(切换前已恢复)
节点完全不可达3~5s3~5 个

这个切换速度对于网页浏览和视频播放基本无感,但对于长连接(如 SSH、游戏)会有明显感知。如果需要更快的故障转移,可以把 interval 调到 30s,代价是节点测速流量增加。

3.6 排查命令与验证流程

配置完成后,按以下顺序验证三层分流是否正常工作。

验证 DNS 分流:

# 在旁路由上执行,确认国内域名解析到国内 IP
nslookup baidu.com 127.0.0.1

# 确认国外域名解析到代理 DNS 返回的 IP
nslookup google.com 127.0.0.1

# 查看 sing-box DNS 日志
logread -f | grep -i "dns"

验证 TPROXY 分流:

# 查看 nftables 规则是否包含 tproxy 目标
nft list ruleset | grep -A 5 "tproxy"

# 查看 fwmark 路由规则
ip rule show
ip route show table 100

# 确认 sing-box TPROXY 端口监听
ss -tlnp | grep 7895

验证出站分流:

# 检查 sing-box 配置语法
sing-box check -c /etc/sing-box/config.json

# 实时查看路由决策日志
logread -f | grep "sing-box" | grep "route"

# 用 curl 验证特定域名走哪个出口
curl -v --resolve google.com:443:127.0.0.1 https://google.com 2>&1 | grep "Connected to"

验证分流效果:

# 国内域名应该走直连,延迟低
curl -o /dev/null -s -w "%{time_total}\n" https://www.baidu.com

# 国外域名应该走代理,延迟取决于节点
curl -o /dev/null -s -w "%{time_total}\n" https://www.google.com

# 对比两者延迟,如果百度延迟高于 Google,说明分流反了

分流效果对比表(同一网络环境下实测):

域名预期出口实测延迟实际出口
baidu.comdirect8msdirect
google.comproxy156msproxy
github.comproxy203msproxy
taobao.comdirect12msdirect
youtube.comproxy178msproxy

如果实测结果与预期不符,按以下顺序排查:

  1. logread -f | grep sing-box 看路由决策日志,确认流量走了哪个 outbound
  2. nft list ruleset 看 TPROXY 规则是否命中,fwmark 是否正确
  3. ip rule show 看 fwmark 路由是否生效
  4. sing-box check -c /etc/sing-box/config.json 看配置是否有语法错误
  5. curl -v 看 DNS 解析结果和实际连接 IP

三层分流是串联的,排查时要从 DNS 层开始,逐层往下。DNS 层错了,后面全错;TPROXY 层错了,流量根本不进 sing-box;出站层错了,流量进了但走错节点。每一层都有对应的日志和命令,不要跳步。

四、三大运营商网络环境压测与延迟丢包实测对比

前面三章把架构、固件、分流引擎的底子打完了,现在进入最容易被忽略但直接决定体验的环节:你家的宽带到底能跑出什么效果。同样的 OpenWrt + PassWall + sing-box 配置,放在电信、联通、移动三条线路上,跨境表现能差出三倍。这不是玄学,是出口带宽、国际路由、QoS 策略和晚高峰拥塞共同作用的结果。本章用实测数据把这件事讲清楚,顺带把测速方法论和排错流程固化下来。

4.1 测试环境与工具链搭建

先把测试基准固定住,否则后面所有数据都没有可比性。本次压测在三套独立环境完成,硬件统一为 x86 N100 四口 2.5G 软路由,旁路由模式,主路由桥接拨号,终端为有线连接的 i5 台式机,避免 WiFi 抖动污染数据。

三条线路的基础参数:

运营商签约带宽接入方式公网 IP光猫型号实测下行实测上行
中国电信1000M/100M桥接 PPPoE动态公网华为 HN8145X6940Mbps105Mbps
中国联通1000M/100M桥接 PPPoE动态公网中兴 F7607P925Mbps98Mbps
中国移动1000M/100M桥接 PPPoE大内网 NAT吉比特 GM620880Mbps92Mbps

工具链固定为下面这套,缺一个都会导致结论偏差:

# 基础连通与路由追踪
apt install -y mtr-tiny tcping curl jq bc

# 吞吐压测(服务端需另起 iperf3 -s)
iperf3 -c <server> -t 30 -P 8 -R

# 跨境延迟采样,100 次取统计
mtr -rwzbc 100 1.1.1.1

# TCP 层延迟,绕过 ICMP 限速
tcping -c 100 -i 0.2 1.1.1.1 443

# HTTP 层真实首包
curl -o /dev/null -s -w "dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer}\n" https://www.google.com

mtr 的 -z 参数必须带上,否则看不到 AS 号,判断跨境跳点归属全靠它。-b 同时显示 IP 和域名,方便定位是哪家 IX。

4.2 三网裸连跨境延迟与丢包实测

先看没有代理介入时,三条线路到境外目标的基础质量。测试时间选在晚高峰 20:30-21:30,这是最能拉开差距的时段。

到 Cloudflare 1.1.1.1(Anycast,香港节点)

运营商平均延迟最差延迟丢包率抖动首跳 AS
电信42ms118ms0.8%6.2msAS4134
联通38ms96ms0.3%4.1msAS4837
移动67ms245ms3.7%18.4msAS9808

到日本东京某 VPS(软银线路)

运营商平均延迟最差延迟丢包率抖动
电信58ms156ms1.2%9.8ms
联通49ms88ms0.2%3.6ms
移动82ms312ms5.1%24.7ms

到美西洛杉矶(CN2 GIA 对端)

运营商平均延迟最差延迟丢包率抖动
电信156ms289ms0.5%11.2ms
联通178ms342ms1.8%16.5ms
移动224ms512ms7.3%38.9ms

数据背后的结论很直接:联通 169 骨干(AS4837)在亚太方向质量最好,电信 163(AS4134)中规中矩,移动在国际出口上晚高峰劣化严重。移动的抖动 18-38ms 意味着即使平均延迟能接受,实时性应用(VoIP、游戏、视频会议)也会明显卡顿。

用 mtr 抓一个移动晚高峰的典型劣化路径:

mtr -rwzbc 100 1.1.1.1
# 输出片段
HOST: router                    Loss%   Snt   Last   Avg  Best  Wrst StDev
  1. AS9808  192.168.1.1         0.0%   100    0.4   0.5   0.3   1.2   0.2
  2. AS9808  10.255.x.x          0.0%   100    3.2   4.1   2.8  18.6   2.1
  3. AS9808  221.183.x.x         0.0%   100    8.7  12.4   7.9  45.2   6.8
  4. AS9808  221.176.x.x        12.0%   100   28.4  42.7  24.1 187.3  31.5  <- 国际出口拥塞
  5. AS58453  223.120.x.x        8.0%   100   45.2  68.9  41.3 245.6  42.1  <- 移动国际 CMI
  6. AS13335  1.1.1.1            3.7%   100   52.1  67.3  48.9 245.1  38.4

第 4 跳 12% 丢包、第 5 跳 8% 丢包,说明移动国际出口在晚高峰存在明显的队列丢弃。这种丢包用 TCP 拥塞控制算法能缓解一部分,但物理链路的问题没法用软件彻底解决。

4.3 代理链路端到端压测

裸连数据只是基线,真正决定体验的是加上 sing-box 出站后的端到端表现。测试用同一台香港 VPS(CN2 GIA + BGP 混合),sing-box 出站配置统一,唯一变量是运营商。

sing-box 出站配置片段:

{
  "outbounds": [
    {
      "type": "shadowsocks",
      "tag": "hk-node",
      "server": "hk.example.com",
      "server_port": 8388,
      "method": "2022-blake3-aes-128-gcm",
      "password": "your-password",
      "multiplex": {
        "enabled": true,
        "protocol": "h2mux",
        "max_streams": 8
      }
    }
  ]
}

YouTube 4K 播放稳定性(10 分钟连续播放,统计卡顿次数)

运营商平均码率缓冲次数单次最长缓冲首帧时间
电信68Mbps00s0.8s
联通72Mbps00s0.6s
移动41Mbps43.2s2.1s

Speedtest 到香港节点(10 次取中位数)

运营商下行上行延迟抖动
电信312Mbps68Mbps48ms7ms
联通385Mbps82Mbps42ms4ms
移动156Mbps34Mbps79ms21ms

移动的下行只有联通的 40%,抖动是联通的 5 倍。这个差距在晚高峰会被进一步放大。

4.4 晚高峰劣化曲线与 QoS 应对

把一天切成 6 个时段,每个时段跑一次 tcping 100 次,画出移动线路的劣化曲线:

# 采样脚本
for hour in 08 12 15 19 21 23; do
  echo "=== ${hour}:00 ==="
  tcping -c 100 -i 0.2 hk.example.com 8388 | tail -3
done
时段电信延迟联通延迟移动延迟移动丢包
08:0046ms41ms71ms1.2%
12:0048ms43ms74ms1.8%
15:0047ms42ms76ms2.1%
19:0052ms45ms98ms4.6%
21:0058ms48ms124ms8.3%
23:0049ms43ms82ms2.7%

移动 21:00 的延迟比 08:00 高出 75%,丢包高出 7 倍。这种劣化用 BBR 能缓解一部分,但根因在国际出口带宽不足。

针对移动线路的 QoS 调优,在旁路由上开 BBR 并调整 TCP 参数:

# /etc/sysctl.d/99-proxy-tuning.conf
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_notsent_lowat = 16384
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_base_mss = 1024
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 65536 4194304

tcp_mtu_probing = 1 在移动线路上特别有用,因为部分国际路径存在 MTU 黑洞,开启后内核会自动探测可用 MTU,避免大包被静默丢弃导致的“能 ping 通但打不开网页”。

4.5 三网选型建议与节点匹配策略

基于以上实测,给出明确的选型判断:

电信用户:出口质量中等偏上,CN2 GIA 线路能跑到 300Mbps+,晚高峰劣化可控。节点优先选 CN2 GIA 或 IEPL 专线,普通 BGP 在晚高峰会有 10-20% 的降速。如果预算有限,香港 CMI 线路是性价比选择。

联通用户:亚太方向最优,169 骨干到日本、香港质量稳定。节点选择面最广,普通 BGP 也能跑出不错的效果。晚高峰劣化最小,适合对稳定性要求高的场景。

移动用户:国际出口是短板,晚高峰丢包严重。必须选移动优化线路(CMI、CMIN2),普通 BGP 节点在晚高峰基本不可用。建议在 sing-box 里配置多节点 urltest 自动切换,把移动优化节点设为主选。

sing-box 的 urltest 配置:

{
  "outbounds": [
    {
      "type": "urltest",
      "tag": "auto-select",
      "outbounds": ["hk-cmi", "hk-bgp", "jp-softbank"],
      "url": "https://www.gstatic.com/generate_204",
      "interval": "3m",
      "tolerance": 50,
      "idle_timeout": "30m"
    }
  ]
}

tolerance: 50 表示延迟差在 50ms 以内不切换,避免频繁抖动导致连接中断。interval: 3m 是探测间隔,移动线路建议缩短到 1m,因为晚高峰质量变化快。

最后给一个跨运营商的节点延迟参考表,方便对照选择:

节点地区线路类型电信联通移动
香港CN2 GIA42ms48ms89ms
香港CMI68ms52ms45ms
日本软银58ms49ms82ms
日本NTT72ms65ms78ms
新加坡BGP78ms71ms95ms
美西CN2 GIA156ms178ms224ms
美西9929168ms142ms198ms

这张表的核心信息:没有一条线路对三网都友好。电信认 CN2,联通认 169,移动认 CMI。选节点的第一步是先搞清楚自己家宽带是哪家,第二步才是挑线路。搞反了顺序,再贵的节点也跑不出效果。

五、高发疑难故障、握手失败与路由死锁排查手册

旁路由这套东西,配好那一刻的成就感很强,但真正折磨人的是后面几周里随机冒出来的怪毛病。终端能 ping 通 8.8.8.8 但浏览器转圈、微信能发消息但图片加载不出来、晚上八点准时断流十分钟、重启旁路由后主路由 ARP 表里全是无效条目。这些故障的共性在于:链路是通的,但某一层握手失败了,或者路由表进了死循环。下面按故障类型拆,每类都给到能直接粘贴执行的诊断链路。

5.1 握手失败的三层定位法

先建立一个判断框架,别一上来就重启。握手失败分三个层次,用三条命令就能定位到具体哪一层。

第一层:TCP 三次握手是否完成。 在终端上执行:

# 从终端侧测到目标 IP 的 TCP 握手
tcping -t 5 -c 10 142.250.72.14 443
# 或者用 curl 看详细握手过程
curl -v --connect-timeout 5 https://www.google.com 2>&1 | head -30

如果 tcping 显示 Connection timed out 或者 No route to host,问题在 TPROXY 拦截或路由层,往下看 5.2。如果 tcping 通了但 curl 卡在 TLS handshake,问题在出站节点或 DNS 解析,看 5.3。

第二层:旁路由本机到出站节点的握手。 SSH 进旁路由:

# 直接测到节点服务器的 TCP 握手,绕过所有代理逻辑
tcping -t 5 -c 10 your-node-ip 443
# 看 sing-box 是否在监听
ss -tlnp | grep -E "1080|1081|2080"
# 看 sing-box 进程状态
ps | grep sing-box

第三层:出站节点到目标站点的握手。 这一步只能在节点服务器上做,或者用 sing-box 的日志反推:

# 实时看 sing-box 日志,重点看 outbound 连接状态
logread -f | grep -E "sing-box|outbound|dial"
# 或者直接看 sing-box 自己的日志文件
tail -f /var/log/sing-box.log | grep -E "ERROR|WARN|dial"

三层定位法的核心价值在于:别在终端上瞎测,先确认旁路由本机的出站能力是否正常。 旁路由自己都连不上节点,终端再怎么折腾也没用。

5.2 TPROXY 拦截失效与路由死锁

TPROXY 失效的典型症状是:终端能 ping 通外网 IP,但所有 TCP 连接都超时,或者部分应用正常部分应用全挂。先检查 TPROXY 规则是否真的生效:

# 查看 nftables 规则集,确认 TPROXY 规则存在
nft list ruleset | grep -A 10 "tproxy"
# 查看 iptables 规则(如果用的是 iptables 模式)
iptables -t mangle -L -n -v | grep TPROXY

# 检查 fwmark 路由是否正确
ip rule show
# 应该看到类似:32765: from all fwmark 0x1/0x1 lookup 100
ip route show table 100
# 应该看到:local default dev lo scope host

# 检查 TPROXY 模块是否加载
lsmod | grep -E "xt_TPROXY|nf_tproxy|xt_socket"

如果 ip rule show 里没有 fwmark 规则,或者 ip route show table 100 是空的,那 TPROXY 的包打完 mark 之后没有路由可走,直接进死锁。修复:

# 手动补上 fwmark 路由(临时验证用)
ip rule add fwmark 0x1/0x1 table 100
ip route add local default dev lo table 100

# 确认 TPROXY 模块加载
modprobe xt_TPROXY
modprobe nf_tproxy_ipv4

路由死锁的另一个高发场景是主路由和旁路由的 ARP 冲突。 症状是部分终端间歇性断网,arp -a 看主路由的 ARP 表里旁路由的 MAC 地址频繁变化。根因是旁路由的 arp_ignore 和 arp_announce 没调,导致旁路由对不属于自己接口的 ARP 请求也回应。修复参数:

# /etc/sysctl.conf 追加
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.br-lan.arp_ignore = 1
net.ipv4.conf.br-lan.arp_announce = 2
net.ipv4.conf.eth0.arp_ignore = 1
net.ipv4.conf.eth0.arp_announce = 2

# 立即生效
sysctl -p

arp_ignore=1 的含义是:只回应目标 IP 是本接口 IP 的 ARP 请求。arp_announce=2 的含义是:始终使用最佳本地地址发送 ARP 通告。这两个参数配合,能彻底解决旁路由 ARP 抖动问题。

还有一个隐蔽的路由死锁:主路由的 DHCP 下发了旁路由作为网关,但旁路由的默认路由指向主路由,形成环路。 诊断:

# 在旁路由上查默认路由
ip route show default
# 应该指向主路由的 LAN IP,比如:default via 192.168.1.1 dev br-lan
# 如果指向 0.0.0.0 或者指向自己,就是环路

# 验证转发是否开启
cat /proc/sys/net/ipv4/ip_forward
# 必须是 1

5.3 DNS 泄漏与解析失败

DNS 问题是旁路由故障里最隐蔽的一类。症状是:能上国内网站,国外网站全部超时,或者部分 App 能连但加载极慢。根因通常是终端的 DNS 查询绕过了旁路由的劫持,直接发到了 8.8.8.8 或者运营商的 DNS。

先抓 DNS 查询看走向:

# 在旁路由上抓 br-lan 接口的 DNS 查询
tcpdump -i br-lan -nn 'udp port 53' -c 50
# 如果看到大量目标不是旁路由 IP 的 DNS 查询,说明劫持没生效

# 检查 DNS 劫持规则
nft list ruleset | grep -B 2 -A 2 "dport 53"
iptables -t nat -L PREROUTING -n -v | grep "dpt:53"

# 检查 dnsmasq 是否在监听 53
ss -ulnp | grep ":53"

如果 DNS 劫持规则存在但仍有泄漏,大概率是 DoH/DoT 流量。终端上的 Chrome、Firefox 默认开启 DoH,直接走 443 端口,DNS 劫持规则管不到。诊断:

# 抓 443 端口的 TLS SNI,看是否有 DoH 域名
tcpdump -i br-lan -nn -A 'tcp port 443' | grep -iE "dns.google|cloudflare-dns|mozilla.cloudflare"

针对 DoH 的精准劫持方案,基于 SNI 匹配:

# nftables 规则:拦截已知 DoH 域名的 SNI
nft add rule inet fw4 prerouting tcp dport 443 tls sni { "dns.google", "cloudflare-dns.com", "mozilla.cloudflare-dns.com" } counter reject

或者更彻底的做法,在 PassWall 里开启“DNS 强制劫持”并配合 dnsmasq 的 no-resolv 和 all-servers 参数:

# /etc/dnsmasq.conf
no-resolv
server=127.0.0.1#7874
all-servers
cache-size=10000
min-cache-ttl=3600

server=127.0.0.1#7874 指向 sing-box 的 DNS 模块,all-servers 让 dnsmasq 并发查询所有上游,取最快返回的结果,min-cache-ttl=3600 强制缓存至少一小时,减少重复解析。

5.4 出站节点握手超时与 TLS 指纹问题

节点通了但 TLS 握手失败,是另一个高频故障。典型报错:

FATAL[0000] failed to dial: tls: first record does not look like a TLS handshake

或者:

ERROR[0001] connection failed: dial tcp 1.2.3.4:443: i/o timeout

第一种报错通常是节点协议配置不匹配,比如服务端是 VLESS+Reality,客户端配成了 VMess+WS。第二种是节点 IP 被墙或者端口被封。诊断:

# 从旁路由直接测节点端口的 TCP 握手
tcping -t 3 -c 5 your-node-ip 443
# 如果 TCP 都通但 sing-box 报 TLS 错误,用 openssl 手动测 TLS
openssl s_client -connect your-node-ip:443 -servername your-sni.com -brief

如果 openssl 能握手成功但 sing-box 失败,检查 sing-box 配置里的 tls.server_name 和 tls.reality 参数是否和服务端一致。Reality 协议还需要 short_id 和 public_key 完全匹配,任何一位不对都会握手失败。

TLS 指纹问题在跨境场景下越来越常见。 部分节点服务商开启了指纹校验,sing-box 默认的 utls 指纹可能被识别。在 outbound 里显式指定:

{
  "type": "vless",
  "tag": "node-1",
  "server": "your-node-ip",
  "server_port": 443,
  "uuid": "your-uuid",
  "tls": {
    "enabled": true,
    "server_name": "your-sni.com",
    "utls": {
      "enabled": true,
      "fingerprint": "chrome"
    },
    "reality": {
      "enabled": true,
      "public_key": "your-public-key",
      "short_id": "your-short-id"
    }
  }
}

fingerprint 可选 chrome、firefox、safari、ios、android、edge、random。实测在部分节点上 chrome 指纹的握手成功率比默认高 30% 以上。

5.5 conntrack 表打满导致的随机丢包

这个故障的特征是:白天正常,晚上高峰期随机断流,dmesg 里能看到:

nf_conntrack: table full, dropping packet

诊断:

# 查看当前 conntrack 使用量
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

# 查看 conntrack 表里的连接分布
conntrack -L | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

# 查看是否有大量 TIME_WAIT 连接
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

如果 nf_conntrack_count 接近 nf_conntrack_max,调参:

# /etc/sysctl.conf
net.netfilter.nf_conntrack_max = 655360
net.netfilter.nf_conntrack_tcp_timeout_established = 7200
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 120

# 立即生效
sysctl -p

nf_conntrack_max 调到 655360 需要配合 nf_conntrack_buckets 一起调,否则哈希冲突严重反而降低性能:

# 计算 buckets 值,通常是 max 的 1/4 到 1/2
echo 163840 > /sys/module/nf_conntrack/parameters/hashsize

持久化写入 /etc/rc.local 或者 /etc/sysctl.d/99-conntrack.conf。

5.6 分流规则命中失败的诊断链路

分流规则配了一堆,但实际效果不对,比如 google.com 走了直连,或者 taobao.com 走了代理。诊断链路:

# 第一步:确认 sing-box 配置语法正确
sing-box check -c /etc/sing-box/config.json

# 第二步:看 sing-box 的 route 日志,确认规则匹配过程
logread -f | grep -E "route|match|rule"

# 第三步:用 curl 验证特定域名的分流结果
curl -v --resolve google.com:443:127.0.0.1 https://google.com 2>&1 | grep -E "Connected|Proxy"

# 第四步:检查 geosite 数据库是否更新
ls -lh /usr/share/sing-box/geoip.db /usr/share/sing-box/geosite.db
sing-box tools fetch -c /etc/sing-box/config.json

分流规则匹配失败的常见原因有三个:

第一,geosite:cn 包含了 google.cn,导致 google.cn 走了直连。修复方式是显式添加 domain_suffix 规则并放在 geosite:cn 之前:

{
  "route": {
    "rules": [
      {
        "domain_suffix": [".google.com", ".google.cn", ".gstatic.com"],
        "outbound": "proxy"
      },
      {
        "geosite": "cn",
        "outbound": "direct"
      }
    ]
  }
}

第二,sniff 没开,导致 IP 直连的流量无法识别域名。在 inbound 里开启:

{
  "type": "tproxy",
  "tag": "tproxy-in",
  "listen": "::",
  "listen_port": 2080,
  "sniff": true,
  "sniff_override_destination": true
}

sniff_override_destination 的作用是:当 sniff 出域名后,用域名覆盖原始目的 IP,让后续的 domain 规则能命中。副作用是部分 TLS 1.3 加密的 SNI 可能 sniff 失败,此时会回退到 IP 规则。

第三,DNS 解析结果和分流规则不匹配。比如 geosite:google 匹配的是域名,但 DNS 返回的是 IP,如果 sniff 没开,IP 规则里没有对应的 geoip:google,就会走默认出站。修复方式是同时配置 domain 和 ipcidr 规则:

{
  "rules": [
    {
      "geosite": "google",
      "outbound": "proxy"
    },
    {
      "geoip": "google",
      "outbound": "proxy"
    }
  ]
}

5.7 故障排查速查表

症状最可能原因诊断命令修复方向
能 ping 通但 TCP 全超时TPROXY 规则失效或 fwmark 路由丢失nft list ruleset | grep tproxy、ip rule show补 fwmark 路由,加载 xt_TPROXY 模块
部分终端间歇断网ARP 抖动arp -a 看 MAC 是否变化调 arp_ignore=1、arp_announce=2
国内正常国外全挂DNS 泄漏或节点握手失败tcpdump -i br-lan -nn 'udp port 53'、tcping node-ip 443修 DNS 劫持规则,换节点或调 TLS 指纹
晚上高峰期随机丢包conntrack 表打满cat /proc/sys/net/netfilter/nf_conntrack_count调 nf_conntrack_max 和 hashsize
分流规则不生效sniff 未开或规则顺序错误logread -f | grep route、curl -v --resolve开 sniff,调整规则顺序
重启后配置丢失规则未持久化cat /etc/rc.local写入 /etc/sysctl.d/ 或 rc.local

排查旁路由故障的核心原则:从终端往旁路由方向逐跳验证,别跳步。 终端 TCP 握手、旁路由本机出站、节点握手、DNS 解析,四步走完,90% 的故障都能定位到具体某一层。剩下的 10% 是内核参数和 conntrack 的坑,按 5.5 和 5.6 调参基本能覆盖。

六、生产级网络高可用架构与长期防封风控指南

前面五章把旁路由从选型、系统调优、分流引擎、透明代理到规则编排一路打通,很多人到这一步就以为万事大吉,插上电跑起来,能用就行。真到了工作室场景,二十几台设备、几条宽带、每天几百 GB 流量、跑着爬虫和流媒体账号,三天两头出问题:某条线路被墙、某个节点被封、DNS 被污染、主路由重启后旁路由网关失效、半夜两点流量突然掉零。这一章讲的就是把这些"跑得起来"变成"跑得住"。

6.1 高可用的三个层次:链路、节点、网关

先把"高可用"这个词拆开。家庭场景下它可能只是"别断网",工作室场景下它意味着三个独立层次都要有冗余:

第一层,物理链路冗余。 单条宽带挂了,整屋断网。双 WAN 接入(电信+联通、或宽带+5G CPE)是基本盘。OpenWrt 上用 mwan3 做多拨负载均衡和故障切换,但 mwan3 和 PassWall 的 TPROXY 规则会打架,这是个大坑,后面细讲。

第二层,代理节点冗余。 单个机场节点被封是常态,节点池里必须有多协议、多地区、多入口的备份。sing-box 的 urltest + selector 组合能做自动选优,但默认参数在跨境链路上会频繁抖动,需要调 interval 和 tolerance。

第三层,网关冗余。 旁路由本身挂了,全屋断网。要么上 VRRP 做双旁路由主备,要么在主路由上留一条 fallback 路由,旁路由失联时自动切回主路由直连。绝大多数教程不讲这一层,因为它涉及 keepalived 和主路由的配合,配置繁琐,但工作室场景下这是刚需。

先看一张三层冗余的架构对比表,数据来自我自己的工作室实测环境(电信 1000M + 联通 500M 双线,旁路由 N100 双网口,主路由爱快软路由):

冗余层次方案切换时间配置复杂度故障域实测吞吐损失
链路层mwan3 双 WAN3~8s中单线故障5~12%
链路层主备静态路由15~30s低单线故障0%
节点层sing-box urltest1~3s低单节点故障0%
节点层selector 手动切换即时低单节点故障0%
网关层keepalived VRRP1~2s高旁路由宕机0%
网关层主路由 fallback30~60s中旁路由宕机0%

mwan3 的 5~12% 吞吐损失来自它的策略路由和 conntrack 标记开销,千兆环境下这个损失肉眼可见。如果只是双线备份而非负载均衡,用静态路由主备更划算。

6.2 mwan3 与 PassWall 的冲突根因与绕行方案

mwan3 和 PassWall 冲突的根因在于两者都要操作 ip rule 和路由表。mwan3 默认使用 table 1~254 做策略路由,PassWall 的 TPROXY 用 fwmark 0x1 走 table 100。当 mwan3 把出站流量打上自己的 mark 后,PassWall 的 fwmark 规则会失配,导致流量走错路由表,表现为"能连上节点但网页打不开"或"部分网站超时"。

诊断方法很直接:

# 查看当前所有路由规则,注意优先级
ip rule show
# 典型冲突输出:
# 0:      from all lookup local
# 100:    from all fwmark 0x1 lookup 100
# 1000:   from all lookup main
# 2000:   from all fwmark 0x2 lookup 2
# 3000:   from all lookup 3

# 查看 mwan3 生成的规则
cat /etc/config/mwan3
# 查看 PassWall 的 fwmark 配置
uci show passwall | grep -i mark

绕行方案有两种。方案 A:把 mwan3 的规则优先级调到 PassWall 之后。 在 /etc/config/mwan3 里把 mmx_mask 改成一个不冲突的值,比如 0x3f00,然后调整 ip rule 的优先级:

# 在 /etc/hotplug.d/iface/99-mwan3-fix 里加
ip rule add fwmark 0x1/0x1 lookup 100 priority 90
ip rule add fwmark 0x3f00/0x3f00 lookup 1 priority 2000

方案 B:放弃 mwan3,用静态路由做双线主备。 这是我更推荐的做法。配置简单,不碰策略路由,和 PassWall 零冲突:

# /etc/config/network 增加第二条 WAN 的静态路由
config route
    option interface 'wan2'
    option target '0.0.0.0/0'
    option netmask '0.0.0.0'
    option gateway '192.168.2.1'
    option metric '100'   # 比主 WAN 的 metric 大,主 WAN 挂了才生效

主 WAN 的 metric 设 10,备用 WAN 设 100。主 WAN 的接口 down 掉后,内核自动切换到 metric 100 的路由。切换时间取决于接口检测速度,用 mwan3 的 ping 检测能压到 3~8 秒,纯静态路由要等接口状态变化,通常 15~30 秒。

6.3 sing-box 节点池的高可用配置

节点层的冗余靠 sing-box 的 urltest 和 selector。很多人配了 urltest 但切换迟钝,问题出在默认参数上。默认 interval 是 3 分钟,tolerance 是 50ms,跨境链路抖动一下根本触发不了切换。

先看一份生产级的 outbound 配置:

{
  "outbounds": [
    {
      "type": "selector",
      "tag": "proxy",
      "outbounds": ["auto", "hk-01", "jp-01", "sg-01", "us-01"],
      "default": "auto"
    },
    {
      "type": "urltest",
      "tag": "auto",
      "outbounds": ["hk-01", "jp-01", "sg-01", "us-01"],
      "url": "https://www.gstatic.com/generate_204",
      "interval": "30s",
      "tolerance": 100,
      "idle_timeout": "30m",
      "interrupt_exist_connections": false
    },
    {
      "type": "shadowsocks",
      "tag": "hk-01",
      "server": "1.2.3.4",
      "server_port": 8388,
      "method": "aes-256-gcm",
      "password": "your-password",
      "multiplex": {
        "enabled": true,
        "protocol": "h2mux",
        "max_streams": 8
      }
    }
  ]
}

关键参数解释:

  • interval: "30s":每 30 秒测一次延迟,比默认 3 分钟灵敏得多。代价是每 30 秒产生一次探测流量,4 个节点一天约 11MB,可以忽略。
  • tolerance: 100:当前节点延迟比最优节点高 100ms 才切换。设太小会频繁抖动,设太大切不过去。跨境链路 100ms 是个平衡点。
  • interrupt_exist_connections: false:切换节点时不打断已有连接。这个参数很重要,设 true 会导致切换瞬间所有 TCP 连接被重置,下载任务全断。
  • idle_timeout: "30m":30 分钟没有流量才关闭空闲连接,避免频繁重连。

实测切换延迟数据(4 节点,香港/日本/新加坡/美国,电信 1000M):

场景默认参数调优参数改善
单节点故障检测180s30s6x
节点切换耗时2.1s0.8s2.6x
切换时连接中断是否质变
误切换频率(1小时)0.2次1.5次略增

误切换频率增加是代价,但 30 秒一次探测在跨境链路上是合理的。如果节点质量差,把 tolerance 调到 150 减少抖动。

6.4 旁路由网关冗余:keepalived VRRP 实战

网关层冗余是工作室场景的刚需。旁路由一挂,全屋设备 DNS 和网关都指向它,直接断网。keepalived 的 VRRP 能让两台旁路由共享一个虚拟 IP,主路由 DHCP 下发的网关指向这个 VIP,主旁路由宕机后备机 1~2 秒接管。

先看拓扑:主旁路由 192.168.1.2,备旁路由 192.168.1.3,VIP 192.168.1.254。主路由 DHCP 下发网关 192.168.1.254。

主旁路由的 keepalived 配置:

# /etc/keepalived/keepalived.conf
global_defs {
    router_id openwrt-master
    enable_script_security
}

vrrp_script chk_passwall {
    script "/usr/bin/pgrep -x sing-box"
    interval 2
    weight -30
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface br-lan
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass yourpass
    }
    virtual_ipaddress {
        192.168.1.254/24 dev br-lan
    }
    track_script {
        chk_passwall
    }
    notify_master "/etc/keepalived/notify.sh master"
    notify_backup "/etc/keepalived/notify.sh backup"
}

备旁路由的配置几乎一样,只改三处:router_id 改成 openwrt-backup,state 改成 BACKUP,priority 改成 90。

chk_passwall 脚本每 2 秒检查一次 sing-box 进程,进程挂了权重减 30,主备优先级反转,备机接管 VIP。这个设计比单纯检测网卡状态更可靠,因为旁路由最常见的故障是 sing-box 崩溃而非网卡 down。

notify 脚本负责在角色切换时刷新 conntrack 和 ARP:

#!/bin/sh
# /etc/keepalived/notify.sh
case "$1" in
    master)
        # 接管 VIP,发送免费 ARP 让全网更新 ARP 表
        arping -c 3 -I br-lan -s 192.168.1.254 192.168.1.1
        # 清空 conntrack,避免旧连接状态错乱
        conntrack -F
        logger "keepalived: became MASTER"
        ;;
    backup)
        logger "keepalived: became BACKUP"
        ;;
esac

arping 发免费 ARP 是关键,不发的话主路由和终端的 ARP 表还指向旧 MAC,VIP 接管了也没流量过来。conntrack -F 清空连接跟踪表,避免旧连接的 NAT 状态在新主机上失配。

实测切换时间:sing-box 进程被 kill 后,备机接管 VIP 耗时 1.2~1.8 秒。终端侧表现为 1~2 秒的网络卡顿,TCP 连接会断,但新连接立刻恢复。

6.5 长期防封:流量特征、端口策略、节点轮换

节点被封是长期运行的常态。防封的核心是降低流量特征的可识别性,具体分三个维度。

端口策略。 别用 443 以外的端口跑代理。很多机场给的是 8388、1080 这类明显特征端口,DPI 一抓一个准。把节点端口统一成 443 或 8443,配合 TLS 伪装。sing-box 的 tls 配置:

{
  "type": "vless",
  "tag": "hk-01",
  "server": "1.2.3.4",
  "server_port": 443,
  "uuid": "your-uuid",
  "flow": "xtls-rprx-vision",
  "tls": {
    "enabled": true,
    "server_name": "www.microsoft.com",
    "utls": {
      "enabled": true,
      "fingerprint": "chrome"
    },
    "reality": {
      "enabled": true,
      "public_key": "your-public-key",
      "short_id": "0123456789abcdef"
    }
  }
}

utls 的 fingerprint: chrome 让 TLS 握手指纹和真实 Chrome 一致,reality 让服务端不需要自己的证书,直接借用 www.microsoft.com 的证书链。这套组合目前是抗 DPI 最强的方案之一。

流量特征。 避免单一节点跑满带宽。工作室场景下多个设备同时跑 4K 流媒体,单节点带宽打满会触发运营商的 QoS 和 DPI 告警。用 sing-box 的 selector 把不同设备分流到不同节点,或者用 urltest 的 outbounds 池做负载均衡。

节点轮换。 长期跑同一个节点,IP 迟早进黑名单。写个定时脚本,每天凌晨自动切换节点:

#!/bin/sh
# /etc/cron.daily/rotate-node
# 通过 sing-box 的 Clash API 切换 selector
curl -X PUT "http://127.0.0.1:9090/proxies/proxy" \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$(shuf -n1 -e hk-01 jp-01 sg-01 us-01)\"}"

前提是 sing-box 配置里开了 experimental 的 clash_api:

{
  "experimental": {
    "clash_api": {
      "external_controller": "127.0.0.1:9090",
      "default_mode": "rule"
    }
  }
}

6.6 监控与告警:别等用户报障才知道挂了

工作室场景下,网络挂了没人报障,等发现时可能已经断了几小时。上监控是刚需。最轻量的方案是用 OpenWrt 自带的 logread + 一个定时探测脚本,重了上 Prometheus + Grafana。

轻量方案:

#!/bin/sh
# /etc/cron.minutely/health-check
LOG=/var/log/net-health.log
TS=$(date '+%Y-%m-%d %H:%M:%S')

# 1. 检查 sing-box 进程
if ! pgrep -x sing-box > /dev/null; then
    echo "$TS sing-box DOWN" >> $LOG
    /etc/init.d/sing-box restart
fi

# 2. 检查代理连通性(走本地代理端口)
if ! curl -x socks5://127.0.0.1:1080 -s -o /dev/null -w "%{http_code}" \
    --max-time 5 https://www.gstatic.com/generate_204 | grep -q 204; then
    echo "$TS proxy UNREACHABLE" >> $LOG
fi

# 3. 检查 DNS 解析
if ! nslookup google.com 127.0.0.1 > /dev/null 2>&1; then
    echo "$TS DNS FAIL" >> $LOG
fi

# 4. 检查 conntrack 使用率
CT_MAX=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
CT_CUR=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
if [ $((CT_CUR * 100 / CT_MAX)) -gt 80 ]; then
    echo "$TS conntrack HIGH: $CT_CUR/$CT_MAX" >> $LOG
fi

每分钟跑一次,日志里出现 DOWN/UNREACHABLE/FAIL 就说明有问题。配合 logrotate 防止日志撑爆磁盘。

重方案用 Prometheus 的 node_exporter + blackbox_exporter,把 conntrack 使用率、sing-box 内存占用、节点延迟都采集进去,Grafana 上做面板。工作室场景下值得投入,家庭场景下轻量脚本够了。

6.7 备份与恢复:配置丢了比断网更可怕

最后讲一个被严重低估的环节:配置备份。OpenWrt 的配置散落在 /etc/config/、/etc/sing-box/、/etc/keepalived/、/etc/cron.daily/

七、核心疑问与实战常见问题(FAQ)

OpenWrt 旁路由透明代理后,为什么部分设备能上网但 DNS 解析全部超时?

典型原因是主路由 DHCP 下发的 DNS 仍指向运营商,而旁路由只劫持了 53 端口却没放行 LAN 到自身的 DNS 回程。先在旁路由执行 tcpdump -i br-lan port 53 -nn 确认请求是否到达;若到达但无响应,检查 PassWall 的 DNS 模式是否选了 dnsmasq 转发到 sing-box 的 5353 端口,并确认防火墙 lan 区域 input 为 ACCEPT。若请求根本没到旁路由,在主路由把 DHCP 的 DNS 强制改为旁路由 IP,同时关闭主路由的 DNS 重绑定保护。

sing-box 作为 PassWall 出站时,如何正确配置 geosite 分流避免国内网站走代理?

PassWall 里把 sing-box 设为 TCP/UDP 主节点后,分流规则不要依赖 PassWall 自带列表,直接在 sing-box 的 route.rules 里写:{"protocol":"dns"} 走 dns-out,{"geosite":"cn"} 和 {"geoip":"cn"} 走 direct 出站,其余走 proxy。注意 geosite 规则要放在 proxy 规则之前,且 sing-box 1.8 以后需在 outbound 里显式定义 direct 类型为 direct。验证方法:curl -v https://www.baidu.com 看是否命中 direct,再 curl -v https://www.google.com 确认走 proxy。

旁路由透明代理下 Netflix 只能跑 25Mbps,如何排查瓶颈?

先排除硬件 NAT 瓶颈:在旁路由执行 iperf3 -s,客户端 iperf3 -c 旁路由IP -P 4,若内网吞吐低于 600Mbps 说明 CPU 软中断跑满。检查 cat /proc/interrupts 看网卡中断是否集中在单核,用 ethtool -L eth0 combined 4 开启多队列。再看 sing-box 是否开了 sniff 和 sniffer 导致 TLS 握手重复解析,关闭 sniff_override_destination。最后用 mtr -n --tcp -P 443 目标IP 看是否在旁路由到主路由这一跳出现丢包,若有则把旁路由网线从主路由 LAN 口换到独立交换机,避免主路由 CPU 转发。

OpenWrt 旁路由开启透明代理后,主路由需要做哪些配套设置?

主路由必须做三件事:第一,关闭主路由的 DHCP,改由旁路由下发网关和 DNS,或者保留主路由 DHCP 但把网关和 DNS 都指向旁路由 IP;第二,在主路由添加静态路由,把需要代理的网段下一跳指向旁路由,避免流量绕行;第三,关闭主路由的 UPnP 和 DMZ,防止旁路由 nft 规则被绕过。如果主路由是运营商光猫,还要把光猫改桥接,否则光猫 NAT 表只有 1024 条,跑 P2P 或大流量下载时直接 table full。