这一篇讲什么
「端口不通」是最常见也最容易瞎折腾的问题:服务起了、防火墙也开了,外面还是连不上。本篇在三台机器上把排查链条从头到尾实跑一遍:先分清报错类型 → 本机在不在监听 → 防火墙 → 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 的服务,防火墙没放行:
# ss -lntp | grep 8080 (Rocky 9) LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=909,fd=3))从另一台机器连:
$ 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 一模一样:
$ 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 链最后一条:
# 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-prohibited1.2 被 ufw 挡:超时
Ubuntu 上ufw enable之后(只放行了 22),从 Rocky 连 8080:
$ 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 abyssnmap 两种情况都显示filtered,但 nc / curl 的表现完全不同。看 nc/curl 的报错能多知道一件事:挡你的大概率是哪一类防火墙。
1.3 端口放行了但没人监听:Connection refused
Rocky 上放行 9999,但不起服务:
$ 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 abyss1.4 服务只监听 127.0.0.1:从外面看也是Connection refused
# 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 → 2008082 防火墙已经放行,从外面连:
$ 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 没运行时:
# systemctl stop firewalld; firewall-cmd --state; echo "退出码=$?" (Rocky 9) not running 退出码=252脚本里判断时看退出码,别只看输出里有没有running。
3. firewalld:--permanent和--reload的两个方向 ✅
3.1 加了--permanent,不 reload,当前不生效
# 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运行时里是空的,只写进了配置。此时从外面连:
$ 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之后才通:
# 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! 2003.2 不加--permanent:不用等重启,一次 reload 就没了
# 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,你临时加的规则就没了,不用等到重启。
稳妥写法就是两条连着敲:
firewall-cmd--zone=public --add-port=8080/tcp--permanentfirewall-cmd--reload3.3 端口段是减号
# firewall-cmd --add-port=8083-8085/tcp; echo "减号 退出码=$?" success 减号 退出码=0 # firewall-cmd --add-port=8083:8085/tcp; echo "冒号 退出码=$?" Error: INVALID_PORT: 8083:8085 冒号 退出码=102ufw 正好反过来(见 §6.3)。
3.4 按服务名开、看网卡在哪个 zone
# 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(全放行):
# 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 开放(富规则)
# 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连不通:
$ 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 就没
# 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 host4.2 Rocky 9:手工 iptables 规则,reload 之后还在
# 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: 1Rocky 9 的 firewalld 用的是 nftables 后端(FirewallBackend=nftables)。推测原因是它只重建自己那张 nft 表,iptables命令(nf_tables 版)写的规则在另一张表里,所以 reload 没碰到 —— 这是推测,实测只证明了「reload 后还在」这个现象。
而直接往 firewalld 自己的 nft 表里加规则,连加都加不进去:
# 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 的规则
# 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:
# 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的计数器说明了一切:
# 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:8080iptables 从上往下匹配、命中即停。第 7 条 REJECT 已经把包处理掉了(3 个包),第 8 条 ACCEPT 一个包都没轮到(0)。换成-I插到最前面:
# 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存不了
# 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:
# iptables-save > /root/iptables.bak.lab; wc -l /root/iptables.bak.lab 175 /root/iptables.bak.lab6. ufw(Ubuntu 24.04)✅
6.1 enable 会先问你
# 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清空规则(它会先备份):
# 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 连接:
$ 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 会话没有断,还能继续敲命令,于是可以自救:
# ufw disable; ufw status (在原来那个会话里) Firewall stopped and disabled on system startup Status: inactive🔑 所以远程改防火墙的铁律:手上留一个已经连着的会话别关,另开一个新会话测试。新会话连得上,再关旧的。
6.3 开端口:冒号表示端口段
# ufw allow 8081-8083/tcp; echo "减号 退出码=$?" ERROR: Bad port 减号 退出码=1 # ufw allow 8081:8083/tcp; echo "冒号 退出码=$?" Rule added Rule added (v6) 冒号 退出码=0ufw allow 8080和ufw allow 8080/tcp是两条不同的规则(前者 tcp+udp 都开):
# 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)只允许某个来源:
# ufw allow from 10.115.180.175 to any port 3306 proto tcp Rule added6.4 删规则:按编号删,会问你,而且只删 IPv4 那一条
# 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 一起删掉:
# ufw delete allow 8080/tcp Rule deleted Rule deleted (v6)6.5ufw limit:第 6 次连接被拒
# 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 默认策略的超时不一样。日志里能看到:
# 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)。调日志级别:
# ufw logging medium; ufw status verbose | grep Logging Logging enabled Logging: on (medium)⚠️ufw logging后面必须带级别,光敲ufw logging会打出帮助和ERROR: Invalid syntax。
6.6 应用配置
# ufw app list Available applications: OpenSSH这台机器上只注册了 OpenSSH。
7. SELinux(Rocky 9 / CentOS 7):防火墙全对还是不通 ✅
两台 RHEL 系机器都是Enforcing:
# 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: enforcing7.1 nginx 改到 8888 端口,起不来
# 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通过,启动失败。日志:
# 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 秒确认:
# setenforce 0; systemctl start nginx; echo "Permissive 下 start 退出码=$?"; systemctl stop nginx; setenforce 1; getenforce Permissive 下 start 退出码=0 Enforcing切到 Permissive 就能起,说明是 SELinux。确认完立刻setenforce 1改回去,然后按规则解决。
看它拦了什么:
# 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会直接给修法:
# 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
# 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,两个版本表现不同:
# 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 退出码=0CentOS 7 上要用-m:
# semanage port -m -t http_port_t -p tcp 8080; echo "-m 退出码=$?" (CentOS 7) -m 退出码=07.3 nginx 反代报 502,日志只说 Permission denied
后端127.0.0.1:5000直连正常,经 nginx 反代就 502:
直连后端 → 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:
# 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打开布尔开关:
# 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 反代 → 2007.4 🔴setsebool不加-P,重启就回去了(重启实测)
不加-P时,semanage boolean -l里「当前值」和「默认值」不一样:
# semanage boolean -l | grep -E '^httpd_can_network_connect ' httpd_can_network_connect (on , off) Allow httpd to can network connect重启后:
# uptime -p; getsebool httpd_can_network_connect up 1 minute httpd_can_network_connect --> off加-P再重启:
# 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.5mv过来的文件 403,cp过来的正常
# 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 → 403cp出来的新文件继承目标目录的标签;mv保留原来在/root下的admin_home_t。ls -l看权限完全正常,只有ls -Z能看出区别。修:
# 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 → 2007.6 网站目录放在/data/www:chcon是临时的
/data/www下的文件标签是default_t,nginx 读不了。用chcon改标签能立刻好:
# 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(或者系统重新打标签),就变回去了:
# 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 是不生效的:
# 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,标签也不会变回去了:
# restorecon -Rv /data/www; ls -Z /data/www/index.html unconfined_u:object_r:httpd_sys_content_t:s0 /data/www/index.html7.7 ⚠️ 脚本里的ausearch会卡住
在远程执行、定时任务这类「有标准输入但不是终端」的环境里,ausearch会去读标准输入,而不是读审计日志,表现就是一直挂着不返回(本次实测三次都卡到超时)。两种写法都能避免:
# 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.8audit2allow:会把日志里所有的拒绝一起放行
# 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)✅
# 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 之后:
# ls /etc/apparmor.d/ | grep -iE 'nginx|mysql' usr.sbin.mysqldnginx 在 Ubuntu 24.04 上没有 profile,所以 Ubuntu 上 nginx 的端口、目录问题基本和 AppArmor 无关;MySQL 则有。
8.2 MySQL 数据目录挪到/data/mysql,起不来
# 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 自己的日志只说失败了:
# 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.原因在内核日志里:
# 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 的本地覆盖文件(别改主文件,升级时会被覆盖),然后重载:
# 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 模式(只记录不拦截),确认完切回来:
# 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(三台都有):
# 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. 本篇速查
排查顺序:
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、开机自启、定时任务,同样三台机器实测。