☰
CentOS7下Firewalld批量放行端口:直接编辑配置文件实战
2026/10/23 8:23:33 网站建设 项目流程

接手过 CentOS7 服务器的人对 firewalld 应该都不陌生,日常放行单个端口很简单,一条firewall-cmd --permanent --add-port=8080/tcp就结束了。可一旦需求变成“这批服务要开几十上百个端口”,逐条敲命令就成了灾难。我前段时间就遇到一次给某数据平台中间件集群批量放行端口的需求,前前后后试了几种方式,最后发现直接操作 firewalld 的规则配置文件,才是最稳妥、最好审计、也最容易批量化的做法。这篇文章就围绕 CentOS7 下 Firewalld 规则配置文件批量放行端口这件事,把配置文件的组织方式、批量操作的几种写法、验证回滚流程和常见坑一次性讲清楚,适合正在做服务器初始化、中间件部署、以及手工维护防火墙规则的人参考。

1. 为什么我选择直接改规则文件而不是一条条敲命令

先说那次经历。某天下午接到需求:一个新环境要接入一套消息中间件集群,三个节点,每个节点需要放行 TCP 20000 到 20100、UDP 5300 到 5400,外加 8080 和 8443。看起来端口段不多,但同一套配置要原样复制到 6 台服务器上。如果用firewall-cmd一条条敲,每台机器少说十几条命令,还得小心 20000-20100 这种范围在命令行里的写法,出一点错就得从头检查。

命令行方案在批量场景下有三个明显的痛点。第一是慢,端口越多命令越多,一台机器敲完还要换下一台;第二是难审计,几十条命令执行下去,事后根本说不清到底放开了哪些端口,想对比两台机器配置是否一致只能肉眼碰运气;第三是不好回滚,临时反悔了要一条条删,删漏一条就是安全隐患。

直接改配置文件完全是另一套体验。/etc/firewalld/下的规则文件是纯文本,可以放在 Git 里做版本管理,可以复制到所有机器保持一致性,改之前cp一份就是天然的回滚点。而且很多人不知道一点:firewall-cmd --permanent --add-port这个命令的本质就是在改写这些 XML 文件,只是通过 CLI 做了一层校验而已。所以直接编辑规则文件并不是什么旁门左道,只是绕过了逐条校验,需要对 XML 格式有把握。

下面这张表是我自己选型时的对比,场景就是“批量、多端口、多机器”:

对比项firewall-cmd 逐条执行直接编辑规则文件
操作效率端口多时极低一次性写入,高
可审计性差,靠日志或屏幕记录好,文件可 diff
多机一致容易遗漏或手滑文件原样分发即可
回滚删除命令逐条执行恢复备份文件
上手门槛低需要理解 XML 结构
适用场景临时调试、少量端口正式需求、批量部署

需要说明的是,CLI 方式也有它的价值,特别是临时开个端口调试问题,firewall-cmd --add-port改运行时配置非常方便。我这里的建议是:临时操作用命令,正式、批量、要长期生效的规则用配置文件。两者不冲突,很多服务器上长期规则和临时规则是并存的。

2. 动手前先搞懂 Firewalld 的配置文件体系

直接改文件之前,必须先把/etc/firewalld/下那几类文件的分工搞清楚。改错文件是最冤枉的踩坑方式,我见过有人把规则写进direct.xml,结果完全没生效,原因就是放错了地方。

2.1 zone 文件:常规端口放行的主战场

正常情况下,批量放行端口的主战场是/etc/firewalld/zones/目录下的 XML 文件。每一台 CentOS7 机器都有一组 zone(区域),比如 public、internal、dmz、work,每个 zone 代表一种信任级别。系统会有一个默认 zone,通常就是 public,所有没有特别指定的网卡流量都走这个 zone。

/etc/firewalld/zones/public.xml对应 public 区域。系统自带的初始文件大概长这样:

<?xml version="1.0" encoding="utf-8"?> <zone> <short>Public</short> <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description> <service name="ssh"/> <service name="dhcpv6-client"/> </zone>

这个文件里<service name="ssh"/>表示放行 SSH 服务(对应 22 端口),<service name="dhcpv6-client"/>放行 DHCPv6 客户端所需端口。我们要做的批量放行,就是在这个文件里增加<port>元素,指定端口号和协议:

<port port="8080" protocol="tcp"/> <port port="20000-20100" protocol="tcp"/>

这里有一个容易忽略的点:/usr/lib/firewalld/zones/下还有一份系统默认模板,而/etc/firewalld/zones/下的文件会覆盖模板。如果你在/usr/lib底下改文件,重启后可能被覆盖,一定要改/etc/firewalld/zones/这一份。

2.2 direct.xml:直接写 iptables 参数的旁路通道

/etc/firewalld/direct.xml是另一个入口,允许你绕过 zone 的抽象,直接塞 iptables 命令行参数。文件长这样:

<?xml version="1.0" encoding="utf-8"?> <direct> <rule ipv="ipv4" table="filter" chain="IN_public_allow" priority="0">-p tcp -m multiport --dports 30000:30050 -j ACCEPT</rule> </direct>

IN_public_allow是 public zone 内部实际使用的自定义链,直接往这个链里塞规则,语义上跟 zone 文件放行端口是一致的。但我一般不会首选direct.xml,原因后面在选型对比里细说。它更适合常规 zone 语法表达不了的场景,比如需要自定义匹配条件、多条件组合的规则。

2.3 rich rule:藏在 zone 文件里的复杂规则

rich rule 不是单独的文件,而是 zone XML 文件内部的<rule>元素。它支持更复杂的条件,比如限定来源 IP 再放行端口:

<rule family="ipv4"> <source address="192.168.1.0/24"/> <port port="8443" protocol="tcp"/> <accept/> </rule>

这条规则的意思是:只有来自192.168.1.0/24的流量才允许访问 8443 端口。rich rule 功能强大,但语法比<port>复杂,批量堆端口时阅读性也差,所以大批量放行场景中我不太用它,后面会展开对比。

2.4 运行时配置与永久配置的区别

理解 firewalld 的关键是它有两套配置:运行时(runtime)和永久(permanent)。运行时配置是当前生效的规则,firewall-cmd --add-port不加--permanent时只改运行时,重启即失效。永久配置存在/etc/firewalld/的 XML 文件里,我们手动编辑文件改的就是永久配置。

改完文件后要让规则立即生效,必须执行重载:

firewall-cmd --reload

--reload会把永久配置重新应用到运行时,并且尽量保留已建立的连接,是最常用的方式。另一个是--complete-reload,它的动作更彻底,会重新初始化整个防火墙,已经建立的连接也会被重置,一般只有规则完全乱掉的时候才用。值得提醒的是,部分版本的 firewalld 会通过文件监听自动感知 zone 文件变化并触发重载,但不同版本行为不一致,发布流程里不要依赖这种自动行为,手动执行 reload 才有确定性。

3. 批量放行的四种姿势及选型对比

搞清楚文件体系之后,就可以聊批量放行的具体写法了。根据端口数量和场景,我通常会在四种方式里选。

3.1 Zone 文件里堆端口条目

最直观的方式就是在 zone 文件里一条条堆<port>元素:

<?xml version="1.0" encoding="utf-8"?> <zone> <short>Public</short> <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description> <service name="ssh"/> <service name="dhcpv6-client"/> <port port="8080" protocol="tcp"/> <port port="8443" protocol="tcp"/> <port port="10000" protocol="tcp"/> <port port="10001" protocol="tcp"/> <port port="10002" protocol="tcp"/> </zone>

优点是结构清晰、任何会看 XML 的人都能一眼看懂;缺点是端口多的时候文件很长。如果只有几十个端口,我推荐这种方式,配合版本管理非常直观。

3.2 端口段 range 写法

对于连续端口,firewalld 支持直接写范围,中间用短横线连接:

<port port="20000-20100" protocol="tcp"/> <port port="5300-5400" protocol="udp"/>

这是批量放行中最该优先使用的写法。一条规则就能覆盖 101 个端口,iptables 底层会自动把它翻译成一个-m multiport --dports 20000:20100的匹配项。要注意的是协议必须写对,TCP 服务写tcp,UDP 服务写udp,混用会导致服务仍然不通。

这里有个性能相关的细节:firewalld 会把 zone 文件里每个<port>元素展开成一条 iptables 规则。如果几百个不连续端口全部逐条列出来,防火墙规则链会变得很长,每次报文匹配都要走一遍链表。iptables 的 multiport 模块单条规则最多支持 15 个端口,超过就得拆多条规则。所以能用范围就用范围,实在是不连续端口,也尽量合并成尽量少的范围段。

3.3 用脚本批量生成 XML 片段

当端口清单来自需求文档、数量上百而且不连续时,手写 XML 不现实,这时候用脚本生成片段是最靠谱的。我常用一个简单的 Python 脚本,把端口列表自动合并成范围:

def to_ranges(ports): ports = sorted(set(ports)) res = [] start = prev = ports[0] for p in ports[1:]: if p == prev + 1: prev = p else: res.append((start, prev)) start = prev = p res.append((start, prev)) return res # 示例:8080、8443 以及 10000 到 10100 连续段 ports = [8080, 8443] + list(range(10000, 10101)) for a, b in to_ranges(ports): tag = str(a) if a == b else f"{a}-{b}" print(f' <port port="{tag}" protocol="tcp"/>')

输出结果:

<port port="8080" protocol="tcp"/> <port port="8443" protocol="tcp"/> <port port="10000-10100" protocol="tcp"/>

如果机器上没有 Python,用 bash 也能做,只是合并范围的逻辑要自己多写几行:

for p in $(seq 30000 30100); do printf ' <port port="%s" protocol="tcp"/>\n' "$p" done

脚本生成的好处是端口清单可以放在一个纯文本文件里维护,脚本负责生成 XML,格式永远统一,不会出现手写时少个斜杠、多个空格的问题。

3.4 direct.xml 处理极端场景

少数极端情况我会用到direct.xml:比如需要同时对多个不连续端口做一条规则,或者需要自定义链的复杂跳转。例如:

<direct> <rule ipv="ipv4" table="filter" chain="IN_public_allow" priority="0">-p tcp -m multiport --dports 30000,30002,30004-30010 -j ACCEPT</rule> </direct>

用 direct 规则可以直接利用 multiport 的逗号组合语法,在不连续端口场景下能控制规则条数。但代价是失去了 zone 的抽象,firewalld 自动生成的规则和手动塞进去的规则混在一起,排查问题时心智负担更重。我的建议是:能用<port>解决就不用 direct,direct 留给真正需要的时候。

3.5 四种方式的选型逻辑

我把选型逻辑总结成一个表格,实际工作里基本照着这个判断:

场景推荐方式理由
少量端口(≤20)zone 文件堆条目直观、可读性好
端口有规律、连续zone 文件 range 写法规则条数少,性能好
端口多且不连续脚本生成 zone 条目避免手写错漏
需要来源 IP 限制等复杂条件rich rule只能用它
需要复杂匹配、自定义链direct.xml兜底选项

4. 完整实战:一次 160 个端口的批量放行全过程

理论讲完,来看一次完整的实际操作。我以某数据平台中间件接入为例,需求是这样的:新部署的中间件集群需要对外提供 TCP 10000-10100 共 101 个端口,UDP 5000-5100 共 101 个端口,另外还有 8080 和 8443 两个 Web 端口。所有服务器都是 CentOS7,默认 public zone。

4.1 第一步:确认当前生效的 zone

改文件之前先弄清楚流量到底走哪个 zone。很多意外都是因为想当然认为默认是 public,结果服务器的接口绑到了别的 zone 上。

# 查看默认 zone firewall-cmd --get-default-zone # 查看所有活跃 zone 以及绑定的网卡 firewall-cmd --get-active-zones

如果--get-active-zones显示public绑定在eth0上,那我们的目标文件就是/etc/firewalld/zones/public.xml。如果绑定的是internal或者自定义 zone,就要改对应的文件。

4.2 第二步:备份规则文件

任何一次修改前都做备份,这是成本最低的保险。我用带日期的备份文件名,方便回滚时辨认:

cp /etc/firewalld/zones/public.xml /etc/firewalld/zones/public.xml.bak.$(date +%F)

4.3 第三步:用脚本生成端口 XML 片段

按照上面的 Python 脚本思路,把需求端口喂进去:

ports = list(range(10000, 10101)) + [8080, 8443] # 按协议区分 tcp_ports = sorted(set(ports)) udp_ports = list(range(5000, 5101)) def to_ranges(ports): ports = sorted(set(ports)) res = [] start = prev = ports[0] for p in ports[1:]: if p == prev + 1: prev = p else: res.append((start, prev)) start = prev = p res.append((start, prev)) return res for a, b in to_ranges(tcp_ports): tag = str(a) if a == b else f"{a}-{b}" print(f' <port port="{tag}" protocol="tcp"/>') for a, b in to_ranges(udp_ports): tag = str(a) if a == b else f"{a}-{b}" print(f' <port port="{tag}" protocol="udp"/>')

运行后输出:

<port port="8080" protocol="tcp"/> <port port="8443" protocol="tcp"/> <port port="10000-10100" protocol="tcp"/> <port port="5000-5100" protocol="udp"/>

80 个手工条目被自动压缩成了 4 条范围规则,这就是批量放行的精髓。

4.4 第四步:把片段写入 zone 文件并校验 XML

用编辑器打开/etc/firewalld/zones/public.xml,把生成的片段插到</zone>之前,保存:

<?xml version="1.0" encoding="utf-8"?> <zone> <short>Public</short> <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description> <service name="ssh"/> <service name="dhcpv6-client"/> <port port="8080" protocol="tcp"/> <port port="8443" protocol="tcp"/> <port port="10000-10100" protocol="tcp"/> <port port="5000-5100" protocol="udp"/> </zone>

保存之前先做 XML 语法校验,避免一个标签写错导致整个 zone 加载失败:

# 方法一:xmllint(需要安装 libxml2) xmllint --noout /etc/firewalld/zones/public.xml # 方法二:没有 xmllint 时用 Python python -c "import xml.dom.minidom,sys; xml.dom.minidom.parse('/etc/firewalld/zones/public.xml'); print('xml ok')"

输出没有报错,文件就算过了语法关。还要顺手确认一下文件属主和权限,避免后面 firewalld 回写时出问题:

chown root:root /etc/firewalld/zones/public.xml chmod 640 /etc/firewalld/zones/public.xml

4.5 第五步:重载并确认规则

firewall-cmd --reload # 确认没有报错,然后查看 public zone 的完整配置 firewall-cmd --zone=public --list-all

正常输出里会看到:

public (active) target: default icmp-block-inversion: no interfaces: eth0 sources: services: ssh dhcpv6-client ports: 8080/tcp 8443/tcp 10000-10100/tcp 5000-5100/udp masquerade: no forward-ports: source-ports: icmp-blocks: rich rules:

看到ports一行列出了我们添加的全部端口和范围,这一步就算成功了。

5. 改完之后的验证与回滚

配置写对只是第一步,端口到底通不通,必须验证。而且安全的变更流程一定要有回滚预案,不然改完上不了线,连恢复的手段都没有。

5.1 用 firewall-cmd 查看规则状态

除了--list-all,还有几个常用查询命令:

# 只查看已放行的端口 firewall-cmd --zone=public --list-ports # 查询单个端口是否已放行 firewall-cmd --zone=public --query-port=8080/tcp

--query-port返回yes表示端口已在放行列表中。对于 range 端口,直接用10000-10100/tcp查询。

5.2 用 iptables-save 核对底层规则

firewall-cmd显示的是它自己维护的逻辑视图,我更习惯再用iptables-save看一眼底层真实规则,确认 firewalld 确实把规则下到了 iptables:

iptables-save | grep IN_public_allow

输出里能看到类似:

-A IN_public_allow -p tcp -m tcp --dport 8443 -m conntrack --ctstate NEW -j ACCEPT -A IN_public_allow -p tcp -m multiport --dports 10000:10100 -m conntrack --ctstate NEW -j ACCEPT -A IN_public_allow -p udp -m multiport --dports 5000:5100 -m conntrack --ctstate NEW -j ACCEPT

看到 multiport 范围匹配正确出现,说明规则已经真正生效。这一步还能发现一个常见问题:范围没有被合并成一条 multiport,而是展开成了几百条单端口规则,那就说明 XML 里的 range 写法没有生效,需要回头检查端口段格式。

5.3 用实际连接测试做端到端验证

规则层面没问题,不代表服务本身通了。我一般会分两层验证:先确认服务在监听,再从外部确认端口可访问。

# 在服务器本机查看监听端口 ss -lntup | grep -E ':(8080|8443|10000|5000)\b' # 用 nc 从本机探测 nc -vz 127.0.0.1 8080 nc -vz 127.0.0.1 10000

如果服务还没启动,或者监听的端口跟放行的端口不一致,ss的输出对不上,就要先查服务配置而不是查防火墙。从外部机器测试时,把127.0.0.1换成服务器实际 IP,ping 通不代表端口通,一定要用 TCP 层的探测工具。

5.4 回滚操作

万一改完之后发现端口放行范围有误,或者中间件集群换方案了,回滚只要两步:

# 用备份覆盖修改后的文件 cp /etc/firewalld/zones/public.xml.bak.2025-01-01 /etc/firewalld/zones/public.xml # 重新加载 firewall-cmd --reload

回滚后再次执行firewall-cmd --zone=public --list-ports确认端口列表已经恢复。这里要强调的是:不要手忙脚乱去删 XML 里的条目,直接恢复备份文件最可靠,因为备份文件里一定是一个曾经正常工作的状态。

6. 配置文件方案踩坑记录

直接编辑配置文件这件事,我前后踩过不少坑,挑几个最典型的分享出来,这些细节在官方文档里往往不会写。

6.1 改错了 zone,规则没生效

最大的一次失误是改完public.xml后 reload,端口就是不生效。排查了半天才发现那台服务器默认 zone 被改成了dmz,网卡绑定在dmz上,我一直改 public 当然没用。现在我的流程固定了:先firewall-cmd --get-active-zones,再决定改哪个文件,绝不跳过这一步。

6.2 XML 语法错误导致整个 zone 失效

XML 对语法零容忍,少一个闭合标签、多用半个引号,整个文件解析失败。最麻烦的是,有些版本在解析失败时会保留旧配置继续运行,有些版本可能直接不加载新配置,表现差异很大。我的解决办法是:写完后一定先跑一遍xmllint --noout再 reload,绝不跳过校验。另外注意别用中文引号、全角空格,编辑器自动替换最容易埋这种雷。

6.3 reload 用错了级别

有一次我在执行变更时用了--complete-reload,结果正在跑的业务连接全部断了一遍。那台机器上有长连接任务,本来只改个端口用--reload就够了,一不留神用了更粗暴的完整重载,代价是业务抖动。现在我的原则是:默认只用--reload,只有防火墙状态已经完全混乱、需要彻底重置时才考虑--complete-reload。

6.4 文件权限和属主被改乱

我遇到过一次手动编辑后文件属主变成了当前登录用户,firewalld 在回写文件时直接报权限错误。建议对/etc/firewalld/zones/下的文件统一执行一次chown root:root和chmod 640,并养成修改后检查权限的习惯。

6.5 端口范围太大导致规则异常

用过一次从 1 写到 65535 的极端范围,虽然语法上没问题,但 firewalld 处理起来并不愉快,而且这种全开行为本身就有很大安全风险。iptables 的 multiport 单条规则最多 15 个端口,范围按 2 个端口计。范围太大时要么拆段,要么考虑用 rich rule 限定来源 IP,而不是把整个防火墙打开。

6.6 忘了区分 TCP 和 UDP

同样的端口号,TCP 和 UDP 是两套完全独立的规则。中间件有些服务用 UDP 做服务发现,我只放了 TCP,结果客户端一直注册不上。查问题的时候一度以为是应用配置出错,最后看到 XML 里 UDP 端口根本没写。批量放行时建议脚本里按协议分别输出,人眼检查时也先数数量再对协议。

6.7 SELinux 在防火墙之外又拦了一道

防火墙放行只是第一层。CentOS7 默认开启 SELinux,如果服务要监听一个非标准端口,而系统的 SELinux 端口策略里没有这个端口,即使防火墙放行了,服务也可能起不来。典型场景是自定义端口上的 HTTP 服务。这种情况需要给端口打上对应的 SELinux 标签,这跟 firewalld 是两码事,排查端口不通时如果防火墙规则看着都对,记得往这个方向想。

7. 几个让批量放行更省心的管理习惯

文章最后分享几个我实际工作中沉淀下来的管理习惯,算不上什么高深技巧,但确实能省不少事。

第一,把端口清单当作跟代码一样的资产来维护。我习惯在项目仓库里放一个ports.txt,一行一个端口加协议加用途说明,比如10000/tcp # 消息中间件主端口。需要放行时直接用脚本从这个文件生成 XML,规则始终跟需求文档一一对应,不会出现“这个端口到底谁让加的”这种历史谜案。

第二,配置文件尽量少写注释。firewalld 自己会回写这些 XML 文件,每次回写都可能把注释丢掉,你精心写的注释可能过几天就没了。有需要说明的内容,写在 Git 提交信息或者变更单里,比写在配置文件里靠谱。

第三,批量部署到多台机器时,优先用模板化的配置管理工具。把public.xml做成模板,通过自动化工具分发到各节点,再在任务里统一执行 reload。相比手动逐台修改,模板化能保证所有机器规则完全一致,后续加端口只要改一处模板重跑一遍,几台、几十台的效率差距非常明显。

回到最初那个中间件集群需求,用配置文件方案后,6 台服务器的规则在十几分钟内全部就位,每台机器的 XML 文件内容完全一致,备份和上线记录都留在 Git 里。后来运维同事要查某台机器到底开了哪些端口,直接看仓库里的文件就行,比登录服务器敲命令翻输出舒服得多。批量放行端口这件事,核心其实不是“放行”本身,而是让规则的变更变得可见、可复现、可回滚,配置文件正是把这三件事做到位的最直接途径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询