我维护了一套目标明确的家庭网络基础设施:国内流量直接连接,其他流量经 VPS 上的 VLESS+Reality 加密隧道转发。家中设备不需要安装 VPN 应用、配置代理或输入账号,只要连接 Wi-Fi 就能按照统一规则访问网络。需要先了解系统代理、透明代理与 VPN 的区别,可以阅读 Linux 代理指南。
Docker 模拟环境很快就能验证账号、DNS 和路由逻辑,但把相同设计部署到运行 ASUSWRT-Merlin 的真实路由器后,硬件架构、内核功能、IPv6 和启动顺序先后暴露出问题。本文保留部署过程中的失败、诊断证据和恢复方案,因为这些差异比一份只能在特定设备上复制的最终配置更有参考价值。
1. 从模拟拓扑到嵌入式路由器
1.1 系统由三个部分组成
服务端运行 sing-box,以 VLESS inbound 接收连接,并使用 Reality 把隧道流量伪装成访问真实网站的普通 HTTPS 流量,伪装目标为 www.bing.com。服务位于 Nginx 后方,Nginx 在 443 端口按 SNI 分流,使代理入口能够与服务器上的其他 HTTPS 服务共存。
本地测试环境包含 router 和 device 两个 Docker 容器,用来模拟网关与终端之间的拓扑。配置变更先在容器中验证,通常一分钟内就能完成一次测试。真实部署目标是一台运行 ASUSWRT-Merlin 的 ASUS 路由器,由路由器拦截并转发局域网设备的流量。
先通过 Docker 测试可以排除许多配置错误,但容器无法复制路由器的 CPU userland、厂商内核选项、开机时序和 WAN 连接状态。模拟测试证明逻辑可行,真实硬件测试仍然不可省略。
1.2 内核架构不等于 userland 架构
sing-box 为不同 CPU 架构提供预编译文件。路由器执行 uname -a 后报告 aarch64,看似应该下载 linux-arm64 版本,但程序启动时报错 error while loading shared libraries: libdl.so.2: cannot open shared object file。系统中确实存在该库,进一步检查后得到 error while loading shared libraries: libdl.so.2: wrong ELF class: ELFCLASS32。
路由器运行 64 位 ARM 内核,厂商 userland 和共享库却是 32 位 ARM。Broadcom 路由器可能沿用以 32 位构建的 SDK 与驱动组件,即使内核后来改为 64 位,也没有同步替换用户空间。uname -m 只报告内核架构,不能代表动态链接器和共享库的 ABI。
最终选择 linux-armv7 版本后,程序可以正常加载。诊断类似问题时,应使用 file、readelf -h 或检查现有共享库的 ELF class,不应只根据内核报告选择二进制文件。
1.3 nftables 模块存在不代表功能完整
sing-box 的 auto_redirect 可以为 tun inbound 自动创建透明转发规则。该方案启动时首先报告 FATAL start service: post-start inbound/tun: auto-redirect: missing nftables support: netlink receive: invalid argument。路由器没有 nft 命令,但固件中存在 nf_tables.ko,而且模块可以成功加载。
加载模块后,错误变成 FATAL auto-redirect: setup nftables: create ipv4 local address set: netlink receive: operation not supported。检查内核编译配置后发现 CONFIG_NF_TABLES_IPV4 没有启用:通用 nftables netlink 接口足以完成初步通信,内核却没有实现所需的 IPv4 table。当前使用的 sing-box 版本也不会让 auto_redirect 自动退回 iptables。
modprobe 只能加载已经构建的模块,无法补上编译时缺失的功能。确认内核能力时,需要同时检查模块、内核配置和实际操作结果。自动路径不可用后,部署改为使用路由器已经支持的 iptables TPROXY。
2. 使用 TPROXY 手工实现透明转发
2.1 数据包标记、本地投递与 sing-box inbound
TPROXY 能在不修改终端设置的情况下拦截流量,同时保留原始目标地址。完整路径包含三个环节:mangle 表的 PREROUTING 规则拦截从局域网接口进入的数据包;sing-box 的 tproxy inbound 在本地端口接收被重定向的连接;policy routing 根据 fwmark 把数据包投递到本机 loopback,而不是继续向 WAN 转发。
部署使用的 IPv4 规则骨架如下,生产脚本还要排除完整的 RFC 1918 与保留网段:
iptables -t mangle -N SB_TPROXY
# 统一截获客户端 DNS,让分流规则使用同一个解析路径。
iptables -t mangle -A SB_TPROXY -p udp --dport 53 -j TPROXY \
--on-port 7893 --on-ip 127.0.0.1 --tproxy-mark 0x1/0xffffffff
# 路由器本机的 SSH 与管理接口不进入代理。
iptables -t mangle -A SB_TPROXY -m addrtype --dst-type LOCAL -j RETURN
# 局域网和保留地址保持原始源地址与本地路径。
iptables -t mangle -A SB_TPROXY -d 10.0.0.0/8 -j RETURN
# 生产脚本继续列出其他 RFC 1918 与保留网段。
# 其余 TCP 流量交给 sing-box。
iptables -t mangle -A SB_TPROXY -p tcp -j TPROXY \
--on-port 7893 --on-ip 127.0.0.1 --tproxy-mark 0x1/0xffffffff
iptables -t mangle -I PREROUTING 1 -i br0 -j SB_TPROXY
ip rule add fwmark 0x1 lookup 100 priority 100
ip route add local 0.0.0.0/0 dev lo table 100
xt_TPROXY.ko 会自动带入 nf_tproxy_ipv4 和 nf_tproxy_ipv6。sing-box 不再创建 tun 设备或自动路由,只负责接受已经标记并本地投递的连接,再按域名和 IP 规则选择 direct 或 proxy outbound。
2.2 路由表编号必须检查占用情况
最初配置选择 table 100,因为 ip rule show 当时没有规则指向该编号。执行 ip route show table 100 后却看到输出使用符号名称 wan0。ASUSWRT-Merlin 的 /etc/iproute2/rt_tables 已经把低编号分配给 VPN client、双 WAN 等固件功能,table 100 并不是空闲资源。
最终脚本从部署中已经唯一的 TPROXY 监听端口推导 table ID,并在写入前检查路由表名称与现有内容。清理逻辑也不再使用 grep “lookup 100” 判断规则是否存在,因为 iproute2 发现符号名称后可能显示 lookup wan0。脚本改为匹配专用 fwmark,再从结构化结果确定需要清理的规则和表。
端口、table ID 和 mark bit 都属于共享命名空间。厂商固件、VPN 插件与 QoS 组件可能预留看似空闲的小整数,部署脚本必须先检查实际占用,而不能依赖常见教程中的示例编号。
3. 在修改网络前建立可验证的回滚
家庭路由器是整个网络的单点入口。错误的防火墙规则不只会让代理失效,还可能中断管理连接和全家网络。因此正式上线前先实现了类似网络设备 commit confirmed 的流程。
执行 start 前,脚本保存当前 iptables、ip6tables、policy rule 和相关路由表状态,然后启动独立的三分钟看门狗。如果管理员没有在期限内执行 confirm,路由器会停止 sing-box 并恢复快照,不依赖原来的 SSH 会话继续存在。revert 可以从仍能连接路由器的任何设备立即触发相同恢复流程。
自动启动尚未启用时,重启还是最后一道恢复手段:规则和进程只存在于内存,没有写入 JFFS 或 services-start,断电重启会清除临时状态。该退路适合开发阶段,但不能替代经过测试的回滚脚本。
为了验证恢复能力,我手工加入一条伪造的防火墙规则、一条 policy rule 和一张孤立路由表,再执行恢复并逐字节比较已知正常的基线。演练发现第一版脚本先删除 policy rule,随后才查询该规则指向的表,导致失去引用的路由表永远不会被清理。修复后的流程会先记录所有待删除对象及其关系,再按依赖顺序清理。没有用真实故障演练过的恢复流程,仍然只是未经证实的假设。
4. IPv6 会绕过只覆盖 IPv4 的规则
4.1 DNS 故障暴露了 IPv6 旁路
IPv4 配置连续运行数天后,一台笔记本浏览特定域名时出现 ERR_CONNECTION_CLOSED,另一台设备却能正常访问。浏览器的加密 DNS 设置曾制造干扰,但 nslookup 最终给出关键证据:故障设备使用的是路由器 IPv6 地址提供的 DNS,而不是被 IPv4 规则拦截的解析路径。
家庭网络向终端分配全球可路由的 IPv6 地址,现代操作系统也会优先尝试可用的 IPv6 路径。只配置 iptables 意味着 IPv6 DNS 和数据连接不会经过任何 TPROXY 规则。客户端可以在没有明显警告的情况下获得不同 DNS 结果或直接绕过代理,浏览器最终只会显示普通连接错误。
修复方案为 IPv6 创建镜像规则:使用 ip6tables、增加绑定 ::1 的 sing-box tproxy inbound,并设置 IPv6 policy routing。IPv4 与 IPv6 policy routing 位于独立的地址族中,因此可以使用相同的 fwmark 和 table ID,但两套规则都必须单独安装、检查和恢复。
4.2 动态排除局域网 IPv6 前缀
IPv4 可以通过 RFC 1918 网段识别多数局域网目标。IPv6 终端通常拥有全球可路由地址,同一局域网内的通信不能仅靠“私有地址”判断。如果 TPROXY 把本地设备之间的 IPv6 流量重新送入隧道,会增加延迟并破坏原始源地址语义。
防火墙脚本需要明确排除家庭网络当前获得的 IPv6 前缀。ISP 委派的前缀可能在续租后改变,所以脚本从内核路由表读取当前 on-link 前缀,而不是在配置中写死一个迟早失效的值。
该修复同时使用真实流量验证。测试时观察 ip6tables 链的包计数器增长,并在 sing-box 日志中确认同一连接被正确分类并进入隧道。配置文件检查只能证明语法成立;数据包计数器、conntrack 和应用日志才能证明真实流量走过预期路径。
5. 无人值守启动需要新的安全模型
5.1 boot-start 不能照搬交互式 start
交互式 start 总会启动看门狗并等待管理员确认。开机脚本如果直接调用相同命令,就会在每次重启三分钟后自动回滚,因为没有人在场执行 confirm。
部署因此增加 boot-start:脚本仍先启动服务并设置看门狗,随后执行小型健康检查。TPROXY 规则只处理从 br0 进入的局域网流量,路由器自身无法直接模拟完整客户端路径,所以健康检查先验证路由器能否建立到 VPS 的 TCP 连接。连接成功后自动确认;连接失败则保留看门狗,让系统在期限到达后恢复网络状态。
VPS 连通性只是最低限度的检查,不能证明 DNS 分流和每个客户端都正常。开机后还要结合防火墙计数器、sing-box 日志或专门的局域网探针验证端到端路径。
5.2 等待存储与 WAN 真正就绪
第一次真实重启暴露出 services-start 执行过早。系统日志显示脚本运行时日期仍是 1 月 1 日,USB 读卡器也尚未完成枚举,存放 sing-box 的存储路径不存在。boot-start 现在会轮询可执行文件,确认存储已挂载后才继续。
第二个问题来自 WAN。配置启用了 route.auto_detect_interface,sing-box 只在启动时检测一次路由环境。如果 PPPoE 接口 ppp0 和默认路由尚未建立,程序不会等网络恢复后自动修正所有判断。部分终端流量会完全绕过 TPROXY,但 sb.sh status 仍显示进程正常。
conntrack 记录揭示了旁路:故障终端的响应经路由器 WAN NAT 地址返回,而不是由 sing-box 在本地接管。等 WAN 稳定后手工重启 sing-box,所有终端立即恢复。最终脚本先等待真实默认路由出现,再增加一段稳定时间,然后启动 sing-box。
复杂测试机还可能造成错误归因。我使用的笔记本同时运行 Tailscale 和多个 Docker bridge,部分早期异常确实来自测试机,但其他终端关闭 Tailscale 后仍然故障,证明路由器也存在独立问题。跨多台环境不同的终端验证,可以把客户端噪声与网关故障分开。最终方案通过四次真实重启,确认存储挂载、WAN 就绪、自动健康检查、自动确认和多终端转发都能在无人操作时完成。
6. 延迟与队列需要分别处理
6.1 VPS 地理位置直接影响交互延迟
隧道最初连接美国东岸 VPS,从家庭网络测得的实际 TCP 往返时间约为四分之一秒。一次网页加载可能经历基础连接、隧道握手和应用请求等多个往返,较高 RTT 会被重复计入。更换到距离接入点更近的美国西岸 VPS 后,实际页面加载延迟大约减半。
评估代理性能时,应测量客户端到 VPS 的 TCP 或应用层 RTT,不能只依赖可能被云防火墙丢弃的 ICMP ping。如果加密隧道功能正常但交互明显缓慢,先比较不同机房的真实往返时间,再调整协议参数。
6.2 Cake SQM 处理 bufferbloat
TPROXY 决定流量走向,bufferbloat 则来自上行或下行饱和时过长的发送队列。视频通话、游戏和新建网页连接可能在带宽仍足够时出现高延迟,因为大量数据包已经在路由器中排队。
ASUSWRT-Merlin 386.2 以后为部分机型提供 Cake SQM。管理员在 WAN 带宽限制页面填写上传和下载上限后,固件会在 WAN 接口安装 cake qdisc 并进行 flow isolation。上限通常从实测速率的 85%–95% 开始调校,为 Cake 保留控制瓶颈队列的余量;具体数值必须根据线路波动和延迟测试确定。
Cake 工作在接口 queueing discipline 层,不依赖 TPROXY 的 —tproxy-mark 0x1/0xffffffff 分类,因此两者可以共存。旧版 Adaptive QoS 或 Traditional QoS 可能使用 CONNMARK 的部分 bit 进行 HTB 分类,与其他 fwmark 方案存在冲突风险。处理 bufferbloat 时应选择一个 QoS 引擎,避免同时启用 Cake 与旧式分类器。
7. 可复用的部署原则
- Docker 拓扑可以快速验证隧道账号、DNS 和路由逻辑,但真实硬件仍需检查 ABI、内核配置、驱动和启动时序。
uname报告内核架构,模块成功加载也只证明入口存在。二进制兼容性要检查 userland ELF,网络功能要执行真实 netlink 或数据包测试。- 端口、路由表编号和 mark bit 都可能被固件保留。脚本应检查当前命名空间,并用部署中稳定且唯一的标识定位自身资源。
- 防火墙变更前先实现快照、超时回滚和手工恢复,再用人为故障演练。清理前必须记录规则与路由表之间的引用关系。
- IPv4 与 IPv6 都要覆盖 DNS、TCP、policy routing、局域网排除和恢复逻辑。只实现 IPv4 会留下现代终端可以自动进入的旁路。
- 开机脚本必须等待存储、接口、默认路由与时间同步等依赖。进程存活只证明程序没有退出,不能证明客户端流量已经被正确拦截。
- 性能诊断要区分路径 RTT 与本地排队。前者通过选择更近的 VPS 改善,后者通过合理限速和 Cake SQM 控制。
透明代理部署的主要困难通常不在 VLESS 或 TPROXY 的基本原理,而在通用文档假设的系统能力与特定路由器实际提供的内核、userland 和固件生命周期之间存在差异。保留可观察证据、独立回滚路径和真实设备测试,才能把一次可运行的实验转化为可以长期维护的家庭网络服务。