2024 年,我在一台服务器上部署了一个新服务,监听 8080 端口。部署完我在这台机器上 curl localhost:8080,响应正常,服务本身没毛病。可换到另一台机器上去连它,telnet 过去就是一个干等到超时——外面死活连不进来。我第一反应是服务没监听对,回去一查,ss -lntp 明明白白显示 8080 端口在 0.0.0.0 上听着,任何地址都该能连。服务在听、本机能连、外面连不上——这三件事凑在一起,把我整懵了:一个端口,它到底是"开着"还是"关着"?这件事逼着我把 Linux 的防火墙、iptables、firewalld 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,部署一个服务监听 8080
事故现象:
- 本机 curl localhost:8080 正常
- 别的机器 telnet 这台机的 8080,超时连不上
- ss 看端口确实在 0.0.0.0:8080 监听着
现场排查:
# 1. 确认服务在监听,而且监听地址是对的
$ ss -lntp | grep 8080
LISTEN 0 128 0.0.0.0:8080 ... # ★ 0.0.0.0,所有地址都该能连
# 2. 本机连本机 —— 通
$ curl -s localhost:8080/health
ok
# 3. 从另一台机器连 —— 不通
[B 机]$ telnet 10.0.1.10 8080
Trying 10.0.1.10...
(长时间卡住,最后 timed out) # ★ 超时,不是 refused
# 4. ★ 看防火墙 —— CentOS 7 默认是 firewalld
$ systemctl status firewalld
Active: active (running) # ★ firewalld 开着
# 5. 看 firewalld 放行了哪些端口
$ firewall-cmd --list-all
services: ssh dhcpv6-client
ports: # ★ ports 是空的!8080 没放行
根因(后来想清楚的):
1. 服务确实在 0.0.0.0:8080 正常监听,服务本身没问题。
2. ★ 本机 curl localhost 通,是因为走 lo 回环网卡,
流量根本没经过防火墙对外的那条链。
3. ★ 外部流量进来,要先过【主机防火墙 firewalld】这一关。
firewalld 默认策略:没明确放行的端口,一律拦掉。
4. 8080 从没被加进放行列表 -> 外部的 SYN 包到了网卡,
被 firewalld 直接 DROP 掉 -> 对方干等,直到 timeout。
5. ★ 超时(而非 refused),正是"被防火墙静默丢弃"的特征。
端口在监听,不代表防火墙放它进来。
修复 1:先确认服务真在监听、且监听地址对
# === 排查"连不上",第一步永远是:服务到底在听吗 ===
$ ss -lntp | grep 8080
LISTEN 0 128 0.0.0.0:8080 users:(("myapp",pid=8123,fd=6))
# -l 只看 LISTEN -n 不解析端口名 -t TCP -p 显示进程
# ★ 看清楚监听地址那一列,它决定了"谁能连":
# === ★ 监听地址的三种情况,含义天差地别 ===
# 0.0.0.0:8080 监听【所有网卡】 -> 本机和外部都能连
# 127.0.0.1:8080 ★ 只监听【回环】 -> 只有本机能连,外部【永远连不上】
# 10.0.1.10:8080 只监听这一个具体 IP
# ★ 一个超高频的坑:服务配置写成 127.0.0.1,
# 于是本机 curl 通、外部死活连不上 —— 这跟防火墙无关,
# 是服务【根本没在对外的网卡上监听】。
# === 这次先排除了"监听地址"嫌疑:是 0.0.0.0,没问题 ===
# 如果你的是 127.0.0.1,改服务配置(bind / listen 地址)
# 成 0.0.0.0,而不是去动防火墙。
# === ★ 分清两种"连不上",它们指向完全不同的方向 ===
# Connection refused(立刻被拒)
# -> 包到了机器,但那个端口【没有进程在监听】,
# 内核直接回了个 RST。或者防火墙用 REJECT 主动拒绝。
# Connection timed out(干等到超时)
# -> 包【石沉大海】,没有任何回应。
# 典型就是被防火墙 DROP 了,或网络根本不通。
# ★ 这次是 timed out -> 强烈指向"包被防火墙静默丢弃"。
# === 本机能连、外部不能连,先想清楚流量路径 ===
# curl localhost -> 走 lo 回环网卡 -> 通常不经过对外防火墙规则
# 外部连进来 -> 进物理网卡 -> ★ 要穿过防火墙的 INPUT 链
# 所以"本机通、外部不通",防火墙是头号嫌疑。
修复 2:firewalld——CentOS 7 默认的防火墙
# === ★ CentOS 7 / RHEL 7 默认的防火墙是 firewalld ===
$ systemctl status firewalld # 它在不在跑
$ firewall-cmd --state # running / not running
# === ★ firewalld 的核心概念:zone(区域)===
# firewalld 把网卡分到不同 zone,每个 zone 有自己的放行规则。
# 常见:public(默认,较严)、trusted(全放行)、internal 等。
$ firewall-cmd --get-default-zone # 默认 zone
public
$ firewall-cmd --get-active-zones # 各网卡属于哪个 zone
# === ★ 看某个 zone 到底放行了什么(排查第一命令)===
$ firewall-cmd --list-all
public (active)
interfaces: eth0
services: ssh dhcpv6-client # 放行的"服务"
ports: # ★ 放行的"端口" —— 空的!
# 这次就卡在这:8080 既不在 services 也不在 ports。
# === 放行一个端口 ===
$ firewall-cmd --add-port=8080/tcp # ★ 临时,立刻生效
$ firewall-cmd --permanent --add-port=8080/tcp # ★ 永久,但不立刻生效
$ firewall-cmd --reload # 让 permanent 生效
# === ★ 这是 firewalld 最大的坑:runtime 与 permanent 是两套 ===
# 不加 --permanent -> 改的是【runtime 运行时】配置:
# 立刻生效,但【重启 firewalld 或重启机器就丢】。
# 加 --permanent -> 改的是【permanent 永久】配置:
# 写进磁盘、重启也在,但【不会立刻生效】,要 --reload。
# ★ 正确姿势:两条都执行 —— 既立刻生效,又重启不丢:
$ firewall-cmd --add-port=8080/tcp
$ firewall-cmd --permanent --add-port=8080/tcp
$ firewall-cmd --reload
# ★ 验证:--reload 之后,再 --list-all 确认 ports 里有了 8080。
# === 也可以按"服务名"放行(firewalld 预定义了一批)===
$ firewall-cmd --permanent --add-service=http # 放行 80
$ firewall-cmd --get-services # 看支持哪些服务名
# === 限定来源 IP 放行(更安全)===
$ firewall-cmd --permanent --add-rich-rule='rule family=ipv4 \
source address=10.0.0.0/8 port port=8080 protocol=tcp accept'
# rich rule:只放行 10.0.0.0/8 网段访问 8080,别的拒掉。
修复 3:iptables——firewalld 底层,规则顺序是关键
# === ★ firewalld 只是个"前端",底层真正干活的是 iptables/netfilter ===
# 有些机器没装 firewalld,直接用 iptables;排查必须会看 iptables。
# === 看 iptables 当前所有规则(★ 带计数,排查神器)===
$ iptables -L -n -v
Chain INPUT (policy ACCEPT 0 packets)
pkts bytes target prot opt in source destination
1234 98K ACCEPT tcp -- * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
5 300 DROP tcp -- * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
# -n 不做 DNS 反查 -v 显示包/字节计数(看哪条规则被命中)
# ★ pkts 列:这条规则匹配了多少包 —— 在涨,说明它真的在起作用。
# === ★ iptables 的灵魂:规则【从上到下,匹配即停】 ===
# 一个包进来,iptables 从链的第一条规则开始,逐条比对:
# 匹配上了 -> 执行那条规则的动作(ACCEPT/DROP/REJECT),【停止】。
# 没匹配 -> 看下一条。
# 所有规则都没匹配 -> 执行链的默认策略(policy)。
# ★ 致命推论:规则的【顺序】决定一切。
# 如果一条 DROP 8080 排在 ACCEPT 8080 的【前面】,
# 那个 ACCEPT 永远轮不到 —— 包在 DROP 那里就被拦下了。
# === ★ 加规则:-A 追加到末尾,-I 插到最前面 ===
$ iptables -A INPUT -p tcp --dport 8080 -j ACCEPT # 追加到最后
$ iptables -I INPUT -p tcp --dport 8080 -j ACCEPT # ★ 插到最前面
# 如果链里已经有一条"兜底 DROP",你必须用 -I 把 ACCEPT
# 插到它前面,用 -A 加在它后面是无效的。
# === 看规则的"行号",精确地删 / 插 ===
$ iptables -L INPUT --line-numbers -n
1 ACCEPT tcp ... dpt:22
2 DROP tcp ... dpt:8080
$ iptables -D INPUT 2 # 按行号删掉第 2 条(那条 DROP)
$ iptables -I INPUT 2 -p tcp --dport 8080 -j ACCEPT # 在第 2 行插入
# === DROP 与 REJECT 的区别(直接影响对方看到什么)===
# DROP 静默丢弃,不回任何包 -> 对方【超时】(这次的现象)
# REJECT 回一个拒绝包 -> 对方立刻收到 Connection refused
# ★ 反推:对方说"超时",查 DROP 规则 / 安全组;
# 对方说"refused",查 REJECT 规则 / 端口没监听。
# === 看链的默认策略 ===
$ iptables -L INPUT -n | head -1
Chain INPUT (policy DROP) # ★ 默认策略若是 DROP,
# 那么"没被任何 ACCEPT 规则放行"的端口,统统连不上。
修复 4:规则没保存——重启之后全没了
# === ★ 一个让人崩溃的坑:iptables 规则【默认不持久】 ===
# 你用 iptables -A 加的规则,只存在【内存】里。
# 机器一重启,内存清空 —— 所有手动加的规则【全部消失】,
# 防火墙打回原形。
# ★ 典型事故:"昨天明明放行了,今天重启完又连不上了。"
# === iptables 规则的保存与恢复 ===
$ iptables-save > /etc/sysconfig/iptables # 把当前规则存盘
$ iptables-restore < /etc/sysconfig/iptables # 从文件恢复
# CentOS 6 时代:service iptables save
# CentOS 7+ 若用 iptables-services 包:
$ systemctl enable iptables
$ service iptables save
# === ★ firewalld 没有这个坑,但前提是你用对了 --permanent ===
# firewalld 的 permanent 配置本来就写在磁盘上、重启不丢。
# 所以这个坑的本质,还是上一节说的:
# 只 --add-port 不加 --permanent -> 重启就丢。
# ★ 想确认 permanent 里到底有什么:
$ firewall-cmd --permanent --list-all
# 对比 firewall-cmd --list-all(runtime)——
# 两者不一致,就说明 runtime 改了但没落盘,重启会变。
# === ★ 排查"重启后防火墙变了",就比这两份配置 ===
# runtime(当前生效) vs permanent(重启后会加载的)
# 不一致 = 有人改了 runtime 没存 permanent,或反之。
# === 一条纪律:改防火墙,runtime 和 permanent 同时改 ===
# firewalld:--add-port 和 --permanent --add-port 都执行 + --reload
# iptables :iptables -A 之后,立刻 iptables-save 落盘
修复 5:两层防火墙——主机防火墙之外,还有云安全组
# === ★ 云服务器上,流量要穿过【两层】防火墙,不是一层 ===
# 第 1 层:云平台的【安全组 / 网络 ACL】——
# 在你的机器【之外】,由云控制台管理。
# 第 2 层:机器【自己】的防火墙 —— firewalld / iptables。
# ★ 一个端口要能从公网连上,这【两层都必须放行】。
# 任何一层没放,结果都是连不上。
# === 怎么判断卡在哪一层 ===
# 在【同一个内网】的另一台机器上测:
$ telnet 10.0.1.10 8080
# 内网能连通 -> 主机防火墙(第 2 层)放行了,
# 那公网连不上就是【安全组】(第 1 层)的事。
# 内网也连不上 -> 主机防火墙(第 2 层)就有问题。
# ★ 这一步能快速把问题切到某一层,别在两层之间来回猜。
# === 第 1 层:云安全组,怎么查 ===
# 阿里云 / 腾讯云 / AWS 都在【控制台】里配安全组:
# - 看入方向(Inbound)规则里,有没有放行 8080
# - 注意来源:是放行了 0.0.0.0/0,还是只放行了某网段
# - 安全组也分"入站"和"出站",通常入站是重点
# ★ 安全组在机器外面,机器上 ss/iptables 任何命令都看不到它,
# 它是排查时最容易被遗忘的一层。
# === 用工具确认"包到底有没有到达机器" ===
# 在目标机上抓包,看外部的 SYN 有没有进来:
$ tcpdump -i any -nn 'tcp port 8080 and tcp[tcpflags] & tcp-syn != 0'
# 对方在连,你这边:
# 抓到 SYN -> 包到了机器 -> 卡在【主机防火墙】或服务
# 抓不到 SYN -> 包根本没到机器 -> 卡在【安全组】或网络路由
# === 排查顺序:由外到内,逐层确认 ===
# ① 云安全组放行了吗(控制台)
# ② 主机防火墙放行了吗(firewall-cmd --list-all)
# ③ 服务在对外网卡上监听吗(ss -lntp,不是 127.0.0.1)
# ④ 服务本身健康吗(本机 curl)
修复 6:防火墙排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ "本机能连、外部连不上",先查防火墙 ===
# 本机走回环不过对外规则,外部进来要穿 INPUT 链。
# 这个症状,防火墙是头号嫌疑。
# === 2. 先分清 refused 还是 timeout ===
# refused -> 端口没监听,或 REJECT 规则
# timeout -> 被 DROP,或安全组拦,或网络不通
# === 3. ★ 服务监听地址要看清:0.0.0.0 还是 127.0.0.1 ===
$ ss -lntp | grep 端口
# 127.0.0.1 的话,外部永远连不上 —— 这不是防火墙问题。
# === 4. firewalld:runtime 和 permanent 要一起改 ===
$ firewall-cmd --add-port=端口/tcp
$ firewall-cmd --permanent --add-port=端口/tcp
$ firewall-cmd --reload
# 只改一个,要么重启丢,要么不立刻生效。
# === 5. iptables:规则顺序决定一切,看清是不是被前面的 DROP 截了 ===
$ iptables -L -n -v --line-numbers
# 用 -v 看 pkts 计数,哪条规则在被命中一目了然。
# === 6. ★ 云服务器别忘了安全组这一层 ===
# 内网能连、公网不能连 -> 八成是安全组没放行。
# 安全组在机器外,本机任何命令都看不到它。
# === 7. 排查"端口连不上"的命令链 ===
$ ss -lntp | grep 端口 # ① 服务在监听吗、地址对吗
$ curl localhost:端口 # ② 服务本身健康吗
$ firewall-cmd --list-all # ③ 主机防火墙放行了吗
$ iptables -L -n -v # ④ 底层规则、顺序、命中计数
$ tcpdump -nn tcp port 端口 # ⑤ 外部的包到底到没到机器
# 再加一步:控制台看云安全组。按这个顺序,基本能定位。
命令速查
需求 命令
=============================================================
看端口监听情况 ss -lntp | grep 端口
看 firewalld 状态 firewall-cmd --state
看某 zone 放行了什么 firewall-cmd --list-all
临时放行端口 firewall-cmd --add-port=8080/tcp
永久放行端口 firewall-cmd --permanent --add-port=8080/tcp
让永久配置生效 firewall-cmd --reload
看 iptables 规则和计数 iptables -L -n -v --line-numbers
追加 / 插入 iptables 规则 iptables -A / -I INPUT ...
保存 iptables 规则 iptables-save > /etc/sysconfig/iptables
抓外部进来的 SYN 包 tcpdump -nn tcp port 8080 and ...syn
口诀:本机通外部不通先查防火墙 -> timeout 多是被 DROP
firewalld 改 runtime 也要改 permanent -> 云上还有安全组一层
避坑清单
- 端口在监听不代表能连,外部流量还要穿过防火墙的 INPUT 链
- 本机 curl 走回环不过对外规则,本机通外部不通先查防火墙
- refused 是端口没监听或被 REJECT,timeout 多是被 DROP 或安全组拦
- ss 看监听地址,127.0.0.1 外部永远连不上,要 bind 成 0.0.0.0
- firewalld 的 runtime 和 permanent 是两套,只改一个会出问题
- firewall-cmd 不加 permanent 重启就丢,加了 permanent 不 reload 不生效
- iptables 规则从上到下匹配即停,顺序错了 ACCEPT 会被前面的 DROP 截掉
- iptables 规则默认只在内存,要 iptables-save 落盘否则重启全丢
- 云服务器有安全组和主机防火墙两层,两层都放行端口才能连
- 安全组在机器外面,本机 ss/iptables 看不到它,排查时最易遗漏
总结
这次"端口在监听、外面却连不上"的事故,纠正了我一个对"端口"这个概念的过度简化的理解。在这次之前,我心里把"端口"想象成一个二元的、非黑即白的东西:一个端口,要么是"开着"的——有进程在监听它,于是全世界都能连进来;要么是"关着"的——没进程监听,谁都连不上。在这个简化模型里,我只要用 ss 确认了有进程在 0.0.0.0:8080 上监听,就等于盖章认定"这个端口是开着的、是对全世界开放的",于是外部连不上这件事,就成了一桩在我的世界观里无法成立的悬案。复盘到根上,我才真正理解,一个数据包想从外部机器,抵达我那个正在监听的服务进程,它要走的根本不是一条畅通无阻的直路,而是一条要【依次穿过好几道独立关卡】的通道——而"进程在监听",仅仅是这条通道【最末端】的那一道门而已。一个从外部发来的 TCP SYN 包,它的旅程是这样的:它先要被云平台的【安全组】放行,这是第一道关卡,它在我的机器之外,由云控制台掌管;穿过安全组之后,包到达我的机器网卡,接着它要穿过机器自己的【主机防火墙】——在 CentOS 7 上,就是 firewalld,这是第二道关卡;只有连这第二道关卡也放行了,包才终于被交到内核的 TCP 协议栈,而协议栈这时才会去看"8080 端口上有没有进程在监听"——这才是我之前以为是【唯一】一道、实际上却是【最后】一道的关卡。我那台机器的真实情况是:服务进程这道末端的门,是敞开的;但它前面的 firewalld 那道门,是死死关着的——firewalld 的默认策略,是"凡没有被明确放行的端口,一律拦截",而 8080 从头到尾就没被加进过它的放行列表。于是外部的 SYN 包,在 firewalld 这一关就被静默地丢弃了,根本没有机会走到"进程监听"那一步。这也解释了另一个我当时觉得很微妙的细节:为什么对方看到的是"超时"而不是"拒绝"。因为 firewalld 用的是 DROP 这个动作——它把包悄无声息地扔掉,不给对方任何回应;对方的机器发出了 SYN,却永远等不到回包,只能一直等到自己的超时计时器耗尽。如果当时拦截我的是一条 REJECT 规则,对方会立刻收到一个明确的拒绝包,看到的就会是 Connection refused。"超时"还是"拒绝",这两个词不是同义词,它们是指向不同病根的、信息量很大的诊断线索:超时,意味着包石沉大海,矛头指向 DROP 规则或安全组;拒绝,意味着包到了但被明确回绝,矛头指向没有进程监听或 REJECT 规则。这次事故还顺带教会了我 firewalld 一个极其反直觉、却极其重要的设计:它的配置分成 runtime 和 permanent 两套——不加 --permanent 改的是运行时配置,立刻生效但重启就丢;加了 --permanent 改的是永久配置,重启不丢却不立刻生效,必须 --reload。无数"昨天放行了、今天重启完又连不上"的怪事,根子都在这里。这次从一个"端口明明开着"的悬案出发,我最大的收获,是把脑子里那个"端口非开即关"的二元模型,换成了一个"数据包要逐层闯关"的通道模型:判断一个服务能不能被连上,我不能只盯着最末端那道"进程监听"的门,而要顺着数据包的旅程,从云安全组、到主机防火墙、再到监听进程,一关一关地确认下来——任何一关的门是关着的,这条通道就是不通的。
—— 别看了 · 2026