2024 年,我在一台新装的服务器上部署一个服务,监听 8080 端口。部署很顺利:进程起来了,我在这台机器上 curl 127.0.0.1:8080,响应正常,接口数据一切都对。我以为大功告成,通知前端同事可以联调了。结果同事那边一连接就报错:连不上。我自己从办公室的机器上试着访问这个服务的公网地址,浏览器转了半天,最后甩给我一个超时。我心里很纳闷:服务明明在跑、本机访问明明好好的,怎么一到外面就连不上?我第一反应是服务没监听对地址,于是回到服务器 ss -tlnp 一看,清清楚楚写着 LISTEN 0.0.0.0:8080——监听的是 0.0.0.0,也就是所有网卡都收,不是只监听 127.0.0.1。服务在监听、监听的还是所有地址、本机访问也通,可外面就是进不来。这中间像是隔着一堵我看不见的墙——本机能穿过它,外面的人穿不过来。我盯着这个"墙内一切正常、墙外完全无法触及"的现象想了很久,最后才意识到:这堵墙是真实存在的,它就是这台服务器上的防火墙。这件事逼着我把 firewalld、iptables、连接被拒和连接超时的区别、还有云安全组这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一台新装的服务器,部署一个监听 8080 的服务
事故现象:
- 服务进程正常运行
- ★ 在服务器本机 curl 127.0.0.1:8080,响应完全正常
- ★ 从外部机器访问公网地址,连接超时,进不来
现场排查:
# 1. 服务在监听,而且监听的是 0.0.0.0(所有网卡)
$ ss -tlnp | grep 8080
LISTEN 0 128 0.0.0.0:8080 users:(("myapp",pid=8901,...))
# ^^^^^^^^^^^^^ ★ 不是 127.0.0.1,是所有地址 —— 没问题
# 2. ★ 本机访问通,排除"服务本身"的问题
$ curl -s 127.0.0.1:8080/health
{"status":"ok"} # ★ 服务没毛病
# 3. ★ 从外部机器连,看是"被拒"还是"超时"
#(在外部机器上执行)
$ curl -v --connect-timeout 5 http://服务器公网IP:8080/health
* connect to 服务器公网IP port 8080 failed: Connection timed out
# ^^^^^^^^^^^^^^^^^^
# ★ 关键:是 timed out(超时),不是 refused(拒绝)
# 4. ★ 回服务器看防火墙开着没
$ systemctl status firewalld
Active: active (running) # ★ firewalld 正开着
# 5. ★ 看 firewalld 放行了哪些端口/服务
$ firewall-cmd --list-all
services: ssh dhcpv6-client
ports: # ★ 空的!8080 根本没放行
# —— SSH 能连(放行了 ssh),8080 没放行,被防火墙挡在门外
根因(后来想清楚的):
1. ★ "服务在 LISTEN" 和 "外面能连上",是两件独立的事。
服务监听端口,只是它自己在内核里"挂了个号";
能不能从外面进来,还要过【防火墙】这一关。
2. 这台服务器的 firewalld 是开着的,而且默认策略是
"只放行明确允许的,其余一律挡掉"。
3. ★ 它的放行名单里有 ssh —— 所以我能 SSH 上来;
但【没有 8080】—— 所以 8080 的连接全被拦下。
4. 防火墙拦一个连接,默认是【直接丢弃数据包】、
不给任何回应。客户端的 SYN 石沉大海,只能干等,
等到 ★ connection timed out。
5. 本机 curl 127.0.0.1 为什么通?★ 因为本机访问走的是
loopback(回环),数据包【根本没经过】对外的那道
防火墙规则 —— 它在墙内,自然畅通无阻。
"服务能跑" ≠ "外面能连",中间还隔着防火墙这堵墙。
修复 1:连不上,先分清"被拒绝"还是"超时"
# === ★ 第一个关键动作:看清是 refused 还是 timed out ===
# === 这两个词,指向完全不同的方向 ===
# 从外部访问连不上,报错通常是这两种之一,★ 它们的
# 含义天差地别,一定要先分清:
#
# 1. Connection refused(连接被拒绝)
# ★ 含义:数据包【到达了】服务器,但服务器明确
# 回了一个"拒绝"(RST)。
# 说明:网络是通的、防火墙也没拦,是【那个端口上
# 没有进程在监听】—— 服务没起、或端口配错了。
#
# 2. Connection timed out(连接超时)
# ★ 含义:数据包发出去了,但【什么回应都没等到】,
# 石沉大海。
# 说明:包多半在【半路被丢弃】了 —— 最典型的就是
# 被某道防火墙【默默丢包】。也可能是路由不通。
# === ★ 为什么"被防火墙挡"表现为超时,而不是拒绝 ===
# 防火墙拦截一个包,有两种做法:
# - REJECT:丢弃,但回客户端一个"拒绝"通知
# -> 客户端立刻收到 refused,失败得很快。
# - DROP:★ 直接丢弃,【一声不吭】,不回任何东西
# -> 客户端收不到任何回应,只能傻等到超时。
# ★ firewalld、云安全组,默认基本都是 DROP 这种
# "闷声丢包"的做法。所以"超时",是被防火墙挡的
# 典型信号。
# === ★ 现场怎么看 ===
# 从外部机器上,用 curl -v 或 telnet 看清是哪种:
$ curl -v --connect-timeout 5 http://服务器IP:8080/
# 或:
$ telnet 服务器IP 8080
# - 立刻 "Connection refused" -> 端口没服务监听,查服务
# - 卡住,最后 "timed out" -> ★ 被挡了,查防火墙
# === 认知 ===
# ★ "连不上"是个笼统的说法。refused 和 timed out
# 是两条完全不同的排查路线 —— 第一步,必须先用
# curl -v / telnet 把这两者分清楚。
修复 2:本机能通、外面不通——这个对比说明了什么
# === ★ "本机 curl 通,外面连不上" 是个极强的线索 ===
# === 这个对比,一下子排除掉一大半可能 ===
# 本机 curl 127.0.0.1:8080 能通,意味着:
# ★ 服务进程是好的、能正常响应。—— 排除"服务挂了"。
# ★ 服务确实绑在了 8080 上。—— 排除"端口配错"。
# 既然服务本身没问题,那"外面连不上"的原因,就只能
# 出在【从外部网卡进来,到这个服务之间】的某一段。
# === ★ 为什么本机走 loopback 能"绕过"防火墙 ===
# 你 curl 127.0.0.1,数据包走的是 loopback(回环)
# 接口 lo —— 它根本不出网卡,在内核里转一圈就回来。
# ★ 对外的防火墙规则,拦的是【从外部网卡进来】的
# 流量。loopback 的流量不经过那些规则,所以本机
# 访问畅通无阻 —— 这恰恰把"防火墙"这个嫌疑顶到
# 了最前面。
# === ★ 但要先排掉一个"假的本机能通" ===
# 注意:如果服务【只监听了 127.0.0.1】,那也是
# "本机通、外面不通",但根因完全不同 —— 是绑错了
# 地址,跟防火墙无关。所以先确认监听地址:
$ ss -tlnp | grep 8080
LISTEN 0 128 0.0.0.0:8080 ... # ★ 0.0.0.0 / :: = 所有网卡,对
LISTEN 0 128 127.0.0.1:8080 ... # ✗ 只监听本机 —— 这才是绑错地址
# ★ 我这次是 0.0.0.0,监听地址没问题 —— 那矛头就
# 明确指向防火墙了。
# === ★ 顺带:用服务器的【内网 IP】再试一次,缩小范围 ===
# 在【同一内网的另一台机器】上,用服务器的内网 IP 连:
$ curl --connect-timeout 5 http://服务器内网IP:8080/
# - 内网 IP 能通、公网 IP 不通 -> ★ 问题在【云安全组】
# 或公网那一层(见修复 4)。
# - 内网 IP 也不通 -> ★ 问题在服务器【自己
# 的防火墙】(firewalld/iptables,见修复 3)。
# 这一步能把"主机防火墙"和"云安全组"两层分开。
# === 认知 ===
# ★ "本机通 / 外面不通" + "超时" —— 这两个线索叠在
# 一起,基本就锁定:服务没问题,是中间有一道墙。
# 接下来就是找出这道墙在哪一层。
修复 3:firewalld——服务器自己的那道墙
# === ★ 查服务器本机的防火墙:firewalld ===
# === 第一步:firewalld 到底开没开 ===
$ systemctl status firewalld
$ firewall-cmd --state
running # ★ running = 防火墙在工作
# 如果是 not running,那本机防火墙这层就排除了。
# === ★ 第二步:看它当前放行了什么 ===
$ firewall-cmd --list-all
public (active)
target: default
interfaces: eth0
services: ssh dhcpv6-client # ★ 只放行了这几个 service
ports: # ★ ports 是空的 —— 8080 没放行!
# ★ 这就是真相:放行名单里有 ssh(所以我能登进来),
# 但根本没有 8080 —— 8080 的连接全被默认策略挡掉。
# === firewalld 的概念:zone、service、port ===
# - zone(区域):一组规则的集合。最常用 public。
# - service:firewalld 预定义的"服务",其实是端口的
# 别名 —— 比如 service http = 放行 80 端口。
# - port:也可以直接按端口号放行。
$ firewall-cmd --get-active-zones # 看当前生效的 zone
# === ★ 第三步:放行 8080 端口 ===
# 加规则,加 --permanent 让它【重启后依然有效】:
$ firewall-cmd --permanent --add-port=8080/tcp
# ★ --permanent 的规则【不会立即生效】,要 reload:
$ firewall-cmd --reload
# 确认加上了:
$ firewall-cmd --list-ports
8080/tcp # ★ 出现了,放行成功
# === ★ 一个高频坑:加了规则忘了 --permanent 或 reload ===
# - 只加规则、不加 --permanent:这次生效,但服务器
# 一重启,规则就没了 —— "过几天又连不上了"。
# - 加了 --permanent、忘了 --reload:写进了配置,
# 但当前还没生效 —— "明明加了怎么还不通"。
# ★ 记牢:--permanent 写配置 + --reload 让它生效,
# 两个一起,才算真正放行。
# === ★ 如果用的是 iptables(没装 firewalld 的系统)===
$ iptables -L -n --line-numbers # 看现有规则
$ iptables -nL INPUT # 重点看 INPUT 链
# 放行 8080(加在合适位置,别加到 DROP 规则后面):
$ iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
# ★ iptables 规则默认【重启丢失】,要用 iptables-save
# 持久化,或交给 service iptables save。
# === 认知 ===
# ★ 防火墙是"白名单"思路:没被明确放行的,默认就是
# 挡。新服务用了新端口,就要记得为它【开一扇门】。
修复 4:墙不止一道——云安全组、地址、SELinux
# === ★ 主机防火墙之外,还有几道"墙" ===
# === ★ 墙 1:云服务器的"安全组" ===
# 如果是云服务器(阿里云/腾讯云/AWS 等),除了服务器
# 【自己的】firewalld,云平台还有一层【安全组】——
# 它在你的服务器【之外】,在云的网络层就把流量挡了。
# ★ 极其常见的坑:服务器里 firewalld 都放行了,还是
# 连不上 —— 因为云控制台的【安全组】没放行 8080。
# ★ 安全组要去【云厂商的控制台网页】上配,服务器里
# 是看不到、也改不了它的。新端口,这两层都要开:
# - 服务器内:firewall-cmd --add-port=8080/tcp
# - 云控制台:安全组入方向规则,放行 8080
# 怎么判断是哪层?-> 用修复 2 的"内网 IP 测试":
# 内网通、公网不通,基本就是安全组。
# === ★ 墙 2:服务只监听了 127.0.0.1(前面说过)===
$ ss -tlnp | grep 8080
# 如果是 127.0.0.1:8080,服务就只收本机请求。要让
# 外部能连,得改服务配置,监听 0.0.0.0(或 ::)。
# 常见:很多框架默认 host 是 127.0.0.1,要显式改成
# 0.0.0.0 才对外。
# === ★ 墙 3:SELinux 限制了端口 ===
# CentOS 上,SELinux 可能不允许某个服务用"非标准端口"。
$ getenforce # Enforcing = SELinux 开着
# 比如让 nginx 监听 8080,SELinux 可能挡。看/放行:
$ semanage port -l | grep http_port
$ semanage port -a -t http_port_t -p tcp 8080
# ★ SELinux 挡的话,/var/log/audit/audit.log 里会有
# denied 记录。
# === 墙 4:Docker 容器没做端口映射 ===
# 服务跑在容器里,容器内监听 8080,但 docker run 时
# 没 -p 映射出来,宿主机外面自然连不到。
$ docker ps # 看 PORTS 列有没有 ->8080
$ docker run -p 8080:8080 ... # ★ 要显式映射
# === ★ 一个排查思路:从外到内,一层层逼近 ===
# 公网连不上 -> 试内网 IP
# 内网也不通 -> 主机 firewalld / iptables / SELinux
# 内网通、公网不通 -> 云安全组
# 内网都不通时,再 curl 127.0.0.1 -> 通则服务没问题,
# 就是主机防火墙;不通则查服务本身/监听地址。
# ★ 按"本机 -> 内网 -> 公网"三个圈层逐层试,
# 墙在哪一层,一目了然。
修复 5:正确解法——为新服务系统性地"开门"
# === ★ 解法:按圈层逐个放行,并且持久化 ===
# === ★ 解法 1:本机防火墙放行(firewalld)===
$ firewall-cmd --permanent --add-port=8080/tcp # 写配置
$ firewall-cmd --reload # 生效
$ firewall-cmd --list-ports # 确认
# ★ --permanent + --reload 一定都要有,否则重启失效
# 或当前不生效。
# === ★ 解法 2:云安全组放行 ===
# 到云厂商控制台 -> 你这台实例 -> 安全组 -> 入方向规则
# -> 添加一条:协议 TCP,端口 8080,来源按需
# (能内网限定就别对 0.0.0.0/0 全开)。
# ★ 这一步在网页上做,服务器命令行里做不了。
# === 解法 3:确认服务监听地址 ===
$ ss -tlnp | grep 8080
# 若是 127.0.0.1,改服务配置监听 0.0.0.0,重启服务。
# === ★ 解法 4:验证 —— 一定要从【外部】验 ===
# 在服务器本机 curl 永远是通的(走 loopback),证明
# 不了对外可达。★ 必须到【另一台机器】上验:
#(外部机器)
$ curl -v --connect-timeout 5 http://服务器IP:8080/health
$ telnet 服务器IP 8080
# ★ 通了,才算真的开放成功。
# === ★ 解法 5:放行时务必【最小化】,别图省事全开 ===
# 排查时心急,容易干两件危险的事,★ 千万别:
# - firewall-cmd ... --add-port=1-65535/tcp(全开)
# - systemctl stop firewalld(把防火墙整个关掉)
# ★ 这等于把整台服务器在公网上"裸奔"。正确做法永远
# 是:只放行【确实需要】的那个端口,来源能收窄就
# 收窄。防火墙是来保护你的,不是用来添乱的。
# === ★ 解法 6:把"开端口"写进部署清单 ===
# 这次事故的本质,是部署服务时漏了"开防火墙"这一步。
# 把它固化进发布流程,新服务上线时和"启动进程"
# 一样,是必做项:
# □ 启动服务进程
# □ ss -tlnp 确认监听地址是 0.0.0.0
# □ firewalld 放行端口(--permanent --reload)
# □ 云安全组放行端口
# □ ★ 从外部机器验证可达
# ★ 五步走完,才算这个服务真的"上线"了。
# === 验证清单 ===
$ ss -tlnp | grep 8080 # 监听 0.0.0.0
$ firewall-cmd --list-ports # 8080/tcp 在列
$ # 云控制台安全组确认有 8080 规则
$ curl 外部机器 -> 服务器IP:8080 # ★ 外部实测通
# ★ 四项都对,这次"连不上"才算彻底根治。
修复 6:对外可达性排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ "服务在 LISTEN" ≠ "外面能连上",中间隔着防火墙 ===
# === 2. ★ 连不上先分清 refused 还是 timed out ===
$ curl -v --connect-timeout 5 http://IP:端口/ # 或 telnet
# refused = 端口没服务;timed out = 多半被防火墙挡。
# === 3. ★ 本机 curl 127.0.0.1 通 = 服务没问题,问题在中间的墙 ===
# 本机走 loopback,绕过了对外防火墙规则。
# === 4. ★ 按本机->内网->公网三个圈层逐层试,定位墙在哪层 ===
# === 5. firewalld 是白名单,新端口要 --add-port 明确放行 ===
$ firewall-cmd --permanent --add-port=端口/tcp && firewall-cmd --reload
# === 6. ★ --permanent 写配置,--reload 才生效,两个都要 ===
# === 7. ★ 云服务器还有一层"安全组",在控制台配,和主机防火墙是两道墙 ===
# === 8. 也要排查:监听地址是不是 127.0.0.1、SELinux、Docker 端口映射 ===
# === 9. ★ 别用关防火墙/全开端口来"解决",那是让服务器裸奔 ===
# === 10. 排查"服务连不上"的步骤链 ===
$ ss -tlnp | grep 端口 # ① 服务在监听吗,监听哪个地址
$ curl 127.0.0.1:端口 # ② 本机通吗(通=服务没问题)
$ 外部 curl -v / telnet # ③ refused 还是 timed out
$ firewall-cmd --list-all # ④ 主机防火墙放行了吗
$ 云控制台看安全组 # ⑤ 云安全组放行了吗
# 按这个顺序,"服务连不上"基本能定位、能根治。
命令速查
需求 命令
=============================================================
看服务监听端口和地址 ss -tlnp | grep 端口
本机测试服务 curl -s 127.0.0.1:端口/
外部测试(看 refused/超时) curl -v --connect-timeout 5 http://IP:端口/
看 firewalld 状态 firewall-cmd --state
看 firewalld 放行了什么 firewall-cmd --list-all
放行一个端口 firewall-cmd --permanent --add-port=8080/tcp
让 firewalld 配置生效 firewall-cmd --reload
看放行的端口列表 firewall-cmd --list-ports
看 iptables 规则 iptables -L -n --line-numbers
看 SELinux 状态 getenforce
看 Docker 端口映射 docker ps
口诀:服务在 LISTEN 不等于外面能连,中间隔着防火墙这堵墙,连不上先分清 refused 还是超时
本机 curl 通就是服务没问题,按本机内网公网三层逐层试,firewalld 和云安全组是两道墙
避坑清单
- 服务在 LISTEN 和外面能连上是两件独立的事,服务监听只是挂个号,进来还要过防火墙这关
- 连不上先分清 Connection refused 还是 timed out,refused 是端口没服务,超时多半被墙挡
- 防火墙拦包默认 DROP 闷声丢弃不回应,客户端收不到回应只能干等到超时,所以表现为超时
- 本机 curl 127.0.0.1 能通说明服务本身没问题,本机走 loopback 绕过了对外的防火墙规则
- 但要先排掉假的本机通,服务若只监听 127.0.0.1 也是本机通外面不通,根因是绑错地址
- 按本机到内网到公网三个圈层逐层测试,内网通公网不通是云安全组,内网也不通是主机防火墙
- firewalld 是白名单思路,没明确放行的端口默认就挡,新服务用新端口要 add-port 开门
- firewall-cmd 加规则要 --permanent 写配置再 --reload 生效,漏一个会重启失效或当前不生效
- 云服务器除了主机 firewalld 还有一层安全组,在云控制台网页上配,这是服务器外的另一道墙
- 绝不能用 stop firewalld 或全开端口来解决,那等于服务器在公网裸奔,只放行确实需要的端口
总结
这次"服务在跑、外面却连不上"的事故,纠正了我一个关于"部署完成"的、想当然的认知。在我的脑子里,把一个服务"部署好",长久以来就是一件事:把进程启动起来,让它正常地监听上那个端口。在我看来,一个进程只要起来了、只要 ss 里能看到它清清楚楚地 LISTEN 在 8080 上,那么"这个服务可以对外提供能力了"就是一个不言自明的结论——监听,在我的理解里,就等于"对外开放"。正因为这个等式在我心里太根深蒂固,所以当我在本机 curl 127.0.0.1:8080 拿到正常响应的那一刻,我心里那块石头就彻底落了地:服务通了,部署成功了,可以叫人联调了。我做的所有确认,都是在反复印证同一件我早已认定的事——服务本身是好的。我从来没有想过,"服务本身是好的"和"外面的人能用上这个服务",这中间还隔着一段我从未纳入视野的距离。复盘到根上,我才明白,我把一个外部请求"到达服务"这件事,想得太一步到位了。在我的想象里,外面一个客户端要访问我的服务,无非就是它的请求"飞"到我的端口上——中间是真空,是直达,没有任何关卡。可真实的路径上,是有关卡的,而且不止一道:一个从公网来的数据包,要先穿过云平台那一层的"安全组",再穿过服务器自己的那道防火墙 firewalld,过了这两道墙之后,它才终于能抵达那个在 LISTEN 的端口。我的服务监听端口,只是它自己在内核里"挂了个号",宣告"我准备好接客了";可外面的客人能不能走到它面前,要看那两道墙肯不肯放行。而 firewalld 的脾气,是一种我没料到的"白名单"逻辑:它不是"默认全开、你挡什么它挡什么",而是"默认全挡、你放行什么它才放什么"。它的放行名单里,有 SSH——这是装系统时就配好的,所以我能登上来,这个"能登上来"反而麻痹了我,让我下意识觉得"网络是通的";可这份名单里,压根没有 8080。我新起的这个服务,用了一个谁都没为它开过门的新端口。于是它的处境就很微妙:它在墙内,活得好好的,本机访问它的请求走的是不出网卡的回环,根本不经过那道对外的墙,所以畅通无阻;而所有从墙外来的、奔着 8080 的请求,都被 firewalld 一声不吭地、连个拒绝的回话都不给地,直接丢弃了。客户端那头发出的连接请求石沉大海,只能干等到超时。我对着 curl 127.0.0.1 的成功沾沾自喜时,我测的恰恰是那个唯一能绕过这堵墙的路径——我用一个"穿不到墙外"的测试,去证明了一件"墙外能不能进来"的事,这个证明从逻辑上就是不成立的。这次最大的收获,是我意识到,我习惯于把一件事的"完成",定义在我自己看得见、够得着的那个范围之内。进程起来了、本机通了——这些都是在我这一侧、由我亲手确认的"完成";可这个服务真正的意义,在于墙的另一侧那些我看不见的人能不能用上它,而那一侧,恰恰是我整个验证过程从未真正触及的地方。我用"我这边一切正常"偷换了"对方那边能用上"这个真正的目标。"可达性"这个东西,有一个朴素却容易被忘记的性质:它必须从【目标用户所在的位置】去验证才算数,在服务自己家里反复自测,无论测得多漂亮,都证明不了它对外开放了。所以下一次,当我又要宣布某样东西"做好了、可以用了"的时候,我会先停下来,问自己一个位置上的问题:我刚才所有的验证,是站在"我自己"的位置上做的,还是站在"真正要用它的那个人"的位置上做的?如果我从没有真正走到墙的另一头去敲一敲门,那么我所谓的"做好了",就还只是一个我单方面的、未经对方确认的猜想而已。
—— 别看了 · 2026