有一类网络问题特别折磨人。

程序明明已经起来了:

$ ss -lntp | grep 8080
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("server",pid=2317,fd=8))

服务器自己访问也没问题:

$ curl http://127.0.0.1:8080/health
ok

可换到自己的电脑:

$ curl http://203.0.113.10:8080/health
curl: (28) Connection timed out

这时候很容易陷入一种循环:检查程序、重启程序、再看一遍端口、怀疑云厂商、关防火墙、重启 Docker,最后不知道究竟是哪一步让它突然好了。

问题在于,“8080 端口开着”其实是一句很含糊的话。

ss 看到 LISTEN,只能证明某个 socket 正在这台机器的某个地址上监听。它不能证明公网的数据包能到这台机器,更不能证明数据包经过安全组、系统防火墙、容器网络和反向代理以后,最后还能找到这个 socket。

一条请求真正要走的路,大概是这样的:

你的电脑curl / 浏览器公网与路由请求先得找到服务器云安全组实例外的一道门主机防火墙firewalld / nftables容器 / 代理 / 应用Docker → Nginx/Caddy → socket 任意一层把包丢掉,最里面的程序都不会知道有人来过。

所以排这种问题,我一般不从“再检查一遍配置”开始,而是先问一个更具体的问题:请求现在死在哪一层?

先看报错,不要急着动服务器

下面这几种现象看起来都是“打不开”,但它们提供的信息完全不同。

现象 我首先怀疑什么
Connection timed out 包被丢了:安全组、防火墙、路由、NAT
Connection refused 已经找到目标主机,但目标端口没有可接受连接的监听者,或者被明确拒绝
502 Bad Gateway 已经到达反向代理,代理连接后端时出了问题
404 Not Found TCP 和 HTTP 基本已经通了,接下来该看 Host、路径、路由配置
本机能访问,外部超时 监听地址、安全组、主机防火墙、端口映射优先排查

这一步很重要,因为如果浏览器已经收到了 Nginx 的 502,你再去研究云安全组通常就是浪费时间——请求都已经穿过安全组到 Nginx 了。

反过来,如果外部连接一直超时,应用日志一条请求都没有,反复改业务代码也没什么意义。

LISTEN 后面那个地址,比端口号更重要

先看最靠近程序的一层:socket 到底监听在哪里。

ss -lntp

常见的几种结果:

127.0.0.1:8080
0.0.0.0:8080
192.168.1.20:8080
[::]:8080

它们不是一回事。

同样是 :8080,监听范围可以完全不同 这台 Linux 主机 lo · 127.0.0.1 eth0 · 192.168.1.20 127.0.0.1:8080只接收 loopback 上的连接 192.168.1.20:8080只绑定指定接口地址 0.0.0.0:8080IPv4 的所有本地地址(INADDR_ANY) [::]:8080IPv6 通配地址是否同时接 IPv4 要看系统与程序配置

如果看到的是:

127.0.0.1:8080

那么“服务器上 curl localhost:8080 正常,但别的机器访问不了”一点也不奇怪。程序从来就没打算接收外部接口上的连接。

很多开发服务器默认只绑定 loopback,是为了避免你在本地随手启动一个服务就暴露给整个局域网。到了服务器上,如果确实要让反向代理或其他机器访问,就要根据拓扑改成合适的监听地址,例如:

0.0.0.0:8080

但别把 0.0.0.0 理解成“公网已开放”。它只解决程序愿不愿意接的问题。后面的门还多着。

一层一层地往外测

我习惯从离程序最近的地方开始。

1. 本机 loopback

curl -v http://127.0.0.1:8080/

这里都不通,先别看安全组。程序、监听端口、协议或者本机代理配置至少有一个不对。

如果这是 HTTPS 服务,就别拿 HTTP 去试:

curl -vk https://127.0.0.1:8443/

2. 本机的实际网卡地址

先找地址:

ip addr

然后从服务器自己访问它,例如:

curl -v http://10.0.1.7:8080/

127.0.0.1 能通,10.0.1.7 不通,监听地址就是一个很值得看的地方。

3. 同一网络里的另一台机器

nc -vz 10.0.1.7 8080

或者直接:

curl -v http://10.0.1.7:8080/

如果内网能通,公网不通,范围已经缩小很多:公网入口、NAT、云安全组、公网 IP 绑定这些东西应该排到前面。

这比一上来把所有防火墙全关掉靠谱得多。

云安全组和 Linux 防火墙不是一个东西

这是我见过最多的混淆之一。

以云服务器为例,外部请求通常要先经过云平台提供的安全组,再进入实例,之后才轮到操作系统自己的包过滤规则。

公网客户端云安全组云平台侧规则主机防火墙firewalld / nftables应用两层都允许,包才有机会走到程序。

阿里云 ECS 的安全组入方向规则,本质上会匹配来源地址、协议和目标端口。腾讯云、AWS、Azure 的具体界面不同,思路差不多。

例如你要从办公网络访问 TCP 8080,更合理的做法通常不是:

0.0.0.0/0  TCP  8080  ALLOW

而是尽可能把来源限制到需要访问它的地址段。

到了 Linux 里面,再看本机的规则。

使用 firewalld 的机器可以先看:

firewall-cmd --get-active-zones
firewall-cmd --list-all

如果要查看某个 zone:

firewall-cmd --zone=public --list-all

这里还有个很容易踩的坑:规则加到了哪个 zone?网卡实际又在哪个 zone?

你在 public 里放行了 8080,但公网网卡属于另一个 zone,那条规则当然不会按你的想象生效。

现在很多发行版底层已经是 nftables。需要继续往下确认时,可以看:

nft list ruleset

生产机上别为了排障直接来一句:

systemctl stop firewalld

它确实可能让问题暂时消失,也可能顺手把另一个更大的问题制造出来。排障的目标是找到哪条规则拦了包,不是把门拆了证明门有问题。

Docker 会再加一层

到了 Docker,这句非常值得记住:

EXPOSE 8080 不等于把宿主机的 8080 端口开放出去。

Dockerfile 里的:

EXPOSE 8080

主要是在镜像层面说明容器预计会监听哪个端口。真正把容器端口发布到宿主机,要用 -p/--publish,或者 Compose 里的 ports

例如:

docker run -p 8080:80 nginx

含义是宿主机的 8080 转到容器的 80。

还有一个经常被忽略的细节:

docker run -p 127.0.0.1:8080:80 nginx

这和前一个命令不一样。它明确把宿主机发布地址限制在 127.0.0.1,适合“只让本机 Nginx/Caddy 再反代过去”的场景。

而:

docker run -p 8080:80 nginx

在默认配置下会把发布端口绑定到宿主机的所有地址。Docker 官方文档也特别提醒,这可能让端口对外可达,所以不要把 -p 当成一个无害的内部配置。

先看实际发布结果:

docker ps

例如:

0.0.0.0:8080->80/tcp

和:

127.0.0.1:8080->80/tcp

它们代表的暴露范围完全不同。

还有一个更隐蔽的情况:宿主机端口已经映射正确,但容器里的程序只监听 127.0.0.1

可以进容器看:

docker exec -it myapp ss -lntp

如果服务只监听容器自己的 loopback,发往容器网卡地址的转发流量仍然找不到它。容器内需要被其他容器或端口映射访问的服务,通常应该监听容器的合适接口,而不是只监听 loopback。

有反向代理,就把它当成一个明确的分界点

假设结构是:

Internet -> Nginx :443 -> app :8080

外部能打开 HTTPS,但页面返回:

502 Bad Gateway

这个信息其实很好。

至少客户端到 Nginx 这一段已经通了。现在优先测试 Nginx 到应用:

curl -v http://127.0.0.1:8080/

再检查代理配置里的 upstream 地址到底写了什么。

Docker 场景里尤其要留意:

proxy_pass http://127.0.0.1:8080;

如果 Nginx 自己也在另一个容器里,它的 127.0.0.1 指的是 Nginx 那个容器,不是宿主机,也不是应用容器。

这时候应该按照容器网络的实际结构访问后端,例如使用 Compose service name:

proxy_pass http://app:8080;

如果得到的是 404,则已经更靠上层了。TCP 建连和 HTTP 交互都已经发生,应该继续看:

  • 请求的 Host 是否匹配正确虚拟主机;
  • 路径有没有被代理规则重写;
  • 应用是否真的存在这个 route;
  • 访问的究竟是不是你以为的那个服务。

不要看到“页面打不开”四个字,就把所有问题都归到端口。

tcpdump 是很好用的分界线

前面的东西都看起来正常,但外面还是超时,我会直接抓一下这个端口:

tcpdump -ni any tcp port 8080

然后从外部机器再请求一次。

这时问题往往一下就清楚了。

什么都抓不到

客户端明明发了请求,但服务器完全看不到 SYN。

那问题大概率还在服务器外面:

  • 云安全组;
  • 公网 IP / EIP 绑定;
  • NAT;
  • 路由;
  • 上游防火墙;
  • 你访问的压根不是这台机器。

至少别继续折腾应用进程了。

能看到 SYN,但服务器一直不回

包已经到主机了。

这时系统防火墙、nftables 规则以及更底层的包过滤就值得查。

SYN 进来,马上回 RST

网络已经找到服务器,而且服务器也回应了。

这通常更像目标地址/端口没有对应监听者,或者连接被明确拒绝。

重新确认:

ss -lntp | grep ':8080'

别只看“8080 出现了”,还要看它绑定的地址、网络 namespace 和实际进程。

三次握手完成了

那 TCP 这一层大体已经过关。

接下来应该看 TLS、HTTP、反向代理和应用协议,而不是继续“开端口”。

外部访问失败tcpdump 没有 SYN包没到主机有 SYN / 无响应主机侧过滤优先握手完成TCP 基本正常安全组 / NAT / 路由公网 IP 是否真指向这里firewalld / nftables再查监听 namespaceTLS / HTTP / 代理 / 应用别再继续“开端口”

IPv4 和 IPv6 也别混着看

还有一种问题很像玄学:

同一个域名,有的网络能打开,有的网络打不开;curl 有时成功,有时失败。

先看域名到底解析出了什么:

dig A example.com
dig AAAA example.com

再强制分别测试:

curl -4 -v https://example.com/
curl -6 -v https://example.com/

如果域名有 AAAA 记录,但服务器的 IPv6 路由、安全组或者监听配置并没有真的准备好,客户端走到 IPv6 时就可能出问题。

同理,ss 里看到:

[::]:8080

也不要不加判断地翻译成“IPv4 和 IPv6 全开”。IPv6 wildcard socket 是否同时接受 IPv4-mapped 连接,还与操作系统的 IPV6_V6ONLY 行为及应用设置有关。最省事的办法不是猜,而是:

curl -4 ...
curl -6 ...

分别验证。

我实际会按这个顺序查

如果现在有人丢给我一句:“服务启动了,端口也开了,但访问不了”,我一般按下面这个顺序走。

1. ss -lntp
   ↓
确认端口、监听地址、进程
   ↓
2. curl 127.0.0.1:端口
   ↓
确认应用在本机能不能工作
   ↓
3. curl 本机实际网卡IP:端口
   ↓
确认不是只绑了 loopback
   ↓
4. 看 firewalld / nftables
   ↓
确认主机没把包丢掉
   ↓
5. 看云安全组
   ↓
确认实例外面的入口允许访问
   ↓
6. 如果有 Docker,看 docker ps 和容器内 ss
   ↓
确认宿主机发布地址、端口映射和容器监听
   ↓
7. 从外部 curl / nc
   ↓
8. 还不行就 tcpdump
   ↓
看 SYN 究竟有没有到这台机器
   ↓
9. TCP 通了再看 TLS / Nginx / Caddy / 应用路由

这里的核心不是记住一堆命令。

真正有用的是始终知道:我刚才这一条命令验证的是链路的哪一段。

curl localhost 成功,只能给本机到应用这一段打勾。

安全组放行,只能说明云平台愿意让这个包进实例。

docker ps 显示端口映射,也不能证明容器里的程序监听对了。

Nginx 返回 502,反而证明前半段网络已经走通了不少。

当这些证据一层层叠起来,“端口访问不了”就不再是一个模糊的大问题,而会逐渐缩成某两个节点之间的一小段。

最后通常不需要重启服务器,更不需要把所有防火墙一起关掉。

你只需要找到那个包,看看它走到哪儿不见了。


参考资料