PO0 到 RFC 再到多落地:一次 nftables 转发链路学习记录
这篇是一次网络转发配置的学习记录。目标不是公开一份可直接照抄的生产配置,而是把「客户端只连入口机,入口机再转到中继机,中继机再转到多台落地机」这条链路里的关键概念整理清楚。文中的 IP 使用 192.0.2.0/24、198.51.100.0/24、203.0.113.0/24 这些文档保留地址,端口和机器名也都做了替换。
这篇在整个系列里更偏实践复盘:默认你已经知道 DNAT、SNAT、masquerade 和 iptables 主机防火墙的大概分工。基础概念可以先看后面拆出来的两篇:
背景
最开始我理解的 IP 中转非常简单:
客户端 -> 中转机:入口端口 -> 落地机:服务端口中转机上写一组 nftables 规则,入口端口做 DNAT 到落地机,再用 SNAT 或 masquerade 保证回包回到中转机。这个模型能跑起来,但当落地机变多、协议从 Snell 切到 SS2022、客户端希望只记一个入口地址时,就需要更清晰的分层。
这次整理后的结构是:
这里的 PO0 和 RFC 都是化名。它们不解析代理协议,只做四层转发。真正的 SS2022 服务运行在 RFC 本机和多台落地机上。
脱敏后的地址规划
为了避免泄露真实环境,下面统一使用占位信息。
| 角色 | 示例地址 | 作用 |
|---|---|---|
| PO0 公网地址 | 203.0.113.10 | 客户端唯一填写的服务器地址 |
| PO0 到 RFC 的内网地址 | 10.0.0.10 | PO0 转发到 RFC 时使用的源地址 |
| RFC 公网地址 | 198.51.100.10 | 二级中继,也是落地机看到的来源 |
| 落地 1 | 192.0.2.11 | SS2022 服务 |
| 落地 2 | 192.0.2.12 | SS2022 服务 |
| 落地 3 | 192.0.2.13 | SS2022 服务 |
| 落地 4 | 192.0.2.14 | SS2022 服务 |
端口也用示例值:
| 线路 | 客户端连接地址 | 客户端连接端口 | 最终服务 |
|---|---|---|---|
| RFC 直出 | 203.0.113.10 | 50000 | RFC 本机 50000 |
| 落地 1 | 203.0.113.10 | 51001 | 192.0.2.11:51001 |
| 落地 2 | 203.0.113.10 | 51002 | 192.0.2.12:51002 |
| 落地 3 | 203.0.113.10 | 51003 | 192.0.2.13:51003 |
| 落地 4 | 203.0.113.10 | 51004 | 192.0.2.14:51004 |
核心原则是:客户端永远只填写 PO0 的公网地址。后面 RFC 和落地机怎么调整,不应该暴露给客户端。
SS2022 服务层
SS2022 服务只需要跑在真正的服务节点上,也就是 RFC 本机和各个落地机。PO0 不需要安装代理服务。
关键配置大概长这样:
{ "server": "0.0.0.0", "server_port": 51001, "password": "REPLACE_WITH_RANDOM_SECRET", "method": "2022-blake3-aes-128-gcm", "mode": "tcp_and_udp"}这里有三个点容易漏:
- 每条线路的
password要对应最终服务节点自己的密码,不是 PO0 或 RFC 的统一密码。 mode要确认支持tcp_and_udp,否则 UDP relay 后面一定会出问题。- SS2022 有时间戳校验,服务端时间不同步时,可能表现为 TCP 连接建立后立刻被断开。
PO0 第一跳转发
PO0 做的事情只有一件:把客户端访问 PO0 的端口转到 RFC,并把源地址改成 PO0 到 RFC 这条路径上的内网地址。
转发方向:
PO0:50000 -> RFC:50000PO0:51001 -> RFC:51001PO0:51002 -> RFC:51002PO0:51003 -> RFC:51003PO0:51004 -> RFC:51004先打开内核转发:
apt updateapt install -y nftables conntrack tcpdumpsystemctl enable --now nftables
cat <<'EOF' > /etc/sysctl.d/99-po0-forward.confnet.ipv4.ip_forward = 1net.ipv4.tcp_mtu_probing = 1net.netfilter.nf_conntrack_max = 65536EOF
sysctl --system再写 nftables。这里不用真实 IP,全部是示例值:
#!/usr/sbin/nft -f
define WAN_IF = "eth0"define PO0_LAN_IP = 10.0.0.10define RFC_IP = 198.51.100.10
define PORT_DIRECT = 50000define PORT_LAND1 = 51001define PORT_LAND2 = 51002define PORT_LAND3 = 51003define PORT_LAND4 = 51004
destroy table inet ss2022_po0_mangledestroy table ip ss2022_po0_natdestroy table inet ss2022_po0_filter
table inet ss2022_po0_mangle { chain forward { type filter hook forward priority -150; policy accept;
ip daddr $RFC_IP tcp dport { $PORT_DIRECT, $PORT_LAND1, $PORT_LAND2, $PORT_LAND3, $PORT_LAND4 } tcp flags syn tcp option maxseg size set 1360 }}
table ip ss2022_po0_nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept;
iifname $WAN_IF meta l4proto { tcp, udp } th dport $PORT_DIRECT dnat to $RFC_IP:$PORT_DIRECT iifname $WAN_IF meta l4proto { tcp, udp } th dport $PORT_LAND1 dnat to $RFC_IP:$PORT_LAND1 iifname $WAN_IF meta l4proto { tcp, udp } th dport $PORT_LAND2 dnat to $RFC_IP:$PORT_LAND2 iifname $WAN_IF meta l4proto { tcp, udp } th dport $PORT_LAND3 dnat to $RFC_IP:$PORT_LAND3 iifname $WAN_IF meta l4proto { tcp, udp } th dport $PORT_LAND4 dnat to $RFC_IP:$PORT_LAND4 }
chain postrouting { type nat hook postrouting priority srcnat; policy accept;
ip daddr $RFC_IP meta l4proto { tcp, udp } th dport { $PORT_DIRECT, $PORT_LAND1, $PORT_LAND2, $PORT_LAND3, $PORT_LAND4 } snat to $PO0_LAN_IP }}
table inet ss2022_po0_filter { chain forward { type filter hook forward priority 0; policy drop;
ct state invalid drop ct state established,related accept
ct status dnat ip daddr $RFC_IP meta l4proto { tcp, udp } th dport { $PORT_DIRECT, $PORT_LAND1, $PORT_LAND2, $PORT_LAND3, $PORT_LAND4 } ct state new accept }}加载前先检查语法:
nft -c -f /etc/nftables.confnft -f /etc/nftables.confnft list ruleset如果系统的 nftables 不支持 destroy table,不要把 2>/dev/null 这种 shell 重定向写进 nft 配置文件。正确做法是在 shell 里先删表:
nft delete table inet ss2022_po0_mangle 2>/dev/null || truenft delete table ip ss2022_po0_nat 2>/dev/null || truenft delete table inet ss2022_po0_filter 2>/dev/null || truenft -c -f /etc/nftables.confnft -f /etc/nftables.confRFC 第二跳转发
RFC 的职责有两部分:
50000端口由 RFC 本机 SS2022 服务直接处理。51001到51004继续转发到不同落地机。
转发方向:
RFC:51001 -> 192.0.2.11:51001RFC:51002 -> 192.0.2.12:51002RFC:51003 -> 192.0.2.13:51003RFC:51004 -> 192.0.2.14:51004RFC 同样要打开 IPv4 转发:
apt updateapt install -y nftables conntrack tcpdumpsystemctl enable --now nftables
cat <<'EOF' > /etc/sysctl.d/99-rfc-forward.confnet.ipv4.ip_forward = 1net.ipv4.tcp_mtu_probing = 1net.netfilter.nf_conntrack_max = 65536EOF
sysctl --systemRFC 的 nftables 模板:
#!/usr/sbin/nft -f
define OUT_IF = "eth0"define PO0_LAN_IP = 10.0.0.10
define LAND1_IP = 192.0.2.11define LAND1_PORT = 51001
define LAND2_IP = 192.0.2.12define LAND2_PORT = 51002
define LAND3_IP = 192.0.2.13define LAND3_PORT = 51003
define LAND4_IP = 192.0.2.14define LAND4_PORT = 51004
destroy table inet ss2022_rfc_mangledestroy table ip ss2022_rfc_natdestroy table inet ss2022_rfc_filter
table inet ss2022_rfc_mangle { chain forward { type filter hook forward priority -150; policy accept;
ip daddr $LAND1_IP tcp dport $LAND1_PORT tcp flags syn tcp option maxseg size set 1360 ip daddr $LAND2_IP tcp dport $LAND2_PORT tcp flags syn tcp option maxseg size set 1360 ip daddr $LAND3_IP tcp dport $LAND3_PORT tcp flags syn tcp option maxseg size set 1360 ip daddr $LAND4_IP tcp dport $LAND4_PORT tcp flags syn tcp option maxseg size set 1360 }}
table ip ss2022_rfc_nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept;
ip saddr $PO0_LAN_IP meta l4proto { tcp, udp } th dport $LAND1_PORT dnat to $LAND1_IP:$LAND1_PORT ip saddr $PO0_LAN_IP meta l4proto { tcp, udp } th dport $LAND2_PORT dnat to $LAND2_IP:$LAND2_PORT ip saddr $PO0_LAN_IP meta l4proto { tcp, udp } th dport $LAND3_PORT dnat to $LAND3_IP:$LAND3_PORT ip saddr $PO0_LAN_IP meta l4proto { tcp, udp } th dport $LAND4_PORT dnat to $LAND4_IP:$LAND4_PORT }
chain postrouting { type nat hook postrouting priority srcnat; policy accept;
oifname $OUT_IF ip daddr $LAND1_IP meta l4proto { tcp, udp } th dport $LAND1_PORT masquerade oifname $OUT_IF ip daddr $LAND2_IP meta l4proto { tcp, udp } th dport $LAND2_PORT masquerade oifname $OUT_IF ip daddr $LAND3_IP meta l4proto { tcp, udp } th dport $LAND3_PORT masquerade oifname $OUT_IF ip daddr $LAND4_IP meta l4proto { tcp, udp } th dport $LAND4_PORT masquerade }}
table inet ss2022_rfc_filter { chain forward { type filter hook forward priority 0; policy drop;
ct state invalid drop ct state established,related accept
ip saddr $PO0_LAN_IP ip daddr $LAND1_IP meta l4proto { tcp, udp } th dport $LAND1_PORT ct state new accept ip saddr $PO0_LAN_IP ip daddr $LAND2_IP meta l4proto { tcp, udp } th dport $LAND2_PORT ct state new accept ip saddr $PO0_LAN_IP ip daddr $LAND3_IP meta l4proto { tcp, udp } th dport $LAND3_PORT ct state new accept ip saddr $PO0_LAN_IP ip daddr $LAND4_IP meta l4proto { tcp, udp } th dport $LAND4_PORT ct state new accept }}这层配置里最重要的是 ip saddr $PO0_LAN_IP。它让 RFC 只处理来自 PO0 的二级转发流量,避免 RFC 公网端口被直接当成入口使用。
落地机只放行 RFC 来源
落地机运行真正的 SS2022 服务。为了减少暴露面,入口规则尽量只放行 RFC 来源。
以落地 1 为例:
RELAY_PUBLIC_IP="198.51.100.10"PORT="51001"
iptables -I INPUT 1 -s "${RELAY_PUBLIC_IP}/32" -p tcp --dport "$PORT" -j ACCEPTiptables -I INPUT 1 -s "${RELAY_PUBLIC_IP}/32" -p udp --dport "$PORT" -j ACCEPT如果机器默认策略不是 DROP,可以按自己的防火墙结构追加拒绝规则。重点是不要为了排障长期全网开放服务端口。
保存规则:
apt install -y iptables-persistentnetfilter-persistent save这里要注意一个分工:
netfilter-persistent save保存的是 iptables/ip6tables 规则。/etc/nftables.conf里的自定义 nftables 转发表需要由nftables服务加载。- 如果混用 iptables-nft 和 nftables,不要随手
nft flush ruleset,它可能把 iptables-nft 生成的表也清掉。
主机防火墙支撑
这条实践链路里,nftables 负责 PO0 和 RFC 的 DNAT、SNAT、masquerade;iptables/ip6tables 则负责每台机器自己的 INPUT/OUTPUT 防护,比如 SSH 限速、服务端口来源限制、DNS 入站阻断、邮件端口出站阻断。
我在这篇里只保留和实践直接相关的三条原则:
- 落地机服务端口尽量只放行 RFC 来源。
- PO0/RFC 上不要把 53、5353、5355 这类 DNS/mDNS/LLMNR 入口暴露到公网。
netfilter-persistent save只保存 iptables/ip6tables,不负责保存/etc/nftables.conf里的自定义 nft 表。
完整的 SSH 和 iptables/ip6tables 加固整理单独放在:VPS 安全加固教程。
与单跳中转的关系
这篇的 PO0 -> RFC -> 多落地,本质上是单跳中转的扩展版。单跳中转只有一层:
客户端 -> 中转机 -> 落地机这次实践变成两层:
客户端 -> PO0 -> RFC -> 落地机基础的 DNAT、forward、masquerade 模型没有变,只是 PO0 和 RFC 各做了一次。单跳中转的基础笔记我拆到了:IP 中转学习:从 DNAT、SNAT 到 nftables 单跳转发。
验证顺序
不要一上来就看客户端报错。链路长了以后,最好按跳排查。
先从外部测 PO0 入口端口:
nc -vz 203.0.113.10 50000nc -vz 203.0.113.10 51001nc -vz 203.0.113.10 51002nc -vz 203.0.113.10 51003nc -vz 203.0.113.10 51004PO0 上抓到 RFC 的流量:
tcpdump -ni any "host 198.51.100.10 and (port 50000 or port 51001 or port 51002 or port 51003 or port 51004)"RFC 上抓 PO0 过来的流量:
tcpdump -ni any "host 10.0.0.10 and (port 50000 or port 51001 or port 51002 or port 51003 or port 51004)"RFC 上抓去落地机的流量:
tcpdump -ni any "host 192.0.2.11 and port 51001"tcpdump -ni any "host 192.0.2.12 and port 51002"tcpdump -ni any "host 192.0.2.13 and port 51003"tcpdump -ni any "host 192.0.2.14 and port 51004"查看 conntrack:
conntrack -L | grep -E "198.51.100.10|192.0.2.11|192.0.2.12|192.0.2.13|192.0.2.14"还有一个容易误判的点:NAT 转发端口不一定出现在 ss -lntup 里。因为它不是某个进程在监听,而是内核在 prerouting 阶段改包。
SS2022 的 invalid timestamp 排障
这次最有价值的排障经验是:Socket closed by remote peer 不一定是端口没开。
当 RFC 能抓到包,而且 TCP 三次握手完成,但服务端随后返回 RST,就要去看 ss-rust 日志:
journalctl -u ss-rust.service -n 100 --no-pager如果日志里出现类似:
invalid timestamp client_timestamp - now server_timestamp = 31这说明 SS2022 的时间戳校验失败。客户端和服务端时间差过大时,服务端会拒绝握手。
修复方向是先同步系统时间:
apt updateapt install -y chronysystemctl enable --now chronychronyc makestepchronyc trackingtimedatectlhwclock --systohcsystemctl restart ss-rust.service排障顺序可以固定成这个模板:
| 现象 | 优先怀疑 |
|---|---|
| PO0 收到连接,RFC 抓不到包 | PO0 nftables、路由、ip_forward、防火墙 |
| RFC 抓到 SYN,但服务端没响应 | RFC 服务未监听或端口写错 |
| RFC 抓到 payload 后返回 RST | SS2022 协议层拒绝,查 method、password、时间戳 |
| TCP 正常但 UDP 超时 | PO0、RFC、落地机任意一层漏了 UDP |
| 外部能连端口但客户端不可用 | 密码、method、mode、NTP、落地机防火墙 |
安全和维护注意点
这类配置最容易踩的坑不是语法,而是维护边界不清楚。
- 改防火墙前先备份
iptables-save、ip6tables-save和nft list ruleset。 - 远程改规则时用
tmux,并准备一个几分钟后的自动回滚。 - 不要关闭当前 SSH 会话,先开新会话验证还能登录。
- nft 配置写完先
nft -c -f,语法检查通过再加载。 flush ruleset只适合明确知道整台机器由 nftables 独占管理的场景。- 公网 DNS、mDNS、LLMNR、邮件端口这类容易被滥用的入口和出口要单独处理。
- 入口服务如果只有自有上游会访问,尽量用 IP 或 ipset 白名单限制来源。
- UDP 不要只在客户端里打开,PO0、RFC、落地机三层都要放。
参考文档
这篇实践的公开参考只保留 Astro 内部文章和外部网页:
最后总结
这次我真正学到的是,nftables 转发不是“写一条 dnat 就完了”。一条稳定的链路至少要同时回答四个问题:
- 包从哪里进来,在哪一层 DNAT。
- 回包如何回到原路,在哪里 SNAT 或 masquerade。
- 防火墙 forward 链是否允许新连接和回包。
- 服务协议本身是否正常,比如 SS2022 的 method、password、UDP mode 和时间戳。
用一句话概括:
PO0 负责统一入口和第一跳 SNAT。RFC 负责本机服务和多落地二级分发。落地机只运行真实服务,并尽量只放行 RFC 来源。客户端永远只连接 PO0。理解这个分工以后,再看单跳中转、多跳转发、realm 前置、iptables 和 nftables 混用,就不会都糊成一团了。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!