☰
firewalld批量端口放行实战:彻底搞懂XML配置文件与富规则
2026/10/23 1:24:28 网站建设 项目流程

CentOS7 上线到现在,firewalld 作为默认防火墙已经被大量生产环境验证过了。但很多人对它的使用仍停留在firewall-cmd --add-port=8080/tcp这种一条一条加命令的阶段,真要遇到批量放行几十上百个端口,或者需要按来源 IP 做精细化放行时,命令行工具不仅繁琐,还容易出错。这篇东西我不打算重复 man 手册,直接把规则配置文件(也就是/etc/firewalld/zones/下的 XML 文件)讲透,并且给出几种适合批量操作的放行方案,以及我在实际运维中踩过的坑。

1. firewalld 规则的基本形态与批量放行的核心思路

1.1 为什么 CentOS7 默认选择 firewalld

CentOS7 之前,多数服务器跑的是 iptables,规则靠iptables -A INPUT -p tcp --dport 22 -j ACCEPT这种链式命令堆叠。链式规则的问题在于:规则多了以后顺序敏感、维护困难,而且默认策略与放行规则之间的逻辑关系要靠人脑去捋。firewalld 的设计思路是改造成“区域(zone)”模型,把不同信任级别的接口、来源放到不同区域里,每个区域定义自己的放行策略,规则本身也支持按服务名、端口段、来源地址等维度描述。

这个改动对运维来说是一种革命式的简化,常见场景不再需要记一堆 iptables 参数,只需搞清楚“流量进了哪个区域”,问题就解决了一大半。默认区域是 public,如果不做任何配置,网卡流量都会落到 public 上。所以排查放行不生效的问题时,第一反应不是“规则写错了”,而是“流量进的这个区域根本不是我以为的那个”。

1.2 规则配置文件的真实地位:--permanent 只不过是在写文件

很多人有一个误区,觉得firewall-cmd是内存操作,配置文件是独立的一套。实际上 firewalld 的持久化规则就存放在/etc/firewalld/zones/下,文件名形如public.xml、internal.xml。你在命令行执行firewall-cmd --permanent --add-port=8080/tcp背后干的唯一事情就是:往public.xml里插入一段 XML 节点,然后再执行firewall-cmd --reload让配置生效。

明白了这一点,批量放行端口的最优方式就浮出水面了:与其在命令行循环执行几十次--add-port,不如直接编辑 XML 文件,一次把端口段、来源限制全部写好,然后重载一次。

这里引出一个重要概念:firewalld 有“运行时配置”和“永久配置”两套。不带--permanent的命令只改运行时,重启后丢失;带--permanent则落盘但不立即生效,必须 reload。直接改 XML 文件本质上就是改永久配置,好处是规则结构一目了然、便于备份和版本管理,坏处是必须注意 XML 语法,写错了 reload 会直接失败而不是跳过错误规则。

1.3 批量放行前的三个前置检查

批量操作前我会先确认三件事,不然容易白忙活:

  • 当前默认区域是哪个:执行firewall-cmd --get-default-zone,记住流量实际落在哪个区域,改的就是哪个区域的 XML。多网卡环境下还要用firewall-cmd --get-active-zones查看每个网卡绑定哪个区域。
  • 是不是需要限制来源 IP:如果端口只允许内网访问,单纯加一个<port>节点是不够的,需要用富规则(rich rule)配合<source>地址段。很多人只写端口不写来源,等于是对全世界开放,这在生产环境里是个严重的配置失误。
  • selinux 是否在拦截:firewalld 放行了不代表服务一定能访问,如果 selinux 处于 enforcing,某些非标准端口的服务还会被 selinux 策略挡住。批量放行端口前顺手查一下getenforce,可以避免“防火墙明明加了规则却还是不通”的诡异现象。

2. 配置文件结构与规则类型解构

2.1 配置文件目录:哪些文件在真正生效

firewalld 有两处配置目录,需要分清:

  • /usr/lib/firewalld/zones/:系统自带的默认配置。这里面是发行版厂商预置的规则模板,比如 public.xml、home.xml、internal.xml 等。
  • /etc/firewalld/zones/:用户自定义配置目录。你手动创建或运行--permanent命令生成的 zone 配置都在这里。

优先级上,/etc/下的同名 zone 会覆盖/usr/lib/下的默认文件,也就是说如果你在/etc/firewalld/zones/里创建了一个public.xml,系统会用它替换掉默认的自带 public 配置。我踩过坑:某次实习生在/usr/lib/firewalld/zones/下直接改了 public.xml,重启后规则全部还原成了出厂状态,因为/usr/lib在 RPM 更新或系统重启某些阶段会被覆盖。教训就是:永远只在/etc/firewalld/zones/下动手。

2.2 zone XML 的常见节点解析

一个典型的 public.xml 长这样:

<?xml version="1.0" encoding="utf-8"?> <zone> <short>Public</short> <description>Unsolicited incoming network packets are rejected.</description> <service name="ssh"/> <service name="dhcpv6-client"/> <port port="80" protocol="tcp"/> <port port="443" protocol="tcp"/> <rich-rule> <rule family="ipv4"> <source address="10.0.0.0/8"/> <port port="3306" protocol="tcp"/> <accept/> </rule> </rich-rule> </zone>

我来逐个节点说明实际含义:

  • <zone>是根节点,target属性控制默认策略,默认是ACCEPT(接受所有未匹配流量),也可改成DROP(拒绝所有未匹配流量)或REJECT。生产环境一般保持默认即可,不用动。
  • <short>和<description>只是显示用的名字与描述,不影响规则行为,但建议保留,firewall-cmd --list-all输出和图形管理工具会读它们。
  • <service name="ssh"/>表示放行 ssh 这个服务,具体端口号由/usr/lib/firewalld/services/ssh.xml定义。用 service 的好处是语义清晰,坏处是放行的端口范围是固定的,不适合自定义端口。
  • <port port="80" protocol="tcp"/>表示放行 TCP 的 80 端口。protocol 可以是tcp、udp,也可以是两个组合,取决于业务。
  • <rich-rule>是富规则容器,内部可以写带来源、带动作、带日志的复杂规则。它是实现“指定 IP 段放行指定端口”的主要手段。

2.3 三种规则形态:port、service、rich rule 各管什么

用表格来对比会更直观:

规则形态写法示例能限制来源IP?适用场景
service<service name="http"/>否放行标准服务端口,语义清晰
port<port port="8080" protocol="tcp"/>否放行单个端口或端口段
rich rule<rule family="ipv4"><source address="192.168.1.0/24"/><port port="3306" protocol="tcp"/><accept/></rule>是限定来源访问指定端口,精细化管控

我的建议是:只要能确认来源 IP 和来源网段,一律用 rich rule,不要用裸的<port>。原因很简单,裸 port 会对所有来源开放,在有些安全审计里这是高风险项,而 rich rule 把来源写进规则里,后续审计、排查、合规检查都很清晰。

2.4 加载顺序与优先级:同名 zone 覆盖规则

需要特别注意的覆盖机制:如果/etc/firewalld/zones/下存在myzone.xml,同时/usr/lib/firewalld/zones/下也存在同名myzone.xml,以/etc/为准,/usr/lib/里的同名文件会被自动忽略,但文件本身还在,不会报错。改规则时如果不确定当前加载的是哪个文件,用firewall-cmd --permanent --get-zones列出来的 zone 列表并不能区分来源,但firewall-cmd --zone=public --list-all的规则内容里,你可以看到实际生效的配置是不是你期望的那一份。

重载配置用firewall-cmd --reload和firewall-cmd --complete-reload要区分清楚。--reload是平滑重载,已建立的连接不会断开,新规则会应用到后建立的连接;--complete-reload会完全重载,可能中断现有连接。生产环境里常规操作一律用--reload,只有在规则结构变化很大、旧连接明显异常时才考虑--complete-reload。

3. 批量放行端口的四种实操方式与选型对比

3.1 方式一:firewall-cmd 循环脚本

这也是网上最常见的方式,无非是写个 shell 脚本循环执行:

for port in 8000 8001 8002 8080 9000; do firewall-cmd --permanent --add-port=${port}/tcp done firewall-cmd --reload

脚本简单直观,适合端口数量在 10 个以内的场景。但两个缺点让我不太推荐作为主力方案:

  • 每次执行firewall-cmd --permanent都会做一次规则校验和 XML 序列化,几十个端口跑下来,脚本执行时间明显长于一次文件编辑加一次 reload。
  • 循环执行过程中一旦端口写错或中间出现报错,很难通过日志快速定位是哪一条出问题,批量操作的原子性很差。

不过小规模临时放行,用它确实最省事。

3.2 方式二:直接编辑 XML 配置文件

这是我最推荐的方式,也是这种技巧的核心。做法是编辑/etc/firewalld/zones/public.xml,在<zone>节点内、</zone>标签前插入需要的<port>或<rich-rule>节点。

比如要放行 TCP 的 10000-10100 端口段:

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

放行多个不连续的端口就是写多个节点:

<port port="8080" protocol="tcp"/> <port port="9000" protocol="tcp"/>

然后重载生效:

firewall-cmd --reload

我能用这种方式批量放行的端口数量达到几百个,XML 文件不会变大到不可控,而且可以用 git 跟踪配置变化,回滚只需恢复旧文件。

需要注意 XML 文件中不能出现重复的同名节点影响不大(多个 port 节点是允许的),但标签顺序必须正确:<port>节点不能写在<rich-rule>内部,<source>必须嵌套在<rule>内部。如果格式不规范,--reload会直接报错而不是静默忽略。

3.3 方式三:firewall-offline-cmd 离线修改

firewalld 提供firewall-offline-cmd这个工具,可以在 firewalld 服务停止时直接修改永久配置。它的写法跟firewall-cmd --permanent基本一致,只是前面加一个 offline:

systemctl stop firewalld firewall-offline-cmd --add-port=8000-8100/tcp systemctl start firewalld

这个方式适合在做系统镜像、容器基础镜像、或者通过脚本在装机后第一时间放行端口时使用,因为没有启动 firewalld 服务也不会报“FirewallD is not running”的错误。

3.4 方式四:富规则(rich rule)限定来源与端口的组合

批量放行一旦涉及来源 IP 限制,命令行循环就会变得异常痛苦,因为--add-rich-rule的参数很长,循环里写起来读起来都费劲。而 XML 里的 rich-rule 字段天然适合批量编辑:

<rich-rule> <rule family="ipv4"> <source address="10.20.0.0/16"/> <port port="5432" protocol="tcp"/> <accept/> </rule> </rich-rule>

如果有多个来源段、多个端口,可以并列多写几个<rule>。富规则里的动作除了<accept/>还能写<reject/>、<drop/>,甚至支持<log>日志记录,这对运维排障价值很大。

要注意 rich rule 里的节点有严格的嵌套顺序:<source>或<destination>要在最前面,然后是服务或端口,最后才是动作。顺序错了 reload 可能报错,这种报错信息往往只有一行 XML 解析错误提示,排起来比较麻烦。

3.5 四种方式的选型对照表

方式适用规模是否支持来源限制优势劣势
firewall-cmd 循环少量端口(10以内)勉强支持,写法臃肿简单、直观慢、易错、无原子性
直接改 XML大规模端口、端口段支持,通过 rich rule高效、可版本管理、回滚方便需要掌握 XML 语法
firewall-offline-cmd服务停止时的离线装环境支持无需启动 firewalld不能覆盖运行时连接
rich rule 富规则精细来源管控完整支持支持来源/日志/动作组合语法复杂、嵌套顺序严格要求

4. 完整实操案例:从端口段到多IP来源的组合放行

4.1 场景一:批量放行不连续端口段

假设业务部门一次提了十多个端口,例如 DNS 用的 53、NTP 的 123、HTTP 的 8080,以及一个连续的业务端口段 20000-20050。直接编辑/etc/firewalld/zones/public.xml:

<?xml version="1.0" encoding="utf-8"?> <zone> <short>Public</short> <description>Unsolicited incoming network packets are rejected.</description> <service name="ssh"/> <service name="dhcpv6-client"/> <port port="53" protocol="tcp"/> <port port="53" protocol="udp"/> <port port="123" protocol="udp"/> <port port="8080" protocol="tcp"/> <port port="20000-20050" protocol="tcp"/> </zone>

这里有两个容易被忽视的细节:

  • 53 端口同时有 TCP 和 UDP,需要写两个节点,protocol="tcp"和protocol="udp"不能合并到一个节点里。
  • 20000-20050 这种连续端口段,firewalld 用-连接,而不是网上的旧教程里写的:。写成20000:20050会被解析成端口名而不是端口段,reload 时直接报错。

改完后执行:

firewall-cmd --reload firewall-cmd --zone=public --list-ports

list-ports输出的应该是20000-20050/tcp 8080/tcp 53/tcp 53/udp 123/udp。验证清单里的一条都别少。

4.2 场景二:放行某 IP 段访问指定端口

这个场景典型的就是数据库端口对外开放,但只允许办公网或者应用服务器所在网段访问。比如让10.20.0.0/16访问本机 MySQL 的 3306,同时又允许172.16.1.0/24访问 Redis 的 6379:

<rich-rule> <rule family="ipv4"> <source address="10.20.0.0/16"/> <port port="3306" protocol="tcp"/> <accept/> </rule> <rich-rule> <rule family="ipv4"> <source address="172.16.1.0/24"/> <port port="6379" protocol="tcp"/> <accept/> </rule> </rich-rule> </zone>

注意family="ipv4"不能写错,如果来源是 IPv6 就写ipv6。另外<source address="10.20.0.0/16"/>里的地址可以是单个 IP,也可以是网段,写法都是放在 address 属性里。

这里我想强调一个容易被忽略的点:富规则的优先级高于普通<port>节点。如果你同时有<port port="3306" protocol="tcp"/>这样对所有人开放的规则,又有一条富规则限制来源 10.20.0.0/16 访问 3306,最终效果是两条规则都生效,也就是说该网段能访问,其他网段也能访问。这在某些场景下可能不是你想要的。想要“只允许该网段访问”,就不要写普通<port>节点,只保留富规则。

4.3 场景三:按业务区分 zone 并批量放行

生产环境里我习惯把不同的服务放到不同的 zone 里,而不是全往里塞 public。比如创建web区域放 Web 服务,db区域放数据库。创建自定义 zone 的方法就是在/etc/firewalld/zones/下新建一个web.xml:

<?xml version="1.0" encoding="utf-8"?> <zone target="ACCEPT"> <short>Web</short> <description>Web Server Zone</description> <port port="80" protocol="tcp"/> <port port="443" protocol="tcp"/> <port port="8000-8100" protocol="tcp"/> </zone>

然后把网卡或来源绑定到这个区域,例如把内网网卡 ens37 绑定到 web 区域:

firewall-cmd --permanent --zone=web --change-interface=ens37 firewall-cmd --reload

这种做法的可维护性非常好:以后想调整 Web 端口,只需改 web.xml 中一个文件,不影响其他区域的规则。批量放行需求也天然按区域收敛,不会出现“为了一个服务在 public 里开一堆洞”的情况。

如果你不想创建新 zone,也可以在已有 internal 或 trusted 里做同样配置,只是注意 default zone 有没有被改成你要用的区域,网卡绑定的区域和流量实际落入的区域要一致。

4.4 修改后的验证与回滚套路

每次批量修改配置文件前我都会先做备份,这是一个成本极低但收益极大的习惯:

cp -a /etc/firewalld/zones/ /root/firewalld_zones_backup_$(date +%F_%H%M%S)/

修改后验证配置是否正确再 reload:

firewall-cmd --check-config

这个命令会检查所有 zone 配置是否有语法或逻辑错误,比如端口段格式、IP 段格式、XML 节点顺序等,有问题会明确报错。确认无报错再执行 reload,可以减少很多“改完 reload 失败导致 firewalld 停工”的尴尬。

回滚操作就是把备份目录里的文件复制回原位置,再 reload:

cp -a /root/firewalld_zones_backup_时间戳/. /etc/firewalld/zones/ firewall-cmd --reload

另外用工具测试端口通断时,一定不要只在服务器本机测,本地回环不会经过网卡进不了 zone 过滤,明白人都懂。要用另一台机器或者本机使用公网/内网 IP 去测,才能真正验证规则是否生效。

5. 常见问题与排查技巧实录

5.1 排查清单:规则没生效时应先查什么

我整理了一份固定排查顺序,每次遇到“端口不通”都按这个走,比瞎猜高效得多:

  1. 端口是否真的在监听:用ss -lntup | grep <端口号>检查服务是否监听了 0.0.0.0 或对应网卡 IP。有时候服务监听 127.0.0.1,外部当然访问不了,跟防火墙没关系。
  2. 规则是否在当前区域:用firewall-cmd --get-default-zone和firewall-cmd --get-active-zones确认流量入口网卡属于哪个 zone,再检查那个 zone 的--list-ports和--list-rich-rules。
  3. 是否 reload 过配置:直接改 XML 或带--permanent的命令都只是落盘,不 reload 不生效。
  4. selinux 是否拦截:临时用setenforce 0验证一次,能访问就说明 selinux 策略问题,需针对服务补放行。
  5. 是否被云平台安全组拦截:如果服务器在云上(某云厂商/某托管机房),本地 firewalld 即使正确放行,云平台安全组没放行也一样不通。

5.2 常见错误与解决方法速查表

错误现象可能原因解决办法
reload 报 XML 解析错误XML 标签没闭合 / 节点顺序错误执行firewall-cmd --check-config定位具体 zone 和行号
修改 public.xml 后不生效写到了/usr/lib而不是/etc把文件复制到/etc/firewalld/zones/下再 reload
端口段写法错误用了:而不是-改成port="20000-20050"
rich rule 不生效family 写错 / source 和 port 顺序写反严格按<source>、<port>、<action>的顺序写
reload 后老连接断了用了--complete-reload常规用--reload,不中断已建立连接
服务重启后规则丢失修改的是运行时配置而非永久配置改用--permanent或直接改 XML 文件
打了 IP 段限制却仍能访问同时存在裸<port>放行规则删除裸<port>,仅保留 rich rule

5.3 几条实测下来的避坑经验

第一,批量修改 XML 时,不要用 Windows 记事本编辑再传上去,文件编码和换行符可能出问题,reload 时 XML 解析直接报“encoding error”。在 Linux 上用vim、nano或者sed都是安全的。

第二,XML 文件里中文注释可能引发编码问题,除非明确调整文件头编码声明,否则少写中文注释。生产环境配置文件用英文注释是最稳妥的。

第三,用脚本批量生成 XML 时,不要自己拼 XML 字符串拼得太随意。我见过有人用 echo 拼接出多个<zone>根节点导致 reload 后 firewalld 直接拒绝启动,最后紧急恢复备份才止损。要生成批量节点,推荐用文本模板 +sed替换,或者干脆用 Python 的 xml 库生成。我这里给一个 bash 循环生成<port>节点的简单示例:

for port in $(seq 9000 9050); do echo " <port port=\"${port}\" protocol=\"tcp\"/>" done

输出结果可以直接粘贴到 public.xml 的<zone>节点内。同理可以扩展到生成富规则。

第四,firewalld 的 XML 文件有严格的 schema 校验,--check-config能发现大部分问题,但有个坑:如果 zone 文件里 target 属性写成了小写或漏了引号,--check-config报错信息可能指向schema限制,这时候不要慌,看看是不是 zone 目标属性写错。默认target="ACCEPT",不要改成别的单词乱试。

第五,也是最重要的一条经验:批量放行端口不等于永久放行。我会在配置里写上规则添加时间、申请人、对应工单号,方便后续按权限收敛端口。比如:

<!-- 2024-06-01 申请单编号T-20240601-001 临时放行API网关端口 --> <port port="18080-18090" protocol="tcp"/>

虽然中文字段之前建议少写,但这种带日期和单号的英文注释我强烈推荐,半年后回来清理端口时,能救你一命。

最后说点个人体会。我一开始上手 firewalld 时也觉得直接改 XML 显得不够“正统”,毕竟那会儿网上教程全是firewall-cmd命令。但用了几次后发现,配置文件方式在大规模、复杂规则、可回滚这三个维度上远远强于命令行。现在我的习惯是:凡是放行端口超过 5 个,直接编辑 XML;凡是涉及来源 IP 限制,全部走富规则;每次改动前先备份、再 check-config、再 reload。这套流程在一个有上百台 CentOS7 的集群里跑了一年多,几乎没有因为防火墙规则改动出过事故。如果你们生产环境也是 CentOS7,批量端口放行建议尽快切到配置文件思路,真的能省下很多时间。

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

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

立即咨询