三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

NAT 回流问题,Linux 服务器内网服务正常、公网也能访问,但服务器自身访问公网地址超时的解决方法

NAT 回流问题,Linux 服务器内网服务正常、公网也能访问,但服务器自身访问公网地址超时的解决方法

Linux 服务器内网服务正常、公网也能访问,但服务器自身访问公网地址超时的解决方法

一、问题背景

服务器上运行了一个 Web 服务,监听一个内网端口。

网络出口设备配置了公网端口映射:

公网地址:PUBLIC_IP:PUBLIC_PORT ↓ 端口映射 / DNAT ↓ 内网地址:PRIVATE_IP:INNER_PORT

外部浏览器访问:

http://PUBLIC_IP:PUBLIC_PORT/

可以正常打开。

但是,在这台服务器自身访问同一个公网地址时,却无法连接。


二、问题现象

服务器执行:

telnet PUBLIC_IP PUBLIC_PORT

一直停留在:

Trying PUBLIC_IP...

没有连接成功。

但是执行:

ping PUBLIC_IP

却可以正常 ping 通。

也就是说:

ping 公网 IP:正常 TCP 公网端口:服务器自身访问失败

需要注意:

ping 通只能说明 ICMP 网络可达,并不能说明对应的 TCP 端口一定能够访问。


三、确认服务本身是否正常

首先测试服务器本机服务:

curl -v --connect-timeout 5 http://127.0.0.1:INNER_PORT/

如果返回:

Connected to 127.0.0.1 HTTP/1.1 200

说明应用服务本身正常。

继续测试服务器内网地址:

curl -v --connect-timeout 5 http://PRIVATE_IP:INNER_PORT/

如果同样返回:

HTTP/1.1 200

说明:

127.0.0.1:INNER_PORT 正常 PRIVATE_IP:INNER_PORT 正常

如果此时外部浏览器访问:

http://PUBLIC_IP:PUBLIC_PORT/

也能正常访问,那么可以确定:

应用服务正常 内网访问正常 公网端口映射正常

四、问题原因:NAT 回流

正常外部访问链路是:

外网客户端 ↓ PUBLIC_IP:PUBLIC_PORT ↓ 公网路由器 / 防火墙 DNAT ↓ PRIVATE_IP:INNER_PORT ↓ Web 服务

但是服务器自己访问公网地址时,链路变成:

服务器 ↓ PUBLIC_IP:PUBLIC_PORT ↓ 公网出口设备 ↓ 再转回内网 ↓ PRIVATE_IP:INNER_PORT

这种场景通常叫:

NAT Loopback Hairpin NAT NAT Reflection NAT 回流

简单来说就是:

内网服务器通过公网 IP,再绕回去访问自身所在内网中的服务。

如果出口防火墙或者路由器没有配置 NAT Loopback,就可能出现:

外网访问公网地址:正常 内网访问内网地址:正常 服务器访问公网地址:失败

五、不修改程序的解决方法

如果程序中已经固定使用:

http://PUBLIC_IP:PUBLIC_PORT

又不希望修改程序配置,可以直接利用 Linuxiptables OUTPUT DNAT

让服务器自身访问:

PUBLIC_IP:PUBLIC_PORT

时,自动转发为:

PRIVATE_IP:INNER_PORT

最终效果:

服务器程序 ↓ PUBLIC_IP:PUBLIC_PORT ↓ iptables OUTPUT DNAT ↓ PRIVATE_IP:INNER_PORT ↓ Web 服务

程序本身完全不需要修改。


六、添加 iptables DNAT 规则

执行:

iptables -t nat -A OUTPUT \ -p tcp \ -d PUBLIC_IP \ --dport PUBLIC_PORT \ -j DNAT \ --to-destination PRIVATE_IP:INNER_PORT

写成一行:

iptables -t nat -A OUTPUT -p tcp -d PUBLIC_IP --dport PUBLIC_PORT -j DNAT --to-destination PRIVATE_IP:INNER_PORT

注意:

PUBLIC_IP PUBLIC_PORT PRIVATE_IP INNER_PORT

需要替换成自己的实际地址和端口。


七、测试是否生效

执行:

curl -v --connect-timeout 5 http://PUBLIC_IP:PUBLIC_PORT/

如果能够看到类似:

Connected to PUBLIC_IP HTTP/1.1 200

说明 DNAT 已经生效。

也可以测试:

telnet PUBLIC_IP PUBLIC_PORT

如果可以连接,则说明问题已经解决。


八、查看 iptables 规则

执行:

iptables -t nat -L OUTPUT -n -v --line-numbers

正常可以看到类似:

DNAT tcp -- 0.0.0.0/0 PUBLIC_IP tcp dpt:PUBLIC_PORT to:PRIVATE_IP:INNER_PORT

还可以执行:

iptables -t nat -S OUTPUT

查看完整规则。


九、为什么 curl 还是显示公网地址?

即使 DNAT 生效后执行:

curl http://PUBLIC_IP:PUBLIC_PORT/

curl 仍然可能显示:

Connected to PUBLIC_IP

这是正常的。

因为应用程序请求的目标本身还是:

PUBLIC_IP:PUBLIC_PORT

只是数据包进入 Linux 内核网络栈以后,被iptables OUTPUT链修改成了:

PRIVATE_IP:INNER_PORT

因此程序本身不需要感知这个转换过程。

这也是这种方案最大的优点:

代码不用改 程序配置不用改 公网 URL 不用改

十、iptables 规则默认不会永久保存

直接执行:

iptables -t nat -A OUTPUT ...

添加的规则通常只在当前系统运行期间有效。

服务器重启以后,规则可能丢失。

因此还需要设置开机自动添加。


十一、firewalld 和 iptables.service 都没运行的情况

可以先检查:

firewall-cmd --state

如果返回:

not running

再检查:

systemctl status iptables

如果返回:

Unit iptables.service could not be found.

说明当前服务器没有使用iptables.service来保存规则。

这种情况下,可以直接使用 systemd 创建一个开机任务。


十二、创建 systemd 服务

创建:

/etc/systemd/system/local-nat.service

执行:

cat >/etc/systemd/system/local-nat.service <<'EOF' [Unit] Description=Local NAT Loopback DNAT After=network.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/bin/sh -c '/usr/sbin/iptables -t nat -C OUTPUT -p tcp -d PUBLIC_IP --dport PUBLIC_PORT -j DNAT --to-destination PRIVATE_IP:INNER_PORT 2>/dev/null || /usr/sbin/iptables -t nat -A OUTPUT -p tcp -d PUBLIC_IP --dport PUBLIC_PORT -j DNAT --to-destination PRIVATE_IP:INNER_PORT' ExecStop=/bin/sh -c '/usr/sbin/iptables -t nat -C OUTPUT -p tcp -d PUBLIC_IP --dport PUBLIC_PORT -j DNAT --to-destination PRIVATE_IP:INNER_PORT 2>/dev/null && /usr/sbin/iptables -t nat -D OUTPUT -p tcp -d PUBLIC_IP --dport PUBLIC_PORT -j DNAT --to-destination PRIVATE_IP:INNER_PORT || true' [Install] WantedBy=multi-user.target EOF

注意把:

PUBLIC_IP PUBLIC_PORT PRIVATE_IP INNER_PORT

全部替换成自己的实际参数。


十三、为什么使用 iptables -C?

这里没有直接每次都执行:

iptables -A

而是先执行:

iptables -C

它的作用是检查规则是否已经存在。

逻辑是:

规则存在 ↓ 不重复添加 规则不存在 ↓ 执行 iptables -A

这样可以避免每次 systemd 启动时重复添加同一条规则。


十四、启动并设置开机自启

执行:

systemctl daemon-reload

启动:

systemctl start local-nat.service

设置开机自启:

systemctl enable local-nat.service

也可以直接:

systemctl enable --now local-nat.service

十五、检查 systemd 状态

执行:

systemctl status local-nat.service

正常情况下会显示:

Active: active (exited)

这里的:

active (exited)

是正常的。

因为该服务使用:

Type=oneshot

它只需要执行一次 iptables 命令,并不需要长期驻留后台。


十六、确认开机自启

执行:

systemctl is-enabled local-nat.service

如果返回:

enabled

就说明服务器重启以后会自动执行这条 NAT 规则。


十七、最终验证

查看 NAT:

iptables -t nat -L OUTPUT -n -v --line-numbers

然后执行:

curl -v --connect-timeout 5 http://PUBLIC_IP:PUBLIC_PORT/

如果返回:

HTTP/1.1 200

则说明配置完成。


十八、最终网络结构

外部用户访问:

外网客户端 ↓ PUBLIC_IP:PUBLIC_PORT ↓ 公网 NAT ↓ PRIVATE_IP:INNER_PORT ↓ Web 服务

服务器自身访问:

服务器程序 ↓ PUBLIC_IP:PUBLIC_PORT ↓ iptables OUTPUT DNAT ↓ PRIVATE_IP:INNER_PORT ↓ Web 服务

最终实现:

外网仍然使用公网地址 服务器程序仍然使用公网地址 不修改程序 不修改业务配置

十九、排查思路总结

遇到类似问题时,可以依次检查:

curl http://127.0.0.1:INNER_PORT/

再检查:

curl http://PRIVATE_IP:INNER_PORT/

再从真正的外网访问:

http://PUBLIC_IP:PUBLIC_PORT/

如果结果是:

localhost 正常 内网 IP 正常 外部访问公网地址正常 服务器自身访问公网地址失败

那么就应该优先怀疑:

NAT Loopback Hairpin NAT NAT Reflection NAT 回流

而不是一直排查应用、Nginx 或服务端口。


二十、核心命令总结

解决服务器自身访问公网地址的问题:

iptables -t nat -A OUTPUT -p tcp -d PUBLIC_IP --dport PUBLIC_PORT -j DNAT --to-destination PRIVATE_IP:INNER_PORT

本质就是:

服务器访问: PUBLIC_IP:PUBLIC_PORT 自动转换为: PRIVATE_IP:INNER_PORT

如果服务器重启后也需要生效,再通过 systemd 自动执行即可。


总结

本次问题并不是服务异常,也不是公网端口映射失败。

真正的问题是:

Linux 服务器通过公网 IP 访问自身所在内网的服务时,网络出口设备没有处理 NAT 回流。

最终通过:

iptables OUTPUT DNAT + systemd 开机自动执行

解决。

该方案适用于:

程序不能修改访问地址 内外网需要统一使用公网 URL 服务器自身必须访问公网映射地址 出口设备无法方便配置 Hairpin NAT

同时不会影响正常的公网访问。

← 返回列表