📌 1. 问题背景与网络拓扑
最近在调整多节点网络,使用了 Tailscale 组网,并在一台内网的 Ubuntu 服务器上开启了 Subnet Router(子网路由),以便身在外地时能直接通过本地 IP 访问内网设备。
当前的网络拓扑:
- 外网设备(Windows): Tailscale 虚拟 IP
100.116.47.82 - 网关节点(Ubuntu): 局域网 IP
192.168.31.177(开启了 Tailscale Subnet Router,开启了内核转发,装有 1Panel 面板和多个 Docker 容器) - 目标服务器: 局域网 IP
192.168.31.220(内网另一台机器,运行着 PostgreSQL 数据库,端口 5432)
核心诉求: 在 Windows 上通过 Tailscale 隧道,直连 192.168.31.220:5432。
👻 2. 诡异的半残网络现象
在配置好一切后,遇到了一个极其反直觉的现象:
- ICMP 畅通: 在 Windows 上
Ping 192.168.31.220,完美连通,延迟仅 5ms。说明 Tailscale 隧道和底层的三层路由毫无问题。 - 网关到目标畅通: 在 Ubuntu (177) 本机用
nc -zv 192.168.31.220 5432测试,返回open。说明 220 目标服务器的防火墙和数据库配置正常。 - TCP 业务瘫痪: 在 Windows 上用客户端连接 220 的 5432 端口,死活连不上(超时)。
初步抓包诊断: 在 Ubuntu (177) 上执行 tcpdump -i any host 192.168.31.220 and port 5432 -nn,发现只有从 tailscale0 进来的包 (In),没有任何发往物理网卡的包 (Out)。
阶段性结论: 数据包安全到达了 177 网关,但被 177 的内部网络栈“私吞”了,未能进入 FORWARD(转发)流程。
🕵️♂️ 3. 排查深入:内核级“天眼”追踪
既然常规的放行规则(在 FORWARD 链首部强行 ACCEPT)无效,说明包死在了更靠前的位置。为了不靠猜排错,我决定利用 iptables -j LOG 在 Linux Netfilter 的 6 个核心关卡埋下日志探头,追踪这个 TCP 包的完整生命周期。
执行部署探头命令:
# 1. 刚接触到网卡,最原始的状态 (raw 表)
sudo iptables -t raw -I PREROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S1-RAW-PRE: "
# 2. 准备被修改标记 (mangle 表)
sudo iptables -t mangle -I PREROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S2-MGL-PRE: "
# 3. 准备被劫持或映射 (nat 表)
sudo iptables -t nat -I PREROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S3-NAT-PRE: "
# 4. 意外关卡:如果包被劫持给本机,会进入 INPUT 链
sudo iptables -I INPUT 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S-X-INPUT: "
# 5. 正常进入转发通道 (FORWARD 链)
sudo iptables -I FORWARD 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S4-FORWARD: "
# 6. 即将离开物理网卡发往目标 (POSTROUTING 链)
sudo iptables -t nat -I POSTROUTING 1 -p tcp -s 100.116.47.82 -j LOG --log-prefix "S5-NAT-POST: "
随后在 Windows 上再次发起 5432 端口的连接请求,然后在 Ubuntu 提取过滤后的内核日志: sudo dmesg -T | grep -E "S1|S2|S3|S4|S5|S-X" | grep "DPT=5432" | tail -n 6
提取到的震撼日志:
[四 2月 26 21:58:22 2026] S1-RAW-PRE: SRC=100.116.47.82 DST=192.168.31.220 DPT=5432
[四 2月 26 21:58:22 2026] S2-MGL-PRE: SRC=100.116.47.82 DST=192.168.31.220 DPT=5432
[四 2月 26 21:58:22 2026] S3-NAT-PRE: SRC=100.116.47.82 DST=192.168.31.220 DPT=5432
[四 2月 26 21:58:22 2026] S4-FORWARD: SRC=100.116.47.82 DST=192.168.31.247 DPT=5432
[四 2月 26 21:58:22 2026] S5-NAT-POST: SRC=100.116.47.82 DST=192.168.31.247 DPT=5432
日志判断依据与分析:
- 包在
S1、S2、S3阶段,目标地址 (DST) 一直都是我预期的192.168.31.220。 - 但是当包走到
S4-FORWARD(进入转发通道)时,DST突然变成了192.168.31.247! - 铁证如山: 存在一个极度隐蔽的 DNAT(目标地址转换) 规则,在
PREROUTING链的尾部强行把我去往 220 的数据包,篡改地址发送给了局域网里的另一台机器(247)。 - 这也解释了为什么 Ping 能通: 因为这个劫持者只对 TCP 5432 端口感兴趣,ICMP 协议的 Ping 包得以幸免,原路走完了正常转发流程。
💥 4. 揪出元凶:1Panel 端口转发的“贪婪”逻辑
顺藤摸瓜,我直接搜查 nat 表中所有的 DNAT 规则: sudo iptables -t nat -S | grep -E "DNAT|REDIRECT"
结果抓到了 1Panel 面板 生成的一批端口转发规则:
-A 1PANEL_PREROUTING -p tcp -m tcp --dport 3308 -j DNAT --to-destination 192.168.31.131:3306
-A 1PANEL_PREROUTING -p tcp -m tcp --dport 5432 -j DNAT --to-destination 192.168.31.247:5432
...
这条规则翻译成人类语言是:“只要经过我这台机器的 TCP 包,目标端口是 5432 的,一律改写目标 IP 为 247。” 它缺少了一个极其重要的限定条件:限定目标 IP 必须是本机。
作为对比,同机器上的 Docker 端口映射规则是这样的: -A DOCKER ! -i docker0 -p tcp -m tcp --dport 3306 -j DNAT ... 在这之前,Docker 会通过 ADDRTYPE match dst-type LOCAL 进行总闸拦截,确保只有访问宿主机本身的流量才会被 DNAT,完美避开了路过的路由流量。
1Panel 这种“裸奔”的规则,导致充当网关的 177 把所有 Subnet 子网内同名端口的流量全部无差别“绑架”了。
🛠️ 5. 修复过程
直接删除规则会导致原本的端口转发功能失效。为了既满足“访问 177 转发到 247”,又不影响“路过 177 去 220”,我编写了一段 Shell 脚本。
它的逻辑是读取 1Panel 生成的每一条缺陷 DNAT 规则,将其删除,并原地替换为带有 -m addrtype --dst-type LOCAL 的完美限制版本:
sudo iptables-save | grep 'A 1PANEL_PREROUTING' | grep DNAT | while read -r line; do
# 提取原始端口和目标地址
port=$(echo "$line" | grep -oP -- '--dport \K\d+')
dest=$(echo "$line" | grep -oP -- '--to-destination \K\S+')
# 1. 拆除 1Panel 贪婪的旧规则
sudo iptables -t nat -D 1PANEL_PREROUTING -p tcp -m tcp --dport $port -j DNAT --to-destination $dest
# 2. 插入带 LOCAL 限制的新规则
sudo iptables -t nat -A 1PANEL_PREROUTING -p tcp -m tcp --dport $port -m addrtype --dst-type LOCAL -j DNAT --to-destination $dest
echo "✅ 已修复端口 $port 的转发规则 (目标: $dest)"
done
最后,一键拆除之前部署的所有日志探头:
sudo iptables -t raw -D PREROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S1-RAW-PRE: "
sudo iptables -t mangle -D PREROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S2-MGL-PRE: "
sudo iptables -t nat -D PREROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S3-NAT-PRE: "
sudo iptables -D INPUT -p tcp -s 100.116.47.82 -j LOG --log-prefix "S-X-INPUT: "
sudo iptables -D FORWARD -p tcp -s 100.116.47.82 -j LOG --log-prefix "S4-FORWARD: "
sudo iptables -t nat -D POSTROUTING -p tcp -s 100.116.47.82 -j LOG --log-prefix "S5-NAT-POST: "
💡 6. 避坑总结
- ICMP 与 TCP 的差别待遇:网络排错时,Ping 能通只代表底层的三层路由没问题,不代表上层协议没在
nat或mangle表被劫持。 - 警惕透明代理与面板转发:把装有 1Panel、宝塔或软路由代理(如 Mihomo/Clash)的机器作为 Subnet 网关时,极易发生
PREROUTING链的连环翻车。1Panel 现有的端口转发逻辑对做网关的机器非常不友好。 - 定位永远比瞎猜重要:利用
iptables -j LOG在内核各个 Hook 点打日志,是解决 Linux 复杂网络迷路问题的快速分析定位思路。
版权属于:DeepFal
本文链接:https://blog.deepfal.cn/index.php/archives/956/
转载时须注明出处及本声明