Linux 实战(二):端口不通 —— CentOS 7、Rocky 9、Ubuntu 24.04 上防火墙和 SELinux 的实测差异

这一篇讲什么

「端口不通」是最常见也最容易瞎折腾的问题:服务起了、防火墙也开了,外面还是连不上。本篇在三台机器上把排查链条从头到尾实跑一遍:先分清报错类型 → 本机在不在监听 → 防火墙 → 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)

这一篇最容易踩的坑:

  1. 🔴 firewalld 挡端口回的是 No route to host,不是超时;而且挡住时看不出后面有没有服务。
  2. 🔴 --permanent 不 --reload 不生效;不加 --permanent 一次 reload 就没。
  3. 🔴 firewalld reload 冲掉手工 iptables 规则:CentOS 7 会,Rocky 9 不会(但会变成 firewalld 看不见的第二套规则)。
  4. 🔴 Rocky 9 上 iptables -L 是空的,规则在 nft list ruleset 里。
  5. 🔴 iptables -A 排在 REJECT 后面永远不命中,看 -v 的 pkts 计数。
  6. 🔴 ufw 忘了放行 22 就 enable:新连接进不来,旧会话还活着 ------ 别关它。
  7. 🔴 ufw 按编号删只删 v4,v6 那条还在。
  8. 🔴 SELinux:bind() ... Permission denied、反代 502 connect() ... Permission denied,日志都不提 SELinux。
  9. 🔴 setsebool 不加 -P 重启失效;chcon 一次 restorecon 就没。
  10. ⚠️ 脚本里的 ausearch 要加 < /dev/null 或 --input-logs,否则会挂住。
  11. ⚠️ CentOS 7 上 semanage port -a 撞已定义端口会报错,要 -m;Rocky 9 自动改成修改。

下一篇

(三)服务起不来:systemctl、开机自启、定时任务,同样三台机器实测。

相关推荐
迷途之人不知返1 小时前
【基础IO】-1-预备知识与准备工作
linux
大鹏的NLP博客2 小时前
WSL Ubuntu 26.04 升级与环境整理记录
linux·ubuntu·wsl
GeW2 小时前
如何打造真正的数字化工厂?需从Red Hat到数据库底层打捞基石
linux
沫璃染墨2 小时前
从零入门计算机网络系列(一):计算机网络初识——从网络发展史到TCP/IP协议》
linux·网络·网络协议·tcp/ip·计算机网络
忆挽篱笙歌4 小时前
gdb/cgdb
linux·ubuntu
Julien20045 小时前
管理 Ansible 配置文件
linux·运维·服务器·ssh·学习方法
MicrosoftCloud6 小时前
性能排查 01|free 显示内存用了 90% 就是快满了吗?buff/cache 与 available 一次讲清
linux·运维·内存·free·buff/cache
xiaoye-duck6 小时前
《Linux 网络编程》深入理解 epoll(下):epoll 实战开发与 LT/ET 触发模式深度剖析
linux·网络
峥无7 小时前
Linux线程深度剖析:轻量级进程、虚拟内存分页、进程线程资源对比
linux·运维·mmap