1. ping 通不等于端口能连,先把这两件事分开看
“服务器能 ping 通,但 telnet 端口就是不通”——这几乎是我在群里、工单里见到重复率最高的一类网络问题。很多人第一反应是“网络没问题啊,能 ping 通”,然后就开始怀疑网线、怀疑交换机、怀疑运营商,绕了一大圈最后发现是 firewalld 里少写了一条--add-port。问题的根源在于,ping 和 telnet 验证的根本不是同一条通路,把它们混为一谈,排查方向从第一步就跑偏了。
ping走的是 ICMP 协议,它属于网络层,做的事情非常朴素:发一个 ICMP Echo Request,对方回一个 Echo Reply,收到就说明这台机器的 IP 层面可达、路由正常、中间设备没有把你的包丢掉。而telnet ip 端口走的是 TCP 协议,它要完成一次完整的三次握手:SYN 发出去、对方回 SYN+ACK、你再回 ACK。只要中间任何一个环节把 TCP 包拦了、丢了、拒了,握手就完不成,telnet 就会失败。ICMP 能过不代表 TCP 能过,这是两套完全独立的策略体系,防火墙里也常常是分开配置的。
所以当你遇到“能 ping 通、telnet 不通”的时候,正确的心理模型不是“网络不通”,而是“目标主机的某个 TCP 端口没有被放行,或者根本没有服务在监听”。这个判断一旦立住,后面的排查就变成了一道有明确层级的选择题:是服务没起、是监听地址不对、是本机防火墙拦了、是云安全组拦了,还是中间还有一台设备做了 ACL。我下面会按这个顺序一层层拆。
适合读这篇的人,大概分三类:刚接手 Linux 服务器、第一次遇到端口不通的新手;被 firewalld 和 iptables 的--permanent坑过一次、想彻底搞明白的人;还有在云主机上折腾了半天,最后发现是安全组没开的人。不管你属于哪一类,下面的内容都可以直接对着敲。
2. 用 ss 和 nc 把问题锁死在某一层
2.1 第一步永远是看本机有没有在监听
在动防火墙之前,先做一件最容易被跳过、但收益最高的事:在目标服务器上确认端口到底有没有被监听。很多人上来就firewall-cmd --add-port,结果服务压根没起来,白折腾半小时。
ss -lntp | grep 8080 # 或者老系统上用 netstat -lntp | grep 8080ss的参数含义值得记一下:-l只看 LISTEN 状态,-n不做端口到服务名的反向解析(直接用数字,避免看到http而不是80这种干扰),-t只看 TCP,-p显示占用端口的进程。这条命令如果没有输出,说明没有任何进程在这个端口上监听,那么 telnet 不通是必然的,跟防火墙一点关系都没有,先去查服务日志。
如果有输出,你还要多看一列:监听地址。
LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=1234,fd=56)) LISTEN 0 128 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=1234,fd=56))这两行的差别是决定性的,我在 2.2 单独讲。
2.2 监听地址是 127.0.0.1 还是 0.0.0.0,这是分水岭
0.0.0.0:8080表示监听所有网卡,外部能连;127.0.0.1:8080表示只监听本地回环,外部永远连不上,哪怕你把防火墙全关了也没用。这是“ping 通但 telnet 不通”里第二高频的原因,仅次于防火墙。
典型踩坑案例我列几个,都是真实遇到过的:
| 服务 | 配置项 | 默认值 | 现象 |
|---|---|---|---|
| Redis | bind | 127.0.0.1 | 本机 redis-cli 正常,外部 telnet 6379 拒绝 |
| MySQL | bind-address | 127.0.0.1 | 应用服务器连不上 3306 |
| Tomcat | address(Connector) | 不写就是全监听 | 一般没这个问题 |
| Nginx | listen | 80(等于 0.0.0.0) | 一般没这个问题 |
| 自己写的 Python 服务 | app.run(host='127.0.0.1') | 本地 | 外部必然不通 |
改法很直接:Redis 把bind 127.0.0.1改成bind 0.0.0.0(生产环境建议配合防火墙和密码,不要裸奔),MySQL 把bind-address = 0.0.0.0,Flask 的host改成0.0.0.0。改完重启服务,再ss看一次,确认监听地址变了。
注意:
0.0.0.0不是“某个地址”,它是“本机所有 IPv4 地址”的通配写法。看到这个才说明服务对外的门是开着的。
2.3 telnet 的三种报错分别指向哪里
telnet失败时的提示语,信息量其实很大,只是很多人不看:
Connection refused:TCP 包到了目标主机,但对方回了 RST。这说明网络是通的,是本机拒绝的。常见原因:服务没起、端口没监听、防火墙用了--reject-with tcp-reset、监听在 127.0.0.1。这个报错反而是好事,说明不用去查中间网络。Connection timed out或一直卡在 Trying 不动:SYN 发出去了但没有回应,包被**静默丢弃(DROP)**了。典型场景是防火墙默认策略是 DROP、云安全组没放行、或者中间有设备丢包。No route to host:路由不可达或对方主机直接不可达,通常是路由表、网关、或者对方机器 down 了。这种情况 ping 一般也不通。
把报错和这三类原因对上,你就能快速判断该往哪个方向查,而不是盲目地把所有防火墙命令都敲一遍。
2.4 一份可以直接照着走的分层排查顺序
我把顺序整理成下面这张表,从下往上查,命中最快:
| 顺序 | 检查位置 | 命令 | 不通时的动作 |
|---|---|---|---|
| 1 | 服务是否监听 | ss -lntp | grep 端口 | 启动服务,查日志 |
| 2 | 监听地址 | 同上,看是 0.0.0.0 还是 127.0.0.1 | 改配置为 0.0.0.0 |
| 3 | 本机自测 | telnet 127.0.0.1 端口 | 本机都不通,服务有问题 |
| 4 | 本机防火墙 | firewall-cmd --list-all | 放行端口 |
| 5 | SELinux | getenforce | 加端口标签或调策略 |
| 6 | 云安全组 | 控制台入方向规则 | 加规则 |
| 7 | 中间设备 | tcpdump抓包 | 找网络同学查 ACL |
第 3 步单独拎出来说一句:先在本机 telnet 127.0.0.1,通不过就是服务自己的问题,压根不用碰防火墙。这个动作能省掉一大半无谓的排查。
3. firewalld 放行端口的正确写法与反直觉细节
3.1 --permanent 和 --reload 为什么必须成对出现
CentOS 7 之后默认的防火墙是 firewalld,它的规则管理有个设计上的“坑”,我第一次用的时候也栽过:
# 这种写法立即生效,但重启后消失 firewall-cmd --add-port=8080/tcp # 这种写法写进了配置文件,但当前不生效 firewall-cmd --permanent --add-port=8080/tcp # 正确姿势:两条一起用,最后 reload firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload原因在于 firewalld 有**运行时配置(runtime)和永久配置(permanent)**两套。不带--permanent的命令改的是内存里的运行时规则,马上生效但重启丢失;带--permanent的命令改的是/etc/firewalld/zones/下的 XML 文件,持久但不会自动同步到运行时。--reload这个动作做的事情就是:把永久配置重新加载成运行时配置。
我踩过的具体坑是这样的:先敲了--permanent --add-port,然后直接 telnet 测试,不通,于是以为命令写错了,又敲一遍,还是不通,最后才发现忘了 reload。所以我的习惯是永远两条一起敲,再 reload,然后用--list-ports确认:
firewall-cmd --list-ports firewall-cmd --list-all--list-all的信息更全,能看到当前默认 zone、绑定的网卡、开放的服务和端口、以及富规则,是排查时的第一手资料。
3.2 zone 选错:规则写了但就是不生效
firewalld 是按zone(区域)组织规则的,每个网卡会绑定到某个 zone,默认通常是public。如果你在不带--zone参数时执行命令,操作的是默认 zone;但如果你手动把网卡绑到了别的 zone,命令就写到了错的地方。
# 先看网卡实际绑在哪个 zone firewall-cmd --get-active-zones # 输出类似 # public # interfaces: eth0 # 再针对这个 zone 放行 firewall-cmd --zone=public --permanent --add-port=8080/tcp firewall-cmd --reload明确的建议是:所有命令都显式带上--zone=public,哪怕默认就是 public。这样写有两个好处,一是不会因为默认 zone 被改过而写错地方,二是复制给别人时不会产生歧义。
还有一个细节:--add-port必须带协议后缀,8080/tcp和8080/udp是两条不同的规则。只写8080会被拒绝。TCP 服务就写tcp,DNS、SNMP 这类才需要udp。
3.3 富规则:只给特定来源网段开门
生产环境里我不太喜欢--add-port这种全放行,因为等于对全球开放。更稳的做法是用 rich rule 限定来源:
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="10.20.30.0/24" port protocol="tcp" port="3306" accept' firewall-cmd --reload这条规则的含义是:只允许10.20.30.0/24这个网段的机器访问本机 3306,其他来源一律按 zone 的默认策略处理。数据库、内部管理端口都应该这么配。删除的时候把--add-rich-rule换成--remove-rich-rule即可,规则串要完全一致。
还有一个容易忽略的点:如果 zone 的默认 target 是DROP或者有REJECT规则在前,你的 accept 规则可能排不上队。firewalld 内部也是按顺序匹配的,用firewall-cmd --list-all看清楚规则全貌再下结论。
4. iptables 老派但在用,顺序和持久化是两个雷
4.1 -A 还是 -I,决定了规则会不会被前面的 REJECT 吃掉
iptables 是纯粹的顺序匹配,从上往下逐条比对,命中第一条就结束。所以规则插入的位置极度重要。
# 追加到链尾,如果前面已经有 REJECT all,这条永远匹配不到 iptables -A INPUT -p tcp --dport 8080 -j ACCEPT # 插入到链首,几乎总能生效 iptables -I INPUT -p tcp --dport 8080 -j ACCEPT我一般直接用-I,也就是插入到第一条,避免被前面的兜底规则截胡。查看时带上--line-numbers,能直观看到顺序:
iptables -L INPUT -n --line-numbers输出里每条规则前面有编号,删除时按编号删,比复制整条规则字符串可靠得多:
iptables -D INPUT 3这里有个很容易误判的现象:你看iptables -L明明有 ACCEPT 规则,端口却还是不通,原因往往就是前面某条更靠前的 REJECT 或者 DROP 已经拦截了。iptables 不会告诉你“哪条规则生效了”,它只按顺序执行,所以排查时必须从第一条往下读。
4.2 重启就白干:规则持久化的正确做法
iptables命令加进去的规则同样活在内存里,重启服务或者重启机器后会全部丢失。持久化方式跟发行版有关:
# CentOS / RHEL 系 service iptables save # 或者 iptables-save > /etc/sysconfig/iptables # 恢复 iptables-restore < /etc/sysconfig/iptablesDebian/Ubuntu 老版本用iptables-persistent,会把规则存到/etc/iptables/rules.v4。新版 Ubuntu 一般直接用 ufw,这个放到第 5 章讲。
我的经验是:调试阶段随便加,稳定之后一定要立刻 save 一次,并且用iptables-save看一眼输出内容,确认规则真的被写进去了。
4.3 一条 REJECT 把自己锁在门外的教训
说一个我自己踩过的真实场景。当时为了收紧安全策略,在 INPUT 链尾部加了一条:
iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited想的是“除了上面放行的,其余全拒”。结果过了一会儿发现 SSH 断了,因为我在加这条规则之前没有先把当前 SSH 端口 22 的位置理清楚,而那条 REJECT 排在了某些必要规则前面。幸好当时有带外管理(云控制台的 VNC),进去把规则删了才恢复。
这件事之后我固定了三条操作纪律:
- 改远程机器的防火墙之前,先开一个不断开的会话(比如 screen/tmux 里的 SSH,或者云控制台 VNC 页面挂在那里)。
- 加兜底 REJECT 之前,先把已有的放行规则和它们的位置列全。
- 改完立刻在另一个窗口测 SSH 和业务端口,两个都通才算过。
这三条没有技术含量,但能救命。
5. ufw 与云安全组:两道门的双重拦截
5.1 ufw 的默认策略比单条规则更关键
Ubuntu/Debian 上更常见的是 ufw。它的命令很好记,但默认策略容易被忽略:
ufw status verbose # 输出会告诉你 # Default: deny (incoming), allow (outgoing), disabled (routed) ufw allow 8080/tcp ufw reload ufw status numbered关键在Default: deny (incoming)这一行:所有入站默认拒绝,你只放行你明确允许的端口。所以放行 8080 之后,其他端口该不通还是不通,这是预期行为,不是 bug。
有一个新手很容易搞错的地方:ufw allow 8080和ufw allow 8080/tcp不一样。不带协议后缀时某些版本会被解释成同时放行 tcp 和 udp,也可能给你警告,建议一律写清楚/tcp。
删除规则推荐用编号:
ufw status numbered ufw delete 3另外,ufw 和 firewalld 不要同时开着,两个防火墙叠加管理会互相覆盖规则,排查时你会看到“我明明没拦它啊”的假象。先确认系统上到底哪个在跑:
systemctl status firewalld systemctl status ufw5.2 云主机安全组是另一道门,很多人只开了一道
这个坑我在云上见过太多次了。现象是:本机ss显示在监听,firewall-cmd --list-all也放行了,本机telnet 127.0.0.1 端口通,但外网就是Connection timed out。
原因很简单:云主机的流量要先经过平台侧的安全组,才到你的操作系统防火墙。安全组不放行,你的 iptables/firewalld 配得再漂亮都没用,因为包根本没进到你的机器。
操作上就是去云控制台找到这台实例的安全组,在入方向加一条规则:
| 字段 | 值 |
|---|---|
| 规则方向 | 入方向 |
| 协议类型 | 自定义 TCP(或 TCP) |
| 端口范围 | 8080/8080 |
| 授权对象 | 0.0.0.0/0(开放)或指定网段 |
| 优先级 | 数字越小优先级越高 |
顺手提一个生产建议:不要习惯性写 0.0.0.0/0。8080 这种业务端口,来源应该限定为负载均衡器或者固定出口 IP 的网段,安全组是很多人唯一能拦住扫描的门,随手全开等于把自己的服务暴露在公网扫描器面前。用tcpdump -i eth0 port 8080 -nn抓一下包就能确认 SYN 到底有没有到达你的机器:抓到了说明安全组放行了,没抓到就说明卡在平台侧。
6. SELinux 与 Docker:两个容易被忽略的隐形拦截者
6.1 SELinux 端口标签不匹配会让服务直接绑定失败
CentOS/RHEL 上 SELinux 默认是 enforcing,它会给每个端口打上一个类型标签,服务只能绑定它被允许的标签。比如 Nginx 默认只能绑http_port_t类型下的端口(80、81、443、488、8008、8009、8443、9000 这些),你想让它跑在 8090 上,就会遇到一个非常迷惑的现象:服务日志报 Permission denied 或者直接起不来,但端口、防火墙全都没问题。
getenforce # Enforcing 表示开启 # 查看 http 允许的端口列表 semanage port -l | grep http_port_t # 给 8090 加上 http 标签 semanage port -a -t http_port_t -p tcp 8090 # 确认 semanage port -l | grep 8090semanage命令来自policycoreutils-python-utils包,最小化安装的机器上可能没有,需要先装。
排查这类问题时,可以用下面这个组合快速定位:
# 临时把 SELinux 调成宽容模式验证 setenforce 0 # 再 telnet 测试,如果通了,基本可以确认是 SELinux 的锅确认之后不要长期开着setenforce 0,重启就恢复了,而且关掉 SELinux 会牺牲安全能力,正确做法是加端口标签。也可以从审计日志里找证据:
ausearch -m avc -ts recent看到avc: denied相关记录,方向就明确了。
6.2 Docker 发布端口绕过 firewalld 的经典现象
用 Docker 的人一定会遇到这个反直觉的情况:Docker 用-p 8080:80发布端口后,宿主机 firewalld 里明明没放行 8080,外部却能访问;反过来,你放行了某个端口,容器却不通。原因是 Docker 会直接往 iptables 的DOCKER链里插规则,而且这条链的处理顺序在很多发行版上是先于 firewalld 的规则的。
所以如果你发现“宿主机防火墙拦不住容器端口”,这是正常现象,不是配置错了。想控制容器端口暴露范围,有两个方向:
- 发布端口时绑到具体地址:
-p 127.0.0.1:8080:80,这样只有本机能连,外网连不上,再由 Nginx 反代对外。 - 在 daemon 配置里关掉 Docker 自动改 iptables:
"iptables": false,然后自己管理转发规则。这个动作影响面大,改动前要评估现有容器。
排查容器端口问题时,记住一条:先docker ps看端口映射对不对,再docker exec进容器里ss -lntp看容器内监听地址,最后才是宿主机防火墙。因为容器内服务如果监听 127.0.0.1,映射到宿主机也是连不上的。
7. 一次 8080 端口不通的完整排查复盘
7.1 现象与第一个错误判断
背景是一台 CentOS 的测试机,上面跑一个 Java 服务。现象是:从我的笔记本ping 10.0.1.20正常,telnet 10.0.1.20 8080卡住十几秒然后Connection timed out。同一台机器上 22 端口 telnet 是通的。
我当时的第一个判断是“防火墙把 8080 拦了”,于是直接敲:
firewall-cmd --add-port=8080/tcp结果还是不痛。注意这里我犯了个错误:没加--permanent,虽然当时是生效的,但我后来 reload 了一次,规则就没了,导致后面观察到的现象反复变化,白白多绕了十分钟。
7.2 分层验证把范围从整条链路缩到一台机器
冷静下来之后我按第 2 章那张表重新走了一遍:
# 1. 服务在不在 ss -lntp | grep 8080 # 有输出:LISTEN 0 128 0.0.0.0:8080 ... java # 2. 本机自测 telnet 127.0.0.1 8080 # 通了,说明服务本身没问题,监听地址也是 0.0.0.0 # 3. 抓包看 SYN 有没有到 tcpdump -i eth0 port 8080 -nn第 3 步是从另一台机器发起 telnet 的同时在本机抓包。结果是:只看到 SYN 进来,没有 SYN+ACK 出去。这个信息非常关键——包到了本机,但本机没回,说明拦截发生在本机,不在中间网络,也不在云安全组(因为 SYN 已经进来了)。
既然包到了、服务在监听、那问题就只可能在本机防火墙这条链上。于是去查 firewalld:
firewall-cmd --list-all看到ports:那一行是空的,心里就有数了。同时确认--get-active-zones显示 eth0 绑在 public 上,我之前的命令操作的就是 public,zone 没问题。
7.3 真正的元凶与验证方式
到这里结论已经清楚了:规则没写进永久配置,中间又被 reload 清掉了。修复动作:
firewall-cmd --zone=public --permanent --add-port=8080/tcp firewall-cmd --reload firewall-cmd --list-ports # 输出 8080/tcp然后再从笔记本 telnet,秒通。为了彻底验证,我做了三件事:
systemctl restart firewalld之后重新测,仍然通,说明永久配置生效了。- 重启整台机器,再测,仍然通。
- 用
curl -v http://10.0.1.20:8080/health确认能拿到预期的 HTTP 响应,不只是 TCP 握手成功。
这三步里,第 2 步是很多人会节省掉的一步,但恰恰是它能验证持久化有没有做对。我现在的习惯是:只要碰了防火墙,最后一定要重启服务或重启机器再测一次,否则你不知道哪天重启之后就又挂了。
顺便说一下这次遗留的一个经验:tcpdump抓包在排查端口问题上性价比极高。它能把“包到底有没有到本机”这个最关键的判定做出来,一下子就把排查范围砍掉一半。命令不用复杂,tcpdump -i eth0 port 目标端口 -nn就够了,看到单向的 SYN 就说明是本机在拦,看到完全没有包就说明卡在更上游。