这一篇讲什么
「端口不通」是最常见也最容易瞎折腾的问题:服务起了、防火墙也开了,外面还是连不上。本篇在三台机器上把排查链条从头到尾实跑一遍:先分清报错类型 → 本机在不在监听 → 防火墙 → SELinux / AppArmor。
实测环境同(一):CentOS 7.9(VMware)、Rocky 9.8、Ubuntu 24.04.5(KVM)。本篇额外装了 nginx、nmap、policycoreutils-python-utils、setroubleshoot-server、mysql-server。所有「从外面连」的测试都是从另一台机器发起的:测 Rocky / CentOS 时从 Ubuntu 连,测 Ubuntu 时从 Rocky 连。
1. 先看报错长什么样 ------ 三种报错对应三件不同的事 ✅
从客户端连一个端口,会看到三种结果之一。实测里它们和原因的对应关系,和很多文章说的不一样:
| 客户端看到 | 实测在什么情况下出现 |
|---|---|
Connection refused(立刻返回) |
包到了机器,但那个端口没人监听;或服务只监听了 127.0.0.1;或 ufw 的 limit 规则触发 |
No route to host(立刻返回) |
被 firewalld 挡了(Rocky 9 和 CentOS 7 的默认配置都是这样) |
| 超时(等满才返回) | 被 ufw 挡了(Ubuntu 默认策略) |
🔴 「超时 = 被防火墙丢了、拒绝 = 没人监听」这条经验,在 firewalld 上不成立。 firewalld 的默认 zone 是用 REJECT 回一个 ICMP 包,客户端立刻看到 No route to host。下面是原始输出。
1.1 被 firewalld 挡:No route to host
Rocky 9 上起一个监听 8080 的服务,防火墙没放行:
bash
# ss -lntp | grep 8080 (Rocky 9)
LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=909,fd=3))
从另一台机器连:
yaml
$ nc -zv -w 3 10.115.180.175 8080
nc: connect to 10.115.180.175 port 8080 (tcp) failed: No route to host
$ curl -sS -m 5 -o /dev/null http://10.115.180.175:8080/
curl: (7) Failed to connect to 10.115.180.175 port 8080 after 0 ms: Couldn't connect to server
$ nmap -Pn -p 8080,9999 10.115.180.175
8080/tcp filtered http-proxy
9999/tcp filtered abyss
注意 9999 端口根本没人监听,结果和 8080 一模一样:
yaml
$ nc -zv -w 3 10.115.180.175 9999
nc: connect to 10.115.180.175 port 9999 (tcp) failed: No route to host
⚠️ 也就是说,端口被 firewalld 挡住时,你从外面看不出后面有没有服务在监听 。要先在本机 ss -lntp 确认服务真的起来了。
CentOS 7 同样是 No route to host,原因在 INPUT 链最后一条:
sql
# iptables -L INPUT -n --line-numbers (CentOS 7,截取)
6 DROP all -- 0.0.0.0/0 0.0.0.0/0 ctstate INVALID
7 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
1.2 被 ufw 挡:超时
Ubuntu 上 ufw enable 之后(只放行了 22),从 Rocky 连 8080:
shell
$ start=$(date +%s); nc -zv -w 5 10.115.180.200 8080; echo "nc 退出码=$? 用时=$(( $(date +%s)-start ))s"
Ncat: TIMEOUT.
nc 退出码=1 用时=5s
$ curl -sS -m 5 -o /dev/null http://10.115.180.200:8080/
curl: (28) Connection timed out after 5002 milliseconds
$ nmap -Pn -p 8080,9999 10.115.180.200
8080/tcp filtered http-proxy
9999/tcp filtered abyss
nmap 两种情况都显示 filtered,但 nc / curl 的表现完全不同。看 nc/curl 的报错能多知道一件事:挡你的大概率是哪一类防火墙。
1.3 端口放行了但没人监听:Connection refused
Rocky 上放行 9999,但不起服务:
yaml
$ nc -zv -w 3 10.115.180.175 9999
nc: connect to 10.115.180.175 port 9999 (tcp) failed: Connection refused
$ nmap -Pn -p 9999 10.115.180.175
9999/tcp closed abyss
1.4 服务只监听 127.0.0.1:从外面看也是 Connection refused
bash
# ss -lntp | grep -E ':808[0-2]' (Rocky 9)
LISTEN 0 5 0.0.0.0:8081 0.0.0.0:* users:(("python3",pid=1009,fd=3))
LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=909,fd=3))
LISTEN 0 5 127.0.0.1:8082 0.0.0.0:* users:(("python3",pid=1102,fd=3))
本机 curl 127.0.0.1:8082 → 200
8082 防火墙已经放行,从外面连:
yaml
$ nc -zv -w 3 10.115.180.175 8082
nc: connect to 10.115.180.175 port 8082 (tcp) failed: Connection refused
$ nmap -Pn -p 8082 10.115.180.175
8082/tcp closed blackice-alerts
🔑 看 ss -lntp 的第四列 :0.0.0.0:端口 才是所有网卡;127.0.0.1:端口 只有本机能连,这种情况改防火墙没用,要改服务的监听地址。
2. 先确认这台机器用的是哪套防火墙 ✅
| CentOS 7 | Rocky 9 | Ubuntu 24.04 | |
|---|---|---|---|
firewall-cmd --state |
running |
running |
没有 firewalld |
ufw status |
没有 ufw | 没有 ufw | Status: inactive(装了但默认没开) |
iptables -V |
iptables v1.4.21 |
iptables v1.8.10 (nf_tables) |
iptables v1.8.10 (nf_tables) |
| firewalld 后端 | iptables(配置文件里没有 FirewallBackend 这一行) |
FirewallBackend=nftables |
--- |
| 网卡名 | ens33 |
enp5s0 |
enp5s0 |
⚠️ firewall-cmd --state 在 firewalld 没运行时:
arduino
# systemctl stop firewalld; firewall-cmd --state; echo "退出码=$?" (Rocky 9)
not running
退出码=252
脚本里判断时看退出码,别只看输出里有没有 running。
3. firewalld:--permanent 和 --reload 的两个方向 ✅
3.1 加了 --permanent,不 reload,当前不生效
css
# firewall-cmd --zone=public --add-port=8080/tcp --permanent
success
# firewall-cmd --zone=public --list-ports
# firewall-cmd --permanent --zone=public --list-ports
8080/tcp
运行时里是空的,只写进了配置。此时从外面连:
yaml
$ nc -zv -w 3 10.115.180.175 8080
nc: connect to 10.115.180.175 port 8080 (tcp) failed: No route to host
--reload 之后才通:
bash
# firewall-cmd --reload; firewall-cmd --zone=public --list-ports
success
8080/tcp
$ nc -zv -w 3 10.115.180.175 8080; curl -sS -m 5 -o /dev/null -w '%{http_code}\n' http://10.115.180.175:8080/
Connection to 10.115.180.175 8080 port [tcp/http-alt] succeeded!
200
3.2 不加 --permanent:不用等重启,一次 reload 就没了
yaml
# firewall-cmd --add-port=8081/tcp; firewall-cmd --list-ports
success
8080/tcp 8081/tcp
$ nc -zv -w 3 10.115.180.175 8081
Connection to 10.115.180.175 8081 port [tcp/tproxy] succeeded!
# firewall-cmd --reload; firewall-cmd --list-ports
success
8080/tcp
$ nc -zv -w 3 10.115.180.175 8081
nc: connect to 10.115.180.175 port 8081 (tcp) failed: No route to host
🔴 很多文章说「不加 --permanent 重启就没了」------ 实际上任何人执行一次 firewall-cmd --reload,你临时加的规则就没了,不用等到重启。
稳妥写法就是两条连着敲:
bash
firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload
3.3 端口段是减号
shell
# firewall-cmd --add-port=8083-8085/tcp; echo "减号 退出码=$?"
success
减号 退出码=0
# firewall-cmd --add-port=8083:8085/tcp; echo "冒号 退出码=$?"
Error: INVALID_PORT: 8083:8085
冒号 退出码=102
ufw 正好反过来(见 §6.3)。
3.4 按服务名开、看网卡在哪个 zone
arduino
# firewall-cmd --get-services | wc -w
225
# firewall-cmd --add-service=http; firewall-cmd --list-services
success
cockpit dhcpv6-client http ssh
# firewall-cmd --get-active-zones
public
interfaces: enp5s0
开端口之前先 --get-active-zones 看网卡在哪个 zone,规则要加在网卡所在的那个 zone 上。
排查时想确认「是不是防火墙的问题」,可以临时把默认 zone 切成 trusted(全放行):
vbnet
# firewall-cmd --set-default-zone=trusted; firewall-cmd --get-active-zones
success
trusted
interfaces: enp5s0
$ nc -zv -w 3 10.115.180.175 8081
Connection to 10.115.180.175 8081 port [tcp/tproxy] succeeded!
# firewall-cmd --set-default-zone=public ← 确认完立刻改回来
3.5 只对某个来源 IP 开放(富规则)
objectivec
# firewall-cmd --add-rich-rule='rule family="ipv4" source address="10.115.180.200" port protocol="tcp" port="8086" accept'
success
# firewall-cmd --list-rich-rules
rule family="ipv4" source address="10.115.180.200" port port="8086" protocol="tcp" accept
从 10.115.180.200 连能通,从 10.115.180.1 连不通:
yaml
$ nc -zv -w 3 10.115.180.175 8086 (从 .200)
Connection to 10.115.180.175 8086 port [tcp/*] succeeded!
$ nc -zv -w 3 10.115.180.175 8086 (从 .1)
nc: connect to 10.115.180.175 port 8086 (tcp) failed: No route to host
整条规则外面用单引号、里面用双引号是标准写法。顺带一提:实测把外层写成双引号也返回 success,存进去的规则一样 ------ 那是因为这几个值里都没有空格,bash 拼接完恰好还是合法的。值里带空格时就不是这样了,别依赖这种巧合,照标准写。
4. 🔴 firewalld 和手工 iptables / nft 规则:7 和 9 表现相反 ✅
4.1 CentOS 7:手工 iptables 规则,一 reload 就没
yaml
# iptables -I INPUT -p tcp --dport 8080 -j ACCEPT (CentOS 7)
$ nc -zv -w 3 192.168.3.100 8080
Connection to 192.168.3.100 8080 port [tcp/http-alt] succeeded!
# firewall-cmd --reload; iptables -S INPUT | grep -c 'dport 8080'
success
0
$ nc -zv -w 3 192.168.3.100 8080
nc: connect to 192.168.3.100 port 8080 (tcp) failed: No route to host
4.2 Rocky 9:手工 iptables 规则,reload 之后还在
css
# iptables -I INPUT -p tcp --dport 8091 -j ACCEPT; iptables -S | grep 8091 (Rocky 9)
-A INPUT -p tcp -m tcp --dport 8091 -j ACCEPT
# firewall-cmd --reload; echo "iptables 里 8091: $(iptables -S | grep -c 8091)"
success
iptables 里 8091: 1
Rocky 9 的 firewalld 用的是 nftables 后端(FirewallBackend=nftables)。推测原因是它只重建自己那张 nft 表,iptables 命令(nf_tables 版)写的规则在另一张表里,所以 reload 没碰到 ------ 这是推测,实测只证明了「reload 后还在」这个现象。
而直接往 firewalld 自己的 nft 表里加规则,连加都加不进去:
yaml
# nft add rule inet firewalld filter_IN_public_allow tcp dport 8090 accept
Error: Could not process rule: Operation not permitted
add rule inet firewalld filter_IN_public_allow tcp dport 8090 accept
^^^^^^^^^
4.3 Rocky 9 上用 iptables -L 看不到 firewalld 的规则
yaml
# iptables -L -n (Rocky 9,firewalld 正在运行、已放行 8080 和 9999)
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
---- iptables -S 行数: 3
# nft list ruleset | grep -nE 'dport (8080|8081|9999)'
156: tcp dport 8080 accept
157: tcp dport 9999 accept
🔴 Rocky 9 上 iptables -L 空着不代表没有防火墙规则 ,要看 nft list ruleset 或者直接 firewall-cmd --list-all。CentOS 7 上则相反,iptables -L 能看到 firewalld 生成的全部链(IN_public_allow 等)。
建议 :跑着 firewalld 的机器,规则一律用 firewall-cmd 加,别手工敲 iptables / nft ------ 它在 7 上会被冲掉、在 9 上会变成 firewalld 看不见的「第二套规则」,两种都会让以后排查的人摸不着头脑。
5. iptables:-A 追加的规则永远轮不到 ✅
CentOS 7(firewalld 运行中)用 -A 追加一条放行 8080:
css
# iptables -A INPUT -p tcp --dport 8080 -j ACCEPT; iptables -L INPUT -n --line-numbers (截取)
6 DROP all -- 0.0.0.0/0 0.0.0.0/0 ctstate INVALID
7 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
8 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
$ nc -zv -w 3 192.168.3.100 8080
nc: connect to 192.168.3.100 port 8080 (tcp) failed: No route to host
规则看得见,但不通。-v 的计数器说明了一切:
sql
# iptables -L INPUT -n -v --line-numbers | tail -3
6 1 40 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate INVALID
7 3 164 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
8 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
iptables 从上往下匹配、命中即停。第 7 条 REJECT 已经把包处理掉了(3 个包),第 8 条 ACCEPT 一个包都没轮到(0)。换成 -I 插到最前面:
css
# iptables -D INPUT -p tcp --dport 8080 -j ACCEPT; iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
$ nc -zv -w 3 192.168.3.100 8080
Connection to 192.168.3.100 8080 port [tcp/http-alt] succeeded!
# iptables -L INPUT -n -v --line-numbers | head -4
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 4 216 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
🔑 某条规则的 pkts 一直是 0,说明包根本没走到它,别再改它了,去看它前面是谁先处理了包。
5.1 CentOS 7 上 service iptables save 存不了
arduino
# service iptables save; echo "退出码=$?" (CentOS 7,未装 iptables-services)
The service command supports only basic LSB actions (start, stop, restart, try-restart, reload, force-reload, status). For other actions, please try to use systemctl.
退出码=2
跑着 firewalld 的 CentOS 7,持久化请走 firewall-cmd --permanent。改规则前想留个底,用 iptables-save:
shell
# iptables-save > /root/iptables.bak.lab; wc -l /root/iptables.bak.lab
175 /root/iptables.bak.lab
6. ufw(Ubuntu 24.04)✅
6.1 enable 会先问你
vbnet
# ufw allow 22/tcp; echo n | ufw enable
Rules updated
Rules updated (v6)
Command may disrupt existing ssh connections. Proceed with operation (y|n)? Aborted
# echo y | ufw enable; ufw status verbose
Command may disrupt existing ssh connections. Proceed with operation (y|n)? Firewall is active and enabled on system startup
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
写进脚本时用 ufw --force enable 跳过确认。
6.2 🔴 忘了放行 22 就 enable:会怎样
先 ufw --force reset 清空规则(它会先备份):
sql
# ufw --force reset (截取)
Backing up 'user.rules' to '/etc/ufw/user.rules.20260921_155319'
Backing up 'before.rules' to '/etc/ufw/before.rules.20260921_155319'
...
# ufw --force enable; ufw status verbose
Firewall is active and enabled on system startup
Status: active
Logging: on (medium)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
一条放行规则都没有。从另一台机器新建 SSH 连接:
shell
$ start=$(date +%s); nc -zv -w 5 10.115.180.200 22; echo "新 SSH 连接 退出码=$? 用时=$(( $(date +%s)-start ))s"
Ncat: TIMEOUT.
新 SSH 连接 退出码=1 用时=5s
但 enable 之前就连着的那个 SSH 会话没有断,还能继续敲命令,于是可以自救:
csharp
# ufw disable; ufw status (在原来那个会话里)
Firewall stopped and disabled on system startup
Status: inactive
🔑 所以远程改防火墙的铁律:手上留一个已经连着的会话别关,另开一个新会话测试。新会话连得上,再关旧的。
6.3 开端口:冒号表示端口段
shell
# ufw allow 8081-8083/tcp; echo "减号 退出码=$?"
ERROR: Bad port
减号 退出码=1
# ufw allow 8081:8083/tcp; echo "冒号 退出码=$?"
Rule added
Rule added (v6)
冒号 退出码=0
ufw allow 8080 和 ufw allow 8080/tcp 是两条不同的规则(前者 tcp+udp 都开):
scss
# ufw allow 8080; ufw allow 8080/tcp; ufw status numbered (截取)
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 8080 ALLOW IN Anywhere
[ 3] 8080/tcp ALLOW IN Anywhere
[ 4] 22/tcp (v6) ALLOW IN Anywhere (v6)
[ 5] 8080 (v6) ALLOW IN Anywhere (v6)
[ 6] 8080/tcp (v6) ALLOW IN Anywhere (v6)
只允许某个来源:
bash
# ufw allow from 10.115.180.175 to any port 3306 proto tcp
Rule added
6.4 删规则:按编号删,会问你,而且只删 IPv4 那一条
yaml
# echo y | ufw delete 2; ufw status numbered
Deleting:
allow 8080
Proceed with operation (y|n)? Rule deleted
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp LIMIT IN Anywhere
[ 2] 8081:8083/tcp ALLOW IN Anywhere
[ 3] 3306/tcp ALLOW IN 10.115.180.175
[ 4] 22/tcp (v6) LIMIT IN Anywhere (v6)
[ 5] 8080 (v6) ALLOW IN Anywhere (v6)
[ 6] 8081:8083/tcp (v6) ALLOW IN Anywhere (v6)
两点:①删完之后编号重排了,要连删多条就每次重新 status numbered;②**8080 (v6) 还留着** ------ 按编号删只删了你指的那一行。按规则原文删会把 v4 和 v6 一起删掉:
bash
# ufw delete allow 8080/tcp
Rule deleted
Rule deleted (v6)
6.5 ufw limit:第 6 次连接被拒
bash
# ufw limit 22/tcp; ufw status | grep 22
Rule updated
Rule updated (v6)
22/tcp LIMIT Anywhere
22/tcp (v6) LIMIT Anywhere (v6)
从另一台机器连续连 8 次:
第 1 次: Connected
第 2 次: Connected
第 3 次: Connected
第 4 次: Connected
第 5 次: Connected
第 6 次: refused
第 7 次: refused
第 8 次: refused
被限速拦下的连接是 refused(拒绝),不是超时 ------ 和 ufw 默认策略的超时不一样。日志里能看到:
ini
# grep 'UFW' /var/log/ufw.log | tail -1 (截取)
2026-09-21T15:52:09.874603+00:00 ubuntu2404 kernel: [UFW LIMIT BLOCK] IN=enp5s0 OUT= MAC=00:16:3e:47:a0:20:00:16:3e:93:b9:fc:08:00 SRC=10.115.180.175 DST=10.115.180.200 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=33344 DF PROTO=TCP SPT=34
日志在 /var/log/ufw.log(这台机器上存在,属主 syslog:adm)。调日志级别:
makefile
# ufw logging medium; ufw status verbose | grep Logging
Logging enabled
Logging: on (medium)
⚠️ ufw logging 后面必须带级别,光敲 ufw logging 会打出帮助和 ERROR: Invalid syntax。
6.6 应用配置
yaml
# ufw app list
Available applications:
OpenSSH
这台机器上只注册了 OpenSSH。
7. SELinux(Rocky 9 / CentOS 7):防火墙全对还是不通 ✅
两台 RHEL 系机器都是 Enforcing:
yaml
# getenforce; sestatus | head -5 (Rocky 9)
Enforcing
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
7.1 nginx 改到 8888 端口,起不来
bash
# cat /etc/nginx/conf.d/lab.conf
server {
listen 8888;
root /usr/share/nginx/html;
location /api/ { proxy_pass http://127.0.0.1:5000/; }
}
# nginx -t
nginx: configuration file /etc/nginx/nginx.conf test is successful
# systemctl start nginx
Job for nginx.service failed because the control process exited with error code.
nginx -t 通过,启动失败。日志:
ini
# journalctl -u nginx --no-pager | grep -iE 'bind|denied'
Sep 21 15:54:07 rocky9 nginx[2502]: nginx: [emerg] bind() to 0.0.0.0:8888 failed (13: Permission denied)
Permission denied,但 nginx 是 root 启动的 ------ 这就是 SELinux 的典型症状。5 秒确认:
shell
# setenforce 0; systemctl start nginx; echo "Permissive 下 start 退出码=$?"; systemctl stop nginx; setenforce 1; getenforce
Permissive 下 start 退出码=0
Enforcing
切到 Permissive 就能起,说明是 SELinux。确认完立刻 setenforce 1 改回去,然后按规则解决。
看它拦了什么:
arduino
# ausearch --input-logs -m avc -ts today | grep -m1 'name_bind'
type=AVC msg=audit(1790006047.686:50): avc: denied { name_bind } for pid=2502 comm="nginx" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
装了 setroubleshoot-server 的话,sealert 会直接给修法:
vbnet
# sealert -a /var/log/audit/audit.log (截取)
SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 8888.
***** Plugin bind_ports (92.2 confidence) suggests ************************
If you want to allow /usr/sbin/nginx to bind to network port 8888
Then you need to modify the port type.
Do
# semanage port -a -t PORT_TYPE -p tcp 8888
where PORT_TYPE is one of the following: http_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_t.
7.2 把端口加进 http_port_t
yaml
# semanage port -l | grep -E '^(http_port_t|http_cache_port_t|ssh_port_t) '
http_cache_port_t tcp 8080, 8118, 8123, 10001-10010
http_cache_port_t udp 3130
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
ssh_port_t tcp 22
# semanage port -a -t http_port_t -p tcp 8888; semanage port -l | grep '^http_port_t '
http_port_t tcp 8888, 80, 81, 443, 488, 8008, 8009, 8443, 9000
# systemctl start nginx; ss -lntp | grep 8888
LISTEN 0 511 0.0.0.0:8888 0.0.0.0:* users:(("nginx",pid=2756,fd=6),("nginx",pid=2755,fd=6),("nginx",pid=2754,fd=6))
semanage 在 Rocky 9 上属于 policycoreutils-python-utils,CentOS 7 上属于 policycoreutils-python(这台 CentOS 7 已装)。
🔴 8080 已经被定义成 http_cache_port_t 了 ,再 -a 加给 http_port_t,两个版本表现不同:
css
# semanage port -a -t http_port_t -p tcp 8080; echo "-a 退出码=$?" (CentOS 7)
ValueError: Port tcp/8080 already defined
-a 退出码=1
# semanage port -a -t http_port_t -p tcp 8080; echo "-a 退出码=$?" (Rocky 9)
Port tcp/8080 already defined, modifying instead
-a 退出码=0
CentOS 7 上要用 -m:
shell
# semanage port -m -t http_port_t -p tcp 8080; echo "-m 退出码=$?" (CentOS 7)
-m 退出码=0
7.3 nginx 反代报 502,日志只说 Permission denied
后端 127.0.0.1:5000 直连正常,经 nginx 反代就 502:
vbscript
直连后端 → 200
经 nginx 反代 → 502
# tail -1 /var/log/nginx/error.log (截取)
2026/09/21 16:04:13 [crit] 2756#2756: *1 connect() to 127.0.0.1:5000 failed (13: Permission denied) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /api/ HTTP/1.1", upstream: "http://127.0.0.1:5000/", host: "127.0.0.1:8888"
nginx 日志里一个字都没提 SELinux 。审计日志里是 name_connect:
arduino
# ausearch --input-logs -m avc -ts today | grep -m1 'name_connect'
type=AVC msg=audit(1790006653.557:135): avc: denied { name_connect } for pid=2756 comm="nginx" dest=5000 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:commplex_main_port_t:s0 tclass=tcp_socket permissive=0
打开布尔开关:
csharp
# getsebool httpd_can_network_connect; setsebool httpd_can_network_connect 1; getsebool httpd_can_network_connect
httpd_can_network_connect --> off
httpd_can_network_connect --> on
经 nginx 反代 → 200
7.4 🔴 setsebool 不加 -P,重启就回去了(重启实测)
不加 -P 时,semanage boolean -l 里「当前值」和「默认值」不一样:
vbnet
# semanage boolean -l | grep -E '^httpd_can_network_connect '
httpd_can_network_connect (on , off) Allow httpd to can network connect
重启后:
shell
# uptime -p; getsebool httpd_can_network_connect
up 1 minute
httpd_can_network_connect --> off
加 -P 再重启:
vbnet
# setsebool -P httpd_can_network_connect 1; semanage boolean -l | grep -E '^httpd_can_network_connect '
httpd_can_network_connect (on , on) Allow httpd to can network connect
(重启)
# uptime -p; getsebool httpd_can_network_connect
up 1 minute
httpd_can_network_connect --> on
和 firewalld 的 --permanent 是同一类坑:当时好了,重启又坏。
7.5 mv 过来的文件 403,cp 过来的正常
shell
# echo hi > /root/mv.html; echo hi > /root/cp.html
# cp /root/cp.html /usr/share/nginx/html/cp.html
# mv /root/mv.html /usr/share/nginx/html/mv.html
# ls -Z /usr/share/nginx/html/cp.html /usr/share/nginx/html/mv.html
unconfined_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/html/cp.html
unconfined_u:object_r:admin_home_t:s0 /usr/share/nginx/html/mv.html
cp.html → 200
mv.html → 403
cp 出来的新文件继承目标目录的标签;mv 保留原来在 /root 下的 admin_home_t。ls -l 看权限完全正常,只有 ls -Z 能看出区别。修:
arduino
# restorecon -v /usr/share/nginx/html/mv.html
Relabeled /usr/share/nginx/html/mv.html from unconfined_u:object_r:admin_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
restorecon 后 mv.html → 200
7.6 网站目录放在 /data/www:chcon 是临时的
/data/www 下的文件标签是 default_t,nginx 读不了。用 chcon 改标签能立刻好:
shell
# chcon -R -t httpd_sys_content_t /data/www; ls -Z /data/www/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /data/www/index.html
chcon 后 → 200
但只要有人跑一次 restorecon(或者系统重新打标签),就变回去了:
arduino
# restorecon -Rv /data/www
Relabeled /data/www from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:default_t:s0
Relabeled /data/www/index.html from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:default_t:s0
restorecon 后 → 403
持久的做法:先 semanage fcontext 注册规则,再 restorecon。只注册不 restorecon 是不生效的:
arduino
# semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"; ls -Z /data/www/index.html
unconfined_u:object_r:default_t:s0 /data/www/index.html
只注册未 restorecon → 403
# restorecon -Rv /data/www
Relabeled /data/www from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /data/www/index.html from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
注册 + restorecon 后 → 200
之后再跑 restorecon,标签也不会变回去了:
shell
# restorecon -Rv /data/www; ls -Z /data/www/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /data/www/index.html
7.7 ⚠️ 脚本里的 ausearch 会卡住
在远程执行、定时任务这类「有标准输入但不是终端」的环境里,ausearch 会去读标准输入,而不是读审计日志,表现就是一直挂着不返回(本次实测三次都卡到超时)。两种写法都能避免:
ini
# ausearch -m avc -ts today < /dev/null | tail -2 (截取)
type=AVC msg=audit(1790007255.865:215): avc: denied { getattr } for pid=3137 comm="nginx" path="/data/www/index.html" dev="sda2" ino=3324675 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
退出码=0
或者加 --input-logs(上面几段就是这么写的)。
7.8 audit2allow:会把日志里所有的拒绝一起放行
arduino
# ausearch --input-logs -m avc -ts today | audit2allow -m labtest (截取)
module labtest 1.0;
require {
type admin_home_t;
type commplex_main_port_t;
type fs_t;
type setroubleshootd_t;
type default_t;
type httpd_t;
type unreserved_port_t;
class tcp_socket { name_bind name_connect };
class file { getattr read };
class filesystem getattr;
}
#============= httpd_t ==============
allow httpd_t admin_home_t:file read;
注意第一条就是 allow httpd_t admin_home_t:file read ------ 也就是「允许 nginx 读 /root 下来的文件」,这是 §7.5 那次 mv 留下的拒绝记录。audit2allow 不分青红皂白,把今天所有的拒绝都变成了允许 。能用 semanage port / setsebool / restorecon 解决的,优先用它们;真要用 audit2allow,先把生成的规则一条条看过。
8. AppArmor(Ubuntu 24.04)✅
lua
# aa-status | head -3
apparmor module is loaded.
112 profiles are loaded.
18 profiles are in enforce mode.
8.1 nginx 没有 AppArmor profile,MySQL 有
装 nginx 和 mysql-server 之后:
shell
# ls /etc/apparmor.d/ | grep -iE 'nginx|mysql'
usr.sbin.mysqld
nginx 在 Ubuntu 24.04 上没有 profile,所以 Ubuntu 上 nginx 的端口、目录问题基本和 AppArmor 无关;MySQL 则有。
8.2 MySQL 数据目录挪到 /data/mysql,起不来
shell
# systemctl stop mysql; cp -a /var/lib/mysql /data/mysql
# (把 /etc/mysql/mysql.conf.d/mysqld.cnf 里的 datadir 改成 /data/mysql)
# grep -E '^\s*datadir' /etc/mysql/mysql.conf.d/mysqld.cnf
datadir = /data/mysql
# systemctl start mysql
Job for mysql.service failed because the control process exited with error code.
MySQL 自己的日志只说失败了:
ini
# journalctl -u mysql --no-pager | tail -3 (截取)
Sep 21 16:05:14 ubuntu2404 systemd[1]: mysql.service: Main process exited, code=exited, status=1/FAILURE
Sep 21 16:05:14 ubuntu2404 systemd[1]: mysql.service: Failed with result 'exit-code'.
Sep 21 16:05:14 ubuntu2404 systemd[1]: Failed to start mysql.service - MySQL Community Server.
原因在内核日志里:
arduino
# journalctl -k --no-pager | grep -i 'apparmor="DENIED"' | tail -1 (截取)
Sep 21 16:05:14 ubuntu2404 kernel: audit: type=1400 audit(1790006714.780:121): apparmor="DENIED" operation="open" class="file" profile="/usr/sbin/mysqld" name="/data/mysql/binlog.index" pid=4205 comm="mysqld" requested_mask="wrc" denied_mask="wrc" fsuid=104 ouid=104
修法:把新路径加进 profile 的本地覆盖文件(别改主文件,升级时会被覆盖),然后重载:
shell
# cat >> /etc/apparmor.d/local/usr.sbin.mysqld <<'AA'
/data/mysql/ r,
/data/mysql/** rwk,
AA
# apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld; echo "parser 退出码=$?"
parser 退出码=0
# systemctl start mysql; systemctl is-active mysql; mysql -NBe 'select @@datadir'
active
/data/mysql/
排查时想确认是不是 AppArmor,可以临时切到 complain 模式(只记录不拦截),确认完切回来:
bash
# aa-complain /etc/apparmor.d/usr.sbin.mysqld
Setting /etc/apparmor.d/usr.sbin.mysqld to complain mode.
# aa-enforce /etc/apparmor.d/usr.sbin.mysqld
Setting /etc/apparmor.d/usr.sbin.mysqld to enforce mode.
9. 查端口的工具:三台都没有 netstat ✅
| CentOS 7 | Rocky 9 | Ubuntu 24.04 | |
|---|---|---|---|
ss |
✅ | ✅ | ✅ |
netstat |
❌ command not found |
❌ | ❌ |
lsof |
❌ | ❌ | ❌ |
fuser |
❌ | ✅ | ✅ |
连 CentOS 7 这台都没装 net-tools 。与其每台去装 netstat,不如直接用 ss -lntp(三台都有):
ini
# ss -lntp | head -4 (CentOS 7)
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:* users:(("sshd",pid=1167,fd=3))
LISTEN 0 100 127.0.0.1:25 *:* users:(("master",pid=1434,fd=13))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1167,fd=4))
另外 nc -zv -w 3 <IP> <端口> 是从外面测端口最顺手的一条(带超时,别干等);nmap -Pn -p <端口> <IP> 能给出 open / closed / filtered 三种状态。
10. 本篇速查
排查顺序:
text
1. 本机 ss -lntp → 服务起来没?监听的是 0.0.0.0 还是 127.0.0.1?
2. 从外面 nc -zv → refused / No route to host / 超时,对照 §1 的表
3. 防火墙 → firewall-cmd --list-all / ufw status numbered
4. SELinux / AppArmor → getenforce + ausearch --input-logs;aa-status + journalctl -k | grep DENIED
| 想做的事 | firewalld | ufw |
|---|---|---|
| 看状态 | firewall-cmd --state(退出码 252 = 没运行) |
ufw status verbose |
| 开 8080/tcp | --add-port=8080/tcp --permanent + --reload |
ufw allow 8080/tcp |
| 端口段 | 8083-8085/tcp(减号) |
8081:8083/tcp(冒号) |
| 限来源 | --add-rich-rule='rule family="ipv4" source address="..." port protocol="tcp" port="..." accept' |
ufw allow from ... to any port ... proto tcp |
| 删规则 | --remove-port=... --permanent + --reload |
ufw delete allow 8080/tcp(v4/v6 一起删) |
| 挡住时客户端看到 | No route to host |
超时(limit 触发时是 refused) |
这一篇最容易踩的坑:
- 🔴 firewalld 挡端口回的是
No route to host,不是超时;而且挡住时看不出后面有没有服务。 - 🔴
--permanent不--reload不生效;不加--permanent一次 reload 就没。 - 🔴 firewalld reload 冲掉手工 iptables 规则:CentOS 7 会,Rocky 9 不会(但会变成 firewalld 看不见的第二套规则)。
- 🔴 Rocky 9 上
iptables -L是空的,规则在nft list ruleset里。 - 🔴 iptables
-A排在 REJECT 后面永远不命中,看-v的 pkts 计数。 - 🔴 ufw 忘了放行 22 就 enable:新连接进不来,旧会话还活着 ------ 别关它。
- 🔴 ufw 按编号删只删 v4,v6 那条还在。
- 🔴 SELinux:
bind() ... Permission denied、反代 502connect() ... Permission denied,日志都不提 SELinux。 - 🔴
setsebool不加-P重启失效;chcon一次restorecon就没。 - ⚠️ 脚本里的
ausearch要加< /dev/null或--input-logs,否则会挂住。 - ⚠️ CentOS 7 上
semanage port -a撞已定义端口会报错,要-m;Rocky 9 自动改成修改。
下一篇
(三)服务起不来:systemctl、开机自启、定时任务,同样三台机器实测。