中兴 G7615V2 Hairpin NAT 排查记录
问题背景
内网服务器通过光猫做端口映射后,外网手机流量访问公网域名和端口可以成功,但同一局域网 Wi-Fi 下访问同一个公网域名和端口失败。这个现象不是服务监听问题,也不是普通公网端口映射失败,而是典型的 Hairpin NAT 缺失。
Hairpin NAT 也常被叫作:
NAT Loopback
NAT Reflection
NAT Hairpinning
内网回流
NAT 回环
NAT 回流它解决的是这条路径:
内网客户端 -> 光猫公网 IP:端口 -> 回流到内网服务器而普通端口映射或 DMZ 主要解决的是:
公网客户端 -> 光猫 WAN -> 端口映射/DMZ -> 内网服务器这两类路径不同,所以手机流量能访问并不代表 Wi-Fi 下访问公网域名也能访问。
现象和初步判断
外网测试中,部分端口返回 Connection refused,部分端口成功。服务端主机上查看监听端口:
ss -lntp | grep -E '5666|5667|3000[2-5]'关键现象是主机确实监听了相关端口:
0.0.0.0:5666
0.0.0.0:5667
0.0.0.0:30002
0.0.0.0:30004
0.0.0.0:30005这排除了“服务没有启动”这一类根因。后续通过局域网访问内网 IP 可以访问,手机流量访问公网域名也可以访问,进一步把问题收敛到内网访问公网地址的回流路径。
Windows 侧可用以下命令区分内网直连和公网域名访问:
$ports = 30002,30004,30005,5666,5667
$ports | ForEach-Object {
[PSCustomObject]@{
Port = $_
PublicDomain = Test-NetConnection example.home.domain -Port $_ -InformationLevel Quiet
LanIP = Test-NetConnection 192.168.1.71 -Port $_ -InformationLevel Quiet
}
}如果 LanIP=True 但 PublicDomain=False,排查重点应转向 NAT 回环、内网 DNS 覆写或光猫路由行为。
端口范围映射的误区
光猫网页上的端口范围规则容易误解。界面中“广域网起始端口 / 广域网结束端口 / 虚拟主机端口”看起来像:
公网 30000-30100 -> 内网 30000-30100但实际语义可能是:
公网 30000-30100 -> 内网 30000也就是整个公网端口范围都转发到同一个内部端口。这会导致访问公网 30002、30004、30005 时,实际都落到内网 30000,如果内网 30000 没有服务,就会出现拒绝连接。
更稳的做法是避免使用端口段规则,改成单端口逐条映射:
公网 30002 -> 内网服务器:30002
公网 30004 -> 内网服务器:30004
公网 30005 -> 内网服务器:30005
公网 5666 -> 内网服务器:5666
公网 5667 -> 内网服务器:5667G7615V2 的运行时规则证据
进入 G7615V2 的 Telnet 后,系统环境是 Linux/Buildroot:
uname -a
cat /proc/version
ip route端口映射生成的 NAT 规则类似:
iptables -t nat -S关键证据是端口映射只对公网接口生效:
-A portmapp -d <WAN_IP>/32 -i ppp0 -p tcp --dport 5666 -j DNAT --to-destination 192.168.1.71:5666
-A portmapp -d <WAN_IP>/32 -i ppp0 -p tcp --dport 5667 -j DNAT --to-destination 192.168.1.71:5667这里的 -i ppp0 表示规则只处理从公网口进来的流量。局域网 Wi-Fi/LAN 侧的流量从 br0 进入,内网客户端访问公网域名时不会命中这些 DNAT 规则。因此外网可用、内网访问公网域名不可用是符合规则行为的。
DMZ 也是类似问题。DMZ 可以把外网入站流量默认转给某台内网主机,但它不等于 Hairpin NAT。删除端口映射后只启用 DMZ,依然可能存在同一 Wi-Fi 下访问公网域名失败的问题。
中兴 DB 表检查
G7615V2 的中兴配置库可以用 sendcmd 查看。先列出相关表名:
sendcmd 1 DB all | grep -i -E 'port|nat|dmz|fw|wan|map|loop|hair|upnp'本次重点表包括:
FWPM
FWDMZ
UPnPCfg
UPnPPortMap
LoopbackGlobalConf
LoopbackEthConf
LoopbackVlanConf端口映射表是 FWPM:
sendcmd 1 DB p FWPM它保存的是外部端口、内部主机、内部端口、协议和 WAN 连接等字段,没有 Hairpin/NAT Loopback 开关:
Enable
Protocol
Name
MinExtPort
MaxExtPort
InternalHost
MinIntPort
MaxIntPort
WANIndexDMZ 表是 FWDMZ:
sendcmd 1 DB p FWDMZ它保存 DMZ 是否启用、WAN 连接和内部主机地址,也没有 Hairpin 字段。
名字看起来相关的 Loopback* 表实际不是 NAT 回环,而是二层以太网环路检测。判断依据是字段中出现:
DMACAddr
EtherType
VlanTag
PortDownPolicyEnable
PortID
AlarmEn
PortDisableEn
PortDetectionStatus这些字段用于检测交换网络环路或关闭端口,不处理 公网 IP:端口 -> 内网服务器 的 NAT 回流。
因此,sendcmd 1 DB save 可以保存端口映射、DMZ 等 DB 配置,但不会自动保存手工添加的运行时 iptables Hairpin NAT 规则。
清理 DMZ 的方法
如果网页关闭 DMZ 后仍然残留数据,可以直接清理 FWDMZ 表并刷新运行时链:
sendcmd 1 DB set FWDMZ 0 Enable 0
sendcmd 1 DB set FWDMZ 0 WANCViewName ""
sendcmd 1 DB set FWDMZ 0 WANCName ""
sendcmd 1 DB set FWDMZ 0 InternalHost 0.0.0.0
sendcmd 1 DB set FWDMZ 0 MacEnable 0
sendcmd 1 DB set FWDMZ 0 InternalMacHost 00:00:00:00:00:00
sendcmd 1 DB save清掉当前运行时 DMZ NAT 链:
iptables -t nat -F dmzmapp检查:
sendcmd 1 DB p FWDMZ
iptables -t nat -S dmzmapp
iptables -t nat -L dmzmapp -n -v期望结果是 Enable=0、InternalHost=0.0.0.0,并且 dmzmapp 链里没有额外 DNAT 规则。必要时重启光猫,让运行时规则重新从 DB 生成。
临时实现 Hairpin NAT
Hairpin NAT 需要两类规则:
- DNAT:内网客户端访问公网 IP:端口时,转到内网服务器。
- SNAT/MASQUERADE:让服务器回包经过光猫,避免服务器直接回给内网客户端造成连接状态不匹配。
同端口映射到内网服务器的示例:
WAN_IP="$(ip -4 addr show ppp0 | awk '/inet / {print $2}' | cut -d/ -f1)"
iptables -t nat -I portmapp 1 \
-i br0 \
-s 192.168.1.0/24 \
-d "$WAN_IP/32" \
-p tcp \
-m multiport --dports 5666,5667,5005,30002,30004,30005 \
-j DNAT --to-destination 192.168.1.71
iptables -t nat -I POSTROUTING 1 \
-o br0 \
-s 192.168.1.0/24 \
-d 192.168.1.71/32 \
-p tcp \
-m multiport --dports 5666,5667,5005,30002,30004,30005 \
-j MASQUERADE外部端口和内部端口不同的规则要单独写。例如外部 SSH 端口映射到内网 22:
iptables -t nat -I portmapp 1 \
-i br0 \
-s 192.168.1.0/24 \
-d "$WAN_IP/32" \
-p tcp --dport 27122 \
-j DNAT --to-destination 192.168.1.71:22
iptables -t nat -I POSTROUTING 1 \
-o br0 \
-s 192.168.1.0/24 \
-d 192.168.1.71/32 \
-p tcp --dport 22 \
-j MASQUERADE验证规则是否命中:
iptables -t nat -L portmapp -n -v --line-numbers
iptables -t nat -L POSTROUTING -n -v --line-numbers如果计数器增长,说明内网访问公网域名已经命中回流规则。
DMZ 全端口回流测试
后续尝试过在 DMZ 模式下不指定端口,直接让内网访问光猫公网 IP 的所有 TCP/UDP 流量回流到 DMZ 主机。测试规则如下:
WAN_IP="$(ip -4 addr show ppp0 | awk '/inet / {print $2}' | cut -d/ -f1)"
LAN_NET="192.168.1.0/24"
DMZ_IP="192.168.1.71"
iptables -t nat -C dmzmapp \
-i br0 \
-s "$LAN_NET" \
-d "$WAN_IP/32" \
-p tcp \
-j DNAT --to-destination "$DMZ_IP" 2>/dev/null || \
iptables -t nat -I dmzmapp 1 \
-i br0 \
-s "$LAN_NET" \
-d "$WAN_IP/32" \
-p tcp \
-j DNAT --to-destination "$DMZ_IP"
iptables -t nat -C POSTROUTING \
-o br0 \
-s "$LAN_NET" \
-d "$DMZ_IP/32" \
-p tcp \
-j MASQUERADE 2>/dev/null || \
iptables -t nat -I POSTROUTING 1 \
-o br0 \
-s "$LAN_NET" \
-d "$DMZ_IP/32" \
-p tcp \
-j MASQUERADE
iptables -t nat -C dmzmapp \
-i br0 \
-s "$LAN_NET" \
-d "$WAN_IP/32" \
-p udp \
-j DNAT --to-destination "$DMZ_IP" 2>/dev/null || \
iptables -t nat -I dmzmapp 1 \
-i br0 \
-s "$LAN_NET" \
-d "$WAN_IP/32" \
-p udp \
-j DNAT --to-destination "$DMZ_IP"
iptables -t nat -C POSTROUTING \
-o br0 \
-s "$LAN_NET" \
-d "$DMZ_IP/32" \
-p udp \
-j MASQUERADE 2>/dev/null || \
iptables -t nat -I POSTROUTING 1 \
-o br0 \
-s "$LAN_NET" \
-d "$DMZ_IP/32" \
-p udp \
-j MASQUERADE执行后检查计数器:
iptables -t nat -L dmzmapp -n -v --line-numbers
iptables -t nat -L POSTROUTING -n -v --line-numbers关键输出特征是:
Chain dmzmapp
DNAT udp -- br0 ... <WAN_IP> to:192.168.1.71 pkts 0
DNAT tcp -- br0 ... <WAN_IP> to:192.168.1.71 pkts 22
DNAT all -- ppp0 ... <WAN_IP> to:192.168.1.71
Chain POSTROUTING
MASQUERADE udp -- * br0 192.168.1.0/24 192.168.1.71 pkts 0
MASQUERADE tcp -- * br0 192.168.1.0/24 192.168.1.71 pkts 0这个结果说明:内网访问公网 IP 的 TCP 包已经进入 dmzmapp 并命中 br0 入口 DNAT,但没有继续走到 POSTROUTING MASQUERADE。因此失败点不是“没有命中 DNAT”,而是 DNAT 之后没有完成常规转发和源地址转换。
进一步加过 FORWARD 放行规则:
iptables -I FORWARD 1 \
-i br0 \
-o br0 \
-s 192.168.1.0/24 \
-d 192.168.1.71 \
-j ACCEPT
iptables -I FORWARD 1 \
-i br0 \
-o br0 \
-s 192.168.1.71 \
-d 192.168.1.0/24 \
-m state --state RELATED,ESTABLISHED \
-j ACCEPT检查 FORWARD 后,新加的两条规则计数仍为 0:
Chain FORWARD
ACCEPT all -- br0 br0 192.168.1.71 192.168.1.0/24 state RELATED,ESTABLISHED pkts 0
ACCEPT all -- br0 br0 192.168.1.0/24 192.168.1.71 pkts 0因此 FORWARD 不是被规则拒绝后失败,而是这类 DMZ 回流包没有进入普通 br0 -> br0 转发链。对这台 G7615V2 来说,DMZ 模式下的全端口 Hairpin NAT 不能只靠在 dmzmapp 添加 DNAT 和在 POSTROUTING 添加 MASQUERADE 完成。
这个测试不影响一个已确认事实:手机流量从外网访问 DMZ 主机可以成功,公网入口是通的。失败范围只限于同一局域网内访问公网域名/公网 IP 的回流路径。
持久化判断
本机检查到 /etc/init.d/rcS 会挂载 /userconfig,但没有现成的用户脚本钩子:
. /etc/default/S01userconfig
for i in /etc/rcS.d/S??*
do
...
done/userconfig 看起来是持久化分区,适合放脚本;但是否能让启动流程自动执行,还取决于 /etc/rcS.d 是否可写且重启后保留。
可检查:
ls -l /etc/rcS.d
mount
df -h如果 /etc/rcS.d 可写且持久化,可以考虑:
/userconfig/cfg/hairpin_nat.sh
/etc/rcS.d/S99hairpin如果 /etc 不持久化,更稳的方案是在内网服务器上用 expect 或其他自动化方式定时 Telnet 登录光猫补规则。这样不改光猫系统文件,重启或重拨后也可以自动恢复。
更稳的替代方案:内网 DNS 覆写
如果目标只是让家里 Wi-Fi 下访问同一个域名,内网 DNS 覆写通常比改光猫 NAT 更稳:
example.home.domain -> 192.168.1.71这样:
外网访问域名 -> 解析到公网 IP
内网访问域名 -> 解析到内网服务器 IP可以在路由器、AdGuard Home、Pi-hole、OpenWrt dnsmasq 或 Windows hosts 中配置。
Windows 临时验证:
C:\Windows\System32\drivers\etc\hosts加入:
192.168.1.71 example.home.domain刷新 DNS 并测试:
ipconfig /flushdns
Resolve-DnsName example.home.domain
Test-NetConnection example.home.domain -Port 30002如果解析成内网 IP 后可访问,说明公网映射和服务都没有问题,缺失的是光猫的 NAT 回流能力。
结论
这次排查的核心结论是:中兴 G7615V2 当前网页端口映射和 DMZ 只覆盖公网入口,不自动生成内网回流规则。手机流量可以访问而同 Wi-Fi 访问公网域名失败,是 Hairpin NAT 缺失的典型表现。
FWPM 和 FWDMZ 可以通过 sendcmd 1 DB save 保存,但它们没有 NAT Loopback 字段;Loopback* 表是二层环路检测,不是 NAT 回环。DMZ 全端口回流测试中,dmzmapp 的 br0 DNAT 计数会增长,但 FORWARD 和 POSTROUTING MASQUERADE 计数不增长,说明这台设备没有按普通 Linux 路由链路完成 DMZ Hairpin NAT。
要解决这个问题,有三条实际路线:
- 用
iptables临时添加br0入口 DNAT 和POSTROUTING MASQUERADE,再通过启动脚本或外部自动化持久化。 - 做内网 DNS 覆写,让内网访问域名时直接解析到内网服务器。
- 长期上把光猫桥接给更完整的路由器或 OpenWrt/软路由处理拨号、NAT、防火墙和回流。
刷固件不应作为优先解法。运营商光猫固件存在 PON 注册、区域配置、Web/Telnet 权限和救砖风险,而且新固件也不保证补齐 Hairpin NAT。