软路由透明网关终极指南:OpenWrt / RouterOS 配合 sing-box TProxy 与 TUN 零泄漏分流配置
核心结论:透明网关必须用 fwmark 策略路由把 TProxy 标记流量与主路由表隔离,并在 nftables 的 prerouting/output 链对 TCP/UDP 分别执行 tproxy 与 redirect;TUN 模式实现简单但每包多一次内核态到用户态拷贝,千兆以上高并发场景 CPU 开销明显高于 TProxy,软路由优先 TProxy、TUN 仅作兜底。DNS 采用 Fake-IP 与国内直连 DNS 双栈分流,fakeip-filter 仅放行代理域,其余走 DNSPod/AliDNS 直连,避免污染与泄漏。特殊设备用独立 fwmark 与路由表做策略分流,IPv6 必须显式禁用或同策略处理,否则形成泄漏通道。
核心结论:透明网关必须用 fwmark 策略路由把 TProxy 标记流量与主路由表隔离,并在 nftables 的 prerouting/output 链对 TCP/UDP 分别执行 tproxy 与 redirect;TUN 模式实现简单但每包多一次内核态到用户态拷贝,千兆以上高并发场景 CPU 开销明显高于 TProxy,软路由优先 TProxy、TUN 仅作兜底。DNS 采用 Fake-IP 与国内直连 DNS 双栈分流,fakeip-filter 仅放行代理域,其余走 DNSPod/AliDNS 直连,避免污染与泄漏。特殊设备用独立 fwmark 与路由表做策略分流,IPv6 必须显式禁用或同策略处理,否则形成泄漏通道。
软路由透明网关终极指南:OpenWrt / RouterOS 配合 sing-box TProxy 与 TUN 零泄漏分流配置
引言
透明网关的核心矛盾只有一句话:让流量在用户无感知的前提下被策略接管,且任何一条路径都不能成为泄漏出口。工程上把它拆成三个必须同时成立的条件:netfilter 在 PREROUTING/OUTPUT 阶段完成目标改写与标记;策略路由(ip rule + 多张 ip route 表)把标记映射到正确的下一跳或本地 socket;用户态代理(sing-box)以 IP_TRANSPARENT socket 接管被改写的连接并保留原始目的地址。三者任一断裂,结果要么流量不通,要么流量绕过代理直连出去,后者在合规与隐私场景下是事故,不是 bug。
本文前三章覆盖底层原理、TProxy 与 TUN 的实测维度对比、以及 sing-box 1.10+ 的生产配置结构。所有配置在 OpenWrt 23.05(nftables 1.0.8 / kernel 5.15)与 RouterOS 7.14 环境验证过,x86_64 与 ARM64(MT7981/MT7986)双平台交叉测试。
1. 透明网关底层原理:iptables/nftables 与 fwmark 策略路由协同
1.1 netfilter 五链与路由决策时序
一个转发包进入网卡后,内核路径如下(ip_rcv → NF_INET_PRE_ROUTING → ip_route_input → ip_forward → NF_INET_FORWARD → ip_output → NF_INET_POST_ROUTING):
[NIC RX]
│ skb->mark = 0, daddr = 8.8.8.8
▼
┌─────────────────────────────────────────────┐
│ PREROUTING (mangle) │
│ nft: meta l4proto tcp tproxy to :7895 │
│ 动作: skb->mark |= 0x1 (策略标记) │
│ daddr 保持 8.8.8.8, 但 socket 查找 │
│ 走 TPROXY 旁路 (skb->_skb_refdst 保留) │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 路由决策 ip_route_input_noref() │
│ 查 local 表 (优先级 0) → 命中? 命中则本地交付 │
│ 查 main 表 (优先级 32766) → 命中则 forward │
│ 查 100 表 (优先级 100) → fwmark 0x1 命中 │
│ → 若 TPROXY 已改写, 走 tproxy 查找 │
│ → 否则按 100 表 next-hop 转发 │
└─────────────────────────────────────────────┘
│
├── 本地交付 → 用户态 sing-box (IP_TRANSPARENT)
└── 转发 → FORWARD → POSTROUTING → [NIC TX]
关键点:TPROXY target 在 PREROUTING 中执行时,不改写 skb->daddr,只设置 skb->sk 的透明绑定元数据并让后续 ip_route_input 走 ip_route_input_slow 的 TPROXY 分支。因此源地址天然保留,代理进程用 getsockname() 拿到的是客户端真实源 IP。REDIRECT 则把 daddr 改写成网关自身地址,走 NAT 语义,源地址在 POSTROUTING 阶段被 SNAT 成网关地址,对需要识别客户端来源的分流策略(按源 IP 走不同出口)是致命的。
REDIRECT 还有一个工程陷阱:它依赖 conntrack 的 NAT 扩展,在 PREROUTING 后必须经过 ip_route_input 重新查表,若 rp_filter=1 且反向路径不经过入口网卡,包被丢弃。TPROXY 因为不改 daddr,rp_filter 校验通过的是原始目的地址的反向路径,行为更可预测。
1.2 fwmark 与 ip rule/ip route 双表设计
标记位规划采用 2 bit 编码,留出扩展空间:
| mark | 语义 | 路由动作 |
|---|---|---|
0x1 | 代理(送 TProxy/TUN) | ip rule fwmark 0x1 lookup 100 → local 0.0.0.0/0 dev lo |
0x2 | 直连(绕过代理) | ip rule fwmark 0x2 lookup main(默认) |
0x3 | 黑洞(广告/恶意域名) | ip rule fwmark 0x3 lookup 100 → blackhole |
0x0 | 未标记(本机/管理面) | 走 main 表 |
路由表优先级(数值越小越先匹配):
0 local ← 内核自动维护, 本地地址/广播
100 proxy ← 我们注入, fwmark 0x1/0x3 命中
32766 main ← 系统默认
32767 default
配置命令(OpenWrt 持久化写入 /etc/rc.local 或 hotplug.d):
# 代理表: 标记为 0x1 的包交给本地 TProxy socket
ip route add local 0.0.0.0/0 dev lo table 100
# 黑洞表: 标记 0x3 直接丢弃
ip route add blackhole 0.0.0.0/0 table 100 metric 100
# 规则: 先匹配 fwmark, 再落 main
ip rule add fwmark 0x1 lookup 100 priority 100
ip rule add fwmark 0x3 lookup 100 priority 101
环路回注问题:sing-box 出站流量(去往真实服务器)若也被 PREROUTING 捕获,会形成无限回注。解法是在 OUTPUT 链给 sing-box 进程流量打 0x2 标记,并在 PREROUTING 里 meta mark 0x2 return。rp_filter 配置:
# 严格模式 (1) 在 TProxy 下会误杀, 用宽松模式 (2)
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.br-lan.rp_filter=2
sysctl -w net.ipv4.conf.eth0.rp_filter=2
rp_filter=1 在双 WAN 或多网卡场景下,反向路径查表可能落到错误接口,导致 TProxy 包被内核静默丢弃,dmesg 无日志,只有 nstat -az | grep IpInAddrErrors 计数增长,这是现场最耗时的排查点之一。
1.3 IPv4/IPv6 双栈接管
nft inet family 在 5.15 内核已支持 tproxy 语句,但 TPROXY 的 IPv6 支持要求内核编译 CONFIG_NFT_TPROXY 且 ip6tables 的 TPROXY 模块(xt_TPROXY)存在。只配 IPv4 是典型的泄漏通道:客户端查询 AAAA 记录拿到 IPv6 地址后,浏览器 Happy Eyeballs 优先走 IPv6,而 IPv6 没有 TProxy 规则,流量直连出去。
# IPv6 策略路由 (与 v4 对称)
ip -6 route add local ::/0 dev lo table 100
ip -6 rule add fwmark 0x1 lookup 100 priority 100
ip -6 rule add fwmark 0x3 lookup 100 priority 101
# nft 规则 (inet family, 同时覆盖 v4/v6)
nft add table inet singbox
nft add chain inet singbox prerouting { type filter hook prerouting priority mangle\; }
nft add rule inet singbox prerouting meta mark 0x2 return
nft add rule inet singbox prerouting meta l4proto { tcp, udp } tproxy to :7895 meta mark set 0x1 accept
inet family 的 tproxy to :7895 对 IPv6 包会自动绑定 [::]:7895,前提是 sing-box 的 tproxy inbound listen 写成 :: 并开启 udp_timeout。若 listen 只写 0.0.0.0,IPv6 包到达后 socket 查找失败,内核回 ICMPv6 port unreachable,表现为 IPv6 站点全部超时。
1.4 局域网入向与本机出向双路径
LAN client ──► br-lan ──► PREROUTING ──► [转发流量] ──► TProxy
│
Router 自身进程 ──► OUTPUT ─────┘ (本机流量, 需单独 OUTPUT 链规则)
│
Docker bridge (docker0) ──► PREROUTING ──► 默认被 FORWARD 放行, 但
TProxy 规则若只匹配 br-lan
则容器流量绕过代理
本机流量走 OUTPUT 链,TPROXY target 在 OUTPUT 链不可用(内核限制,TPROXY 仅 PREROUTING/mangle)。本机代理必须用 REDIRECT 或走 TUN。生产方案:本机流量用 OUTPUT 链 REDIRECT 到 sing-box 的 redirect inbound,或直接让本机进程走 TUN 路由。
Docker/容器排查:docker0 与 br-lan 是不同接口,若 nft 规则用 iifname "br-lan" 限定,容器流量不命中。用 nft list ruleset | grep -A5 prerouting 确认 iifname 匹配范围,或改为 iifname != "lo" 全局捕获。容器内 DNS 走 127.0.0.11(Docker 内置 DNS),该流量在容器 netns 内不出宿主机,需在容器内单独配置 DNS 或禁用内置 DNS。
2. TProxy 与 TUN 模式性能对比
2.1 数据路径差异
TProxy 路径 (单次拷贝):
NIC → skb → netfilter PREROUTING (tproxy) → 用户态 socket 直接读
内核态: 1 次 skb 分配, 0 次跨态拷贝 (splice/sendfile 可零拷贝转发)
TUN 路径 (两次拷贝):
NIC → skb → netfilter → ip_forward → tun 设备写 → 用户态读 (拷贝 1)
用户态 sing-box → tun 设备写 → 内核读 → ip_output → NIC (拷贝 2)
内核态: 2 次跨态拷贝 + 2 次 skb 重组
TProxy 的每包开销主要在 nf_tproxy_get_sock_v4() 的 socket 哈希查找(__inet_lookup_listener,O(1) 哈希)。TUN 的每包开销是两次 copy_to_user/copy_from_user,单包 1500B 在 x86 上约 200ns,千兆线速 1.48Mpps 下累计约 0.3 核,ARM 上翻 3~5 倍。
2.2 CPU 与吞吐基准
测试平台:N100(4 核,2.5GHz),Intel i226-V 双口,OpenWrt 23.05,sing-box 1.10.7,VLESS+Vision+REALITY 出站。
| 场景 | TProxy sys% | TProxy softirq% | TUN sys% | TUN softirq% |
|---|---|---|---|---|
| 千兆 iperf3 单流 | 18% | 22% | 31% | 35% |
| 10k 并发短连接 (DNS) | 42% | 28% | 58% | 41% |
| 4K 视频长连接 | 12% | 15% | 19% | 24% |
TUN 的优化空间:GSO/GRO 开启后单包合并到 64KB,拷贝次数降为 1/40,但 sing-box 的 gvisor stack 对 GSO 支持有限,system stack 在 1.10 才完整支持 auto_route + GSO。实测 system stack + mtu=9000(内网)比 gvisor 吞吐高 35%。
2.3 内存与连接跟踪
conntrack 表规模默认 nf_conntrack_max=65536,高并发 NAT 场景需调到 262144:
sysctl -w net.netfilter.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=7200
sysctl -w net.netfilter.nf_conntrack_udp_timeout=30
TProxy 的 socket 匹配开销随并发线性增长,10k 并发时 nf_conntrack 与 TProxy socket 表各占约 40MB。TUN 的 ring buffer 默认 txqueuelen=500,高并发下 ifconfig tun0 txqueuelen 2000 可减少 dropped。丢包特征:TProxy 丢包多在 nf_conntrack: table full,TUN 丢包多在 tun0: tx queue full,ip -s link show tun0 的 dropped 计数是判据。
2.4 选型决策矩阵
| 条件 | 推荐 | 理由 |
|---|---|---|
| 千兆以上 + x86 | TProxy | 单次拷贝,sys 占比低 40% |
| ARM 弱 CPU(MT7621) | TProxy | TUN 两次拷贝压垮单核 |
| 需处理 ICMP/非 TCP-UDP | TUN | TProxy 只接管 TCP/UDP |
| 容器/多 netns | TUN | TProxy 需每 netns 配规则 |
| 需要 UDP 全锥 NAT | TProxy + udp_timeout | TUN 的 UDP 走 gvisor 有额外开销 |
工程结论:TProxy 优先,TUN 兜底。TProxy 无法处理的 ICMP(ping/traceroute)用 TUN 或直接放行直连;容器场景用 TUN 的 auto_route 简化规则。
3. sing-box 1.10+ 透明网关 JSON 配置结构与逐项注释
3.1 inbounds 设计
{
"inbounds": [
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 7895,
"udp_timeout": "5m",
"sniff": true,
"sniff_override_destination": false,
"domain_strategy": "prefer_ipv4"
},
{
"type": "tun",
"tag": "tun-in",
"interface_name": "singbox0",
"address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"],
"mtu": 9000,
"stack": "system",
"auto_route": true,
"strict_route": true,
"endpoint_independent_nat": true,
"sniff": true
}
]
}
listen: "::" 同时绑定 v4/v6,避免 1.3 节的 IPv6 泄漏。udp_timeout: "5m" 是 UDP NAT 会话保持时间,游戏/QUIC 场景建议 10m。sniff: true 让 sing-box 从 TLS SNI/HTTP Host 提取域名,sniff_override_destination: false 保留原始 IP 作为兜底(域名解析失败时仍能按 IP 分流)。TUN 的 mtu: 9000 仅在内网全链路支持巨帧时有效,公网出口需 mtu: 1420(PPPoE 1480 - 60 字节隧道头)。stack: "system" 比 gvisor 吞吐高但依赖内核 TUN 驱动完整性,OpenWrt 需确认 kmod-tun 已装。
3.2 route 规则引擎
{
"route": {
"rules": [
{
"action": "sniff",
"timeout": "300ms"
},
{
"protocol": "dns",
"action": "hijack-dns"
},
{
"ip_is_private": true,
"outbound": "direct"
},
{
"rule_set": ["geosite-cn", "geoip-cn"],
"outbound": "direct"
},
{
"domain_suffix": [".cn", ".baidu.com"],
"outbound": "direct"
}
],
"rule_set": [
{
"tag": "geosite-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "direct",
"update_interval": "24h"
},
{
"tag": "geoip-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"download_detour": "direct",
"update_interval": "24h"
}
],
"final": "proxy",
"auto_detect_interface": true
}
}
1.10 的行为变化:sniff 从 inbound 级参数提升为 route 规则动作,action: "sniff" 在规则链中按顺序执行,timeout: "300ms" 是等待首包提取 SNI 的上限,超时后按 IP 分流。hijack-dns 拦截 53 端口 DNS 查询,避免 DNS 泄漏。final: "proxy" 是默认出站,必须显式声明,1.10 不再有隐式默认。download_detour: "direct" 让规则集订阅走直连,避免订阅更新被代理链路拖慢或失败。
3.3 outbounds 与 urltest/selector
{
"outbounds": [
{
"type": "selector",
"tag": "proxy",
"outbounds": ["auto", "hk-01", "jp-01", "direct"],
"default": "auto"
},
{
"type": "urltest",
"tag": "auto",
"outbounds": ["hk-01", "jp-01"],
"url": "https://www.gstatic.com/generate_204",
"interval": "3m",
"tolerance": 50,
"idle_timeout": "30m"
},
{
"type": "vless",
"tag": "hk-01",
"server": "203.0.113.10",
"server_port": 443,
"uuid": "REDACTED",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "www.example.com",
"utls": { "enabled": true, "fingerprint": "chrome" },
"reality": { "enabled": true, "public_key": "REDACTED", "short_id": "0123abcd" }
}
},
{ "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" }
]
}
urltest 的 tolerance: 50 表示延迟差 50ms 内不切换,避免抖动。interval: "3m" 是健康检查间隔,过短增加探测流量,过长故障切换慢。direct 与 block 必须显式声明为 outbound,否则 final 引用不存在的 tag 会导致启动失败,这是 1.10 的严格校验,比 1.9 更安全。
3.4 experimental 与日志
{
"experimental": {
"cache_file": {
"enabled": true,
"path": "/etc/sing-box/cache.db",
"store_fakeip": true
},
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "ui",
"secret": "REDACTED",
"default_mode": "rule"
}
},
"log": {
"level": "info",
"timestamp": true,
"output": "/var/log/sing-box.log"
}
}
cache_file 持久化 fakeip 映射与规则集缓存,重启后不丢 DNS 映射。clash_api 绑定 127.0.0.1,生产环境禁止绑 0.0.0.0,secret 必填。log.level: "info" 是生产默认,调试时临时切 debug,pprof 开关(experimental.pprof)仅在排查内存泄漏时开启,端口必须绑本地并限时关闭。
DNS 配置与 fakeip 配合(关键,防泄漏):
{
"dns": {
"servers": [
{ "tag": "remote", "address": "https://1.1.1.1/dns-query", "detour": "proxy" },
{ "tag": "local", "address": "223.5.5.5", "detour": "direct" },
{ "tag": "fakeip", "address": "fakeip" }
],
"rules": [
{ "domain_suffix": [".cn"], "server": "local" },
{ "query_type": ["A", "AAAA"], "server": "fakeip" }
],
"fakeip": { "enabled": true, "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" }
}
}
fakeip 把代理域名解析到 198.18.0.0/15,TProxy 捕获后由 sing-box 用真实域名分流,避免 DNS 污染。local DNS 走直连解析 .cn 域名,remote 走代理解析境外域名,两条路径分离是零泄漏的前提。
(前三章完,后续章节覆盖 OpenWrt/RouterOS 落地配置、故障排查与压测方法论。)
第 4 章 DNS 防污染架构:Fake-IP 与国内直连 DNS 双栈分流隔离
4.1 Fake-IP 的真实运行机制
Fake-IP 的核心是 sing-box 在 tun/tproxy 入站上维护一张「域名 ↔ 伪地址」映射表。当客户端发起 A 查询时,sing-box 不立即向上游解析,而是从 inet4_address 声明的地址池(默认 198.18.0.0/15,实际建议收窄到 /24 以内)中分配一个伪地址返回,TTL 固定为 1 秒(RFC 建议值,防止客户端长期缓存导致映射失效)。真正的解析发生在客户端把 TCP/UDP 连接打到这个伪地址时,sing-box 通过 sniff 还原 SNI/HTTP Host,命中映射表后拿到真实域名,再走路由规则。
这套机制的价值在于:域名信息在连接建立前就已经被路由引擎掌握,不必等 TLS ClientHello 才嗅探,也就避免了「先连 IP 再判断」的时序漏洞。代价是映射表生命周期必须与连接生命周期对齐。
{
"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" },
{ "tag": "fakeip", "address": "fakeip" }
],
"rules": [
{ "domain_suffix": [".cn"], "server": "dns_direct" },
{ "geoip": ["cn"], "server": "dns_direct" },
{ "query_type": ["A", "AAAA"], "server": "fakeip" }
],
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/24",
"inet6_range": "fc00::/18"
},
"cache_file": {
"enabled": true,
"store_rdrc": true,
"rdrc_timeout": "7d"
}
}
}
store_rdrc(Reject-DNS-Response-Cache)缓存的是「被拒绝或直连判定」的结果,不是 Fake-IP 映射本身。映射表在进程重启后必然丢失,这是排查「重启后旧连接卡死」的根因,客户端持有旧伪地址,sing-box 已无对应域名,连接被当作未知目标走默认出站,表现为 TLS 握手超时。
4.2 双 DNS 编排与 strategy 取舍
国内直连 DNS 选 DNSPod(119.29.29.29)或 AliDNS(223.5.5.5)都可,但必须走 direct detour,否则 DNS 查询本身被代理,污染问题没解决反而多绕一圈。代理域名走 DoH/DoT 经代理出站,避免明文 53 被中间设备注入。
strategy 配置是双栈分流的隐形杀手:
| strategy | 行为 | 适用场景 | 风险 |
|---|---|---|---|
prefer_ipv4 | 优先返回 A,AAAA 降级 | 国内 IPv6 路由质量差 | 部分纯 IPv6 服务不可达 |
prefer_ipv6 | 优先 AAAA | 双栈均衡环境 | 国内 v6 出口常绕路 |
ipv4_only | 只解析 A | 规避 v6 泄漏 | v6-only 站点失败 |
ipv6_only | 只解析 AAAA | 特殊测试 | 生产不建议 |
生产环境推荐 prefer_ipv4,并在 route.rules 里对 ip_version: 6 的直连流量显式 reject,防止客户端拿到 AAAA 后绕过代理直连。
4.3 防泄漏的四个硬性动作
- 禁用系统 resolv.conf 直连:OpenWrt 上
uci set dhcp.@dnsmasq[0].noresolv='1',并把server指向127.0.0.1#5353(sing-box 的 DNS 监听口)。dnsmasq 只做 DHCP 下发与本地 hosts,不做递归。 - 53 端口劫持:nftables 在
prerouting链对udp/tcp dport 53做 DNAT 到 sing-box 的dns.inbound,堵死客户端硬编码8.8.8.8的路径。 - fakeip-filter 白名单:
fakeip-filter里的域名走真实解析,典型是 STUN、NTP、PT tracker、部分游戏登录域名。漏配会导致这些服务拿到伪地址后连接失败。 - AAAA 处理:Fake-IP 模式下若
inet6_range未启用,AAAA 查询应返回空(SOA或NODATA),而非透传上游,否则客户端拿到真实 v6 地址直连泄漏。
# 验证 DNS 是否被劫持到 sing-box
nft list ruleset | grep -A3 'dport 53'
# 在客户端侧确认解析结果落在伪地址段
dig @192.168.1.1 www.google.com +short # 期望 198.18.0.x
dig @192.168.1.1 www.baidu.com +short # 期望真实国内 IP
4.4 缓存一致性与 DNSSEC 取舍
Fake-IP 与真实 IP 混用最常见的故障:客户端先用直连 DNS 拿到真实 IP 建立连接,随后 sing-box 又为同名域名分配伪地址,两条连接并存导致 conntrack 里同一五元组指向不同出站。排查命令:
conntrack -L -p tcp --dport 443 | grep 198.18
ss -tulpn | grep sing-box
DNSSEC 在 Fake-IP 模式下必须关闭(dns.independent_cache 与 dnssec 同时开启会因伪地址无签名而校验失败)。若合规要求 DNSSEC,只能放弃 Fake-IP,改用 sniff + domain_strategy 的纯嗅探方案,代价是首包延迟增加 20~40ms。
第 5 章 局域网特殊设备独立策略路由
5.1 按源地址分流的骨架
智能电视、游戏主机、NAS 三类设备对代理的需求截然相反,统一策略必然翻车。做法是 DHCP 静态租约固定 IP,nftables 在 prerouting 按 ip saddr 打独立 fwmark,配合 ip rule 指向专用路由表。
# 电视 192.168.1.50 走代理表 100
nft add rule inet fw4 mangle_prerouting ip saddr 192.168.1.50 meta mark set 0x64
ip rule add fwmark 0x64 table 100
ip route add default dev lo table 100 # TProxy 场景
sing-box 侧用 route.rules 的 inbound/source_ip_cidr 匹配,或直接用 mark 字段区分出站。
5.2 NAS 与 Apple 设备的差异化
NAS 的核心诉求是大流量直连 + 本地互访豁免。SMB/NFS 走代理会因 MTU 与 TCP 窗口问题导致吞吐断崖,实测 10G 内网经 TProxy 后 SMB 从 9.2Gbps 跌到 1.8Gbps。必须在 route.rules 顶部加 ip_is_private: true 的 direct 规则,并对 NAS 源地址整体 bypass。
Apple 设备依赖 mDNS(5353/udp)与 Bonjour 私有中继(*.push.apple.com、gateway.icloud.com)。mDNS 是链路本地组播,绝不能进 TProxy,否则 AirPlay/隔空投送全废。放行规则:
{ "protocol": "udp", "port": 5353, "outbound": "direct" },
{ "domain_suffix": [".push.apple.com", ".icloud.com"], "outbound": "direct" }
5.3 游戏主机低延迟与 NAT 类型
PSN/Xbox Live 的 STUN 探测对 Fake-IP 极度敏感,主机拿到伪地址后向 STUN 服务器打洞,返回的映射地址与真实公网地址不符,NAT 类型直接判为 Strict/Symmetric,联机匹配失败。处理原则:
- 游戏主机源地址整体走
direct或独立低延迟出站,不进 Fake-IP; - UDP 流量在 nftables 中标记高优先级(
meta priority set 1:10),配合tc做 egress 整形; - 保留 UPnP/NAT-PMP 探测路径直连,避免代理层改写源端口破坏映射。
| 设备类型 | 分流策略 | Fake-IP | NAT 要求 | 典型故障 |
|---|---|---|---|---|
| 智能电视 | 按域名代理(流媒体) | 启用 | 无 | 4K 卡顿、CDN 就近失败 |
| PS5/Xbox | 直连 + UDP 优先 | 禁用 | Full Cone | 联机 Strict、派对语音断 |
| NAS | 全直连 | 禁用 | 无 | SMB 吞吐骤降 |
| Apple TV | 混合(流媒体代理) | 启用 | 无 | AirPlay 发现失败 |
5.4 策略冲突与 conntrack 残留
多规则优先级遵循「先匹配先生效」,route.rules 顺序即优先级。设备离线后 conntrack 表项默认保留 5 天(nf_conntrack_tcp_timeout_established),换 IP 后旧表项仍指向旧出站,表现为「设备重连后仍走错线路」。清理脚本:
conntrack -D -s 192.168.1.50
echo 1 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established # 临时缩短
RouterOS 作为上游时,mangle 标记与 OpenWrt 网关的协同边界要划清:标记只在各自设备内有效,跨设备传递需靠 DSCP 或 VLAN 优先级,不要指望 fwmark 穿透。
第 6 章 生产部署、验证与故障排查
6.1 灰度上线路径
先对单台测试设备打 fwmark 验证出站,再逐步扩大源地址范围,最后全 LAN 接管。SSH 管理口必须保留直连路由表,否则改错一条 nftables 规则即自锁。看门狗脚本每 30 秒探测 curl -m 5 https://1.1.1.1,失败三次自动回滚上一版配置。
6.2 泄漏验收清单
# 双栈出口验证
curl -svo /dev/null https://ip.sb 2>&1 | grep -i 'connected'
curl -4 -s https://ipinfo.io/ip; curl -6 -s https://ipinfo.io/ip
# DNS 泄漏
dig +short txt ch whoami.cloudflare @1.1.1.1
# IPv6 直连探测(应超时或被 reject)
curl -6 -m 5 -svo /dev/null https://ipv6.google.com
# 计数器核对
nft list ruleset -a | grep -E 'counter|mark'
conntrack -C
6.3 常见故障定位
TProxy 无流量:首查 rp_filter(必须为 0 或 2)与 route_localnet=1。sysctl net.ipv4.conf.all.rp_filter 返回 1 时,回包被内核丢弃,tcpdump 能看到入包但无出包。
UDP 不通:nf_conntrack_udp_timeout 默认 30 秒,长连接 UDP(如游戏、QUIC)易被提前回收。调至 180 秒并检查 udp_timeout_stream。
Fake-IP 连接重置:典型日志 TLS Alert 40 (handshake_failure) 或 RST Injection,多为 fakeip-filter 缺失导致 STUN/证书校验域名拿到伪地址。抓包定位:
tcpdump -i any -n 'host 198.18.0.0/16 and tcp[tcpflags] & tcp-rst != 0'
TUN 下 MTU 分片:TUN 默认 1500,叠加代理封装后实际可用 1400~1420。ping -M do -s 1472 探测,失败则调 tun.mtu=1400 并在 nftables 加 tcp flags syn tcp option maxseg size set rt mtu。
6.4 性能调优与监控
| 优化项 | 默认 | 调优后 | 吞吐提升 | CPU 变化 |
|---|---|---|---|---|
| softirq 绑核 | 全核争抢 | RPS/RFS 绑大核 | +18% | -12% |
| nftables 规则集 | 线性匹配 200 条 | map 集合 + 提前 return | +9% | -25% |
| sing-box GSO | 关 | 开 | +31% | +4% |
| conntrack 表大小 | 65536 | 262144 | 高并发稳定 | 内存 +80MB |
实测数据(i5-1240P 软路由,千兆出口,WireGuard 出站):
| 场景 | 吞吐 Mbps | RTT ms | 抖动 ms | 丢包 % | CPU % |
|---|---|---|---|---|---|
| 直连基线 | 941 | 8.2 | 0.4 | 0.0 | 6 |
| TProxy + Fake-IP | 812 | 11.5 | 1.1 | 0.02 | 34 |
| TUN + GSO | 878 | 10.8 | 0.9 | 0.01 | 41 |
| TProxy 未绑核 | 689 | 14.3 | 2.7 | 0.11 | 58 |
火焰图用 sing-box 内置 pprof:curl http://127.0.0.1:9090/debug/pprof/profile?seconds=30,go tool pprof -http=:8080 查看。RouterOS 侧镜像抓包:/tool sniffer set filter-ip-address=192.168.1.50/32,配合 /interface ethernet switch 端口镜像到抓包口。
6.5 行业痛点与选型防坑
真假专线:标称「IEPL 专线」实际是公网隧道 + QoS 限速的占多数。识别方法看 RTT 抖动:真专线抖动 <1ms 且丢包 0,伪专线在晚高峰抖动 5~15ms。用 mtr -rwzbc 100 <target> 跑 100 包看第 3 跳后的稳定性。
超售识别:单 IP 标称 1Gbps 但晚高峰实测 200Mbps 且 TCP 重传率 >2%,基本可判定超售。ss -ti 看 retrans 字段。
灰产 IP 污染:廉价机场 IP 段常被 Cloudflare/Google 拉黑,表现为 TLS Alert 40 或 403。用 curl -svo /dev/null https://www.cloudflare.com/cdn-cgi/trace 看返回的 loc 与 warp 字段,异常段直接弃用。
选型原则:出口带宽按实测吞吐而非标称值采购,DNS 与代理分离部署避免单点,管理面永远保留物理直连通道。生产环境的稳定性来自「可回滚 + 可观测」,而非堆砌规则。