防火墙与 SELinux

本文合并自:02-iptables防火墙.md、1-14-防火墙与SELinux基础.md

本文系统讲解 Linux 主机的两大安全子系统:基于 netfilter 的包过滤防火墙(CentOS 6 及以前的 iptables、CentOS 7 起的 firewalld)与强制访问控制 SELinux。内容涵盖:防火墙基本概念与分类、netfilter 框架与数据包流向、四表五链、iptables 规则语法与全部匹配条件、filter 表包过滤实战(放行 ssh/web、防 ping、FTP 主动/被动、多端口与 MAC 控制)、nat 表网络地址转换(SNAT/MASQUERADE 共享上网、DNAT 端口映射与端口转发、REDIRECT 重定向)、连接跟踪 conntrack 与状态检测(NEW/ESTABLISHED/RELATED/INVALID)、recent 与 limit 扩展模块、规则保存恢复与开机加载、firewalld 的 zone/service/富规则/端口转发/命令全集、SELinux 的模式/上下文/布尔值/端口标签/排错,以及排错清单与综合练习。

适用环境:示例以 CentOS 7 / RHEL 7 为主(firewalld 为默认防火墙,SELinux 默认 enforcing)。CentOS 6 及以前的 iptables / iptables-services 写法在文中并列保留并标注适用版本,两套体系都要会:老系统、容器网络、云主机脚本中 iptables 仍然大量存在。

为什么需要系统学习这两个子系统

在大量部署文档中,下面这些命令被反复使用,却很少被系统讲解:

命令 出现位置
firewall-cmd --add-service=... 网络配置文档、FTP/Samba/HTTPD/DHCP 部署文档
systemctl stop firewalld 几乎所有部署文档的"前置准备"步骤
setenforce 0 同上,且很多文档的前置条件里都有
chcon -t ... /path HTTPD 自定义站点目录、FTP 目录授权

结果是:学习者被告知"先把防火墙和 SELinux 都关掉",却不知道它们是什么、关掉之后失去了什么、以及正确的做法是什么。生产环境中 setenforce 0 长期挂着的坏习惯,就是这样养成的。

本文的目的是把这两个子系统一次讲透,使得:

  1. 其他文档中的 iptables、firewall-cmd、setenforce、chcon 不再是"照抄的咒语"。
  2. 部署服务时能够开着防火墙和 SELinux 正确放行,而不是一律关闭。
  3. 遇到"配置全对但服务就是访问不了"时,有明确的排查路径(见"排错清单"章节)。

一、防火墙概述

1.1 什么是防火墙

业界常见的个人电脑防火墙软件有:瑞星、卡巴斯基、360、金山、江民、腾讯管家、诺顿、天网等。

在网络中,所谓"防火墙",是指一种将内部网和公众访问网(如 Internet)分开的方法,它实际上是一种隔离技术。防火墙是在两个网络通讯时执行的一种访问控制尺度,它能允许你"同意"的人和数据进入你的网络,同时将你"不同意"的人和数据拒之门外,最大限度地阻止网络中的非法访问。

按部署位置可分为:

  • 网络防火墙:部署在网络边界(网关/路由器),保护整个网段,常兼做 NAT 与端口映射。
  • 主机防火墙:运行在单台服务器上,只保护本机,本文的 iptables / firewalld 主要工作在这一层。

重要提醒:云服务器(阿里云/腾讯云/AWS)在主机防火墙之外还有一层安全组。主机内规则全部放行后仍不通,绝大多数是安全组没开对应端口。排查时两层都要看。

1.2 防火墙的分类(按工作原理)

类型 工作层次 原理 优点 缺点
包过滤防火墙(Packet Filter) 网络层、传输层 逐个检查数据包的源/目的 IP、协议、源/目的端口、标志位,与规则表逐条匹配后决定放行或丢弃 速度快、对应用透明、开销小 不理解应用语义,无法防应用层攻击;不记录连接状态,规则易写复杂
应用代理防火墙(Application Proxy / 应用层网关) 应用层 客户端不直连服务端,由代理进程代为建立连接并解析应用协议内容(HTTP、FTP 等)后转发 可做内容过滤、URL 过滤、用户认证,安全性最高 每种协议要单独写代理,性能开销大,不透明
状态检测防火墙(Stateful Inspection) 网络层至应用层之间 在包过滤基础上引入连接跟踪表,记录每条连接的状态,属于已建立连接的返回包自动放行 规则大幅简化、安全性高、性能接近包过滤 连接跟踪表占内存,高并发时需调整 nf_conntrack_max

Linux 的 netfilter/iptables 同时具备包过滤与状态检测能力:-p、-s、--dport 等属于包过滤;-m state / -m conntrack 属于状态检测(见"连接跟踪与状态检测"章节)。应用代理则由 squid、nginx、HAProxy 等用户态软件实现,不属于 netfilter 的范畴。

1.3 Linux 防火墙的演进

内核版本 用户态工具 内核框架 说明
2.0 ipfwadm firewall 代码 最早的包过滤实现,功能简单
2.2 ipchains ipchains 引入"链"的概念,可自定义链
2.4 / 2.6 / 3.x iptables netfilter 引入表与链的二维结构、连接跟踪、NAT、扩展模块,是本文重点
3.13 起 nft(nftables) nftables 用单一框架统一 iptables/ip6tables/arptables/ebtables,语法更简洁、性能更好;RHEL 8 起 firewalld 的默认后端

演进关系可以概括为:

ipfwadm(2.0) → ipchains(2.2) → iptables(2.4+,netfilter) → nftables(3.13+)
  • CentOS 6:iptables 是唯一的官方防火墙管理方式,服务名 iptables。
  • CentOS 7:内核仍是 netfilter/iptables,但默认管理前端换成了 firewalld;iptables-services 仍可安装使用。
  • CentOS 8 / RHEL 8 起:底层换为 nftables,iptables 命令变成 iptables-nft 兼容层,firewalld 默认使用 nftables 后端。

结论:学 iptables 不会过时——它的表/链/规则模型是理解 firewalld 与 nftables 的基础,而且大量脚本、Docker、Kubernetes(kube-proxy 的 iptables 模式)仍在直接下发 iptables 规则。

1.4 netfilter 与 iptables 的关系

Linux 内核从 2.4 版本开始内置 netfilter 框架,它工作在内核态,负责对数据包进行过滤、网络地址转换、连接跟踪等处理。iptables 是工作在用户态的命令行工具,用于向 netfilter 写入规则,二者配合共同完成防火墙功能。

  • netfilter:内核态的数据包处理框架,由若干张表(table)和链(chain)组成,按照规则对数据包做匹配和处理。它才是"真正干活的防火墙"。
  • iptables:用户态的规则管理工具,通过它对内核中的 netfilter 规则进行增、删、改、查。它本身不过滤任何数据包。

层次关系:

iptables / firewall-cmd / nft(用户态命令行工具)
      ↓  写入规则
  netfilter(Linux 内核中真正过滤数据包的框架)
      ↓
  放行 / 丢弃 / 改写数据包

数据包流经 netfilter 时,会依次经过多个"钩子"(hook,即链),用户可以在这些链上插入规则。

1.5 firewalld 与 iptables 的关系与选用

版本 默认防火墙管理工具 服务名
CentOS 6 及以前 iptables(iptables-services) iptables
CentOS 7 起 firewalld firewalld

关键认知:firewalld 不是新的防火墙,它是 iptables 命令的替代前端。

firewall-cmd(命令行工具)
      ↓
  firewalld(守护进程,管理规则集与 zone)
      ↓
  iptables / ip6tables / ebtables 命令
      ↓
  netfilter(Linux 内核中真正过滤数据包的框架)

底层做包过滤的始终是内核的 netfilter。iptables 和 firewalld 只是两种往 netfilter 里写规则的方式。

firewalld 相比直接写 iptables 的优势:

能力 iptables firewalld
修改规则 需重启整个规则集,连接会中断 动态更新,已建立连接不受影响
组织方式 扁平的规则链,靠顺序决定优先级 按 zone(区域)分组,按接口/来源归属
服务定义 需手动记住端口号 内置 http、ftp 等服务名
配置持久化 需 service iptables save runtime/permanent 双配置模型
IP 协议支持 只写 IPv4 规则,IPv6 需 ip6tables 另写 一套命令同时下发 IPv4/IPv6/ebtables

两者的具体差异:

  • iptables:在 RHEL 7 之前非常流行,但相对 firewalld 来说过于底层;它自身只能编写 IPv4 的规则,IPv6 需要使用 ip6tables 等其他程序实例。
  • firewalld:RHEL 7 引入的、与 netfilter 子系统交互的前端程序,本质上是一个服务(守护进程)。它将网络划分为多个区域(zone)来进行管理,支持动态更新规则而无需全部刷新。官网地址:http://www.firewalld.org/

选用原则

场景 建议
CentOS 7+ 的普通服务器 用 firewalld,按服务名/端口放行
CentOS 6,或需沿用已有的 iptables 规则脚本 用 iptables + iptables-services
需要复杂 NAT、mangle 标记、精细规则顺序控制的网关 用 iptables(或 firewalld 的 --direct 直接规则)
只读排查"当前真正生效的规则" 无论用哪个前端,都可以用 iptables -S / iptables -L -n -v 查看

绝对不能同时启用 firewalld 与 iptables-services。 两者都会向 netfilter 写入规则且互不知晓,结果是规则互相覆盖、行为不可预测、排障时无法判断当前生效的是谁的规则。二者只能择一。

为什么 iptables -F 不是"关闭防火墙"的正确做法(CentOS 7)

新手常见做法是执行 iptables -F 清空规则来"关闭防火墙"。在 CentOS 7 上这么做有两个问题:

  1. firewalld 会把规则重新写回去。 firewalld 守护进程持有自己的规则模型,任何一次 firewall-cmd --reload、网络接口变动、甚至 NetworkManager 事件都可能触发它重新下发规则,你手动清空的效果随时被覆盖。
  2. -F 清掉的是全部主机防护。 它不区分"我只想放行 8080"和"我不要任何防护",执行后主机所有端口对外暴露。这不是排障,这是把门拆了。

正确做法:

# 排障时临时关闭(重启后恢复)
sudo systemctl stop firewalld

# 更好的做法:不关防火墙,只放行需要的端口
sudo firewall-cmd --add-port=8080/tcp

# 应急全阻断(比 -F 安全)
sudo firewall-cmd --panic-on

退回传统 iptables 的正确方式(CentOS 7)

若确实需要使用 iptables 语法管理规则(例如沿用已有的 CentOS 6 规则脚本):

sudo systemctl stop firewalld
sudo systemctl disable firewalld
sudo systemctl mask firewalld          # 彻底屏蔽,防止被依赖关系唤醒

sudo yum install iptables-services -y
sudo systemctl enable --now iptables

sudo iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
sudo service iptables save             # 保存到 /etc/sysconfig/iptables

二、iptables 原理:四表五链与数据包流向

netfilter 通过"表"来组织不同功能的规则,通过"链"来表示数据包经过的位置。理解这个二维结构是写对规则的前提。

2.1 四张表(table)

表名 作用 包含的链
filter 包过滤,决定是否放行数据包(最常用,默认表) INPUT、OUTPUT、FORWARD
nat 网络地址转换(Network Address Translation),允许内网地址块通过 NAT 转换成公网 IP 访问公网,解决 IP 地址不足问题 PREROUTING、INPUT、POSTROUTING、OUTPUT
mangle 对数据包进行标记(MARK)、修改服务类型 TOS/TTL 等 PREROUTING、INPUT、OUTPUT、FORWARD、POSTROUTING
raw 对原始数据包进行处理,可在连接跟踪之前处理数据包,常用于标记 NOTRACK(关闭连接跟踪以提升性能) PREROUTING、OUTPUT

四张表的优先级由高到低依次为:raw → mangle → nat → filter。

说明:nat 表的 INPUT 链在较早的内核中不存在,新内核里存在但极少使用;日常 NAT 只用 PREROUTING(做 DNAT)和 POSTROUTING(做 SNAT/MASQUERADE)。

2.2 五条链(chain)

链名 触发时机
PREROUTING 数据包进入网卡、进行路由判断之前
INPUT 数据包目标地址为本机、经过路由判断之后
FORWARD 数据包经由本机转发(目标地址非本机)
OUTPUT 由本机进程产生、向外发出的数据包
POSTROUTING 数据包即将从网卡发出之前(路由判断之后)

2.3 表与链的对应关系表

竖向看是链、横向看是表,"√"表示该表在该链上有钩子(即该位置可以写这类规则):

链 \ 表 raw mangle nat filter 该链的典型用途
PREROUTING √ √ √(DNAT/REDIRECT) — 改写目标地址、端口映射、透明代理
INPUT — √ √(少用) √ 主机防火墙入站控制(最常写)
FORWARD — √ — √ 网关转发控制
OUTPUT √ √ √(本机 DNAT) √ 本机出站控制
POSTROUTING — √ √(SNAT/MASQUERADE) — 改写源地址、共享上网

反过来记(表 → 链):

  • filter:INPUT / OUTPUT / FORWARD —— 没有 PREROUTING 和 POSTROUTING,所以"过滤"不能写在这两条链上。
  • nat:PREROUTING / POSTROUTING / OUTPUT(/ INPUT)—— 没有 FORWARD,所以 NAT 不能写在 FORWARD 链上。
  • mangle:五条链全有。
  • raw:PREROUTING / OUTPUT。

2.4 数据包在链中的完整流向

整体流向示意(简化版):

 Incoming                 /     \         Outgoing
       -->[Routing ]--->|FORWARD|------->
          [Decision]     \_____/        ^
               |                        |
               |                      ____
              ___                    /    \
             /   \                  |OUTPUT|
            |INPUT|                  \____/
             \___/                      ^
               |                        |
                ----> Local Process ----

三种路径的完整经过顺序(含表的优先级):

路径一:目标地址是本机的数据包(入本机)

网卡收包
  → raw:PREROUTING → mangle:PREROUTING → nat:PREROUTING(DNAT)
  → 【路由决策:目标是本机】
  → mangle:INPUT → filter:INPUT
  → 本机进程(应用程序)

路径二:由本机进程发出的数据包(出本机)

本机进程
  → 【路由决策:选出接口与源地址】
  → raw:OUTPUT → mangle:OUTPUT → nat:OUTPUT(本机 DNAT)→ filter:OUTPUT
  → mangle:POSTROUTING → nat:POSTROUTING(SNAT/MASQUERADE)
  → 网卡发包

路径三:经由本机转发的数据包(转发/网关)

网卡收包
  → raw:PREROUTING → mangle:PREROUTING → nat:PREROUTING(DNAT)
  → 【路由决策:目标不是本机,需要转发】
  → mangle:FORWARD → filter:FORWARD
  → mangle:POSTROUTING → nat:POSTROUTING(SNAT/MASQUERADE)
  → 网卡发包

简记版(源文档表述):

  • 目标地址是本机的数据包:进入网卡 → PREROUTING → 路由决策 → INPUT → 本机进程。
  • 由本机进程发出的数据包:本机进程 → OUTPUT → 路由决策 → POSTROUTING → 发出。
  • 经由本机转发的数据包:进入网卡 → PREROUTING → 路由决策 → FORWARD → POSTROUTING → 发出。

由这三条路径可以直接推出许多实践结论:

  1. 做入站放行(放行 22/80)写在 filter 表 INPUT 链。
  2. 做端口映射/透明代理写在 nat 表 PREROUTING 链——因为必须在路由决策前改写目标地址。
  3. 做共享上网写在 nat 表 POSTROUTING 链——因为要在最后一刻改写源地址。
  4. 转发的数据包不经过 INPUT / OUTPUT,所以网关上做转发控制必须写 FORWARD 链。
  5. 本机自己产生的数据包不经过 PREROUTING,所以在网关本机上访问自己做的 DNAT 端口映射会失败(需要额外在 nat:OUTPUT 上做 REDIRECT/DNAT)。

2.5 安装与启动 iptables 服务

CentOS 7.3 已自带 iptables 相关软件包,可不必额外安装。如需安装,执行:

yum install iptables* -y

查看已安装的软件包:

rpm -qa | grep iptables
iptables-devel-1.4.21-28.el7.x86_64
iptables-services-1.4.21-28.el7.x86_64
iptables-utils-1.4.21-28.el7.x86_64
iptables-1.4.21-28.el7.x86_64

启动、设置开机自启并查看状态(CentOS 7,systemd):

systemctl start iptables.service
systemctl enable iptables.service
systemctl status iptables.service

CentOS 6 及以前(SysV init):

service iptables start
service iptables stop
service iptables restart
service iptables status
chkconfig iptables on

注意:CentOS 7 默认使用 firewalld 作为前端防火墙。如果要使用 iptables-services,需要先停止并禁用 firewalld(systemctl stop firewalld && systemctl disable firewalld),再启动 iptables 服务。


三、iptables 规则管理

3.1 规则语法总览

iptables 命令的基本格式为:

iptables [-t 表名] 管理选项 [链名] [匹配条件] [-j 处理动作]

四个组成部分的含义:

组成部分 说明 举例
-t 表名 要操作哪张表;不指定时默认操作 filter 表 -t nat
管理选项 对规则做什么操作(增、删、改、查、清空等) -A、-I、-D、-L
链名 规则挂在哪条链上 INPUT、POSTROUTING
匹配条件 什么样的数据包算命中(协议、地址、端口、接口、扩展模块等) -p tcp --dport 22 -s 10.0.0.0/8
-j 处理动作 命中之后怎么处理 -j ACCEPT、-j DROP

一条完整规则的读法示例:

iptables -t filter -A INPUT -p tcp -s 192.168.224.0/24 --dport 22 -j ACCEPT
#         ↑表      ↑追加 ↑链  ↑------- 匹配条件 -------↑  ↑动作

读作:"在 filter 表的 INPUT 链末尾追加一条规则:来自 192.168.224.0/24 网段、协议为 TCP、目标端口为 22 的数据包,放行。"

记忆要点:表决定能干什么,链决定在哪干,匹配条件决定对谁干,target 决定怎么干。 四者缺一不可,其中表可省略(默认 filter)、匹配条件可省略(省略即匹配全部)。

3.2 管理类命令逐项详解

选项 全称 含义
-L List 列出指定链(或整表)中的规则
-S list-rules 以"可直接执行的命令形式"导出规则
-A Append 在指定链的末尾追加一条规则
-I Insert 在指定链的开头插入一条规则;-I 链名 序号 可插到指定序号位置
-D Delete 删除一条规则,可按规则内容删,也可按序号删(-D 链名 序号)
-R Replace 替换指定链中指定序号的规则(-R 链名 序号 新规则)
-F Flush 清空指定链(或整张表)中的全部规则
-N New chain 新建一条用户自定义链
-X delete chain 删除用户自定义链(须先清空且无引用)
-E rEname chain 重命名用户自定义链
-P Policy 设置指定链的默认策略
-Z Zero 清零规则的数据包/字节计数器
-n numeric 以数字形式显示地址与端口,不做域名/服务名反解
-v verbose 显示详细信息(计数器、接口、协议等),常与 -L 连用
-x exact 在 -v 基础上显示精确计数值,不做 K/M/G 换算
--line-numbers(简写 --line) — 列出规则时显示规则序号

注意区分:大写 -P 是设置默认策略(Policy),小写 -p 是指定协议(protocol)。两者极易混淆。

3.2.1 查看规则:-L 与 -n / -v / --line-numbers

查看 filter 表(默认表)规则:

iptables -L
iptables -t filter -L
iptables -L INPUT              # 只看 INPUT 链

查看其它表规则:

iptables -L -t nat
iptables -L -t mangle
iptables -L -t raw

常用组合(强烈建议养成 -L -n -v --line-numbers 的习惯):

iptables -L -n                          # 不反解,显示快
iptables -L -n -v                       # 附带包数/字节数计数器,判断规则是否被命中
iptables -L -n -v -x                    # 计数器显示精确值
iptables -L -n --line-numbers           # 显示序号,便于按序号删除
iptables -L -n -v --line-numbers        # 排障标准写法
iptables -t nat -L -n -v --line-numbers # 查 nat 表

-L -n -v 输出各列含义:

Chain INPUT (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source          destination
  128  9216 ACCEPT     tcp  --  *      *       0.0.0.0/0       0.0.0.0/0    tcp dpt:22
列 含义
pkts 命中该规则的数据包个数(为 0 说明这条规则从未被匹配到,排障关键线索)
bytes 命中的字节数
target 处理动作
prot 协议
opt 选项(如分片标记)
in / out 入接口 / 出接口,* 表示任意
source / destination 源地址 / 目标地址
末列 扩展匹配条件,如 tcp dpt:22、state ESTABLISHED

-n 不加时,iptables 会把 22 反解成 ssh、把 IP 反解成主机名,DNS 不可用时会卡住数秒到数十秒,因此排障时一律加 -n。

3.2.2 -S 导出规则

-S 把当前规则以"可直接粘贴执行的命令"形式打印出来,比 -L 更适合备份、对比与复现:

iptables -S                    # 导出 filter 表全部规则
iptables -S INPUT              # 只导出 INPUT 链
iptables -t nat -S             # 导出 nat 表
-P INPUT DROP
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT

说明:即使系统运行的是 firewalld,iptables -S / iptables -L -n -v 依然是有价值的只读排查工具——它能看到 firewalld 最终下发到 netfilter 的真实规则(会看到大量 IN_public_allow 之类的自定义链)。只是不要用 iptables 命令去修改 firewalld 环境下的规则。

3.2.3 -A 追加规则

-A 把新规则放在链的最末尾:

iptables -t filter -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 80 -j ACCEPT

由于 iptables 是"从上往下、匹配即停止",追加到末尾意味着优先级最低。如果前面已经有一条"全部丢弃"的规则,追加的放行规则永远不会被匹配到。

3.2.4 -I 插入规则与指定序号

-I 默认插入到链的第一条(优先级最高):

iptables -t filter -I INPUT -p icmp -s 192.168.224.11 -j ACCEPT

-I 后接数字表示插入到指定序号位置(插入后该规则就成为第 N 条,原第 N 条及以后依次下移):

iptables -t filter -I INPUT 2 -p icmp -s 192.168.224.13 -j ACCEPT

实践建议:临时放行、紧急加白名单一律用 -I(保证立刻生效、不被前面的 DROP 挡住);批量下发完整规则集时用 -A(顺序可控、脚本可读)。

3.2.5 -D 删除规则(按序号与按规则内容)

方法一:按规则内容删除。 添加时怎么写,删除时把 -A 换成 -D 即可,其余部分必须完全一致:

iptables -t filter -D INPUT -s 192.168.224.11 -p icmp -j ACCEPT

方法二:按序号删除。 先用 --line-numbers 查出行号,再按序号删除:

iptables -L -n --line-numbers
iptables -D INPUT 2

在规则较多或规则内容不好书写时,推荐用行号删除。

注意:删除多条规则时要从大序号往小序号删,否则删掉前面的规则后,后面的序号会全部前移,导致删错。例如要删第 2、5 条,应先 -D INPUT 5 再 -D INPUT 2。

3.2.6 -R 替换规则

用 -R 替换指定链中指定序号的规则。例如把 filter 表 INPUT 链第 3 行修改为新内容:

iptables -t filter -R INPUT 3 -p icmp -s 192.168.224.100 -j ACCEPT

注意:-R 是整条替换,不是"局部修改"。必须把新规则的匹配条件与动作完整写出来,漏写的部分会变成默认值(例如漏写 -s 就变成匹配所有源地址)。

3.2.7 -F 清空规则

iptables -F                    # 清空 filter 表所有链的规则
iptables -F INPUT              # 只清空 filter 表 INPUT 链
iptables -t nat -F             # 清空 nat 表
iptables -t mangle -F
iptables -t raw -F

实验前把各表清空(filter/nat/mangle 表通常带有默认规则):

iptables -t filter -F
iptables -t nat -F
iptables -t mangle -F
iptables -t raw -F

重要:-F 只清空规则,不会改变链的默认策略。 如果 INPUT 的默认策略是 DROP,执行 iptables -F 后所有放行规则消失、默认策略依然是 DROP,结果是你的 SSH 立刻断开且再也连不上。正确顺序永远是"先把策略改成 ACCEPT,再清空规则":

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -F

3.2.8 -N 新建自定义链、-X 删除自定义链

当规则很多时,可以把同一类规则收拢进一条自定义链,再从内置链跳转过去,使规则结构清晰、便于整段启用/禁用:

iptables -N WEB_RULES                                   # 新建自定义链
iptables -A WEB_RULES -p tcp --dport 80 -j ACCEPT       # 往自定义链里加规则
iptables -A WEB_RULES -p tcp --dport 443 -j ACCEPT
iptables -A WEB_RULES -j RETURN                         # 未命中则返回调用链继续匹配

iptables -A INPUT -p tcp -j WEB_RULES                   # 从 INPUT 跳转到自定义链

删除自定义链(必须先清空链内规则,并去掉所有对它的引用):

iptables -D INPUT -p tcp -j WEB_RULES     # 1. 先删除引用它的跳转规则
iptables -F WEB_RULES                     # 2. 清空链内规则
iptables -X WEB_RULES                     # 3. 删除该链
iptables -X                               # 删除所有(空且无引用的)自定义链

重命名自定义链:

iptables -E WEB_RULES HTTP_RULES

说明:自定义链不能设置默认策略(-P 只对内置链有效)。数据包在自定义链里全部规则都没匹配上时,会自动返回调用它的链,从跳转规则的下一条继续匹配。

3.2.9 -P 设置默认策略

用 -P 设置链的默认策略——所有规则都没匹配上时执行的动作:

iptables -P INPUT DROP           # 将 INPUT 链默认策略改为 DROP
iptables -P OUTPUT DROP          # 将 OUTPUT 链默认策略改为 DROP
iptables -P FORWARD DROP

改回允许时把 DROP 换成 ACCEPT 即可:

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT

说明:默认策略只能是 ACCEPT 或 DROP,不能是 REJECT。

危险操作警告:远程管理时,在把 INPUT 默认策略改为 DROP 之前,必须先放行 SSH,否则命令执行的瞬间你就被锁在门外:

iptables -I INPUT -p tcp --dport 22 -j ACCEPT   # 先放行
iptables -I INPUT -i lo -j ACCEPT               # 放行回环
iptables -P INPUT DROP                          # 再改策略

更稳妥的做法是在改策略前先挂一个"定时救命"任务,例如 echo "iptables -P INPUT ACCEPT; iptables -F" | at now + 5 minutes,验证无误后再 atrm 取消。

3.2.10 -Z 清零计数器

排障时常用于"清零后再复现一次操作,看哪条规则的计数增加了",从而准确判断数据包命中了哪条规则:

iptables -Z                    # 清零 filter 表所有计数器
iptables -Z INPUT              # 只清零 INPUT 链
iptables -Z INPUT 3            # 只清零 INPUT 链第 3 条规则的计数器
iptables -t nat -Z

标准排障套路:

iptables -Z                              # 1. 清零
# 2. 从客户端重新发起一次访问
iptables -L -n -v --line-numbers         # 3. 看哪条规则 pkts 涨了

3.3 基本匹配条件

选项 含义
-p 协议名 匹配协议(protocol),可用 tcp、udp、icmp、all,也可用协议号
-s 地址[/掩码] 匹配源地址(source);不写表示匹配所有源
-d 地址[/掩码] 匹配目标地址(destination)
-i 网卡名 匹配数据包进入(input)本机所经过的接口,如 eth0、ens33、lo
-o 网卡名 匹配数据包离开(output)本机所经过的接口
--sport 端口[:端口] 匹配源端口(source port),需配合 -p tcp/udp;支持连续端口 起始:结束
--dport 端口[:端口] 匹配目标端口(destination port),用法同 --sport
-m 模块名 显式加载扩展匹配模块,如 state、multiport、mac、recent、limit、tcp 等
! 条件取反,放在选项之前

3.3.1 -p 协议

iptables -A INPUT -p tcp  --dport 22 -j ACCEPT
iptables -A INPUT -p udp  --dport 53 -j ACCEPT
iptables -A INPUT -p icmp -j DROP
iptables -A INPUT -p all -j ACCEPT            # all 表示所有协议(等同于不写 -p)
iptables -A INPUT ! -p tcp -j DROP            # 取反:非 TCP 的全部丢弃

-p 可写协议名或 /etc/protocols 中的协议号,例如 -p 47(GRE)、-p 50(ESP,IPSec 常用)。

注意:只有指定了 -p tcp 或 -p udp,才能使用 --sport / --dport;只指定了 -p icmp 才能使用 --icmp-type。不写 -p 直接写 --dport 会报错。

3.3.2 -s / -d 地址、网段与取反

iptables -A INPUT -s 192.168.224.11 -j ACCEPT          # 单个 IP(等价于 /32)
iptables -A INPUT -s 192.168.224.0/24 -j ACCEPT        # 网段(CIDR 写法)
iptables -A INPUT -s 192.168.224.0/255.255.255.0 -j ACCEPT   # 网段(掩码写法)
iptables -A INPUT -d 192.168.224.10 -j ACCEPT          # 目标地址
iptables -A INPUT -s 0.0.0.0/0 -j DROP                 # 所有地址(等同于不写 -s)

# 取反:除 192.168.224.11 之外的所有源地址都丢弃
iptables -A INPUT ! -s 192.168.224.11 -j DROP

# 取反也可用于网段
iptables -A INPUT -p tcp --dport 3306 ! -s 192.168.224.0/24 -j DROP

说明:-s / -d 也接受主机名(如 -s www.example.com),但 iptables 只在添加规则的那一刻做一次 DNS 解析并把结果固化为 IP。域名解析变化后规则不会自动更新,因此生产环境规则中不要使用主机名。

3.3.3 -i / -o 接口

iptables -A INPUT  -i lo -j ACCEPT               # 放行本机回环(几乎所有规则集的第一条)
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT  -i ens33 -p tcp --dport 80 -j ACCEPT    # 只放行从 ens33 进来的 80
iptables -A FORWARD -i ens37 -o ens33 -j ACCEPT             # 内网口进、外网口出,允许转发
iptables -A INPUT ! -i lo -p tcp --dport 3306 -j DROP       # 除本机外禁止访问 MySQL
iptables -A INPUT -i eth+ -j ACCEPT                         # + 为通配符,匹配 eth0/eth1/...

参数解释:-i 表示进入的请求,-o 表示出去的请求,lo 表示 localhost 回环接口(对应 IP 127.0.0.1)。

各链可用的接口条件:

链 可用 -i 可用 -o
PREROUTING 是 否(还没做路由决策,不知道从哪出去)
INPUT 是 否
FORWARD 是 是
OUTPUT 否 是
POSTROUTING 否 是

3.3.4 --sport / --dport、端口范围与多端口

iptables -A INPUT  -p tcp --dport 22 -j ACCEPT           # 单端口
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT           # 源端口
iptables -A INPUT  -p tcp --dport 1:1000 -j ACCEPT       # 连续端口 1~1000
iptables -A INPUT  -p tcp --dport :1023 -j ACCEPT        # 省略起始,等于 0:1023
iptables -A INPUT  -p tcp --dport 1024: -j ACCEPT        # 省略结束,等于 1024:65535
iptables -A INPUT  -p tcp --dport 1024:65535 -j ACCEPT   # 客户端随机端口范围
iptables -A INPUT  -p tcp ! --dport 22 -j DROP           # 取反:除 22 外全丢

# 多个不连续端口:必须用 multiport 扩展模块(逗号分隔,最多 15 个)
iptables -A INPUT -p tcp -m multiport --dport 25,110 -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dport 20,21,3000:3005 -j ACCEPT   # 可混合范围

说明:--dport 与 --dports 的区别——单端口/单范围用 --dport(tcp/udp 隐式扩展提供);多个逗号分隔的端口必须先 -m multiport,此时 --dport 与 --dports 都可写,语义相同。

3.4 处理动作(target)全解

-j(jump)后面接对匹配到的数据包所执行的处理动作。

动作 所属表 含义 是否终结匹配
ACCEPT 任意 接收(允许通过)数据包 是
DROP 任意 直接丢弃数据包,不返回任何回应信息 是
REJECT filter 拒绝数据包,与 DROP 的区别是会向对方返回错误信息 是
LOG 任意 记录日志,数据包继续匹配后续规则 否
SNAT nat(POSTROUTING) 源地址转换,改写源 IP,用于固定公网 IP 场景 是
MASQUERADE nat(POSTROUTING) 地址伪装,SNAT 的特例,自动取出站接口 IP 是
DNAT nat(PREROUTING/OUTPUT) 目标地址转换,改写目标 IP,用于发布内网服务器、端口映射 是
REDIRECT nat(PREROUTING/OUTPUT) 端口重定向到本机另一个端口(本机 DNAT 的简写),常用于透明代理 是
RETURN 任意 从当前自定义链返回上一层链继续匹配 否(返回)
MARK mangle 给数据包打标记,供路由策略/tc 使用 否
自定义链名 任意 跳转到指定的自定义链继续匹配 否(跳转)

3.4.1 DROP 与 REJECT 的区别

动作 对方感受 适用场景
DROP 静默丢弃,对方一直等到超时(ping 显示无响应、telnet 长时间挂住) 对公网、对扫描器,能拖慢对方探测速度,不暴露主机存在
REJECT 立即返回 ICMP 错误报文,对方马上知道被拒绝(Destination Port Unreachable) 对内网,能让用户快速得到明确反馈,便于自查

REJECT 可用 --reject-with 指定返回的报文类型:

iptables -A INPUT -p tcp --dport 3306 -j REJECT                                  # 默认
iptables -A INPUT -p tcp --dport 3306 -j REJECT --reject-with icmp-port-unreachable
iptables -A INPUT -p tcp --dport 3306 -j REJECT --reject-with tcp-reset           # 返回 RST,最像"端口没开"
iptables -A INPUT -p icmp -j REJECT --reject-with icmp-host-prohibited

--reject-with 可取的值:

取值 返回内容
icmp-port-unreachable ICMP 端口不可达(默认值)
icmp-net-unreachable ICMP 网络不可达
icmp-host-unreachable ICMP 主机不可达
icmp-proto-unreachable ICMP 协议不可达
icmp-net-prohibited ICMP 网络被禁止
icmp-host-prohibited ICMP 主机被禁止(firewalld 的 block zone 用的就是这个)
icmp-admin-prohibited ICMP 管理员禁止
tcp-reset 返回 TCP RST 包(仅对 -p tcp 有效)

说明:REJECT 只能用在 filter 表的 INPUT、FORWARD、OUTPUT 链(以及被这些链调用的自定义链)上,不能用在 nat / mangle 的 PREROUTING、POSTROUTING 上;默认策略(-P)也不能设为 REJECT。

3.4.2 LOG 与 --log-prefix

LOG 只记录日志,不改变数据包命运,数据包会继续匹配后续规则。因此 LOG 规则必须紧跟一条真正的处理规则(通常是 DROP):

# 记录并丢弃所有到 23 端口(telnet)的连接尝试
iptables -A INPUT -p tcp --dport 23 -j LOG --log-prefix "TELNET_TRY: " --log-level warning
iptables -A INPUT -p tcp --dport 23 -j DROP

常用选项:

选项 含义
--log-prefix "字符串" 给日志加前缀,便于 grep(建议末尾留一个空格,长度上限 29 字符)
--log-level 级别 日志级别:emerg/alert/crit/error/warning/notice/info/debug
--log-tcp-sequence 记录 TCP 序列号(有安全风险,谨慎使用)
--log-tcp-options 记录 TCP 选项字段
--log-ip-options 记录 IP 选项字段
--log-uid 记录产生该包的本地进程 UID(仅 OUTPUT 链有意义)

日志去哪看:LOG 由内核产生,经 rsyslog 落盘,默认写入 /var/log/messages(Debian/Ubuntu 为 /var/log/kern.log):

tail -f /var/log/messages | grep TELNET_TRY
grep "TELNET_TRY" /var/log/messages
journalctl -k -f | grep TELNET_TRY          # CentOS 7 也可从内核日志看
dmesg | tail -20                            # 直接看内核环形缓冲区

若希望把 iptables 日志单独存放,可在 /etc/rsyslog.conf 中添加:

kern.warning    /var/log/iptables.log

然后 systemctl restart rsyslog(CentOS 6:service rsyslog restart)。

生产建议:LOG 在流量大时会瞬间刷爆磁盘,必须配合 -m limit 限速(见 4.2 节):

iptables -A INPUT -m limit --limit 5/minute --limit-burst 10 -j LOG --log-prefix "iptables dropped: "

3.4.3 NAT 类动作速览

以下四个动作只能用在 nat 表,完整原理与实验见"五、NAT 与转发实战":

# SNAT:改源地址,固定公网 IP 场景,写在 POSTROUTING
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 -j SNAT --to-source 1.2.3.4

# MASQUERADE:改源地址为出站接口当前 IP,动态 IP(拨号)场景,写在 POSTROUTING
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o ppp0 -j MASQUERADE

# DNAT:改目标地址,端口映射/发布内网服务器,写在 PREROUTING
iptables -t nat -A PREROUTING -p tcp -d 1.2.3.4 --dport 80 -j DNAT --to-destination 192.168.224.13:80

# REDIRECT:重定向到本机另一个端口,透明代理,写在 PREROUTING 或 OUTPUT
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080

3.4.4 RETURN 与跳转到自定义链

iptables -N SSH_CHECK
iptables -A SSH_CHECK -s 192.168.224.0/24 -j ACCEPT      # 内网直接放行
iptables -A SSH_CHECK -s 10.0.0.0/8 -j ACCEPT            # 办公网放行
iptables -A SSH_CHECK -j RETURN                          # 其余情况返回 INPUT 继续匹配

iptables -A INPUT -p tcp --dport 22 -j SSH_CHECK          # 跳转
iptables -A INPUT -p tcp --dport 22 -j DROP               # RETURN 回来后落到这条

匹配流程:数据包到 INPUT 第 1 条 → 命中 --dport 22 → 跳进 SSH_CHECK → 逐条匹配 → 若命中 ACCEPT 则整体放行结束;若都不命中,走到 RETURN 返回 INPUT,从跳转规则的下一条(即 -j DROP)继续匹配 → 被丢弃。

说明:在内置链(INPUT/OUTPUT/FORWARD 等)上使用 RETURN,等效于直接执行该链的默认策略。

3.5 规则顺序与"匹配即停止"原则

iptables 规则的读取顺序是从上到下逐条匹配:一旦某条规则匹配成功,就执行该规则的动作(ACCEPT/DROP/REJECT 等终结型动作会立即结束匹配),不再继续向下匹配;如果所有规则都没有匹配,则最终执行该链的默认策略(Policy)。

因此规则的先后顺序至关重要——更精确、更优先的规则必须放在前面。

3.5.1 实验准备:控制 ICMP(ping)

网络拓扑示例:

 192.168.224.0/24 ------> 192.168.224.10
       客户端                      服务器
                   <-------

拒绝来自 192.168.224.0/24 网段的 ping 请求(客户端会收到拒绝信息):

iptables -t filter -A INPUT -p icmp -s 192.168.224.0/24 -j REJECT

丢弃来自 192.168.224.0/24 网段的 ping 请求(不做任何回应,客户端等待超时):

iptables -t filter -A INPUT -p icmp -s 192.168.224.0/24 -j DROP

拒绝本机通往 192.168.224.0/24 网段的 ICMP 通讯:

iptables -t filter -A OUTPUT -p icmp -d 192.168.224.0/24 -j REJECT

丢弃本机通往 192.168.224.0/24 网段的 ICMP 通讯:

iptables -t filter -A OUTPUT -p icmp -d 192.168.224.0/24 -j DROP

以上四种方法都可以拒绝 192.168.224.0/24 网段 ping 本机:前两种是不允许请求进入,后两种是不做回应。不写 -s 或 -d 时默认匹配所有地址:

iptables -t filter -A INPUT -p icmp -j REJECT

3.5.2 实验验证:仅允许特定 IP ping 通本机

需求:所有人都 ping 不通本机,但 192.168.224.11 能 ping 通。这个实验能最直观地验证"匹配即停止"。

错误写法(默认拒绝在前,允许规则永远无法匹配):

iptables -t filter -A INPUT -p icmp -j REJECT
iptables -t filter -A INPUT -p icmp -s 192.168.224.11 -j ACCEPT

规则表变成:

num  target  prot  source
1    REJECT  icmp  0.0.0.0/0            ← 192.168.224.11 的包在这里就被拒绝了
2    ACCEPT  icmp  192.168.224.11       ← 永远走不到这一条

验证方法:从 192.168.224.11 执行 ping 192.168.224.10,仍然不通;再执行 iptables -L -n -v --line-numbers,会看到第 1 条的 pkts 在增长,第 2 条始终为 0——这就是"规则从未被命中"的铁证。

正确写法:用 -I 把允许特定 IP 的规则插到拒绝规则之前。

iptables -F
iptables -t filter -I INPUT -p icmp -s 192.168.224.11 -j ACCEPT
iptables -t filter -A INPUT -p icmp -j REJECT

规则表变成:

num  target  prot  source
1    ACCEPT  icmp  192.168.224.11       ← 白名单先命中,直接放行
2    REJECT  icmp  0.0.0.0/0            ← 其余全部拒绝

再次验证:192.168.224.11 能 ping 通,其他主机不能;-L -n -v 中第 1 条的计数器随白名单主机的 ping 增长。

结论可归纳为一条铁律:白名单(精确放行)在前,黑名单(兜底拒绝)在后。 反过来写等于白名单不存在。

3.5.3 顺序调整的三种手段

需求 做法
新规则要最先生效 用 -I 链名(插到第 1 条)
新规则要插到第 N 条位置 用 -I 链名 N
已有规则顺序错了 先 -D 链名 序号 删掉,再用 -I 链名 正确序号 插回;或整链 -F 后按正确顺序重新 -A

实践建议:生产环境的规则集应写成一个幂等的 shell 脚本(先 -F 清空,再按固定顺序 -A 全部规则),而不是靠人工零散地 -I / -D 修补。这样规则顺序永远可预期,也便于版本管理与回滚(脚本示例见 4.4 节)。

3.6 filter 表包过滤实战

3.6.1 放行 SSH(INPUT/OUTPUT 双链默认 DROP)

需求:允许 SSH 连入(本机作为服务器)和 SSH 连出(本机作为客户端),其它任何访问都拒绝。要求 INPUT、OUTPUT 双链默认策略都为 DROP。

TCP/22 的通信涉及三次握手、数据传输、四次挥手。SSH 服务器监听 22 端口,SSH 客户端使用 1024–65535 的随机端口:

       client                          server
   192.168.224.11                 192.168.224.10

   OUTPUT(随机端口) ---SYN--->  INPUT(22)
   INPUT (随机端口) <--SYN+ACK--  OUTPUT(22)

服务器端防火墙:

iptables -F
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT

客户端防火墙:

iptables -F
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT

执行上面的规则后,本机将无法连接自己(localhost 回环)。需要放行 lo 接口才能实现本地访问:

iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

参数解释:-i 表示进入的请求,-o 表示出去的请求,lo 表示 localhost 回环接口(对应 IP 127.0.0.1)。

结论:在设置白名单放行 22 端口后,本机作为 SSH 服务器可以正常提供 SSH 服务;但应用上述规则后,本机无法作为 SSH 客户端连接出去,因为 SSH 客户端使用 1024–65535 的随机端口去连接远程 SSH 服务器。

3.6.2 让本机能作为 SSH 客户端访问出去

如果要让本机作为客户端访问远程 SSH,需要允许 1024–65535 端口的流量进入。在 server 上执行:

iptables -F
iptables -P INPUT DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 1024:65535 -j ACCEPT

验证:server 可以作为 SSH 客户端连接远程主机。

说明:放行 1024:65535 是一种"粗放"的做法——它把本机所有高位端口都对外暴露了。更优雅的等价方案是用状态检测只放行 ESTABLISHED 的回程包(见 4.3 节),那才是生产环境的标准写法:

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

3.6.3 练习:只允许特定 IP 访问本机 80 端口

需求:只允许特定的 IP 访问本地的 80 端口,其他主机发来的请求一律忽略。

答案:和上面放行 22 端口的做法一样,把 22 换成 80 即可。

iptables -F
iptables -P INPUT DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp -s 192.168.224.11 --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -s 192.168.224.11 --dport 22 -j ACCEPT   # 别忘了给自己留 SSH

3.6.4 查询本机连接远程服务所使用的端口

使用 netstat 查看连接:

netstat -ntp | grep 192.168.224.10
tcp   0   0 192.168.224.12:33228   192.168.224.10:22   ESTABLISHED 4506/ssh

参数解释:

参数 含义
-n 端口以数字形式显示(例如 SSH 显示为 22)
-p 显示进程 PID/程序名
-l 显示处于监听(LISTEN)状态的连接
-t 显示 TCP 连接
-u 显示 UDP 连接

由查询结果可知本机使用 33228 端口连接远程 22 端口。使用 lsof 查看端口被哪个进程占用:

lsof -i:33228
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
ssh     4506 root    3u  IPv4  38908      0t0  TCP client2.example.com:33228->server.example.com:ssh (ESTABLISHED)

再用 ps 查询该进程对应的应用:

ps -ef | grep 4506
root   4506   4420  0 23:20 pts/0   00:00:00 ssh 192.168.224.10
root   5287   3055  0 23:24 pts/1   00:00:00 grep --color=auto 4506

说明:CentOS 7 起推荐用 ss 替代 netstat(netstat 属于已废弃的 net-tools 包,最小化安装可能不带)。等价命令:ss -ntp | grep 192.168.224.10、ss -lntp(看监听)。

3.6.5 连续端口、多端口与 MAC 地址匹配

连续端口(用冒号表示端口范围):

iptables -A INPUT -p tcp --dport 1:1000 -j ACCEPT

多端口(用 -m multiport 加载多端口匹配模块,端口之间以逗号分隔):

iptables -A INPUT -p tcp -m multiport --dport 25,110 -j ACCEPT

按 MAC 硬件地址进行访问控制(用 -m mac 加载 MAC 匹配模块):

iptables -A INPUT -m mac --mac-source 00:23:CD:95:DA:0B -p all -j ACCEPT

注意:MAC 地址只在同一个二层广播域内有效。数据包经过路由器后源 MAC 会被改写成路由器接口的 MAC,因此 -m mac 只能用于限制同网段主机,无法用于跨网段/公网来源的识别。

3.7 FTP 主动模式与被动模式实验

需求:在 INPUT、OUTPUT 双链默认拒绝的情况下,让客户端通过 FTP 的主动模式和被动模式都能访问服务器。

3.7.1 准备工作

在服务器端和客户端都清空防火墙规则(建议使用干净的虚拟机):

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -F
iptables -F -t nat
iptables -F -t mangle
iptables -F -t raw

安装并配置 vsftpd:

yum install vsftpd -y

编辑 /etc/vsftpd/vsftpd.conf,在文件末尾添加以下两句,限定被动模式的数据端口范围:

pasv_min_port=3000
pasv_max_port=3005

重启服务并设置双链默认拒绝:

systemctl restart vsftpd
iptables -P INPUT DROP
iptables -P OUTPUT DROP

设置双链拒绝后,客户端将无法访问 FTP。

客户端测试方法:

  1. 命令连接测试:能成功连接就表示命令端口连接没问题。

    ftp 192.168.224.10

    (使用服务器端已有用户的账号密码登录)

  2. 数据传输测试:登录成功后,在客户端使用 passive 指令切换主动/被动模式(服务器端无需切换,默认主动和被动都支持),然后执行 ls,能看到目录就表示数据传输 OK(默认登录到服务器用户的 HOME 目录)。

3.7.2 FTP 两种连接模式说明

FTP 有主动(PORT)和被动(PASV)两种连接方式:

  1. 为什么有主动和被动两种模式? 这是一种较古老的设计,它假设客户端用户有防火墙且不会配置防火墙。防火墙默认只会拒绝进来的包,而不会拒绝出去的包或"出去后回来"的包。
  2. 一般用主动还是被动? 被动模式比较常见(原因参考上一条)。
  3. 使用上的区别? 没有防火墙时二者使用起来没有区别,只是底层传输包的方式不同;有防火墙时,防火墙规则的写法也不同。

两种模式的端口对照:

模式 命令连接 数据连接 由谁发起数据连接
主动 PORT 客户端随机端口 → 服务器 21 服务器 20 → 客户端随机端口 服务器(客户端防火墙容易挡)
被动 PASV 客户端随机端口 → 服务器 21 客户端随机端口 → 服务器随机高位端口 客户端(服务器防火墙需放行端口范围)

3.7.3 FTP 主动模式规则

主动模式下,服务器使用 21 端口(命令连接)和 20 端口(数据连接)。

在服务器端执行:

iptables -A INPUT -p tcp --dport 21 -j ACCEPT
iptables -A INPUT -p tcp --dport 20 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 21 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 20 -j ACCEPT

在客户端执行:

ftp 192.168.224.10
ftp> ls       # 可以正常运行(主动模式)
ftp> passive  # 切换为被动模式
ftp> ls       # 出现问题,无法正常运行

passive on 命令用于切换主动模式,切换后执行 ls 即可;但此时被动模式无法正常工作。

3.7.4 FTP 被动模式规则

被动模式下,命令连接仍使用 21 端口,数据连接使用服务器端的随机端口(本例限定为 3000–3005)。

iptables -F
iptables -F -t nat
iptables -F -t mangle
iptables -F -t raw

iptables -A INPUT -p tcp --dport 21 -j ACCEPT
iptables -A INPUT -p tcp --dport 20 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 20 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 21 -j ACCEPT

iptables -A INPUT -p tcp --dport 3000:3005 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 3000:3005 -j ACCEPT

使用 multiport 模块将上面的规则合并为两条:

iptables -A INPUT -p tcp -m multiport --dport 20,21,3000:3005 -j ACCEPT
iptables -A OUTPUT -p tcp -m multiport --sport 20,21,3000:3005 -j ACCEPT

进阶:加载 nf_conntrack_ftp 内核模块后,可以用状态检测的 RELATED 状态自动识别 FTP 数据连接,从而无需固定端口范围:

modprobe nf_conntrack_ftp                                   # CentOS 7
modprobe ip_conntrack_ftp                                   # CentOS 6
iptables -A INPUT -p tcp --dport 21 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

这也是 firewalld 的 --add-service=ftp 能"一条命令搞定主动+被动"的原理。


四、iptables 扩展匹配与规则持久化

4.1 隐式扩展与显式扩展

iptables 的匹配条件分为三类:

类别 说明 是否需要 -m 举例
基本匹配 netfilter 核心自带,直接可用 不需要 -s、-d、-p、-i、-o
隐式扩展 指定 -p 协议 后自动加载对应协议的扩展模块 不需要(可省略) -p tcp 后的 --dport、--sport、--tcp-flags、--syn;-p icmp 后的 --icmp-type
显式扩展 必须用 -m 模块名 显式加载后才能使用其选项 需要 -m multiport、-m state、-m limit、-m recent …

隐式扩展的两种等价写法:

iptables -A INPUT -p tcp --dport 22 -j ACCEPT              # 省略 -m tcp(推荐,简洁)
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT       # 显式写出(iptables-save 导出时是这种形式)

说明:iptables-save / iptables -S 的输出总是把隐式扩展写成完整形式(-m tcp --dport 22),所以看到导出的规则里到处是 -m tcp 不必奇怪,它和你手写的短形式是同一条规则。

查看某个扩展模块支持哪些选项:

man iptables-extensions           # CentOS 7,全部扩展模块的手册
man iptables                      # CentOS 6,扩展说明在同一页
iptables -m state --help          # 查看 state 模块的选项说明
iptables -m limit --help

4.2 常用扩展模块逐项详解

4.2.1 multiport:多端口匹配

一次匹配多个不连续端口,最多 15 个(连续范围 a:b 算作 2 个)。

选项 含义
--dports / --dport 匹配多个目标端口
--sports / --sport 匹配多个源端口
--ports 源端口或目标端口任一命中即匹配
iptables -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 20,21,3000:3005 -j ACCEPT
iptables -A INPUT -p udp -m multiport --dports 53,123 -j ACCEPT
iptables -A INPUT -p tcp -m multiport ! --dports 22,80 -j DROP     # 取反
iptables -A INPUT -p tcp -m multiport --ports 137,138,139,445 -j ACCEPT

4.2.2 iprange:IP 地址范围匹配

当地址范围不是一个规整的网段时(例如 192.168.224.20 到 192.168.224.50),无法用 CIDR 表示,这时用 iprange:

选项 含义
--src-range 起始IP-结束IP 匹配源地址范围
--dst-range 起始IP-结束IP 匹配目标地址范围
iptables -A INPUT -m iprange --src-range 192.168.224.20-192.168.224.50 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m iprange ! --src-range 192.168.224.20-192.168.224.50 -j DROP
iptables -A FORWARD -m iprange --dst-range 10.0.0.100-10.0.0.200 -j ACCEPT

4.2.3 mac:MAC 地址匹配

选项 含义
--mac-source MAC 匹配源 MAC 地址
iptables -A INPUT -m mac --mac-source 00:23:CD:95:DA:0B -p all -j ACCEPT
iptables -A INPUT -m mac ! --mac-source 00:23:CD:95:DA:0B -j DROP
iptables -A FORWARD -m mac --mac-source 00:0C:29:AA:BB:CC -j DROP     # 禁止某台内网机上网

注意:只有 --mac-source,没有 --mac-destination(数据包进入本机时目标 MAC 必然是本机,匹配无意义)。且只能用在 PREROUTING、FORWARD、INPUT 链上。跨路由器后源 MAC 会被改写,故只对同网段有效。

4.2.4 state:连接状态匹配(最重要的扩展模块)

连接跟踪原理。 状态检测是 netfilter 通过 conntrack(连接跟踪)模块实现的一种包过滤机制。内核为每一条经过的"连接"在内存中维护一张表项(记录源/目的 IP、源/目的端口、协议、状态、超时时间),后续数据包进来时先去这张表里查找是否属于某条已知连接,从而无需再逐条对比规则即可判定其"身份"。

# 查看当前连接跟踪表
cat /proc/net/nf_conntrack | head            # CentOS 7
cat /proc/net/ip_conntrack | head            # CentOS 6
conntrack -L                                 # 需安装 conntrack-tools

# 查看/调整连接跟踪表容量(高并发服务器必须关注)
cat /proc/sys/net/netfilter/nf_conntrack_max
sysctl -w net.netfilter.nf_conntrack_max=655360
cat /proc/sys/net/netfilter/nf_conntrack_count       # 当前已用条目数

说明:nf_conntrack 表满时内核会打印 nf_conntrack: table full, dropping packet,表现为新连接大量失败。高并发场景需要加大 nf_conntrack_max 并调小 nf_conntrack_tcp_timeout_established(默认 5 天,过长)。

常用的包过滤条件包括:

-p 协议
--sport/--dport 端口
-s/-d 地址
-m state --state 状态

按照 TCP/IP 协议标准,连接状态有 11 种之多;但在 iptables 的 state 模块中,只定义了 4 种状态:ESTABLISHED、NEW、RELATED、INVALID。这两个分类是互不相干的定义——例如在 TCP/IP 标准中 UDP 和 ICMP 数据包没有连接状态,但在 state 模块的描述下,任何数据包都有连接状态。

1. ESTABLISHED(已建立连接状态)

只要第一个数据包能够成功穿越防火墙,那么之后该连接上的所有数据包(包含反向的所有数据包)状态都会是 ESTABLISHED。

  • 与 TCP 的关系:在防火墙主机上执行 SSH 客户端,向网络上的 SSH 服务器发起请求,第一个数据包是服务请求包;如果它成功穿越防火墙,那么接下来 SSH 服务器与客户端之间的所有 SSH 数据包状态都是 ESTABLISHED。
  • 与 UDP 的关系:在防火墙主机上用 firefox 浏览网页(通过域名方式),需要 DNS 服务器完成名称解析。firefox 向 DNS Server 发送一个 UDP 请求包,如果该包成功穿越防火墙,之后 DNS Server 与 firefox 之间的所有数据包状态都是 ESTABLISHED。
  • 与 ICMP 的关系:在防火墙主机上用 ping 检测其它主机时,ping 发出的第一个 ICMP 包如果成功穿越防火墙,之后该主机与防火墙主机之间的所有 ICMP 包状态都是 ESTABLISHED。

2. NEW(新连接状态)

NEW 与协议无关,指的是每条连接中的第一个数据包。例如使用 SSH 客户端连接 SSH 服务器时,这条连接中的第一个数据包状态就是 NEW。

3. RELATED(相关连接状态)

RELATED 状态的数据包是指被动产生的数据包,且这个连接不属于当前任何已有连接。RELATED 与协议无关,只要回应回来的数据包是因为本机发出一个数据包导致另一个新连接的产生,这条新连接上的所有数据包都属于 RELATED 状态。典型场景如 FTP 数据连接、ICMP 差错报文。

4. INVALID(无效状态)

INVALID 状态指状态不明的数据包,即不属于以上三种状态的数据包。凡是 INVALID 状态的数据包通常被视为恶意数据包,应丢弃。匹配方法:

iptables -A INPUT -p all -m state --state INVALID -j DROP

建议将 INVALID 规则放在所有规则的第一条。

状态示意图

                            |
            随机端口         |            80 (web)
            --------- ->
        client              |              server
                 <-----------
            随机端口         |            80
                            |
  • client → server:第一个数据包为 NEW 状态。如果被拒绝,后续包也会被拒绝(因为后续每个首包都被视为 NEW);如果允许通过,后续包状态变为 ESTABLISHED。
  • server → client:返回的所有包状态均为 ESTABLISHED。

"只放行必要入站 + 放行 ESTABLISHED"的标准套路

这是生产环境主机防火墙的标准骨架,务必背下来:

iptables -F
iptables -X
iptables -Z
iptables -P INPUT DROP                 # 入站默认拒绝
iptables -P FORWARD DROP               # 不做网关就禁止转发
iptables -P OUTPUT ACCEPT              # 出站默认放行(本机可信)

iptables -A INPUT -m state --state INVALID -j DROP                  # 1. 丢弃无效包
iptables -A INPUT -i lo -j ACCEPT                                   # 2. 放行回环
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT     # 3. 放行回程流量
iptables -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT   # 4. 放行必要服务
iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT                    # 5. 允许被 ping(可选)

这套写法的精髓:

  1. 第 3 条一句话解决了"本机主动发出的请求,回来的包怎么放行"的全部问题——不需要再去放行 1024:65535,也不需要为每个服务写一条 OUTPUT 规则。
  2. 只有第 4 条需要按业务修改,规则集长期稳定。
  3. OUTPUT 设为 ACCEPT 简化了配置;若安全要求极高(如堡垒机、金融环境)才需把 OUTPUT 也设为 DROP 并逐一放行。

关键坑:忘了第 3 条会导致"能连上但立刻断"或"三次握手不成功"。 症状是 ssh 卡在 Connection established 之后没有任何输出、curl 一直挂着——因为请求包出去了,回来的 SYN+ACK 被 INPUT 的默认 DROP 挡住了。

状态检测实战例 1:放行 SSH 与 Web 的简化写法

两台主机:192.168.224.11(client)、192.168.224.10(server)。client 可以通过 SSH 和 curl 访问 server。

首先在 server 上安装并启动 httpd,然后设置双链默认 DROP:

iptables -F
iptables -F -t nat
iptables -P INPUT DROP
iptables -P OUTPUT DROP

传统写法(分别放行入站和出站):

# 允许别人 SSH 进来
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT
# 允许别人 curl 进来
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 80 -j ACCEPT

或用 multiport 合并:

iptables -A INPUT -p tcp -m multiport --dport 22,80 -j ACCEPT
iptables -A OUTPUT -p tcp -m multiport --sport 22,80 -j ACCEPT

基于状态的简化写法(两条规则达到同等效果):

iptables -A INPUT -p tcp -m multiport --dport 22,80 -j ACCEPT
iptables -A OUTPUT -p tcp -m state --state ESTABLISHED -j ACCEPT

后一句可理解为:TCP 协议的连接"只要进得来,就回得去"——无论客户端用哪个随机端口访问进来,只要连接能建立,后续返回包都属于 ESTABLISHED 状态而被放行。

验证:在 client 上执行 curl http://192.168.224.10 可以正常访问。

状态检测实战例 2:INPUT 默认 DROP 但保证本机对外请求正常

client(192.168.224.11)为保证安全,将 INPUT 默认设为 DROP,但要保证由本机主动发出的请求能正常收到回应。

在 client 上设置:

iptables -F
iptables -F -t nat
iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -p all -m state --state ESTABLISHED -j ACCEPT

验证:

  1. client 通过 SSH 连接其它机器可以正常使用。
  2. 从 server ping client 会失败(因为外部主动发起的连接首包为 NEW 状态,被 INPUT 默认 DROP 拒绝)。

状态检测实战例 3:只能 ping 别人,不允许别人 ping 自己

需求:服务器 192.168.224.10 能 ping 通所有人,但所有人不能 ping 通它(前提是客户端没有防火墙限制)。

方法一:修改系统内核参数

将 /proc/sys/net/ipv4/icmp_echo_ignore_all 的值改为 1。

临时修改(两种方式):

echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
# 或
sysctl -w net.ipv4.icmp_echo_ignore_all=1

永久修改:编辑 /etc/sysctl.conf,添加:

net.ipv4.icmp_echo_ignore_all = 1

然后执行以下命令使其生效:

sysctl -p

方法二:通过 iptables 状态检测实现

状态对应关系:

                 NEW       ESTABLISHED
   INPUT          拒绝        允许
   OUTPUT         允许        允许

在 192.168.224.10 上先设置双链默认 DROP:

iptables -P INPUT DROP
iptables -P OUTPUT DROP

然后添加状态规则:

iptables -A INPUT -p icmp -m state --state ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p icmp -m state --state NEW,ESTABLISHED -j ACCEPT
  • 重点是 INPUT 那条不能允许 NEW 状态(否则别人就能 ping 通本机)。
  • OUTPUT 那条如果只写 NEW,那么本机 ping 别人只能通第一个包;加上 ESTABLISHED 状态后,所有包都能通。

4.2.5 conntrack:state 的增强版

conntrack 模块是 state 的超集,除状态外还能匹配原始(NAT 转换前)的地址端口、连接建立时间等。新内核推荐用 conntrack 替代 state(state 现已是 conntrack 的兼容别名)。

选项 含义
--ctstate 状态列表 匹配连接状态,取值 NEW/ESTABLISHED/RELATED/INVALID/UNTRACKED/SNAT/DNAT
--ctorigsrc / --ctorigdst 匹配连接原始方向的源/目的地址(NAT 前)
--ctreplsrc / --ctrepldst 匹配回复方向的源/目的地址(NAT 后)
--ctproto 匹配连接的协议
--ctstatus 匹配连接的内部状态(EXPECTED/SEEN_REPLY/ASSURED/CONFIRMED)
--ctexpire 匹配剩余超时时间
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
iptables -A INPUT -m conntrack --ctstate NEW -p tcp --dport 22 -j ACCEPT

# 只有 UNTRACKED 才能匹配 raw 表标了 NOTRACK 的包
iptables -t raw -A PREROUTING -p udp --dport 53 -j NOTRACK
iptables -A INPUT -m conntrack --ctstate UNTRACKED -p udp --dport 53 -j ACCEPT

两者对照:

state 写法 conntrack 等价写法
-m state --state ESTABLISHED,RELATED -m conntrack --ctstate ESTABLISHED,RELATED
-m state --state NEW -m conntrack --ctstate NEW

4.2.6 limit:速率限制(防洪水与限制日志)

limit 模块用于限制数据包匹配的速率,可实现限流、防止 ping 洪水或控制日志写入频率。它基于令牌桶算法:桶里有令牌就放行并消耗一个,桶空则不匹配这条规则。

参数 含义
--limit 速率 平均匹配速率(令牌补充速度),如 10/second、10/minute、1/hour、5/day
--limit-burst N 初始允许的突发包数量(令牌桶容量),默认为 5

示例:限制 ICMP 包每秒只放行 1 个,突发上限 5 个:

iptables -A INPUT -p icmp -m limit --limit 1/second --limit-burst 5 -j ACCEPT
iptables -A INPUT -p icmp -j DROP

示例:对被丢弃的包做日志记录,但限制日志写入频率为每分钟 5 条,避免日志被刷爆:

iptables -A INPUT -m limit --limit 5/minute --limit-burst 10 -j LOG --log-prefix "iptables dropped: "

示例:限制新建 HTTP 连接速率,缓解 CC 攻击:

iptables -A INPUT -p tcp --dport 80 -m state --state NEW -m limit --limit 50/second --limit-burst 100 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j DROP

LOG 动作只记录日志,数据包会继续匹配后续规则,因此通常在其后再加一条 DROP。

理解要点:limit 规则只在令牌足够时才"匹配成功"。所以标准用法是"limit + ACCEPT"后面紧跟一条兜底的 DROP——超速的包因为不匹配 limit 规则而落到 DROP 上被丢弃。只写 limit + ACCEPT 而不写兜底 DROP,等于毫无限速效果。

4.2.7 connlimit:并发连接数限制

限制每个客户端 IP(或每个网段)能同时保持的连接数,用于防止单个 IP 耗尽服务器连接资源。

参数 含义
--connlimit-above N 当该来源的并发连接数超过 N 时匹配
--connlimit-upto N 当并发连接数不超过 N 时匹配
--connlimit-mask 位数 按多少位掩码归组统计,默认 32(即按单个 IP);写 24 则按 C 类网段统计
--connlimit-saddr / --connlimit-daddr 按源地址 / 目标地址统计(新版本)
# 每个 IP 最多 10 个并发 SSH 连接,超出的拒绝
iptables -A INPUT -p tcp --dport 22 -m connlimit --connlimit-above 10 -j REJECT

# 每个 IP 最多 50 个并发 HTTP 连接
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 50 -j DROP

# 按 C 类网段统计:整个 /24 网段最多 100 个并发连接
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 100 --connlimit-mask 24 -j DROP

# 取反写法:并发不超过 20 时放行
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-upto 20 -j ACCEPT

limit 与 connlimit 的区别:limit 限制的是单位时间内的包/连接速率(针对全局,不区分来源);connlimit 限制的是同一来源的同时在线连接数。防 CC 攻击通常两者配合使用。

4.2.8 recent:防暴力破解

recent 模块可记录某源 IP 在一段时间内的访问情况,并据此做判断,常用于防止暴力破解、端口扫描等。

参数 含义
--name 名称 指定记录列表的名称(默认 DEFAULT)
--set 将匹配到的源地址加入记录列表,并记录时间戳
--rcheck 检查源地址是否在记录列表中(不更新时间戳)
--update 与 --rcheck 类似,但会刷新该条记录的时间戳
--remove 检查到地址存在时将其从列表中删除
--seconds N 只匹配最近 N 秒内的记录,常与 --rcheck/--update 配合
--hitcount N 匹配命中次数达到 N 次(含)的记录
--rsource / --rdest 按源地址(默认)/ 目标地址记录

示例:限制每个 IP 在 60 秒内最多新建 3 个到 22 端口的 SSH 连接,超过则丢弃。

iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP

说明:第一条规则把每个新连接的源 IP 记入名为 SSH 的列表;第二条规则检查该 IP 在最近 60 秒内是否已是第 4 次(含)以上发起新连接,是则丢弃。

更严格的版本(超限后拉黑 1 小时,并记录日志):

iptables -N SSH_BRUTE
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j SSH_BRUTE
iptables -A SSH_BRUTE -m recent --name SSHBAN --update --seconds 3600 -j DROP     # 黑名单期内直接丢
iptables -A SSH_BRUTE -m recent --name SSHTRY --set
iptables -A SSH_BRUTE -m recent --name SSHTRY --update --seconds 60 --hitcount 5 \
         -j LOG --log-prefix "SSH_BRUTE_FORCE: "
iptables -A SSH_BRUTE -m recent --name SSHTRY --update --seconds 60 --hitcount 5 \
         -m recent --name SSHBAN --set -j DROP

查看与清理 recent 列表:

ls /proc/net/xt_recent/                        # 列出所有列表
cat /proc/net/xt_recent/SSH                    # 查看某个列表中记录的 IP
echo +192.168.224.99 > /proc/net/xt_recent/SSH # 手动加入一个 IP
echo -192.168.224.99 > /proc/net/xt_recent/SSH # 手动移除一个 IP
echo / > /proc/net/xt_recent/SSH               # 清空该列表

注意:recent 默认最多记录 100 个地址(ip_list_tot)、每个地址最多 20 个时间戳(ip_pkt_list_tot)。需要更大容量时通过模块参数调整:/etc/modprobe.d/xt_recent.conf 中写 options xt_recent ip_list_tot=1000 ip_pkt_list_tot=100。

4.2.9 string:内容匹配

在数据包载荷中查找指定字符串,可用于简单的应用层过滤(例如拦截含特定关键字的 HTTP 请求)。

参数 含义
--algo bm\|kmp 匹配算法(必填),bm=Boyer-Moore,kmp=Knuth-Pratt-Morris
--string "字符串" 要匹配的字符串
--hex-string "\|十六进制\|" 以十六进制形式指定内容
--from N / --to N 只在载荷的第 N 字节范围内查找
# 拦截访问指定域名的 HTTP 请求
iptables -A FORWARD -p tcp --dport 80 -m string --algo bm --string "baidu.com" -j REJECT

# 拦截含有 SQL 注入特征的请求
iptables -A INPUT -p tcp --dport 80 -m string --algo bm --string "union select" -j DROP

# 限定在前 200 字节内查找(提高性能)
iptables -A FORWARD -p tcp --dport 80 -m string --algo kmp --from 0 --to 200 --string "cmd.exe" -j DROP

局限:string 只能匹配明文载荷,对 HTTPS 完全无效;且逐包扫描开销大、易被分片绕过。应用层过滤应交给 nginx/WAF/squid 处理,string 仅适合临时应急。

4.2.10 time:时间段匹配

按时间控制规则是否生效,典型用于"上班时间禁止访问某些网站"、"夜间关闭某服务"。

参数 含义
--timestart HH:MM[:SS] 起始时间
--timestop HH:MM[:SS] 结束时间
--weekdays 星期 星期,可用 Mon,Tue,...,Sun 或 1~7
--monthdays N 每月的第几天
--datestart YYYY-MM-DD[Thh:mm:ss] 起始日期
--datestop YYYY-MM-DD[Thh:mm:ss] 结束日期
--kerneltz 使用内核本地时区(不加则按 UTC 判断,最常见的坑)
# 工作日 09:00-18:00 禁止内网主机访问外网 80 端口
iptables -A FORWARD -p tcp --dport 80 \
  -m time --timestart 09:00 --timestop 18:00 --weekdays Mon,Tue,Wed,Thu,Fri --kerneltz -j DROP

# 每天 00:00-06:00 允许远程维护
iptables -A INPUT -p tcp --dport 22 -m time --timestart 00:00 --timestop 06:00 --kerneltz -j ACCEPT

# 指定日期区间生效(临时活动放行)
iptables -A INPUT -p tcp --dport 8080 \
  -m time --datestart 2026-09-01T00:00:00 --datestop 2026-09-07T23:59:59 -j ACCEPT

注意:不加 --kerneltz 时,time 模块按 UTC 判断时间,中国时区(UTC+8)会整体偏差 8 小时,导致"规则在错误的时间段生效"。老内核不支持 --kerneltz 时,需要手工把时间减去 8 小时来写。

4.2.11 tcp 扩展:--tcp-flags 与 --syn

-p tcp 的隐式扩展,用于匹配 TCP 标志位。

参数 含义
--tcp-flags 检查的标志位 必须置1的标志位 前一组指定要检查哪些位,后一组指定其中哪些必须为 1(其余必须为 0)
--syn 等价于 --tcp-flags SYN,RST,ACK,FIN SYN,即"仅 SYN 置位"的连接请求首包
--tcp-option N 匹配 TCP 选项号

可用标志位:SYN、ACK、FIN、RST、URG、PSH,以及 ALL(全部)、NONE(无)。

# 只放行 SSH 的连接请求首包(与 -m state --state NEW 效果相近)
iptables -A INPUT -p tcp --dport 22 --syn -j ACCEPT

# 拒绝所有新建连接请求(ESTABLISHED 不受影响)
iptables -A INPUT -p tcp --syn -j DROP

# 防 SYN Flood:限制 SYN 包速率
iptables -A INPUT -p tcp --syn -m limit --limit 100/second --limit-burst 200 -j ACCEPT
iptables -A INPUT -p tcp --syn -j DROP

# 丢弃常见扫描包:NULL 扫描(无任何标志位)
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
# XMAS 扫描(FIN+URG+PSH 全置位)
iptables -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
# 非法组合 SYN+FIN
iptables -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
# 非法组合 SYN+RST
iptables -A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j DROP

4.2.12 icmp 扩展:--icmp-type 与 ping 的双向控制

-p icmp 的隐式扩展。ICMP 的 type 决定报文用途,做 ping 控制时只需记住两个:

type 名称 含义
8 echo-request ping 请求(别人 ping 我时进来的包 / 我 ping 别人时出去的包)
0 echo-reply ping 回应
3 destination-unreachable 目标不可达(差错报文,不要全丢,否则影响 MTU 探测)
11 time-exceeded 超时(traceroute 依赖)
# 允许别人 ping 我(放行进来的请求 + 出去的回应)
iptables -A INPUT  -p icmp --icmp-type 8 -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type 0 -j ACCEPT

# 允许我 ping 别人(放行出去的请求 + 进来的回应)
iptables -A OUTPUT -p icmp --icmp-type 8 -j ACCEPT
iptables -A INPUT  -p icmp --icmp-type 0 -j ACCEPT

# 只允许我 ping 别人,不允许别人 ping 我
iptables -A INPUT  -p icmp --icmp-type 8 -j DROP
iptables -A INPUT  -p icmp --icmp-type 0 -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type 8 -j ACCEPT

# 用名字代替数字(等价写法)
iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT

# 查看所有可用的 icmp-type 名称
iptables -p icmp --help | tail -30

建议:不要简单地 -p icmp -j DROP 一刀切。ICMP type 3(不可达)中的 fragmentation needed 承担 PMTU 发现职责,全部丢弃会导致大包无法传输、出现"小页面能打开、大文件下载卡死"的诡异故障。正确做法是只丢 type 8。

4.2.13 comment:给规则加注释

规则一多就看不懂了。comment 让规则自带说明,-L -n -v 时会显示出来。

iptables -A INPUT -p tcp --dport 3306 -s 192.168.224.0/24 \
  -m comment --comment "allow intranet mysql" -j ACCEPT

iptables -A INPUT -p tcp --dport 6379 -i lo \
  -m comment --comment "redis only localhost" -j ACCEPT

注释最长 256 字符。生产环境的每条非显而易见的规则都应加注释,否则半年后没人敢删。

4.3 规则持久化

iptables 命令写下的规则只存在于内存中,重启后全部消失。 这是新手最容易踩的坑之一:辛苦调好的规则,重启服务器后全没了。要持久化必须显式保存。

4.3.1 CentOS 6:service iptables save

CentOS 6 的 iptables 服务脚本自带 save 子命令,一条命令即可把当前内存中的规则写入配置文件:

service iptables save                # 保存到 /etc/sysconfig/iptables
service iptables start
service iptables stop
service iptables restart
service iptables status
chkconfig iptables on                # 设置开机自启(开机时自动加载配置文件中的规则)
chkconfig --list iptables

保存的目标文件由 /etc/sysconfig/iptables-config 中的变量控制,其中两个常用开关:

# 停止 iptables 服务时是否自动保存当前规则
IPTABLES_SAVE_ON_STOP="no"
# 重启 iptables 服务时是否自动保存当前规则
IPTABLES_SAVE_ON_RESTART="no"

4.3.2 /etc/sysconfig/iptables 文件格式与手工编辑

该文件就是 iptables-save 的输出格式,可以直接用编辑器修改:

cat /etc/sysconfig/iptables
# Generated by iptables-save v1.4.21
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
COMMIT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 192.168.224.0/24 -o eth0 -j MASQUERADE
COMMIT

格式说明:

行 含义
*filter 表名,一张表一段
:INPUT DROP [0:0] 链名 + 默认策略 + [包计数:字节计数]
-A INPUT ... 规则,格式与 iptables 命令去掉 iptables 后完全一致
COMMIT 该表规则结束标志,必须有,漏写则整表规则不生效
# 开头 注释

手工编辑的完整流程:

cp /etc/sysconfig/iptables /etc/sysconfig/iptables.bak     # 1. 先备份
vi /etc/sysconfig/iptables                                 # 2. 编辑
iptables-restore -t < /etc/sysconfig/iptables              # 3. 用 -t 只做语法检查,不实际加载
service iptables restart                                   # 4. CentOS 6 加载
systemctl restart iptables                                 # 4. CentOS 7 加载
iptables -L -n -v --line-numbers                            # 5. 验证

注意:手工编辑最大的风险是写错语法导致整个规则集加载失败——此时链的默认策略可能已被设为 DROP 而放行规则却没加载上,直接失去 SSH。务必先用 iptables-restore -t 做语法检查,并在有控制台/带外管理的情况下操作。

4.3.3 CentOS 7:iptables-services

CentOS 7 默认不装 iptables-services(只有 iptables 命令,没有 iptables 服务),需要先安装:

systemctl stop firewalld
systemctl disable firewalld
systemctl mask firewalld              # 彻底屏蔽,防止被依赖关系唤醒

yum install iptables-services -y
systemctl enable --now iptables       # 启动并设为开机自启
systemctl status iptables

CentOS 7 上 service iptables save 仍然可用(被重定向到服务脚本),但更推荐直接用 iptables-save:

service iptables save                            # 兼容写法,等效于下一条
iptables-save > /etc/sysconfig/iptables          # 推荐写法

两个版本的操作对照:

操作 CentOS 6 CentOS 7
启动 service iptables start systemctl start iptables
停止 service iptables stop systemctl stop iptables
重启 service iptables restart systemctl restart iptables
查看状态 service iptables status systemctl status iptables
开机自启 chkconfig iptables on systemctl enable iptables
保存规则 service iptables save iptables-save > /etc/sysconfig/iptables
是否默认安装 是 否,需 yum install iptables-services
默认防火墙 iptables firewalld

4.3.4 iptables-save 与 iptables-restore

# 导出(保存)
iptables-save                                    # 打印到屏幕
iptables-save > /etc/sysconfig/iptables          # 保存到默认文件
iptables-save > /root/iptables-$(date +%F).rules # 保存到自定义文件(带日期)
iptables-save -c > /root/rules-with-counters     # -c 同时保存计数器
iptables-save -t nat > /root/nat.rules           # 只导出指定表

# 导入(恢复)
iptables-restore < /etc/sysconfig/iptables       # 恢复(会先清空现有规则)
iptables-restore -n < /root/extra.rules          # -n 不清空,追加到现有规则之上
iptables-restore -t < /root/nat.rules            # -t 只做语法测试,不加载
iptables-restore -c < /root/rules-with-counters  # -c 同时恢复计数器

/etc/sysconfig/iptables 是 iptables 服务的默认保存文件,重启 iptables 服务时会自动加载该文件中的规则。也可以手工保存到其它自定义文件,恢复时用 iptables-restore 手动导入。

说明:ip6tables-save / ip6tables-restore 是 IPv6 的对应命令,配置文件为 /etc/sysconfig/ip6tables。iptables 的规则完全不影响 IPv6——只放行了 IPv4 而忘记 IPv6 时,攻击者可能通过 IPv6 地址绕过全部限制。这是 firewalld 相比 iptables 的重要优势之一(一套命令同时下发两族规则)。

4.3.5 开机自动加载的三种做法

做法一:使用 iptables 服务(推荐)

要让规则在重启后依然生效:

  1. 用 iptables-save > /etc/sysconfig/iptables 保存当前规则。
  2. 执行 systemctl enable iptables.service(CentOS 6:chkconfig iptables on)设置服务开机自启。

如果希望开机时为空规则(无任何 iptables 策略),可以将 /etc/sysconfig/iptables 保存为空规则,再设置 systemctl enable iptables.service。

做法二:写进开机脚本

# CentOS 6:/etc/rc.local
echo '/sbin/iptables-restore < /etc/sysconfig/iptables' >> /etc/rc.local

# CentOS 7:/etc/rc.d/rc.local(注意需要手工加执行权限)
echo '/sbin/iptables-restore < /etc/sysconfig/iptables' >> /etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local

做法三:自定义 systemd 单元(CentOS 7,适合规则由脚本生成的场景)

cat > /etc/systemd/system/myfirewall.service <<'EOF'
[Unit]
Description=Custom iptables rules
After=network.target
Before=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/firewall.sh

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now myfirewall

4.3.6 规则备份与还原的完整流程

备份(变更前必做):

mkdir -p /root/iptables-backup

# 1. 备份规则本体
iptables-save  > /root/iptables-backup/filter-nat-$(date +%F-%H%M).rules
ip6tables-save > /root/iptables-backup/ipv6-$(date +%F-%H%M).rules

# 2. 备份配置文件
cp -a /etc/sysconfig/iptables /root/iptables-backup/iptables.$(date +%F-%H%M)

# 3. 记录变更前的规则快照(便于 diff 对比)
iptables -L -n -v --line-numbers > /root/iptables-backup/before.txt
iptables -t nat -L -n -v         >> /root/iptables-backup/before.txt

变更:

# 建议先挂一个"5 分钟后自动恢复"的保险,防止把自己锁在外面
echo "iptables -P INPUT ACCEPT; iptables -F; iptables-restore < /root/iptables-backup/filter-nat-*.rules" \
  | at now + 5 minutes
atq                                  # 查看已排队的任务

# ... 执行变更 ...

# 验证 SSH/业务正常后取消保险
atrm <任务号>

还原:

# 方式一:从备份文件恢复
iptables-restore -t < /root/iptables-backup/filter-nat-2026-08-22-1030.rules   # 先语法检查
iptables-restore   < /root/iptables-backup/filter-nat-2026-08-22-1030.rules

# 方式二:恢复配置文件后重启服务
cp -a /root/iptables-backup/iptables.2026-08-22-1030 /etc/sysconfig/iptables
systemctl restart iptables            # CentOS 7
service iptables restart              # CentOS 6

# 验证并与变更前快照对比
iptables -L -n -v --line-numbers > /root/iptables-backup/after.txt
diff /root/iptables-backup/before.txt /root/iptables-backup/after.txt

4.3.7 实验结束清理

结束实验后清空 iptables 规则,建议准备干净的虚拟机继续后续实验:

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -F
iptables -F -t nat
iptables -F -t mangle
iptables -F -t raw
systemctl restart iptables
systemctl stop iptables
systemctl disable iptables

一键彻底重置(含默认策略、自定义链与计数器):

for t in filter nat mangle raw; do
  iptables -t $t -F        # 清空规则
  iptables -t $t -X        # 删除自定义链
  iptables -t $t -Z        # 清零计数器
done
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT

4.4 生产环境 iptables 规则脚本示例(逐行注释)

把规则写成幂等脚本是生产环境的标准做法:每次执行都先清空再按固定顺序重建,规则顺序永远可预期,可纳入版本管理。

#!/bin/bash
# /usr/local/sbin/firewall.sh
# 用途:单机 Web 服务器的 iptables 规则集(幂等,可反复执行)
# 适用:CentOS 6 / CentOS 7(CentOS 7 需先停用 firewalld)

IPT=/sbin/iptables                        # iptables 命令路径(脚本中用绝对路径,避免 PATH 问题)
ADMIN_NET="192.168.224.0/24"              # 管理网段:允许 SSH 与 ping
INTRANET="10.0.0.0/8"                     # 内网网段:允许访问数据库
SSH_PORT=22                               # SSH 端口,改端口时同步修改

# ---------- 1. 先把默认策略放开,避免清空规则期间把自己锁死 ----------
$IPT -P INPUT ACCEPT
$IPT -P FORWARD ACCEPT
$IPT -P OUTPUT ACCEPT

# ---------- 2. 清空全部表的规则、自定义链与计数器(保证幂等) ----------
for t in filter nat mangle raw; do
    $IPT -t $t -F
    $IPT -t $t -X
    $IPT -t $t -Z
done

# ---------- 3. 设定最终默认策略:入站与转发拒绝,出站放行 ----------
$IPT -P INPUT DROP                        # 未明确放行的入站流量一律丢弃
$IPT -P FORWARD DROP                      # 本机不做网关,禁止转发
$IPT -P OUTPUT ACCEPT                     # 本机进程可信,出站不限制

# ---------- 4. 基础放行:无效包、回环、回程流量 ----------
$IPT -A INPUT -m state --state INVALID -j DROP          # 状态非法的包直接丢,放在最前面
$IPT -A INPUT -i lo -j ACCEPT                           # 放行本机回环,否则本机自访问全部失败
$IPT -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
                                                        # 放行已建立连接与相关连接(FTP 数据、ICMP 差错)
                                                        # 这一条使本机主动发出的请求能收到回应

# ---------- 5. 抗扫描与抗洪水 ----------
$IPT -A INPUT -p tcp --tcp-flags ALL NONE -j DROP           # NULL 扫描
$IPT -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP    # XMAS 扫描
$IPT -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP    # 非法标志组合
$IPT -A INPUT -p tcp --syn -m limit --limit 200/second --limit-burst 400 -j ACCEPT
                                                            # SYN 包限速,缓解 SYN Flood
$IPT -A INPUT -p tcp --syn -m limit --limit 1/second -j LOG --log-prefix "SYN_FLOOD: "
                                                            # 超速的 SYN 记日志(日志本身也限速)

# ---------- 6. SSH:仅管理网段可访问,并做暴力破解防护 ----------
$IPT -N SSH_GUARD                                                        # 新建自定义链收拢 SSH 规则
$IPT -A INPUT -p tcp --dport $SSH_PORT -s $ADMIN_NET -j SSH_GUARD        # 只有管理网段能进入该链
$IPT -A SSH_GUARD -m recent --name SSHBAN --update --seconds 3600 -j DROP  # 黑名单期内直接丢弃
$IPT -A SSH_GUARD -m state --state NEW -m recent --name SSHTRY --set      # 记录每次新连接
$IPT -A SSH_GUARD -m state --state NEW -m recent --name SSHTRY \
     --update --seconds 60 --hitcount 5 -m recent --name SSHBAN --set \
     -j LOG --log-prefix "SSH_BRUTE: "                                    # 60 秒内第 5 次:记日志并拉黑
$IPT -A SSH_GUARD -m state --state NEW -m recent --name SSHTRY \
     --update --seconds 60 --hitcount 5 -j DROP                           # 并丢弃该连接
$IPT -A SSH_GUARD -m comment --comment "ssh from admin net" -j ACCEPT     # 通过检查则放行

# ---------- 7. 对外提供的业务端口 ----------
$IPT -A INPUT -p tcp -m multiport --dports 80,443 \
     -m comment --comment "public web" -j ACCEPT                # HTTP/HTTPS 对所有人开放
$IPT -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 100 -j REJECT
                                                                # 单 IP 并发超过 100 拒绝,防 CC
$IPT -A INPUT -p tcp --dport 3306 -s $INTRANET \
     -m comment --comment "mysql intranet only" -j ACCEPT       # 数据库只对内网开放,绝不对公网
$IPT -A INPUT -p udp --dport 123 -j ACCEPT                      # NTP(若本机提供时间同步服务)

# ---------- 8. ICMP:只放行 ping 请求与必要的差错报文 ----------
$IPT -A INPUT -p icmp --icmp-type 8 -s $ADMIN_NET -m limit --limit 5/second -j ACCEPT
                                                    # 只允许管理网段 ping,且限速
$IPT -A INPUT -p icmp --icmp-type 3 -j ACCEPT       # 目标不可达:PMTU 发现必需,不能丢
$IPT -A INPUT -p icmp --icmp-type 11 -j ACCEPT      # 超时:traceroute 依赖

# ---------- 9. 兜底:记录并拒绝其余入站流量 ----------
$IPT -A INPUT -m limit --limit 5/minute --limit-burst 10 \
     -j LOG --log-prefix "IPT_DROP_IN: "                        # 兜底日志,限速防刷爆磁盘
$IPT -A INPUT -j REJECT --reject-with icmp-host-prohibited      # 明确拒绝(内网友好)
                                                                # 对公网可改为 -j DROP

# ---------- 10. 保存规则,使其重启后依然生效 ----------
if [ -d /run/systemd/system ]; then
    /sbin/iptables-save > /etc/sysconfig/iptables               # CentOS 7
else
    /sbin/service iptables save                                 # CentOS 6
fi

echo "firewall rules applied at $(date)"

部署与使用:

chmod 700 /usr/local/sbin/firewall.sh
/usr/local/sbin/firewall.sh                # 应用规则
iptables -L -n -v --line-numbers            # 验证

强烈建议:首次在远程主机上执行这类脚本前,先挂 at 保险(见 4.3.6 节),并确认第 6 节的 ADMIN_NET 确实包含你当前的来源 IP。可以用 who am i 或 ss -tnp | grep :22 确认自己的来源地址。


五、NAT 与转发实战

nat 表用于网络地址转换,主要包含四类应用:

  • SNAT(源地址转换):改写数据包的源 IP,多用于内网主机通过网关共享上网。
  • MASQUERADE(地址伪装):SNAT 的特例,自动使用出站接口 IP,适用于外网 IP 动态变化(如拨号)的场景。
  • DNAT(目标地址转换):改写数据包的目标 IP,多用于将内网服务器发布到公网、端口映射。
  • REDIRECT(端口重定向):将数据包重定向到本机另一个端口,多用于透明代理。

5.1 开启 Linux 路由转发

这是所有 NAT / 网关配置的第一步,也是最常被遗忘的一步。 Linux 默认不转发数据包(只当主机,不当路由器),ip_forward 为 0 时,无论 NAT 规则写得多正确,转发流量都不会通。

临时开启(立即生效,重启后失效):

echo 1 > /proc/sys/net/ipv4/ip_forward
# 或
sysctl -w net.ipv4.ip_forward=1

cat /proc/sys/net/ipv4/ip_forward        # 验证,应输出 1
sysctl net.ipv4.ip_forward               # 另一种查看方式

永久开启:编辑 /etc/sysctl.conf,设置:

net.ipv4.ip_forward = 1

执行 sysctl -p 生效:

sysctl -p                                # 重新加载 /etc/sysctl.conf
sysctl -p /etc/sysctl.d/99-router.conf   # 加载指定文件
sysctl -a | grep ip_forward              # 查看当前所有相关参数

CentOS 6 与 CentOS 7 的差异:

项目 CentOS 6 CentOS 7
主配置文件 /etc/sysctl.conf(默认已含 net.ipv4.ip_forward = 0 一行,改为 1 即可) /etc/sysctl.conf(默认为空,需自行添加)
推荐做法 直接编辑 /etc/sysctl.conf 新建 /etc/sysctl.d/99-router.conf 写入配置(不污染主文件)
参数名 net.ipv4.ip_forward net.ipv4.ip_forward(相同)
生效命令 sysctl -p sysctl -p 或 sysctl --system(加载所有 sysctl.d 目录)

CentOS 7 推荐写法:

cat > /etc/sysctl.d/99-router.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
EOF
sysctl --system

说明:做非对称路由(进出走不同接口)时,rp_filter(反向路径过滤)会把包判定为伪造而丢弃,需要关闭。普通对称路由的网关不需要动这个参数。

另外注意:CentOS 7 上如果用 firewalld 的 --add-masquerade,firewalld 会自动帮你把 ip_forward 打开;但直接写 iptables 规则时必须手工开启。

5.2 SNAT 与 MASQUERADE:原理、区别与选用

SNAT 的原理。 内网主机 192.168.224.20 访问公网 1.1.1.1 时,数据包源地址是私有地址,公网设备无法把回包路由回来。网关在数据包离开前的最后一刻(nat 表 POSTROUTING 链)把源地址改写成自己的公网地址,并在连接跟踪表中记录这次映射;回包到达网关时,网关按映射表把目标地址改回 192.168.224.20 再转发进内网。

内网主机                     网关(SNAT)                     公网服务器
192.168.224.20  ──►  改源地址 192.168.224.20 → 1.2.3.4  ──►  1.1.1.1
   ▲                        ▲ 查连接跟踪表反向还原                │
   └────────────────────────┴────────────────────────────────────┘

SNAT 与 MASQUERADE 的区别:

对比项 SNAT MASQUERADE
转换后的源 IP 必须用 --to-source 显式指定 自动取出站接口当前的 IP
适用场景 公网 IP 固定(专线、云主机绑定 EIP) 公网 IP 不固定(PPPoE 拨号、DHCP、4G)
性能 略高(无需每包查接口 IP) 略低(每个包都要查出站接口地址)
接口 down/up 后 规则里的 IP 变成失效的旧地址,需手工改 自动跟随新 IP,无需改规则
连接保持 接口重连后连接跟踪表项仍有效 接口 down 时会清除相关连接跟踪表项
是否支持地址池 支持(--to-source 1.2.3.4-1.2.3.8) 不支持

选用原则:固定公网 IP 用 SNAT --to-source,动态拨号用 MASQUERADE。

SNAT 的完整选项:

# 单个固定公网 IP
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 -j SNAT --to-source 1.2.3.4

# 地址池(多个公网 IP 轮流使用,缓解端口耗尽)
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 -j SNAT --to-source 1.2.3.4-1.2.3.8

# 指定转换后使用的端口范围
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 \
  -j SNAT --to-source 1.2.3.4:20000-40000

# --random 随机分配源端口(增强安全性);--persistent 让同一客户端始终映射到同一公网 IP
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 -j SNAT --to-source 1.2.3.4 --random

MASQUERADE 的完整选项:

iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o ppp0 -j MASQUERADE
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 -j MASQUERADE --to-ports 20000-40000
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE          # 不限来源,所有转发流量都伪装

铁律:SNAT / MASQUERADE 只能写在 nat 表的 POSTROUTING 链上。 因为只有在路由决策完成、确定了出站接口之后,才知道该改成哪个源地址。写在 PREROUTING 上会直接报错。

5.3 共享上网完整实验(SNAT / MASQUERADE)

5.3.1 环境规划

网关服务器双网卡,内网主机通过它访问公网:

                  ┌──────────────── 网关 Gateway ────────────────┐
   公网 Internet ─┤ eth0(外网口)1.2.3.4 / 或 192.168.2.73      │
                  │ eth1(内网口)192.168.224.10                 │
                  └──────────────────┬───────────────────────────┘
                                     │ 192.168.224.0/24
                        ┌────────────┴────────────┐
                   Client1 224.20            Client2 224.21
                   GW: 224.10                GW: 224.10
主机 外网 IP 内网 IP 网关 角色
Gateway 1.2.3.4(eth0) 192.168.224.10(eth1) 上级路由 NAT 网关
Client1 — 192.168.224.20 192.168.224.10 内网主机
Client2 — 192.168.224.21 192.168.224.10 内网主机

5.3.2 第一步:内网主机配置网关与 DNS

内网主机必须把默认网关指向网关服务器的内网口地址,否则数据包根本不会送到网关上。

CentOS 7(/etc/sysconfig/network-scripts/ifcfg-ens33):

TYPE=Ethernet
BOOTPROTO=static
NAME=ens33
DEVICE=ens33
ONBOOT=yes
IPADDR=192.168.224.20
NETMASK=255.255.255.0
GATEWAY=192.168.224.10
DNS1=223.5.5.5
DNS2=8.8.8.8
systemctl restart network            # CentOS 7
service network restart              # CentOS 6

# 验证
ip route show                        # 应看到 default via 192.168.224.10
ip addr show ens33
cat /etc/resolv.conf                 # 应包含 nameserver 223.5.5.5
ping -c2 192.168.224.10              # 先确认能到网关

常见坑:只配了 IP 和网关却没配 DNS,表现为"ping 8.8.8.8 通,但 ping www.baidu.com 报未知主机"。这不是 NAT 的问题,是 DNS 没配。用 ping IP 与 ping 域名 两步就能区分。

5.3.3 第二步:网关开启转发

echo 1 > /proc/sys/net/ipv4/ip_forward                 # 临时
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf     # 永久
sysctl -p

5.3.4 第三步:在 POSTROUTING 链上配置 SNAT

在内网服务器(网关)上执行:

# 开启内核转发(必须)
echo 1 > /proc/sys/net/ipv4/ip_forward

# 在 POSTROUTING 链上做 SNAT,将内网源地址改写为公网 IP
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o 外网网卡名 -j SNAT --to-source 公网IP

本实验环境下的具体命令(固定公网 IP,用 SNAT):

iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o eth0 -j SNAT --to-source 1.2.3.4

外网 IP 不固定时(例如 PPPoE 拨号、DHCP 获取),改用 MASQUERADE,它会自动读取出站接口的 IP 作为转换后的源地址:

echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -s 192.168.224.0/24 -o ppp0 -j MASQUERADE

SNAT 与 MASQUERADE 的区别:SNAT 需指定固定的转换后 IP;MASQUERADE 自动使用出站接口 IP,适合 IP 不固定场景,但开销略大。

5.3.5 第四步:放行 FORWARD 链

如果 FORWARD 链的默认策略是 DROP(生产环境应当如此),必须显式放行转发流量,否则 NAT 规则正确但包依然过不去:

iptables -A FORWARD -i eth1 -o eth0 -s 192.168.224.0/24 -j ACCEPT        # 内网 → 外网
iptables -A FORWARD -i eth0 -o eth1 -m state --state ESTABLISHED,RELATED -j ACCEPT
                                                                          # 外网回程包

5.3.6 第五步:验证与抓包

在内网主机上验证:

ping -c3 223.5.5.5                       # 能通说明转发+SNAT 成功
curl -I http://www.baidu.com             # 能返回 HTTP 头说明 DNS 也正常
traceroute -n 223.5.5.5                  # 第一跳应是 192.168.224.10
curl ifconfig.me                         # 查看出口公网 IP,应显示网关的公网 IP

在网关上验证规则与计数器:

iptables -t nat -L -n -v --line-numbers  # SNAT 规则的 pkts 应随内网访问增长
iptables -L FORWARD -n -v                # FORWARD 放行规则的 pkts 应增长
cat /proc/net/nf_conntrack | grep 192.168.224.20    # 能看到该内网主机的连接跟踪表项
conntrack -L -s 192.168.224.20                       # 需装 conntrack-tools

抓包确认地址已被改写(这是判定 NAT 是否生效的决定性证据):

# 在内网口抓包:应看到源地址是内网私有地址
tcpdump -i eth1 -nn host 223.5.5.5
# 22:01:11.123 IP 192.168.224.20 > 223.5.5.5: ICMP echo request

# 在外网口抓同一个流:源地址应已变成公网 IP
tcpdump -i eth0 -nn host 223.5.5.5
# 22:01:11.124 IP 1.2.3.4 > 223.5.5.5: ICMP echo request

排障对照表:

现象 抓包结果 原因
内网 ping 不通网关 eth1 上无包 内网主机 IP/掩码错,或二层不通
内网口有包,外网口无包 eth0 上无包 ip_forward 没开,或 FORWARD 被 DROP
外网口有包但源地址仍是私网 源为 192.168.x.x SNAT 规则没匹配上(网段写错、-o 接口写错)
外网口源地址正确但无回包 只有出没有回 上游设备没有到该公网 IP 的路由,或云安全组拦截
ping IP 通、ping 域名不通 — DNS 未配置,与 NAT 无关

5.4 DNAT 端口映射完整实验(发布内网服务器)

5.4.1 原理

DNAT 把访问网关某端口的流量,在路由决策之前(nat 表 PREROUTING 链)改写目标地址,转到内网真实服务器上。这就是"端口映射"、"服务发布"。

公网客户端 ──► 网关 1.2.3.4:80 ──DNAT──► 内网 Web 192.168.224.13:80
                (PREROUTING 改目标地址)

铁律:DNAT 必须写在 PREROUTING(或本机流量的 OUTPUT)链上。 因为改写目标地址必须发生在路由决策之前——路由决策要根据目标地址来决定这个包是进 INPUT 还是进 FORWARD。

5.4.2 基本命令

将访问公网 IP 某端口的流量转发到内网某台服务器的指定端口。例如把访问网关 80 端口的请求转发到内网 Web 服务器 192.168.224.13:80:

# 在 PREROUTING 链做 DNAT,改写目标地址
iptables -t nat -A PREROUTING -p tcp -d 公网IP --dport 80 -j DNAT --to-destination 192.168.224.13:80

如果同时需要让内网返回的流量经过网关(配合 SNAT 保证回程包可达),再在 POSTROUTING 上做 SNAT:

iptables -t nat -A POSTROUTING -p tcp -d 192.168.224.13 --dport 80 -j SNAT --to-source 192.168.224.10

--to-destination 的几种写法:

-j DNAT --to-destination 192.168.224.13              # 只改 IP,端口不变
-j DNAT --to-destination 192.168.224.13:8080         # 同时改 IP 和端口(端口映射)
-j DNAT --to-destination 192.168.224.13-192.168.224.15   # 地址池,做简单的负载均衡
-j DNAT --to-destination 192.168.224.13:8080-8090     # 端口范围

5.4.3 端口转发综合实验

环境:

Server:外网 IP 192.168.2.73,内网 IP 192.168.224.10
Client3:IP 192.168.224.13(安装 HTTP 服务)
  1. 在 Client3 上安装并启动 HTTP 服务。

    yum install httpd -y
    echo "This is Client3 Web" > /var/www/html/index.html
    systemctl restart httpd
    systemctl enable httpd
    # Client3 的网关必须指向 192.168.224.10
  2. 在 Server 上设置入站规则:将来访问 80 端口的流量做 DNAT,目标地址改为 192.168.224.13:

    iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.224.13
  3. 在 Server 上设置出站规则:将从 80 端口发出的流量做 SNAT,源地址改为 192.168.224.10:

    iptables -t nat -A POSTROUTING -p tcp --dport 80 -j SNAT --to-source 192.168.224.10

别忘了开启转发(否则包到不了 Client3):

echo 1 > /proc/sys/net/ipv4/ip_forward

同时需要的 FORWARD 放行规则(当 FORWARD 默认策略为 DROP 时必须加,这是 DNAT 实验最常见的失败原因):

iptables -A FORWARD -p tcp -d 192.168.224.13 --dport 80 -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT

验证:

  1. 在物理机访问 http://192.168.2.73 和 http://192.168.224.10,可以显示 Client3 上的 Web 网页。
  2. 在 Server 本机访问 192.168.224.10 以及 192.168.2.73 均无法访问(因为本机产生的包走 OUTPUT 链,不经过 PREROUTING)。

针对第 2 点,如果需要在网关本机也能访问自己发布出去的映射端口,必须额外在 nat 表 OUTPUT 链上再做一次 DNAT:

iptables -t nat -A OUTPUT -p tcp -d 192.168.2.73 --dport 80 -j DNAT --to-destination 192.168.224.13:80

排查命令:

iptables -t nat -L -n -v --line-numbers      # 看 PREROUTING 的 DNAT 规则 pkts 是否增长
tcpdump -i eth1 -nn port 80                  # 在内网口抓包,确认目标地址已被改写
curl -v http://192.168.2.73/                 # 从外部验证

关于 SNAT 那一步的作用:如果内网 Web 服务器的默认网关不是本网关(例如它有自己的出口),回包会直接发给客户端,而客户端并不认识这个源地址(它期待的回包来自 1.2.3.4:80),连接就建立不起来。加上 POSTROUTING 的 SNAT 后,回包被强制绕回网关,问题解决。这种"进出都经过网关"的做法也称为 full NAT / 双向 NAT。

5.5 REDIRECT 本机端口重定向

REDIRECT 是 DNAT 的一种特殊形式,将数据包重定向到本机的另一个端口,常用于透明代理。例如将本机 80 端口的流量重定向到 8080(代理端口):

iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080

若重定向本机自身产生的流量,则在 OUTPUT 链上设置:

iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 8080

其他常见用法:

# 只重定向来自内网的流量
iptables -t nat -A PREROUTING -i eth1 -s 192.168.224.0/24 -p tcp --dport 80 -j REDIRECT --to-ports 3128

# 让非 root 用户能"监听"80 端口:应用实际监听 8080,用 REDIRECT 把 80 映射过去
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080
iptables -t nat -A OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-ports 8080   # 本机访问也生效

# 强制内网 DNS 走本地 DNS 服务器(防止用户自行改 DNS)
iptables -t nat -A PREROUTING -i eth1 -p udp --dport 53 -j REDIRECT --to-ports 53

REDIRECT 与 DNAT 的区别:REDIRECT 的目标地址固定为收包接口的地址,只能改端口不能改 IP,因此不需要写 --to-destination;DNAT 可以改成任意 IP。跨主机转发必须用 DNAT。

5.6 FORWARD 链的过滤策略

网关上的访问控制必须写在 FORWARD 链——转发的数据包既不经过 INPUT 也不经过 OUTPUT,写在那两条链上完全无效。

生产网关的 FORWARD 骨架:

iptables -P FORWARD DROP                                       # 默认拒绝转发

# 1. 回程流量统一放行(必须有,否则所有连接都只出不回)
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT

# 2. 内网 → 外网:按需放行
iptables -A FORWARD -i eth1 -o eth0 -s 192.168.224.0/24 -p tcp -m multiport --dports 80,443 -j ACCEPT
iptables -A FORWARD -i eth1 -o eth0 -s 192.168.224.0/24 -p udp --dport 53 -j ACCEPT
iptables -A FORWARD -i eth1 -o eth0 -s 192.168.224.0/24 -p icmp -j ACCEPT

# 3. 禁止特定内网主机上网(黑名单,要放在放行规则之前)
iptables -I FORWARD -s 192.168.224.66 -j REJECT

# 4. 外网 → 内网:只放行已发布的服务(配合 DNAT)
iptables -A FORWARD -i eth0 -o eth1 -p tcp -d 192.168.224.13 --dport 80 -j ACCEPT

# 5. 禁止内网互访(隔离不同部门网段)
iptables -A FORWARD -s 192.168.10.0/24 -d 192.168.20.0/24 -j DROP

# 6. 工作时间限制(配合 time 模块)
iptables -A FORWARD -s 192.168.224.0/24 -p tcp --dport 443 \
  -m time --timestart 09:00 --timestop 18:00 --weekdays Mon,Tue,Wed,Thu,Fri --kerneltz -j DROP

# 7. 兜底记录
iptables -A FORWARD -m limit --limit 5/min -j LOG --log-prefix "FWD_DROP: "

重要顺序问题:DNAT 发生在 PREROUTING(早于 FORWARD),所以 FORWARD 链里看到的目标地址是 DNAT 之后的内网地址,写规则时要用 -d 192.168.224.13(内网 IP)而不是 -d 1.2.3.4(公网 IP)。这是 DNAT + FORWARD 组合最常见的写错点。

5.7 透明代理场景简述

透明代理指客户端无需任何配置(不用设浏览器代理),流量在网关上被自动重定向到代理服务器(如 squid)处理,从而实现缓存加速、访问审计、内容过滤:

# 内网 HTTP 流量全部劫持到本机 squid 的 3128 端口
iptables -t nat -A PREROUTING -i eth1 -s 192.168.224.0/24 -p tcp --dport 80 -j REDIRECT --to-ports 3128

# 代理在另一台主机上时用 DNAT
iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 80 -j DNAT --to-destination 192.168.224.50:3128
iptables -t nat -A POSTROUTING -d 192.168.224.50 -p tcp --dport 3128 -j SNAT --to-source 192.168.224.10

squid 侧需要配置为支持透明拦截模式(http_port 3128 intercept),否则会因收到的请求缺少绝对 URI 而报错。

局限:HTTPS 无法被简单透明代理(会触发证书告警),需要 SNI 嗅探或部署自签 CA 做 SSL Bump,涉及合规问题,企业环境需谨慎。

详见 09-07-代理服务与缓存.md

5.8 NAT 常见场景速查

场景 关键命令
NAT 共享上网(固定 IP) iptables -t nat -A POSTROUTING -s 内网网段 -o 外网接口 -j SNAT --to-source 公网IP
NAT 共享上网(动态 IP) iptables -t nat -A POSTROUTING -s 内网网段 -o 外网接口 -j MASQUERADE
端口映射/发布内网服务器 iptables -t nat -A PREROUTING -p tcp --dport 端口 -j DNAT --to-destination 内网IP:端口
改端口的映射 iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.224.13:80
透明代理重定向 iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080
本机端口重定向 iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 8080
查看 NAT 规则与命中数 iptables -t nat -L -n -v --line-numbers
查看连接跟踪表 cat /proc/net/nf_conntrack / conntrack -L
开启转发 echo 1 > /proc/sys/net/ipv4/ip_forward + /etc/sysctl.conf

六、firewalld 防火墙

6.1 架构与优势

RHEL 7 和 CentOS 7 引入了新的防火墙软件 firewalld,但系统仍然可以使用 iptables。firewalld 是与 netfilter 子系统交互的前端程序,本质上是一个守护进程(服务)。官网地址:http://www.firewalld.org/

firewall-cmd / firewall-config / firewall-offline-cmd / D-Bus 客户端
      ↓
  firewalld(守护进程,管理规则集与 zone)
      ↓
  iptables / ip6tables / ebtables 命令(RHEL 8 起为 nftables)
      ↓
  netfilter(Linux 内核中真正过滤数据包的框架)

firewalld 的四大优势:

优势 说明
动态生效,不断连接 修改规则只增删对应条目,不重建整个规则集,已建立的连接不会中断。传统 service iptables restart 会重载全部规则,导致连接闪断
区域(zone)概念 按信任级别把网络划分为若干区域,接口/来源地址归属某个区域,规则按区域组织,比扁平的规则链更易管理
服务(service)抽象 用 http、ftp、samba 等服务名代替端口号,避免记错端口;一个 samba 名字覆盖 137/138 UDP 与 139/445 TCP 四个端口
支持 D-Bus 接口 提供 D-Bus API,NetworkManager、libvirt、docker、图形工具都可以通过它动态修改防火墙,无需了解 iptables 语法

此外 firewalld 一套命令即可同时下发 IPv4、IPv6、ebtables 规则,而 iptables 只管 IPv4(IPv6 需另用 ip6tables 写一遍)。

6.2 安装与服务管理

准备干净的虚拟机:

yum install firewalld firewall-config -y
systemctl restart firewalld     # 启动服务
systemctl status firewalld      # 确认状态
systemctl enable firewalld      # 设置开机自启(可选)
systemctl disable firewalld     # 禁止开机自启(可选)

完整的服务与状态管理命令:

sudo systemctl status firewalld          # 查看运行状态
sudo systemctl start firewalld           # 启动
sudo systemctl stop firewalld            # 停止
sudo systemctl restart firewalld         # 重启(会中断连接,慎用)
sudo systemctl enable firewalld          # 开机自启
sudo systemctl disable firewalld         # 取消开机自启
sudo systemctl mask firewalld            # 彻底屏蔽(防止被依赖关系唤醒)

firewall-cmd --state                     # running / not running
firewall-cmd --version
firewall-cmd --help
firewall-cmd --reload                    # 重载 permanent 配置(不中断连接)
firewall-cmd --complete-reload           # 完全重载(会中断连接,仅在异常时用)

--reload 与 --complete-reload 的区别:--reload 保留连接跟踪状态,已建立的连接不受影响,日常一律用它;--complete-reload 会清空连接跟踪表,所有连接(包括你的 SSH)都会断开,只在规则集出现异常状态、--reload 无法恢复时使用。

6.3 区域(zone)详解

Zone 简单来说就是一套防火墙规则集(防火墙方案),可以切换使用哪一个 zone。firewalld 把网络划分为若干信任级别不同的区域,每个网络接口或来源地址归属于某一个 zone,进入的流量按其所属 zone 的规则处理。

查看所有可用 zone:

firewall-cmd --get-zones
work drop internal external trusted home dmz public block

6.3.1 九个预定义区域对照表

zone 默认策略 默认放行的服务 典型用途
drop 丢弃所有入站流量,不回应任何数据包;仅允许出站 无 最严格,对方表现为超时;拉黑 IP 用它
block 拒绝所有外部连接,IPv4 返回 icmp-host-prohibited,IPv6 返回 icmp6-adm-prohibited;允许内部发起的连接 无 严格但明确告知对方被拒
public 适用于公共环境,拒绝所有外部连接,但指定的外部连接可以进入 ssh、dhcpv6-client 默认 zone,公网服务器
external 适用于启用了伪装功能的外部网(路由器),拒绝所有外部连接,只接收经过选择的连接;默认开启 masquerade(NAT 伪装) ssh 做路由/网关的外网口
dmz 用于非军事区(DMZ)内的电脑,可公开访问,可有限进入内部网络,仅接收经过选择的连接 ssh 隔离区服务器,对内网也不完全信任
work 适用于工作网络环境,类似 workgroup,指定的外部连接允许进入 ssh、dhcpv6-client、ipp-client 办公网络,基本信任
internal 用于内部网络,可基本信任网络内的其他计算机,仅接收经过选择的连接 同 work,额外放行 mdns、samba-client 内部网络
home 类似家庭组,用于家庭网络,可基本信任网络内的其他计算机,仅接收经过选择的连接 ssh、mdns、samba-client、dhcpv6-client 家庭网络,较信任
trusted 接受所有网络连接(最不安全) 全部 完全信任,等同于不设防

按信任级别从低到高排序:drop < block < public < external < dmz < work < internal < home < trusted。

查看规则集:

firewall-cmd --list-all                     # 查看当前(默认)zone 的规则集
firewall-cmd --zone=work --list-all         # 查看指定 zone 的规则集
firewall-cmd --list-all-zones               # 查看所有 zone 的配置(输出很长)
firewall-cmd --get-active-zones             # 查看哪些 zone 正在生效及其绑定的接口
# public
#   interfaces: ens33

6.3.2 默认区域的查看与修改

firewall-cmd --get-default-zone             # 查看当前使用的 zone,默认 public
firewall-cmd --set-default-zone=work        # 切换为 work
firewall-cmd --set-default-zone=public      # 切换回 public
firewall-cmd --set-default-zone=dmz

说明:--set-default-zone 直接持久化并立即生效,既不需要加 --permanent,也不需要 --reload。这是 firewalld 命令中的一个特例。

危险操作:--set-default-zone=drop 会立即切断你的 SSH(除非该接口已被显式绑定到别的 zone,或已用 --add-source 把你的地址放进信任 zone)。远程执行前务必先确认。

6.3.3 网卡绑定区域

firewall-cmd --get-zone-of-interface=eth0           # 查看网卡属于哪个 zone
firewall-cmd --get-zone-of-interface=ens33
firewall-cmd --zone=public --add-interface=eth0     # 指定网卡加入某个 zone
sudo firewall-cmd --zone=internal --add-interface=ens37 --permanent
sudo firewall-cmd --zone=internal --change-interface=ens37 --permanent   # 从原 zone 移到新 zone
sudo firewall-cmd --zone=internal --remove-interface=ens37 --permanent
firewall-cmd --zone=internal --query-interface=ens37                     # 返回 yes/no
firewall-cmd --zone=public --list-interfaces                             # 列出该 zone 的接口

--add-interface 与 --change-interface 的区别:

命令 行为
--add-interface 接口尚未属于任何 zone 时才成功;已属于其他 zone 会报 ZONE_CONFLICT
--change-interface 无论接口当前属于哪个 zone,都强制改到新 zone(更常用)

说明:接口与 zone 的绑定关系还可以写在网卡配置文件里,这样重启网络后依然保持:

# /etc/sysconfig/network-scripts/ifcfg-ens37
ZONE=internal

若网卡配置里没写 ZONE=,且未用 --permanent 绑定,则该接口归属默认 zone。

典型的双网卡网关配置:

sudo firewall-cmd --permanent --zone=external --change-interface=eth0   # 外网口:自带 masquerade
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1   # 内网口:较信任
sudo firewall-cmd --reload
firewall-cmd --get-active-zones

6.3.4 按源地址分配区域

除了按接口,还可以按来源地址把流量归入某个 zone。这是"某个网段完全放行/完全拉黑"的标准做法:

firewall-cmd --add-source=source[/mask]                                    # 加入当前 zone
sudo firewall-cmd --zone=trusted --add-source=192.168.1.0/24 --permanent   # 网段完全放行
sudo firewall-cmd --zone=drop --add-source=10.10.10.66/32 --permanent      # 拉黑单个 IP
sudo firewall-cmd --zone=trusted --remove-source=192.168.1.0/24 --permanent
firewall-cmd --zone=trusted --query-source=192.168.1.0/24                  # 返回 yes/no
firewall-cmd --zone=trusted --list-sources                                 # 列出该 zone 的来源
sudo firewall-cmd --zone=drop --add-source=ipset:blacklist --permanent      # 也可绑定 ipset
sudo firewall-cmd --reload

6.3.5 区域的优先级判定顺序

一个数据包到达时,firewalld 按以下顺序决定用哪个 zone 的规则处理它:

① 源地址是否匹配某个 zone 的 source?   ── 是 ──► 用该 zone 的规则(不再看接口)
        │ 否
        ▼
② 入接口是否绑定到某个 zone?           ── 是 ──► 用该 zone 的规则
        │ 否
        ▼
③ 使用默认 zone(--get-default-zone)

关键结论:--add-source 的优先级高于 --add-interface。 当一个数据包的来源地址匹配了某个 zone 的 source,就按该 zone 处理,不再看它从哪个接口进来。这既是"某个网段完全放行、其余走默认策略"的实现方式,也是"规则明明加对了却不生效"的常见原因——你的来源地址可能被某个 zone 的 source 吸走了。

排查方法:

firewall-cmd --get-active-zones                    # 看每个 zone 绑了哪些接口和来源
firewall-cmd --list-all-zones | grep -A3 sources   # 找出所有配了 source 的 zone

6.3.6 zone 配置文件

默认 public zone 的配置文件为 /etc/firewalld/zones/public.xml,可按格式添加服务后重新加载。预置服务定义位于 /usr/lib/firewalld/services/ 目录。

路径 说明
/usr/lib/firewalld/zones/ 系统自带 zone 定义,不要直接修改
/etc/firewalld/zones/ 用户修改后的 zone 定义,优先级更高

说明:--permanent 修改的实际效果就是在 /etc/firewalld/zones/ 下生成或更新对应的 .xml 文件。同名文件存在时,/etc/ 下的完全覆盖 /usr/lib/ 下的。删掉 /etc/firewalld/zones/public.xml 再 --reload,即可把该 zone 恢复为出厂默认。

6.4 运行时(runtime)与永久(permanent)配置

这是 firewalld 最容易出错的地方,也是本章最需要重点强调的一节,必须先理解模型再敲命令。

firewalld 同时维护两套配置:

配置 存放位置 立即生效 重启/reload 后
runtime(运行时) 内存 是 丢失
permanent(永久) /etc/firewalld/ 下的 xml 否 保留
  • 立即生效(运行时规则):不加 --permanent 的命令默认都是立即生效,但无法永久保存。重启防火墙服务、重启机器或执行 --reload 都会导致这些规则丢失。
  • 永久生效:在命令中加入 --permanent 可永久保存,但运行时不会立即生效,需要执行 firewall-cmd --reload 才能生效。

由此产生两种典型错误:

命令写法 结果 症状
--add-port=8080/tcp 只改了 runtime 当下能访问,重启服务器/reload 后又不通了
--add-port=8080/tcp --permanent 只改了 permanent 配置文件里有了,但现在访问不通
--add-port=8080/tcp --permanent + --reload 两套都对 正确

所以标准写法永远是两条命令:

sudo firewall-cmd --add-service=http --permanent   # 写入 permanent
sudo firewall-cmd --reload                         # 使其载入 runtime
firewall-cmd --list-all                            # 验证(默认查 runtime)
firewall-cmd --permanent --add-service=ftp    # 加 --permanent 后不立即生效,需 reload
firewall-cmd --reload

如需分别查看两套配置:

firewall-cmd --list-all                    # 查 runtime(默认)
firewall-cmd --permanent --list-all        # 查 permanent

6.4.1 先测试后固化:--runtime-to-permanent

实际写规则时,建议先不加 --permanent 直接写入并测试,全部验证通过后再一次性固化。这样万一规则写错导致自己被挡在外面,只要重启 firewalld 或执行 --reload 就恢复原状:

# 1. 只改 runtime,立即生效但不落盘
sudo firewall-cmd --add-service=http
sudo firewall-cmd --add-port=8443/tcp
sudo firewall-cmd --add-source=192.168.1.0/24 --zone=internal

# 2. 实际测试服务是否可达
curl -I http://192.168.1.100/

# 3. 确认无误,把当前 runtime 全部固化为 permanent
sudo firewall-cmd --runtime-to-permanent

# 若测试失败或规则写错,直接丢弃 runtime 改动
sudo firewall-cmd --reload      # 用 permanent 覆盖 runtime,回到原状

注意:--runtime-to-permanent 会把当前 runtime 的全部内容固化,包括你临时加的调试规则。执行前先 --list-all 确认一遍。

6.5 放行与拒绝

6.5.1 按服务名放行

firewalld 允许用服务名代替端口号,避免记忆"http 是 80、ftp 是 21、samba 要开哪几个端口"。

firewall-cmd --get-services            # 列出所有可用的服务名(系统支持的全部)
firewall-cmd --list-services           # 只看当前 zone 已放行的服务
cat /usr/lib/firewalld/services/http.xml

http.xml 内容:

<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>WWW (HTTP)</short>
  <description>HTTP is the protocol used to serve Web pages.</description>
  <port protocol="tcp" port="80"/>
</service>
路径 说明
/usr/lib/firewalld/services/*.xml 系统自带服务定义,不要直接修改
/etc/firewalld/services/*.xml 自定义服务定义,优先级更高

放行与移除:

firewall-cmd --add-service=http                            # 允许 http 服务入站(只需记住服务名)
firewall-cmd --remove-service=http
firewall-cmd --add-service=ftp                             # 允许 ftp(主动、被动均可,简化 iptables 写法)
firewall-cmd --remove-service=ftp

sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
sudo firewall-cmd --add-service={ftp,samba} --permanent    # 一次加多个
sudo firewall-cmd --remove-service=http --permanent        # 移除放行
sudo firewall-cmd --zone=dmz --add-service=http --permanent  # 指定 zone(不指定则作用于默认 zone)
sudo firewall-cmd --reload

用服务名的好处:samba 一个名字覆盖 137/138/UDP 与 139/445/TCP,比手写四条 --add-port 更不易出错;ftp 一个名字自动加载 nf_conntrack_ftp 从而同时支持主动与被动模式。

自定义一个服务(以内部 API 的 9090 端口为例):

sudo vi /etc/firewalld/services/myapi.xml
<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>MyAPI</short>
  <description>Internal API service on 9090</description>
  <port protocol="tcp" port="9090"/>
</service>
sudo firewall-cmd --reload                          # 重载后新服务名才可用
sudo firewall-cmd --add-service=myapi --permanent
sudo firewall-cmd --reload

也可以用命令方式创建服务定义(无需手写 XML):

sudo firewall-cmd --permanent --new-service=myapi
sudo firewall-cmd --permanent --service=myapi --set-description="Internal API"
sudo firewall-cmd --permanent --service=myapi --add-port=9090/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --delete-service=myapi      # 删除自定义服务

6.5.2 按端口放行

firewall-cmd --add-port=80/tcp                  # 允许 TCP 80 端口入站(类似 iptables 的 INPUT)
firewall-cmd --remove-port=80/tcp               # 删除上面的规则

sudo firewall-cmd --add-port=8080/tcp --permanent
sudo firewall-cmd --add-port=5000-5010/udp --permanent    # 端口范围(用连字符,不是冒号)
sudo firewall-cmd --add-port=53/udp --permanent
sudo firewall-cmd --remove-port=8080/tcp --permanent
firewall-cmd --list-ports                       # 只看当前 zone 放行的端口
sudo firewall-cmd --reload

注意:端口范围用连字符 5000-5010,而 iptables 用冒号 5000:5010。两者容易混淆。

6.5.3 按协议放行

放行整个协议(不区分端口),常用于 GRE、ESP(IPSec VPN)、IGMP 等:

sudo firewall-cmd --add-protocol=gre --permanent
sudo firewall-cmd --add-protocol=esp --permanent
sudo firewall-cmd --add-protocol=icmp --permanent
sudo firewall-cmd --remove-protocol=gre --permanent
firewall-cmd --list-protocols
sudo firewall-cmd --reload

6.5.4 查询与列出

常用命令汇总:

firewall-cmd --reload                       # 重新载入防火墙策略,未设置永久的规则会丢失
firewall-cmd --permanent                    # 永久策略属性(配合其它命令使用)
firewall-cmd --add-source=source[/mask]     # 添加源地址
firewall-cmd --add-service=http             # 添加服务
firewall-cmd --remove-...                   # 删除对应策略
firewall-cmd --list-all                     # 查询已有防火墙规则

firewall-cmd --get-default-zone             # 查看默认 zone,通常是 public
firewall-cmd --get-active-zones             # 查看哪些 zone 正在生效及其绑定的接口
firewall-cmd --get-zones                    # 列出所有 zone 名称
firewall-cmd --zone=dmz --list-all          # 查看指定 zone 的完整配置
firewall-cmd --list-all-zones               # 查看所有 zone 的配置(输出很长)
firewall-cmd --list-services                # 只看当前 zone 放行的服务
firewall-cmd --list-ports                   # 只看当前 zone 放行的端口
firewall-cmd --get-services                 # 系统支持的全部服务名

查询类命令(返回 yes/no,适合写在脚本里做判断):

firewall-cmd --query-port=8080/tcp                    # 该端口是否已放行
firewall-cmd --query-service=http                     # 该服务是否已放行
firewall-cmd --zone=dmz --query-service=ssh
firewall-cmd --query-masquerade                       # 是否开启伪装
firewall-cmd --query-panic                            # 是否处于 panic 状态
firewall-cmd --zone=trusted --query-source=192.168.1.0/24
firewall-cmd --zone=internal --query-interface=ens37
firewall-cmd --permanent --query-port=8080/tcp         # 查 permanent 配置

--list-all 输出各字段含义:

public (active)
  target: default                # 未匹配任何规则时的默认动作(default/ACCEPT/DROP/%%REJECT%%)
  icmp-block-inversion: no       # ICMP 阻断是否反转(no=只阻断列出的类型)
  interfaces: ens33              # 绑定到本 zone 的网络接口
  sources:                       # 绑定到本 zone 的来源地址(优先级高于 interfaces)
  services: dhcpv6-client ssh    # 放行的服务
  ports: 8080/tcp                # 放行的端口
  protocols:                     # 放行的协议(gre/esp 等)
  masquerade: no                 # 是否开启 NAT 伪装(SNAT)
  forward-ports:                 # 端口转发规则
  source-ports:                  # 按源端口放行的规则
  icmp-blocks:                   # 被阻断的 ICMP 类型
  rich rules:                    # 富规则,见 6.7 节

其中 (active) 标记表示该 zone 当前有接口或来源绑定、正在实际生效;没有该标记的 zone 虽有配置但不作用于任何流量。

ICMP 阻断:

firewall-cmd --get-icmptypes                                  # 列出所有可阻断的 ICMP 类型
sudo firewall-cmd --add-icmp-block=echo-request --permanent    # 禁止别人 ping 本机
sudo firewall-cmd --remove-icmp-block=echo-request --permanent
firewall-cmd --list-icmp-blocks
sudo firewall-cmd --add-icmp-block-inversion --permanent       # 反转:只放行列出的类型,其余全阻断
sudo firewall-cmd --reload

6.5.5 panic 模式与应急阻断

遭受攻击时可进入紧急(panic)模式,阻断所有网络连接以保护服务器:

firewall-cmd --panic-on      # 开启紧急模式,丢弃所有入站和出站包,网络完全断开
firewall-cmd --panic-off     # 关闭紧急模式,恢复正常
firewall-cmd --query-panic   # 查询当前是否处于 panic 状态

用于确认主机正在被攻击、需要立刻切断一切网络连接的场景。

注意:--panic-on 会立即断开你自己的 SSH 会话,且之后无法再通过网络连回来。除非你有物理控制台或带外管理(IPMI/iDRAC/云控制台 VNC),否则不要在远程执行这条命令。

6.6 端口转发与地址伪装

6.6.1 端口转发(forward-port)

把访问本机某端口的流量转到另一个端口或另一台主机。典型场景:Tomcat 监听 8080,希望用户访问 80 端口。

完整语法:

--add-forward-port=port=<源端口>:proto=<tcp|udp>:toport=<目标端口>[:toaddr=<目标IP>]
# 本机 80 → 本机 8080
sudo firewall-cmd --permanent \
  --add-forward-port=port=80:proto=tcp:toport=8080

# 本机 80 → 后端主机 192.168.1.20 的 8080(需同时开 masquerade)
sudo firewall-cmd --permanent \
  --add-forward-port=port=80:proto=tcp:toport=8080:toaddr=192.168.1.20
sudo firewall-cmd --permanent --add-masquerade

# 端口范围转发
sudo firewall-cmd --permanent \
  --add-forward-port=port=5000-5010:proto=tcp:toport=6000-6010

# 只改地址不改端口
sudo firewall-cmd --permanent \
  --add-forward-port=port=3306:proto=tcp:toport=3306:toaddr=192.168.1.30

sudo firewall-cmd --reload
firewall-cmd --list-forward-ports          # 验证

# 删除
sudo firewall-cmd --permanent \
  --remove-forward-port=port=80:proto=tcp:toport=8080
sudo firewall-cmd --reload

# 查询
firewall-cmd --query-forward-port=port=80:proto=tcp:toport=8080
参数 含义
port= 本机被访问的端口
proto= 协议,tcp 或 udp
toport= 转发到的目标端口
toaddr= 转发到的目标 IP(省略则为本机)

注意:转发到其他主机时必须开启 masquerade,否则返回包不会经过本机,连接建立不起来。另外,toport 对应的端口本身不需要在防火墙里放行,因为流量是被转发过去的,不走 INPUT 链。

6.6.2 地址伪装(masquerade / SNAT)

让内网主机通过本机访问外网,即最简单的 NAT 网关:

sudo firewall-cmd --permanent --zone=external --add-masquerade
sudo firewall-cmd --reload
firewall-cmd --zone=external --query-masquerade      # 返回 yes / no
firewall-cmd --list-all | grep masquerade

sudo firewall-cmd --permanent --zone=external --remove-masquerade   # 关闭伪装

# 别忘了内核转发(否则包不会跨接口转发)
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

说明:external zone 默认已开启 masquerade,把外网接口设为 external 是配置 NAT 网关的最省事做法。firewalld 在开启 masquerade 时通常也会自动设置 ip_forward,但显式配置 /etc/sysctl.conf 更可靠。

用 firewalld 搭建 NAT 网关的完整步骤:

sudo firewall-cmd --permanent --zone=external --change-interface=eth0   # 外网口
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1   # 内网口
sudo firewall-cmd --permanent --zone=external --add-masquerade
sudo firewall-cmd --reload
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

# 验证
firewall-cmd --get-active-zones
firewall-cmd --zone=external --query-masquerade
iptables -t nat -L POSTROUTING -n -v                # 能看到 firewalld 下发的 MASQUERADE 规则

6.7 富规则(rich rules)

富规则(复杂规则)可包含 IP 地址、端口、处理动作、日志等多种网络元素,用于精确控制访问流量。当需要表达"仅允许某网段访问某端口"、"限速"、"记录日志再拒绝"这类带条件的规则时,--add-service / --add-port 不够用,就要用富规则。

6.7.1 语法结构

rule [family="ipv4|ipv6"]
     [source address="..." | NOT source ...]
     [destination address="..."]
     [service name="..." | port port="..." protocol="..." | protocol value="..."
      | icmp-block name="..." | masquerade | forward-port ...]
     [log [prefix="..."] [level="..."] [limit value="n/unit"]]
     [audit]
     accept | reject [type="..."] | drop | mark set="..."

各部分说明:

部分 说明
family ipv4 或 ipv6;写了 source address 时建议明确指定
source / destination 来源 / 目标地址,可加 NOT 取反,可用 ipset:名称
元素部分 service、port、protocol、masquerade、forward-port、icmp-block 之一
log 记录日志,可带 prefix、level、limit
audit 写入审计日志(/var/log/audit/audit.log),比 log 更规范
动作 accept(放行)、reject(拒绝并回应)、drop(静默丢弃)、mark(打标记)

6.7.2 示例一:按源地址放行指定服务

只允许 192.168.224.11 访问 SSH:

firewall-cmd --add-rich-rule="rule family='ipv4' source address=192.168.224.11 service name='ssh' accept"

6.7.3 示例二:按源地址拒绝

允许所有人访问 HTTP,但丢弃来自 192.168.224.11 的 HTTP 包:

firewall-cmd --add-service=http
firewall-cmd --add-rich-rule="rule family='ipv4' source address=192.168.224.11 service name='http' drop"
firewall-cmd --list-rich-rules        # 查询现有富规则

6.7.4 示例三:只允许指定网段访问 MySQL 3306

最常用的一条。数据库端口绝不应对公网开放:

sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4"
  source address="192.168.1.0/24"
  port port="3306" protocol="tcp"
  accept'
sudo firewall-cmd --reload

等价于:仅 192.168.1.0/24 的主机能连 3306,其他来源因未匹配任何放行规则而被默认策略拒绝。

6.7.5 示例四:限速,防止 SSH 暴力破解

sudo firewall-cmd --permanent --add-rich-rule='
  rule service name="ssh"
  log prefix="SSH_ACCESS " level="info" limit value="3/m"
  accept'
sudo firewall-cmd --reload

limit value="3/m" 表示日志每分钟最多记 3 条,避免日志被刷爆。若要限制连接频率:

# 每分钟最多 5 个新的 SSH 连接,超出的直接丢弃
sudo firewall-cmd --permanent --add-rich-rule='
  rule service name="ssh" limit value="5/m" accept'
sudo firewall-cmd --reload
时间单位 含义
s 每秒
m 每分钟
h 每小时
d 每天

6.7.6 示例五:记录日志后丢弃(用于观察攻击来源)

sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4"
  source address="10.10.10.0/24"
  log prefix="DROPPED_SUBNET " level="warning" limit value="10/m"
  drop'
sudo firewall-cmd --reload

# 日志落到 /var/log/messages,用 prefix 过滤
sudo grep DROPPED_SUBNET /var/log/messages
sudo journalctl -k | grep DROPPED_SUBNET

6.7.7 示例六:其他实用富规则

# 1. 取反:除某网段外全部拒绝访问 8080
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" source NOT address="192.168.1.0/24"
  port port="8080" protocol="tcp" reject'

# 2. 用 reject 并指定回应类型
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" source address="10.0.0.0/8"
  service name="http" reject type="icmp-host-prohibited"'

# 3. audit 审计(记录到 audit.log,便于合规审计)
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" source address="192.168.1.50" service name="ssh" audit accept'

# 4. 富规则中做端口转发(仅对指定来源生效)
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" source address="192.168.1.0/24"
  forward-port port="80" protocol="tcp" to-port="8080"'

# 5. 富规则中做地址伪装(仅对指定网段生效)
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" source address="192.168.224.0/24" masquerade'

# 6. 按目标地址放行(多 IP 主机上只对某个 IP 开放服务)
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" destination address="192.168.1.100"
  port port="443" protocol="tcp" accept'

# 7. 阻断 ping
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv4" icmp-block name="echo-request"'

# 8. IPv6 规则
sudo firewall-cmd --permanent --add-rich-rule='
  rule family="ipv6" source address="2001:db8::/64" service name="http" accept'

sudo firewall-cmd --reload

6.7.8 富规则的查询与删除

firewall-cmd --list-rich-rules                  # 列出当前 zone 的富规则
firewall-cmd --permanent --list-rich-rules
firewall-cmd --zone=dmz --list-rich-rules

# 删除时规则字符串必须与添加时完全一致(含空格),建议先 list 出来再复制
sudo firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="10.10.10.0/24" drop'
sudo firewall-cmd --reload

# 查询某条富规则是否存在
firewall-cmd --query-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="3306" protocol="tcp" accept'
reject 与 drop 的区别 行为
reject 回应拒绝报文,对方立即知道被拒绝
drop 静默丢弃,对方一直等到超时(更能拖慢扫描器)

排障提示:富规则的优先级高于 --add-service / --add-port(在 firewalld 内部,富规则被下发到 zone 链的更前面)。但多条富规则之间,drop/reject 与 accept 的相对顺序由 firewalld 内部规则决定(拒绝类规则优先),不由添加顺序决定——因此不要指望像 iptables 那样通过调整添加顺序来控制优先级。规则冲突时用 iptables -S | grep -i rich 或 iptables -L -n -v 看最终下发结果。

6.8 直接规则(--direct)

当 firewalld 的抽象层表达不了需求(例如 mangle 表打标记、复杂的 nat 规则、需要精确控制规则顺序)时,可以用 --direct 直接注入原生 iptables 规则,且这些规则由 firewalld 统一管理与持久化,不会被 --reload 冲掉。

# 语法:--direct --add-rule {ipv4|ipv6|eb} <表> <链> <优先级> <iptables 参数...>
# 优先级数字越小越靠前

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
  -p tcp --dport 9000 -s 192.168.1.0/24 -j ACCEPT

sudo firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 0 \
  -s 192.168.224.0/24 -o eth0 -j SNAT --to-source 1.2.3.4

sudo firewall-cmd --permanent --direct --add-rule ipv4 mangle PREROUTING 0 \
  -p tcp --dport 80 -j MARK --set-mark 10

# 查询与删除
firewall-cmd --direct --get-all-rules                     # 列出所有直接规则
firewall-cmd --permanent --direct --get-all-rules
sudo firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \
  -p tcp --dport 9000 -s 192.168.1.0/24 -j ACCEPT
sudo firewall-cmd --permanent --direct --remove-rules ipv4 filter INPUT   # 删除该链上全部直接规则

# 自定义链与"透传"执行
sudo firewall-cmd --permanent --direct --add-chain ipv4 filter MYCHAIN
firewall-cmd --direct --get-all-chains
firewall-cmd --direct --passthrough ipv4 -L -n -v          # 直接透传给 iptables 执行(不被管理)

sudo firewall-cmd --reload

使用原则:--direct 是最后手段。 它绕过了 zone 模型,规则不会出现在 --list-all 输出中,排障时极易被忽略("规则明明在但 list-all 里看不到")。能用 zone + 富规则表达的需求,就不要用 direct。

6.9 图形工具与离线配置

firewall-config(图形化配置工具):

yum install firewall-config -y
firewall-config

界面要点:顶部的 Configuration 下拉框必须注意——选 Runtime 时修改立即生效但不持久,选 Permanent 时需要通过菜单 Options → Reload Firewalld 才生效。这与命令行的 runtime/permanent 模型完全一致。

firewall-offline-cmd(离线配置):

在 firewalld 未运行时也能修改永久配置,常用于系统安装脚本、镜像制作、firewalld 起不来时的抢救:

firewall-offline-cmd --state                                # 输出 not running
firewall-offline-cmd --zone=public --add-port=8080/tcp
firewall-offline-cmd --set-default-zone=dmz
firewall-offline-cmd --zone=public --add-service=http
firewall-offline-cmd --zone=public --list-all

它直接读写 /etc/firewalld/ 下的 XML 文件,所有修改都是永久性的(不需要 --permanent),下次 firewalld 启动时生效。

典型用途:某次误操作把默认 zone 设为 drop 导致远程连不上,通过控制台登录后 systemctl stop firewalld + firewall-offline-cmd --set-default-zone=public + systemctl start firewalld 即可恢复。

6.10 配置文件目录的层级关系

路径 说明
/usr/lib/firewalld/ 系统自带的默认配置(随软件包安装/升级),不要直接修改
/usr/lib/firewalld/zones/*.xml 九个预定义 zone 的出厂定义
/usr/lib/firewalld/services/*.xml 数百个预定义服务定义
/usr/lib/firewalld/icmptypes/*.xml ICMP 类型定义
/usr/lib/firewalld/helpers/*.xml 连接跟踪辅助模块定义(如 ftp、tftp)
/etc/firewalld/ 用户配置,优先级更高,--permanent 写入的就是这里
/etc/firewalld/firewalld.conf 主配置文件(默认 zone、日志、后端等)
/etc/firewalld/zones/*.xml 用户修改后的 zone 定义
/etc/firewalld/services/*.xml 自定义服务定义
/etc/firewalld/direct.xml 直接规则(--direct --permanent)

层级规则:同名文件时,/etc/firewalld/ 下的完全覆盖 /usr/lib/firewalld/ 下的(不是合并,是整体替换)。因此:

  • 想恢复某个 zone 的出厂设置:删掉 /etc/firewalld/zones/该zone.xml 后 firewall-cmd --reload。
  • 想在预定义服务基础上加端口:把 /usr/lib/firewalld/services/http.xml 复制到 /etc/firewalld/services/ 再修改。

firewalld.conf 的关键项:

DefaultZone=public              # 默认 zone
MinimalMark=100
CleanupOnExit=yes               # 停止服务时是否清除规则
Lockdown=no                     # 锁定模式:只允许白名单程序通过 D-Bus 修改防火墙
IPv6_rpfilter=yes               # IPv6 反向路径过滤
IndividualCalls=no
LogDenied=off                   # 可设为 all/unicast/broadcast/multicast,自动记录被拒流量
FirewallBackend=nftables        # RHEL 8 起可选 nftables 或 iptables

实用技巧:把 LogDenied=off 改为 LogDenied=all 后重启 firewalld,所有被拒绝的包都会打日志到 /var/log/messages(带 _DROP / _REJECT 前缀),排查"到底是哪条规则挡了我"非常有效。排障完记得改回 off,否则日志量很大。

6.11 firewalld 与 iptables 的共存问题与切换

绝对不能同时启用 firewalld 与 iptables-services。 两者都会向 netfilter 写入规则且互不知晓,结果是规则互相覆盖、行为不可预测、排障时无法判断当前生效的是谁的规则。二者只能择一。

具体表现:firewalld 启动时会先清空 netfilter 规则再下发自己的规则集,所以你用 iptables -I 加的规则会在下一次 firewall-cmd --reload、网络接口变动、甚至 NetworkManager 事件时被静默清除。反之,systemctl start iptables 也会覆盖 firewalld 的规则。

判断当前生效的是谁:

systemctl is-active firewalld              # active 则是 firewalld 在管
systemctl is-active iptables
iptables -S | head -30                     # 出现 IN_public_allow / FWDI_public 等链名 → firewalld
                                           # 只有扁平的 INPUT/FORWARD/OUTPUT 规则 → 传统 iptables
firewall-cmd --state

从 firewalld 切到 iptables(CentOS 7):

sudo systemctl stop firewalld
sudo systemctl disable firewalld
sudo systemctl mask firewalld          # 彻底屏蔽,防止被依赖关系唤醒

sudo yum install iptables-services -y
sudo systemctl enable --now iptables

sudo iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
sudo service iptables save             # 保存到 /etc/sysconfig/iptables

从 iptables 切回 firewalld:

sudo systemctl stop iptables ip6tables
sudo systemctl disable iptables ip6tables
sudo systemctl unmask firewalld
sudo systemctl enable --now firewalld
firewall-cmd --state
firewall-cmd --list-all

已有 iptables 规则如何迁移到 firewalld:

  1. iptables-save > /root/old.rules 备份。
  2. 逐条翻译:简单端口放行 → --add-port;服务放行 → --add-service;带源地址条件的 → 富规则;NAT → --add-masquerade / --add-forward-port;实在翻译不了的 → --direct --add-rule。
  3. 用下表对照。
目的 iptables 写法 firewalld 写法
查看规则 iptables -L -n -v firewall-cmd --list-all
查看底层实际规则 iptables -S iptables -S(firewalld 下仍可用于查看)
放行 80 端口 iptables -I INPUT -p tcp --dport 80 -j ACCEPT firewall-cmd --add-service=http --permanent
放行 8080 端口 iptables -I INPUT -p tcp --dport 8080 -j ACCEPT firewall-cmd --add-port=8080/tcp --permanent
放行某网段 iptables -I INPUT -s 192.168.1.0/24 -j ACCEPT firewall-cmd --zone=trusted --add-source=192.168.1.0/24 --permanent
拉黑某 IP iptables -I INPUT -s 10.0.0.5 -j DROP firewall-cmd --zone=drop --add-source=10.0.0.5/32 --permanent
网段+端口条件放行 iptables -I INPUT -s 192.168.1.0/24 -p tcp --dport 3306 -j ACCEPT 富规则(见 6.7.4 节)
端口转发 iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 firewall-cmd --add-forward-port=port=80:proto=tcp:toport=8080 --permanent
地址伪装 iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE firewall-cmd --zone=external --add-masquerade --permanent
保存配置 service iptables save firewall-cmd --runtime-to-permanent
生效配置 service iptables restart(会断连接) firewall-cmd --reload(不断连接)
清空规则 iptables -F(危险) firewall-cmd --panic-on 或 systemctl stop firewalld

说明:即使在 firewalld 环境下,iptables -S、iptables -L -n 仍然是有价值的只读排查工具——它能看到 firewalld 最终下发到 netfilter 的真实规则(会看到大量 IN_public_allow 之类的自定义链)。只是不要用 iptables 去修改规则。

6.12 firewalld 排障:服务访问不通的检查清单

按顺序逐项排除,绝大多数问题在前四步内定位:

# 第 0 步:确认服务自身在监听(防火墙背锅最多的一步)
sudo ss -tlnp | grep 8080
# 若监听在 127.0.0.1:8080 而非 0.0.0.0:8080,是服务配置问题,不是防火墙问题

# 第 1 步:确认 firewalld 在运行
firewall-cmd --state

# 第 2 步:确认规则在 runtime 中生效(是否忘了 --reload)
firewall-cmd --list-all | grep -E 'ports|services'

# 第 3 步:确认规则在 permanent 中存在(是否忘了 --permanent)
firewall-cmd --permanent --list-all | grep -E 'ports|services'

# 第 4 步:确认规则加在了正确的 zone,且接口绑定的就是这个 zone
firewall-cmd --get-active-zones
firewall-cmd --get-zone-of-interface=ens33
firewall-cmd --get-default-zone

# 第 5 步:临时关闭防火墙判定责任方
sudo systemctl stop firewalld
# 若此时能访问 → 确实是防火墙规则问题,回到第 2~4 步
# 若仍不能访问 → 不是防火墙,继续第 6 步
sudo systemctl start firewalld

# 第 6 步:检查 SELinux 是否在拦(详见第七章)
getenforce
sudo ausearch -m avc -ts recent

常见误区归纳:

症状 真实原因
加了规则当时能通,重启后不通 忘了 --permanent
加了 --permanent 但当时不通 忘了 --reload
规则明明在,就是不生效 规则加在了 A zone,接口却绑在 B zone
只有部分客户端能访问 某网段被 --add-source 归入了 drop/block zone
端口通了但页面 403/无法读文件 不是防火墙,是 SELinux 上下文(见 7.5 节)
关掉防火墙也不通 服务没起、监听在 127.0.0.1、或上游还有云安全组/硬件防火墙
--list-all 里看不到规则但确实生效 用了 --direct 注入的规则(见 6.8 节)

注意:云服务器(阿里云/腾讯云/AWS)在主机防火墙之外还有一层安全组。主机内 firewall-cmd 全部放行后仍不通,绝大多数是安全组没开对应端口。排查时两层都要看。


七、SELinux

7.1 SELinux 是什么:DAC 与 MAC

SELinux 不是防火墙。 它不看端口、不看 IP、不管数据包,它管的是"某个进程能不能访问某个文件/端口/资源"。SELinux(Security-Enhanced Linux)由美国国家安全局(NSA)主导开发,自 Linux 2.6 内核起集成到主线。

Linux 上并存两套访问控制机制:

机制 全称 判断依据 表现形式
DAC 自主访问控制(Discretionary Access Control) 文件的属主、属组、权限位 rwxr-xr-x、chmod、chown
MAC 强制访问控制(Mandatory Access Control) 系统预定义的策略,与文件属主无关 SELinux 上下文、布尔值、策略模块

两者的检查顺序是串联的:

进程发起访问(如 httpd 读 /web/index.html)
      ↓
【第一关】DAC 检查:属主/属组/权限位是否允许?
      ↓ 通过(不通过则 Permission denied,与 SELinux 无关)
【第二关】SELinux MAC 检查:策略是否允许 httpd_t 读该文件的类型?
      ↓ 通过(不通过则记录 AVC denied 并拒绝)
访问成功

这个顺序解释了最令人困惑的现象:chmod 777 之后依然 Permission denied。 因为 777 只解决了第一关,第二关的策略仍在拒绝。

为什么 root 也会被限制

DAC 的根本局限,也正是 SELinux 存在的理由:DAC 下"权限属于用户"。以 root 身份运行的 httpd 在 DAC 眼里可以读写整个文件系统,一旦 Web 应用被攻破,攻击者就能通过它读 /etc/shadow。MAC 下"权限属于进程类型"——策略规定 httpd_t 类型的进程只能访问 httpd_sys_content_t 等少数几类文件,即使它是 root 运行的,也读不了 shadow_t 的 /etc/shadow。

SELinux 的价值是限制被攻破之后的损害范围。 它假定"程序总有漏洞、总会被攻破",因此预先给每个进程画好活动范围,把一次 Web 漏洞的后果从"整台服务器失守"压缩为"只能动网站目录"。这也解释了为什么"我这台机器不重要,关掉 SELinux 没关系"是错误的思路——正是不受重视的机器最容易被当作跳板。

7.2 三种模式:enforcing / permissive / disabled

模式 拦截违规 记录日志 用途
enforcing 是 是 强制模式,生产环境的正确状态(CentOS 7 默认)
permissive 否 是 宽容模式:放行但记录,调试的正确模式
disabled 否 否 完全关闭,策略不加载

permissive 与 disabled 的区别是本节最关键的一点。

  • permissive:策略仍在加载和评估,所有本应被拒绝的操作照常放行,但全部写入 /var/log/audit/audit.log。所以你可以在 permissive 下把业务完整跑一遍,然后用 ausearch 一次性收集出所有需要放行的项,再切回 enforcing。这是上线新服务前的标准做法。
  • disabled:策略根本不加载,没有任何日志。你完全不知道"如果开着 SELinux 会挡住什么",未来想启用时只能从零试错。

disabled 的代价:切回来需要全盘重打标签

这是 SELINUX=disabled 最被低估的成本。SELinux 关闭期间,新建和修改的文件不会被打上安全上下文标签(或标签失效)。等你想重新启用时,文件系统上遍布无标签/错标签的文件,服务会大面积异常。

因此从 disabled 切回 enforcing 的正确流程是:

# 1. 先改成 permissive(不要直接跳到 enforcing)
sudo vi /etc/selinux/config
#   SELINUX=permissive

# 2. 请求下次开机时对整个文件系统重新打标签
sudo touch /.autorelabel

# 3. 重启。开机过程会执行全盘 relabel,
#    磁盘大时可能耗时数十分钟甚至更久,且期间不可中断
sudo reboot

# 4. 开机后收集 permissive 下产生的拒绝记录,逐项修复(见 7.8 节)
sudo ausearch -m avc -ts boot

# 5. 确认干净后再改 SELINUX=enforcing 并重启

注意:正因为 disabled 的回退成本如此之高(一次强制重启 + 一次不可控时长的全盘 relabel),需要临时排除 SELinux 干扰时请用 permissive,不要用 disabled。 permissive 与 enforcing 之间可以用 setenforce 随时切换,无需重启、无需 relabel。

7.3 查看与切换模式、策略类型

7.3.1 查看当前状态

getenforce                    # 输出 Enforcing / Permissive / Disabled

sestatus                      # 查看完整状态
# SELinux status:                 enabled
# SELinuxfs mount:                /sys/fs/selinux
# SELinux root directory:         /etc/selinux
# Loaded policy name:             targeted
# Current mode:                   enforcing        ← 当前生效模式
# Mode from config file:          enforcing        ← 配置文件中的模式
# Policy MLS status:              enabled
# Policy deny_unknown status:     allowed
# Max kernel policy version:      31

sestatus -v                   # 额外显示进程与文件的上下文

sestatus 输出各字段含义:

字段 含义
SELinux status enabled / disabled,策略是否加载
SELinuxfs mount SELinux 虚拟文件系统挂载点,固定为 /sys/fs/selinux
SELinux root directory 策略文件根目录,/etc/selinux
Loaded policy name 当前加载的策略类型,通常是 targeted
Current mode 当前实际生效的模式
Mode from config file 配置文件里写的模式(重启后会用这个)
Policy MLS status 策略是否支持多级安全
Policy deny_unknown status 遇到策略中未定义的类别时是拒绝还是允许
Max kernel policy version 内核支持的最高策略版本号

说明:Current mode 与 Mode from config file 不一致,说明有人用 setenforce 临时改过。重启后会回到配置文件里的值。排障时先看这两行是否一致。

7.3.2 临时切换(重启后失效)

sudo setenforce 0             # enforcing → permissive
sudo setenforce 1             # permissive → enforcing
sudo setenforce Permissive    # 也可以用名字
sudo setenforce Enforcing

注意:setenforce 只能在 enforcing 与 permissive 之间切换。当前是 disabled 时执行 setenforce 1 会报 setenforce: SELinux is disabled,因为策略根本没加载。切到 disabled 或从 disabled 切出都必须改配置文件 + 重启。

7.3.3 永久修改(必须重启)

sudo vi /etc/selinux/config
# SELINUX 可选值:
#   enforcing  - 强制执行策略(生产环境推荐)
#   permissive - 只告警不拦截(调试用)
#   disabled   - 完全不加载策略(不推荐,回退成本高)
SELINUX=enforcing

# SELINUXTYPE 可选值:
#   targeted - 只保护主要的网络服务(默认,绝大多数场景用这个)
#   minimum  - 只保护极少数选定进程
#   mls      - 多级安全,军政级别,配置极复杂
SELINUXTYPE=targeted
sudo reboot                   # 改配置文件后必须重启才生效

说明:/etc/sysconfig/selinux 是指向 /etc/selinux/config 的符号链接,改哪个都一样。

一行命令修改(适合脚本,但仍需重启):

sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
grep -v '^#' /etc/selinux/config | grep -v '^$'      # 确认修改结果

也可以通过内核启动参数控制(用于紧急抢救——SELinux 配错导致系统起不来时,在 GRUB 编辑界面的 linux16 行末追加):

selinux=0            # 本次启动完全禁用 SELinux
enforcing=0          # 本次启动以 permissive 运行(推荐,仍保留日志)
autorelabel=1        # 本次启动执行全盘重打标签

7.3.4 策略类型:targeted 与 mls

策略类型 保护范围 说明
targeted 只保护主要的网络服务进程(httpd、sshd、named、vsftpd、smbd、mysqld 等约百余个),其余进程运行在不受限的 unconfined_t 域中 默认策略,兼顾安全与可用性,99% 的场景用它
minimum 只保护极少数选定的进程,其余全部 unconfined 用于对兼容性极度敏感、只想保护个别服务的场景
mls Multi-Level Security,多级安全。基于 BLP 模型实现严格的信息流控制("不上读、不下写"),所有主体客体都带安全级别 军政、涉密环境,配置极复杂,普通服务器不用
sestatus | grep "Loaded policy"           # 查看当前策略类型
ls /etc/selinux/                          # 已安装的策略目录
# config  final  targeted  tmp
yum install selinux-policy-mls -y         # 安装 mls 策略(切换前需全盘 relabel)

说明:切换 SELINUXTYPE 需要重启并全盘 relabel(touch /.autorelabel),因为不同策略的类型定义不同。没有明确的合规要求时不要碰 mls——它会让绝大多数常规操作都被拒绝。

在 targeted 策略下,"不受限进程"(unconfined_t)几乎不受 SELinux 约束,这解释了为什么自己写的脚本、手工编译的程序通常不会遇到 SELinux 问题——而一旦用系统的 httpd、vsftpd、smbd 服务,就必须遵守策略。

7.4 安全上下文(Security Context)

SELinux 给系统中每一个进程、文件、目录、端口、套接字都贴一个标签,称为安全上下文,格式为:

user:role:type:level
system_u:object_r:httpd_sys_content_t:s0
   ↓        ↓             ↓            ↓
SELinux用户 角色         类型        安全级别
字段 说明 实践中的重要性
user SELinux 用户(system_u、unconfined_u、user_u),不是 Linux 用户 低
role 角色(object_r 用于文件,system_r 用于进程) 低
type 类型,文件上叫 type,进程上叫 domain 决定一切
level MLS 安全级别(s0),targeted 策略下基本不用 低

在 targeted 策略下,日常运维 99% 的问题都只与 type 有关。 看到上下文时,直接看第三段。

7.4.1 查看上下文

ls -Z /var/www/html/index.html
# -rw-r--r--. root root system_u:object_r:httpd_sys_content_t:s0 index.html

ls -Zd /var/www/html                 # 查看目录本身
ls -lZ /var/www/html/                # 结合详细列表

ps -eZ | grep httpd                  # 查看进程的 domain
# system_u:system_r:httpd_t:s0    1523 ? 00:00:00 httpd

ps -Z -p 1523                        # 查看指定 PID
ps auxZ | grep nginx                 # 结合完整进程信息
id -Z                                # 查看当前登录用户的上下文
# unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

netstat -Z -tlnp                     # 查看套接字上下文
ss -Z -tlnp                          # CentOS 7 推荐用 ss

记忆要点:几乎所有查看类命令加 -Z 就能显示 SELinux 上下文——ls -Z、ps -Z、id -Z、ss -Z、netstat -Z、cp -Z、mkdir -Z。这是一条非常好用的通用规律。

7.4.2 策略规则的本质

SELinux 规则的本质就是一条条"哪个 domain 允许对哪个 type 做什么操作":

allow httpd_t httpd_sys_content_t : file { read getattr open };
      ↑             ↑                          ↑
   进程domain     文件type                 允许的操作

所以当 httpd(httpd_t)去读一个类型是 user_home_t 或 default_t 的文件时,没有对应的 allow 规则,访问就被拒绝——这正是"自定义目录建站失败"的根本原因。

查询策略规则(需安装 setools-console):

sesearch -A -s httpd_t -t httpd_sys_content_t -c file       # 查 httpd_t 对该类型允许的操作
sesearch -A -s httpd_t | grep default_t                      # 确认是否允许访问 default_t
seinfo -t | grep httpd                                       # 列出所有含 httpd 的类型
seinfo -u                                                    # 列出所有 SELinux 用户
seinfo -r                                                    # 列出所有角色
seinfo --portcon=8080                                        # 查 8080 端口的上下文

7.4.3 常见类型举例

类型 用途
httpd_sys_content_t Apache 可读的静态内容
httpd_sys_rw_content_t Apache 可读写的目录(上传目录、缓存)
httpd_sys_script_exec_t Apache 可执行的 CGI 脚本
httpd_log_t Apache 日志目录
httpd_config_t Apache 配置文件
public_content_t 多服务共享的只读内容(FTP/HTTP/Samba 都可读)
public_content_rw_t 多服务共享的可读写内容
samba_share_t Samba 共享目录
nfs_t NFS 导出目录
ftpd_anon_t FTP 匿名访问目录
mysqld_db_t MySQL 数据目录
default_t 无明确策略的默认类型(自建目录常见,通常就是问题所在)
user_home_t 用户家目录下的文件
user_home_dir_t 用户家目录本身
ssh_home_t ~/.ssh 下的文件
tmp_t /tmp 下的文件
etc_t /etc 下的配置文件
bin_t 可执行程序
shadow_t /etc/shadow(任何服务域都读不到)

7.5 修改文件上下文:chcon、restorecon、semanage fcontext

这三个命令的区别是 SELinux 头号误解点,必须分清。

命令 作用 是否持久
chcon 直接改文件当前的上下文标签 不持久,任何一次 relabel 都会被覆盖
restorecon 按策略数据库中记录的规则,把文件恢复成"应有的"上下文 —
semanage fcontext 修改策略数据库本身,即修改"应有的上下文"这条规则 持久

理解关键:系统里有一份"路径 → 应有上下文"的映射数据库。chcon 改的是文件上的标签,没碰数据库;restorecon 是拿数据库去覆盖文件标签。所以 chcon 完之后一旦有人跑了 restorecon(或触发了 relabel、或执行了 yum update 中某些操作),你的修改就被数据库的旧值冲掉了。

这就是"为什么必须用 semanage 而不能只用 chcon"的完整答案。

7.5.1 chcon:临时修改

sudo chcon -t httpd_sys_content_t /web/index.html
sudo chcon -R -t httpd_sys_content_t /web/            # -R 递归
sudo chcon -u system_u -r object_r -t httpd_sys_content_t /web/index.html

# 把 B 文件的上下文复制给 A(--reference)
sudo chcon --reference=/var/www/html /web
sudo chcon -R --reference=/var/www/html /web/
参数 说明
-t 修改 type(最常用)
-u 修改 SELinux user
-r 修改 role
-l 修改 level(MLS)
-R 递归处理目录
--reference=FILE 使用参照文件的上下文
-v 显示修改过程
-h 作用于符号链接本身而非其指向的文件

注意:chcon 适合快速验证猜想——怀疑是上下文问题时,先 chcon 一下看服务是否恢复正常。一旦确认,就应该改用 semanage fcontext 做持久化。把 chcon 当最终方案,等于埋了一个"几个月后某次系统维护突然全站 403"的雷。

7.5.2 restorecon:按策略恢复

sudo restorecon -v /web/index.html         # -v 显示改动了什么
sudo restorecon -Rv /web/                  # 递归恢复整个目录(最常用)
sudo restorecon -Rv /var/www/html/         # 修复被误改的 Apache 目录

restorecon -Rvn /web/                      # -n 只报告不实际修改(dry-run,先看再动)
sudo restorecon -RvF /web/                 # -F 强制重置全部四段上下文(含 user/role)

# 全盘重新打标签的两种方式
sudo touch /.autorelabel && sudo reboot    # 开机时全盘 relabel(推荐)
sudo restorecon -Rv /                      # 在线全盘(耗时长,风险较高)
sudo fixfiles relabel                      # 另一种全盘 relabel 工具
sudo fixfiles -R httpd restore             # 只恢复某个 RPM 包安装的文件
参数 说明
-R / -r 递归
-v 显示改动详情
-n 只报告不修改(dry-run)
-F 强制重置整个上下文(默认只改 type)
-p 显示进度
-e 路径 排除某个目录

典型用途:从别处 cp 或 mv 来的文件上下文不对,或者被 chcon 改错了,一条 restorecon -Rv 恢复成策略规定的正确值。

说明:cp 会让新文件继承目标目录的上下文(正常);mv 会保留源文件原有的上下文(常出问题)。从家目录 mv 一个网页文件到 /var/www/html/,它的类型仍是 user_home_t,Apache 读不了。这种情况 restorecon -Rv /var/www/html/ 即可解决。用 cp 代替 mv 也能避免。

相关技巧:cp -Z 类型 可在复制时直接指定上下文;cp --preserve=context 则保留源文件上下文(相当于模拟 mv 的行为);rsync -X / rsync --xattrs 可在同步时携带 SELinux 属性。

7.5.3 semanage fcontext:正确的持久化方法

# 1. 向策略数据库添加"路径 → 上下文"规则
sudo semanage fcontext -a -t httpd_sys_content_t "/web(/.*)?"

# 2. 按新规则给已存在的文件打标签(这一步不能省!)
sudo restorecon -Rv /web/

# 3. 验证
ls -Zd /web

第 1 步只是登记规则,不会自动修改已有文件;第 2 步才真正生效。两条命令必须配对执行——这与 firewalld 的 --permanent + --reload 是完全一样的思路。

路径正则的写法:

写法 匹配范围
"/web(/.*)?" /web 目录本身及其下所有内容(最常用)
"/web/.*" 只匹配 /web 下的内容,不含 /web 本身
"/web(/.*)?\.html" 只匹配 .html 文件
"/data/[^/]*/upload(/.*)?" 匹配 /data/任意一级/upload 及其下内容

管理已添加的规则:

sudo semanage fcontext -l                          # 列出所有规则(很长)
sudo semanage fcontext -l -C                       # 只列出本地自定义的规则(常用)
sudo semanage fcontext -l | grep '/var/www'        # 按路径过滤
sudo semanage fcontext -m -t httpd_sys_rw_content_t "/web/upload(/.*)?"   # 修改已有规则
sudo semanage fcontext -d "/web(/.*)?"             # 删除自定义规则
sudo restorecon -Rv /web/                          # 删除后同样要 restorecon

完整实例:把网站根目录改到 /data/www,上传目录需可写:

sudo mkdir -p /data/www/upload

# 站点主目录:只读内容
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
# 上传目录:需要 Apache 可写,用更具体的规则覆盖(更长的路径优先匹配)
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/www/upload(/.*)?"

sudo restorecon -Rv /data/www/

ls -Zd /data/www /data/www/upload      # 确认两个目录类型不同

7.6 布尔值(Boolean)

有些行为不是"路径标错了",而是"策略默认就不允许",例如"允许 Apache 发起网络连接"。这类开关由布尔值控制,无需编写策略模块,一条命令即可开关。

getsebool -a                            # 列出所有布尔值及当前状态(约 300 个)
getsebool httpd_can_network_connect     # 查看单个
getsebool -a | grep httpd               # 按服务名过滤(最实用)
getsebool -a | grep ftp
getsebool -a | grep samba

sudo semanage boolean -l                # 列出布尔值,含默认值与可读的说明
sudo semanage boolean -l -C             # 只看被本地修改过的(排障时很有用)
# SELinux boolean       State  Default  Description
# httpd_can_network_connect (on , off)  Allow httpd to can network connect

semanage boolean -l 输出的 (on , off) 含义是"当前值 , 默认值",两者不同即说明被人改过。

设置布尔值:

sudo setsebool httpd_can_network_connect on       # 临时,重启后失效
sudo setsebool -P httpd_can_network_connect on    # -P 持久化,写入策略
sudo setsebool -P httpd_enable_homedirs on ftp_home_dir on    # 一次设多个
sudo semanage boolean -m --on httpd_can_network_connect        # 等价的 semanage 写法
sudo semanage boolean -m --off httpd_can_network_connect       # 关闭

注意:-P 是唯一需要记住的参数。 没有 -P 的 setsebool 只改内存,重启后回到原值,症状表现为"上周明明修好了,服务器重启后又坏了"。-P 会重建策略文件,执行时可能需要几秒到几十秒,属正常现象。

常用布尔值(其他文档中会实际用到):

布尔值 作用 相关文档
httpd_enable_homedirs 允许 Apache 访问用户家目录(~user 个人主页) HTTPD 部署
httpd_can_network_connect 允许 Apache 发起任意对外网络连接(反向代理必需) HTTPD 部署
httpd_can_network_connect_db 允许 Apache 连接数据库端口(PHP 连远端 MySQL) HTTPD 部署
httpd_can_network_relay 允许 Apache 作为正向/反向代理中继 HTTPD 部署
httpd_use_nfs / httpd_use_cifs 允许 Apache 读取 NFS / CIFS 挂载的目录 HTTPD 部署
httpd_unified 允许 Apache 统一读写所有 httpd 类型文件 HTTPD 部署
httpd_execmem 允许 Apache 执行可写内存(某些 PHP 加速器需要) HTTPD 部署
ftpd_full_access 允许 FTP 读写所有非 SELinux 保护的文件 FTP 部署
ftpd_anon_write 允许 FTP 匿名用户写入 FTP 部署
ftp_home_dir 允许 FTP 用户访问自己的家目录 FTP 部署
samba_enable_home_dirs 允许 Samba 共享用户家目录 Samba 部署
samba_export_all_ro / samba_export_all_rw 允许 Samba 只读/读写共享任意目录 Samba 部署
nfs_export_all_rw / nfs_export_all_ro 允许 NFS 读写/只读导出任意目录 NFS 部署
use_nfs_home_dirs 允许家目录挂载在 NFS 上 NFS 部署
nis_enabled 允许使用 NIS 认证 认证服务
allow_ftpd_use_nfs 允许 FTP 使用 NFS 挂载目录 FTP 部署
mysql_connect_any 允许 MySQL 连接任意端口 数据库部署
named_write_master_zones 允许 BIND 写主区域文件(动态 DNS) DNS 部署
rsync_full_access 允许 rsync 读写所有文件 备份同步

典型场景:PHP 页面连不上远程 MySQL,日志里报连接被拒,但 telnet 数据库端口是通的。

sudo ausearch -m avc -ts recent | grep httpd        # 会看到 name_connect 被拒
sudo setsebool -P httpd_can_network_connect_db on   # 一条命令解决

7.7 端口标签

SELinux 同样给端口号打类型标签。策略规定 httpd_t 只能绑定 http_port_t 类型的端口,sshd_t 只能绑定 ssh_port_t。所以让服务监听非标准端口时,光改服务配置和防火墙是不够的——服务会启动失败。

sudo semanage port -l                        # 列出所有端口标签
sudo semanage port -l | grep http_port_t
# http_port_t   tcp   80, 81, 443, 488, 8008, 8009, 8443, 9000
sudo semanage port -l | grep ssh
# ssh_port_t    tcp   22
sudo semanage port -l -C                     # 只看本地添加的(排障用)
sudo semanage port -l | grep 8080             # 查某个端口当前属于哪个类型

添加端口标签:

# 让 Apache 能监听 8080
sudo semanage port -a -t http_port_t -p tcp 8080

# 让 sshd 能监听 2222
sudo semanage port -a -t ssh_port_t -p tcp 2222

# 端口范围
sudo semanage port -a -t http_port_t -p tcp 9001-9010

# 修改已存在的标签(端口已属于其他类型时 -a 会报错,改用 -m)
sudo semanage port -m -t http_port_t -p tcp 8080

# 删除自定义标签
sudo semanage port -d -t http_port_t -p tcp 8080

# UDP 端口
sudo semanage port -a -t dns_port_t -p udp 5353
参数 说明
-a 添加新的端口标签
-m 修改已有端口的标签
-d 删除自定义端口标签
-t 指定类型
-p 协议,tcp 或 udp
-l 列出
-C 配合 -l,只显示本地修改

报错解读:执行 -a 时若提示 ValueError: Port tcp/8080 already defined,说明该端口已有标签(可能属于 http_cache_port_t),改用 -m 修改即可。

7.7.1 完整实例:把 SSH 端口改为 2222

这是最能体现"三件事要一起做"的例子。三步缺任何一步都会失败,且失败症状各不相同:

# 第 1 步:改 sshd 配置
sudo vi /etc/ssh/sshd_config
#   Port 2222

# 第 2 步:加 SELinux 端口标签
#   缺这一步:sshd 启动失败,报 "Bind to port 2222 failed: Permission denied"
sudo semanage port -a -t ssh_port_t -p tcp 2222

# 第 3 步:放行防火墙
#   缺这一步:sshd 正常监听,但外部连接超时
sudo firewall-cmd --add-port=2222/tcp --permanent
sudo firewall-cmd --reload

sudo systemctl restart sshd
sudo ss -tlnp | grep 2222                  # 确认已监听

CentOS 6 的对应第 3 步:

iptables -I INPUT -p tcp --dport 2222 -j ACCEPT
service iptables save

注意:改 SSH 端口时不要立刻断开当前会话。另开一个终端用新端口测试 ssh -p 2222 user@host 成功后,再关闭旧会话。否则一旦哪一步出错,你就把自己锁在门外了。

三种"权限"的完整对照——把服务改到非标准端口时,需要同时满足:

层次 检查项 失败症状 修复命令
应用层 服务配置里的监听端口 服务仍在老端口 改服务配置文件
SELinux 端口标签允许该 domain 绑定 服务启动失败,Permission denied semanage port -a
防火墙 端口已放行 服务正常监听但外部连接超时 firewall-cmd --add-port

7.8 SELinux 排错方法论

这一节是 SELinux 最有价值的部分。掌握了方法论,遇到没见过的服务也能自己定位。

7.8.1 先装齐工具包

最小化安装的系统常常缺少管理工具,表现为 semanage: command not found。

软件包 提供的命令 说明
policycoreutils getenforce、setenforce、restorecon、sestatus、semodule、load_policy 基础包,默认已装
policycoreutils-python(CentOS 7)
policycoreutils-python-utils(CentOS 8+)
semanage、audit2allow、audit2why 最重要,最小化安装常缺
setroubleshoot-server sealert 人类可读的排障建议
setools-console sesearch、seinfo 查询策略规则
selinux-policy-devel make、策略开发头文件 自定义策略模块开发
mcstrans mcstransd 把安全级别翻译成可读文字
# CentOS 7
sudo yum install -y policycoreutils-python setroubleshoot-server setools-console
# CentOS 8 / Rocky / Alma
sudo dnf install -y policycoreutils-python-utils setroubleshoot-server setools-console

7.8.2 第一步:确认真的是 SELinux 拦的

最快的判定方法——临时切到宽容模式再试一次:

sudo setenforce 0          # 切 permissive
# 重新触发故障操作,例如 curl 一下页面
sudo setenforce 1          # 立刻切回 enforcing
  • 切 0 后问题消失 → 确定是 SELinux 拦的,继续往下查具体原因。
  • 切 0 后问题依旧 → 与 SELinux 无关,去查权限、配置、防火墙、监听地址。

setenforce 0 是诊断动作,不是修复方案。 用完必须立刻 setenforce 1 切回。很多线上系统之所以长期跑在 permissive 甚至 disabled,就是因为当年有人"临时关一下"之后忘了打开。诊断结束后请按 7.8.5 的优先级正式修复。

7.8.3 第二步:读 AVC 拒绝日志

SELinux 的每一次拒绝都会在审计日志里留下 AVC(Access Vector Cache)记录。

# 原始日志
sudo grep AVC /var/log/audit/audit.log
sudo grep denied /var/log/audit/audit.log | tail -20

# ausearch:更结构化,推荐
sudo ausearch -m avc                       # 所有 AVC 记录
sudo ausearch -m avc -ts recent            # 最近 10 分钟(最常用)
sudo ausearch -m avc -ts today
sudo ausearch -m avc -ts 09:00 -te 10:00   # 指定时间段
sudo ausearch -m avc -c httpd              # 按进程名过滤
sudo ausearch -m avc,user_avc -i           # -i 把数字翻译成可读名称
sudo ausearch -m avc -ts recent -i | less

# 实时观察(一边操作一边看)
sudo tail -f /var/log/audit/audit.log | grep --line-buffered AVC

一条典型的 AVC 记录:

type=AVC msg=audit(1690000000.123:456): avc:  denied  { getattr } for  pid=2345
comm="httpd" path="/data/www/index.html" dev="dm-0" ino=12345
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0
tclass=file permissive=0
字段 含义 排障价值
denied { getattr } 被拒绝的动作(read / write / open / getattr / name_connect / name_bind / create) 判断缺什么权限
comm="httpd" 发起访问的进程名 定位是哪个服务
path= / name= 被访问的目标对象 定位是哪个文件/目录
scontext=...httpd_t 源上下文(进程的 domain) 谁在访问
tcontext=...default_t 目标上下文(文件的 type) 一眼看出标签错了
tclass=file 目标对象类别(file / dir / tcp_socket / port) 判断是文件问题还是端口问题
permissive=0 0=enforcing 已阻断;1=permissive 仅记录 确认当前是否真被拦

上例的诊断结论:httpd_t 想读 default_t 的文件——这是典型的文件上下文错误,用 semanage fcontext + restorecon 修复。

判读口诀:

  • tclass=file / dir,tcontext 是 default_t、user_home_t、admin_home_t → 上下文标错,改标签。
  • denied { name_bind },tclass=tcp_socket → 端口标签问题,用 semanage port -a。
  • denied { name_connect } → 服务要对外连接,通常是布尔值(如 httpd_can_network_connect)。
  • denied { write } 而 type 是 httpd_sys_content_t → 需要可写类型 httpd_sys_rw_content_t。

7.8.4 第三步:用 sealert / audit2why 拿到修复建议

sealert 由 setroubleshootd 服务生成人类可读的分析报告,包含推荐命令。

sudo systemctl status setroubleshootd
sudo sealert -a /var/log/audit/audit.log      # 分析整个日志
sudo sealert -l "*"                            # 列出所有告警
sudo sealert -l <告警ID>                       # 按 /var/log/messages 里的 ID 查看
grep sealert /var/log/messages                 # 找告警 ID

典型输出(节选):

SELinux is preventing /usr/sbin/httpd from getattr access on the file /data/www/index.html.

*****  Plugin restorecon (99.5 confidence) suggests   ************************
If you want to fix the label.
/data/www/index.html default label should be httpd_sys_content_t.
Then you can run restorecon. The access attempt may have been stopped due to
insufficient permissions to access a parent directory in which case try to
change the following command accordingly.
Do
# /sbin/restorecon -v /data/www/index.html

confidence 是置信度,越高越可信。99% 以上的建议基本可以直接采纳;置信度低的建议(尤其是让你安装自定义模块的)必须先自己判断。

audit2why 解释原因,audit2allow 生成允许策略:

# 解释为什么被拒
sudo ausearch -m avc -ts recent | audit2why
sudo audit2why < /var/log/audit/audit.log

# 只看会生成什么规则,不安装(务必先看一遍)
sudo ausearch -m avc -ts recent | audit2allow

# 生成模块文件并安装
sudo ausearch -m avc -ts recent | audit2allow -M mypolicy
#   会生成 mypolicy.te(可读的策略源码)和 mypolicy.pp(编译后的模块)
cat mypolicy.te                    # 一定要看!
sudo semodule -i mypolicy.pp       # 安装模块

# 模块管理
sudo semodule -l                   # 列出已加载模块
sudo semodule -l | grep mypolicy
sudo semodule -r mypolicy          # 移除模块
sudo semodule -d mypolicy          # 禁用模块(不删除)
sudo semodule -e mypolicy          # 重新启用

警告:audit2allow 生成的规则必须人工审查,绝不能盲目安装。 原因有三:

  1. 它只是"把当前被拒的动作全部放行",可能放行远超实际需要的权限,等于给这个服务开了后门。
  2. 如果根因是上下文标错,正确做法是改标签;用 audit2allow 相当于"因为钥匙插错了锁孔,就把锁拆了"。
  3. 若日志里混有攻击行为产生的 AVC,audit2allow 会把攻击者需要的权限一起放行。

只有在确认"标签正确、布尔值也没有对应开关,是策略本身缺规则"时,才使用 audit2allow。

7.8.5 修复手段的优先级

同一个故障往往有多种改法,请从上往下依次尝试,越靠上越安全:

优先级 手段 命令 适用场景
1 改文件上下文 semanage fcontext -a + restorecon -Rv 自定义目录、数据盘、mv 过来的文件(最常见)
2 开布尔值 setsebool -P xxx on 策略已预留开关,如对外连接、家目录访问
3 加端口标签 semanage port -a -t xxx_port_t -p tcp N 服务改用非标准端口
4 自定义策略模块 audit2allow -M + semodule -i 以上都不适用,且已人工审查规则
5 单服务宽容 semanage permissive -a httpd_t 临时过渡,只放宽一个 domain,比全局 permissive 安全
6 全局 permissive setenforce 0 仅用于诊断,不是修复
7 彻底关闭 改 /etc/selinux/config 强烈不推荐,等于放弃一层防护

单服务宽容模式(第 5 项)的用法:

sudo semanage permissive -a httpd_t     # 只让 httpd 进入宽容模式
sudo semanage permissive -l             # 查看哪些 domain 处于宽容
sudo semanage permissive -d httpd_t     # 取消

7.8.6 标准排错流程

① 现象:服务起不来 / 页面 403 / 端口连不上
        ↓
② setenforce 0 再试一次
        ├── 问题消失 → 是 SELinux,进入 ③(并立刻 setenforce 1)
        └── 问题依旧 → 不是 SELinux,去查权限/配置/防火墙/监听地址
        ↓
③ ausearch -m avc -ts recent -i   看 AVC
        ↓
④ 读 scontext / tcontext / tclass / denied 动作,判断类别
        ├── 文件标签错   → semanage fcontext + restorecon
        ├── 端口没标签   → semanage port -a
        ├── 缺网络许可   → setsebool -P
        └── 策略缺规则   → 审查后 audit2allow -M + semodule -i
        ↓
⑤ setenforce 1 恢复强制模式,重新验证故障已消失

补充:一次访问失败可能连续产生多条 AVC。因为 SELinux 是逐项检查的,修好第一条后会暴露第二条(例如先报目录 search 被拒,改完再报文件 read 被拒)。所以修完一条要重新触发一次操作并再看日志,直到没有新的 AVC 为止,不要以为"改了还报错"就是没生效。

7.9 典型故障案例

以下四个案例覆盖了实际运维中 90% 的 SELinux 故障,务必动手复现一遍。

案例一:Apache 使用自定义站点目录,页面 403 Forbidden

现象:把网站放在 /data/www 而不是默认的 /var/www/html,Apache 配置无误、文件权限 755、httpd 进程正常监听 80,但浏览器返回 403,/var/log/httpd/error_log 里是 Permission denied: access to / denied。

诊断:

ls -Zd /data/www                   # unconfined_u:object_r:default_t:s0  ← 类型不对
sudo ausearch -m avc -ts recent -i | grep httpd
#   denied { getattr } ... comm="httpd" scontext=...httpd_t tcontext=...default_t tclass=file
sudo setenforce 0 && curl -I http://localhost/     # 200 → 确认是 SELinux
sudo setenforce 1

修复:

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www/
ls -Zd /data/www                   # 变为 httpd_sys_content_t
curl -I http://localhost/          # 200 OK

若站点内有上传目录需要写入,再补一条更具体的规则:

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/www/upload(/.*)?"
sudo restorecon -Rv /data/www/upload/

详见 09-01-HTTP服务基础与Apache.md。

案例二:修改 SSH 端口后 sshd 启动失败

现象:把 sshd_config 的 Port 改成 2222 后重启服务,systemctl status sshd 报失败,日志里是 error: Bind to port 2222 on 0.0.0.0 failed: Permission denied.

关键判读:进程以 root 运行却报 Permission denied,这正是 MAC 生效的典型特征(DAC 层 root 本不该被拒)。

诊断:

sudo ausearch -m avc -ts recent -i | grep sshd
#   denied { name_bind } ... scontext=...sshd_t tcontext=...unreserved_port_t tclass=tcp_socket
sudo semanage port -l | grep ssh_port_t      # ssh_port_t tcp 22  ← 没有 2222

修复:

sudo semanage port -a -t ssh_port_t -p tcp 2222
sudo systemctl restart sshd
sudo ss -tlnp | grep 2222
sudo firewall-cmd --add-port=2222/tcp --permanent && sudo firewall-cmd --reload

详见 05-02-SSH服务与安全加固.md。

案例三:FTP / Samba 无法访问用户家目录

现象:vsftpd 本地用户登录成功,但 ls 家目录报 550 Failed to change directory;或 Samba 能连上但看不到家目录共享。文件权限都正确。

诊断:

sudo ausearch -m avc -ts recent -i | grep -E 'vsftpd|smbd'
#   tcontext=...user_home_t / user_home_dir_t
getsebool -a | grep -E 'ftp|samba'

修复(属于"策略已预留开关",用布尔值而不是改标签):

# vsftpd
sudo setsebool -P ftp_home_dir on
sudo setsebool -P ftpd_full_access on        # 需要读写非家目录的其他位置时
sudo setsebool -P ftpd_anon_write on         # 匿名上传

# Samba
sudo setsebool -P samba_enable_home_dirs on
sudo setsebool -P samba_export_all_rw on     # 共享任意目录且需要可写

# 若共享的是自定义目录,则应打标签而非开全局布尔值(更安全)
sudo semanage fcontext -a -t samba_share_t "/data/share(/.*)?"
sudo restorecon -Rv /data/share/
sudo semanage fcontext -a -t public_content_rw_t "/data/ftp/pub(/.*)?"
sudo restorecon -Rv /data/ftp/pub/

详见 09-04-FTP服务与文件传输.md、10-02-Samba与Windows互通.md。

案例四:Nginx / Apache 反向代理到后端被拒

现象:Nginx 作为反向代理指向 http://127.0.0.1:8080 或后端服务器,浏览器返回 502 Bad Gateway,Nginx 错误日志是 connect() to ... failed (13: Permission denied)。后端服务用 curl 从本机直连是正常的。

关键判读:(13: Permission denied) 而不是 Connection refused——前者是被 SELinux 拦,后者才是后端没起。

诊断:

sudo ausearch -m avc -ts recent -i | grep -E 'nginx|httpd'
#   denied { name_connect } ... tclass=tcp_socket

修复:

sudo setsebool -P httpd_can_network_connect on        # nginx 也归 httpd_t 管
# 若后端是数据库
sudo setsebool -P httpd_can_network_connect_db on
# 若做正向代理/中继
sudo setsebool -P httpd_can_network_relay on

说明:Nginx 在 targeted 策略里同样运行在 httpd_t 域中,所以用的是 httpd_* 系列布尔值,不要去找 nginx_*。

详见 09-02-Nginx部署与优化.md、09-03-负载均衡与反向代理.md。

案例五:日志、数据目录换位置后服务写不进去

现象:为了扩容,把 MySQL 数据目录从 /var/lib/mysql 改到 /data/mysql,或把 Nginx 日志改到 /data/logs,服务启动即失败或日志为空。

修复思路:新位置必须继承旧位置的标签。用 -e(equal)让新路径等同于旧路径的所有标签规则,这是最省事的写法:

sudo semanage fcontext -a -e /var/lib/mysql /data/mysql
sudo restorecon -Rv /data/mysql/

sudo semanage fcontext -a -e /var/log/nginx /data/logs
sudo restorecon -Rv /data/logs/

# 或者手工指定类型
sudo semanage fcontext -a -t mysqld_db_t "/data/mysql(/.*)?"
sudo semanage fcontext -a -t httpd_log_t "/data/logs(/.*)?"

提示:semanage fcontext -a -e 新路径等同于旧路径 中 -e 后面跟的是参照路径,最后是新路径。它会把参照路径下的全套标签规则映射过来,比逐个目录手工指定类型更准确。

详见 11-01-MySQL部署与管理.md。

7.10 SELinux 命令速查

类别 命令 作用
状态 getenforce 查看当前模式
状态 sestatus 查看详细状态
状态 sestatus -v 含进程与文件上下文检查
状态 setenforce 0 / 1 临时切换 permissive / enforcing
状态 /etc/selinux/config 永久修改模式与策略类型
上下文 ls -Z / ps -Z / id -Z / ss -Z 查看文件/进程/用户/端口上下文
上下文 chcon -t 类型 文件 临时改上下文
上下文 chcon -R --reference=参照 目标 按参照文件改
上下文 restorecon -Rv 路径 按策略恢复默认上下文
上下文 restorecon -Rvn 路径 只预演不修改
上下文 semanage fcontext -a -t 类型 "路径(/.*)?" 永久登记默认上下文
上下文 semanage fcontext -l -C 查看本地登记的规则
上下文 semanage fcontext -d "路径(/.*)?" 删除登记
上下文 touch /.autorelabel && reboot 全盘重新打标签
布尔值 getsebool -a \| grep 服务名 查布尔值
布尔值 setsebool -P 名称 on 永久开启
布尔值 semanage boolean -l -C 查看被改过的布尔值
端口 semanage port -l \| grep 类型 查端口标签
端口 semanage port -a -t 类型 -p tcp 端口 添加端口标签
端口 semanage port -m -t 类型 -p tcp 端口 修改已有端口标签
排错 ausearch -m avc -ts recent -i 看最近的拒绝记录
排错 sealert -a /var/log/audit/audit.log 生成可读建议
排错 audit2why < /var/log/audit/audit.log 解释拒绝原因
排错 audit2allow -M 模块名 生成策略模块
排错 semodule -i 模块.pp / -l / -r 安装/列出/移除模块
排错 semanage permissive -a httpd_t 单服务宽容模式
策略 sesearch -A -s httpd_t -t httpd_sys_content_t 查询允许规则
策略 seinfo -t / seinfo -b 列出所有类型 / 布尔值

成对命令的对称规律(记住这张表,等于记住了大半 SELinux 命令):

目的 查看 修改(临时) 修改(永久)
运行模式 getenforce setenforce 0/1 改 /etc/selinux/config
文件上下文 ls -Z chcon -t semanage fcontext -a + restorecon
布尔值 getsebool setsebool setsebool -P
端口标签 semanage port -l (无临时手段) semanage port -a

规律:get* 看 / set* 改;semanage 系列一律是永久生效;带 -P 或走 semanage 才能扛住重启。

八、综合排错与练习

8.1 "服务连不上"的分层排错流程

线上最高频的问题就是"服务明明启动了,但客户端连不上"。原因可能在六个层次中的任意一层,按顺序逐层排除比乱猜快得多:

客户端 curl/telnet 失败
   │
   ├─① 服务是否真的在监听?在哪个地址监听?
   │     ss -lntp | grep 端口
   │     监听 127.0.0.1 → 只能本机访问,改配置为 0.0.0.0
   │
   ├─② 本机自己能访问吗?
   │     curl -I http://127.0.0.1/
   │     本机不通 → 问题在服务/SELinux,与防火墙无关
   │     本机通、外部不通 → 问题在防火墙/路由
   │
   ├─③ 本机防火墙放行了吗?
   │     firewall-cmd --list-all  /  iptables -L -n -v --line-numbers
   │     看计数器是否增长,看规则顺序,看默认策略
   │
   ├─④ SELinux 拦了吗?
   │     setenforce 0 再试;ausearch -m avc -ts recent
   │
   ├─⑤ 路由 / 网关 / 转发对吗?
   │     ip r、ping 网关、cat /proc/sys/net/ipv4/ip_forward
   │
   └─⑥ 对端(客户端侧、中间设备、云安全组)拦了吗?
         对端 iptables OUTPUT、云平台安全组、公司出口 ACL

分层判断的核心技巧是用"本机能否访问"这一刀切开责任范围:

本机 curl 外部 curl 结论 排查方向
失败 失败 服务层问题 服务是否启动、配置、SELinux、监听地址
成功 超时(timeout) 数据包被丢弃 防火墙 DROP、安全组、路由不通
成功 立刻拒绝(refused) 数据包被拒绝或没监听 防火墙 REJECT、监听地址是 127.0.0.1
成功 成功但很慢 DNS 或反向解析 dig、/etc/hosts、服务端 DNS 反查

记忆要点:timeout 多半是 DROP,refused 多半是 REJECT 或没人监听。 这一条能省掉大量无效排查。

8.2 排错命令清单

按上一节的层次整理成可直接照抄的命令序列:

# ① 服务与监听
systemctl status httpd
ss -lntp | grep :80                 # 看监听地址与进程
ss -lnup | grep :53                 # UDP 服务
netstat -antup | grep :80           # CentOS 6 写法
lsof -i :80                         # 哪个进程占用了端口
ps aux | grep httpd

# ② 本机连通性
curl -I http://127.0.0.1/
curl -I http://本机IP/               # 用本机 IP 测,能顺带验证监听地址
telnet 127.0.0.1 80
nc -zv 127.0.0.1 80

# ③ 防火墙
firewall-cmd --state
firewall-cmd --get-active-zones
firewall-cmd --list-all             # 默认 zone 的全部规则
firewall-cmd --list-all-zones | less
firewall-cmd --query-port=80/tcp
firewall-cmd --direct --get-all-rules
iptables -L -n -v --line-numbers    # 看计数器和顺序
iptables -t nat -L -n -v
iptables -S                         # 以命令形式查看,方便复制
iptables -Z && 重新测试 && iptables -L -n -v    # 计数器归零法,最可靠

# ④ SELinux
getenforce
sestatus
ausearch -m avc -ts recent -i
sealert -a /var/log/audit/audit.log
ls -Zd /目标目录
getsebool -a | grep 服务名
semanage port -l | grep 端口

# ⑤ 路由与转发
ip a
ip r
ping -c 2 网关IP
cat /proc/sys/net/ipv4/ip_forward
sysctl net.ipv4.ip_forward
traceroute 目标IP
tracepath 目标IP
mtr 目标IP

# ⑥ 抓包定位(终极手段:看包到底到没到)
tcpdump -i any -nn port 80
tcpdump -i eth0 -nn host 客户端IP and port 80
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0'    # 只看 SYN

抓包结果的判读,是区分"防火墙"和"路由"问题的决定性证据:

抓包现象 结论
服务端看不到 SYN 包 包在路上就没了:路由、上游设备、云安全组、客户端本机防火墙
看到 SYN,没有任何回包 本机防火墙 DROP 了
看到 SYN,回了 RST 本机防火墙 REJECT 了,或该端口无人监听
看到 SYN 和 SYN-ACK,但客户端反复重传 回程路由不对(常见于双网卡、NAT 环境)
三次握手完成后连接被断 应用层问题(服务配置、SELinux、认证),与防火墙无关

8.3 "关掉防火墙就好了"的危害

排障时最常见的错误做法是 systemctl stop firewalld 或 iptables -F,看到服务通了就收工。这么做的问题:

  1. 暴露全部端口。服务器上往往还跑着 MySQL 3306、Redis 6379、Memcached 11211、ES 9200 等本不该对外的服务。未授权的 Redis 是历史上被入侵最多的入口之一。
  2. 掩盖真实原因。可能根本不是防火墙的问题(比如监听在 127.0.0.1),关了防火墙"好了"只是巧合,问题会再次出现。
  3. 无法回滚与交接。清空的规则没有记录,同事接手时不知道原本该放行什么。
  4. iptables -F 在默认策略为 DROP 时会把自己锁死(详见 3.2.7),远程操作时直接失联。
  5. 合规不通过。等级保护、PCI-DSS 等审计明确要求主机层有访问控制。

正确的替代做法:

想做的事 错误做法 正确做法
验证是否防火墙问题 systemctl stop firewalld 只临时放行目标端口:firewall-cmd --add-port=80/tcp(不加 --permanent,重载即失效)
验证 iptables 是否拦了 iptables -F iptables -Z 清计数器后重测,看哪条规则计数增长
临时开放整机测试 停服务 firewall-cmd --add-source=测试机IP --zone=trusted,只信任一台机器
排查完想恢复 凭记忆重敲规则 事前 iptables-save > 备份文件 / firewall-cmd --runtime-to-permanent
确认是否 SELinux 改 config 后重启 setenforce 0 测完立刻 setenforce 1

记忆要点:排障时"临时放行"永远优于"整体关闭",而"临时"的技术含义是不加 --permanent——这样即使忘了收尾,一次 firewall-cmd --reload 或重启就会自动恢复。

8.4 常见坑与踩坑排查表

以下每一条都是实际工作中反复出现的问题。遇到"规则明明写了却不生效",先对照这张表。

序号 现象 根本原因 解决办法
1 firewall-cmd --permanent 加完规则,立刻测试仍不通 忘了 --reload,--permanent 只写配置文件不影响运行时 firewall-cmd --reload;或运行时+永久各加一遍
2 重启服务器后规则全丢 只加了运行时规则,没加 --permanent;iptables 没 save firewall-cmd --runtime-to-permanent;service iptables save / iptables-save > /etc/sysconfig/iptables
3 iptables 规则写了但没效果 顺序问题,前面已有 DROP/ACCEPT 先匹配了 iptables -L -n -v --line-numbers 看计数器;用 -I 插到前面而不是 -A
4 放行了 80,但服务器自己上不了网、yum 也不通 缺少 ESTABLISHED,RELATED 放行,出去的包能出、回来的包被拦 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
5 执行 iptables -P INPUT DROP 后 SSH 立刻断开 先设默认策略、后放行 SSH 永远先放行 SSH 再改默认策略;用 at 定时回滚兜底
6 iptables -F 后彻底失联 默认策略是 DROP,-F 清掉了放行 SSH 的规则,只剩 DROP 先 -P INPUT ACCEPT 再 -F;操作前挂 at 回滚任务
7 firewalld 和 iptables-services 同时启用,规则互相覆盖 两者都操作 netfilter,后启动的会清空前者 只能启用一个,另一个 stop + disable + mask
8 iptables -L 里看到大量 FWDI_public、IN_public 之类的链 这是 firewalld 生成的规则,说明当前在用 firewalld 用 firewall-cmd 管理,不要直接改这些链
9 SELinux 配置看起来都对,服务还是访问不了文件 用了 chcon,被后续 restorecon 或全盘重打标签冲掉 改用 semanage fcontext -a + restorecon
10 昨天 setsebool 修好的问题,重启后复现 忘了 -P setsebool -P;用 semanage boolean -l -C 复查
11 mv 过来的文件访问被拒,cp 的却正常 mv 保留原上下文,cp 继承目标目录上下文 restorecon -Rv 目标路径;或用 cp 代替 mv
12 服务改到非标准端口后启动失败,报 Permission denied SELinux 端口标签未添加 semanage port -a -t 对应类型 -p tcp 端口
13 NAT 网关配好了 SNAT,内网机器还是上不了网 忘开 ip_forward,或只临时开了没写配置文件 echo 1 > /proc/sys/net/ipv4/ip_forward + 写入 /etc/sysctl.conf 或 /etc/sysctl.d/
14 DNAT 配好了外网访问不到内网服务 FORWARD 链默认 DROP,且 FORWARD 里要匹配转换后的目标地址 补 FORWARD 规则;用内网真实 IP 而非公网 IP 作为 -d
15 内网能上网但 DNS 不通 只放行了 TCP,漏了 UDP 53 同时放行 -p udp --dport 53
16 FTP 能登录但 ls 卡住 被动模式的数据端口未放行,或没加载 conntrack 助手模块 固定被动端口范围并放行;modprobe nf_conntrack_ftp
17 装了 Docker 后自己写的 iptables 规则失效 Docker 会自行插入 DOCKER、DOCKER-USER 链和 MASQUERADE 规则,且优先级在自定义规则之前 自定义规则写入 DOCKER-USER 链;不要 iptables -F;改 /etc/docker/daemon.json 的 iptables: false 需自行接管 NAT
18 容器端口用 firewall-cmd 关了却还能访问 Docker 的 DNAT 发生在 nat 表 PREROUTING,绕过了 filter 的 INPUT 链 在 DOCKER-USER 链做限制:iptables -I DOCKER-USER -s 恶意IP -j DROP
19 云服务器规则都对但外网访问不了 云平台安全组是另一层防火墙 在云控制台放行对应端口
20 --zone 写了规则却不生效 网卡实际绑定的 zone 与你操作的 zone 不是同一个 firewall-cmd --get-active-zones 确认;source zone 优先级高于 interface zone
21 rich rule 写了但被前面的规则抢先匹配 firewalld 内部有固定处理顺序,rich rule 之间也有先后 用 --list-rich-rules 检查;必要时改用 --direct 精确控制位置
22 --kerneltz 没加,time 模块的时间对不上 iptables time 模块默认按 UTC 判断 加 --kerneltz 使用内核本地时区
23 conntrack 表满,新连接全部失败,日志报 table full, dropping packet 高并发下 nf_conntrack_max 太小 调大 nf_conntrack_max、缩短超时;对无状态服务可用 -j NOTRACK
24 IPv6 访问绕过了所有 iptables 规则 iptables 只管 IPv4 同步配置 ip6tables;或在内核关闭 IPv6

关于 Docker 与防火墙的完整机制(DOCKER-USER 链、docker-proxy、iptables: false 的后果、如何在 Docker 环境下正确限制来源 IP),详见 13-01-Docker基础与进阶.md。

8.5 firewalld 命令速查

类别 命令 作用
状态 firewall-cmd --state 是否运行
状态 systemctl status firewalld 服务状态
状态 firewall-cmd --reload 重载永久配置(不断连接)
状态 firewall-cmd --complete-reload 彻底重载(会断开连接)
状态 firewall-cmd --runtime-to-permanent 把运行时规则一次性存为永久
zone firewall-cmd --get-zones 列出所有 zone
zone firewall-cmd --get-default-zone 查看默认 zone
zone firewall-cmd --set-default-zone=public 设置默认 zone
zone firewall-cmd --get-active-zones 查看生效的 zone 及绑定
zone firewall-cmd --list-all 默认 zone 的完整规则
zone firewall-cmd --list-all-zones 所有 zone 的规则
网卡 firewall-cmd --zone=X --add-interface=eth0 --permanent 网卡绑定 zone
网卡 firewall-cmd --zone=X --change-interface=eth0 --permanent 改绑(更安全)
来源 firewall-cmd --zone=trusted --add-source=IP --permanent 按源地址绑定 zone
服务 firewall-cmd --get-services 列出预定义服务
服务 firewall-cmd --add-service=http --permanent 放行服务
服务 firewall-cmd --remove-service=http --permanent 移除服务
端口 firewall-cmd --add-port=8080/tcp --permanent 放行端口
端口 firewall-cmd --add-port=5000-5100/udp --permanent 放行端口范围
端口 firewall-cmd --list-ports 已放行端口
查询 firewall-cmd --query-port=80/tcp 判断是否放行(返回 yes/no)
查询 firewall-cmd --query-service=http 判断服务是否放行
转发 firewall-cmd --add-forward-port=port=80:proto=tcp:toport=8080 端口转发
转发 firewall-cmd --add-masquerade --permanent 开启地址伪装
富规则 firewall-cmd --add-rich-rule='...' --permanent 添加富规则
富规则 firewall-cmd --list-rich-rules 列出富规则
直接 firewall-cmd --direct --add-rule ipv4 filter INPUT 0 ... 直接下发 iptables 规则
直接 firewall-cmd --direct --get-all-rules 查看所有直接规则
应急 firewall-cmd --panic-on / --panic-off 恐慌模式开关
应急 firewall-cmd --query-panic 查询恐慌模式
离线 firewall-offline-cmd --add-service=ssh 服务未运行时改配置

8.6 注意事项

  1. 远程操作防火墙前,先给自己留退路。 任何修改默认策略、清空规则、切换防火墙前端的操作,都要先挂一个定时恢复任务:

    echo "iptables -P INPUT ACCEPT; iptables -F" | at now + 5 minutes
    # firewalld 版本
    echo "firewall-cmd --add-service=ssh" | at now + 5 minutes
    atq                                    # 查看待执行任务
    atrm 任务号                             # 确认无误后删掉

    同时保留至少两个已建立的 SSH 会话:一个用来操作,一个用来救火。已建立的连接通常不受新规则影响(有 ESTABLISHED 放行时),是最后的生命线。

  2. firewalld 的绝大多数命令都要写两遍或加 --permanent 后 --reload。 这是初学者踩坑第一名。推荐固定套路:

    firewall-cmd --add-port=8080/tcp                  # 立即生效,先验证效果
    firewall-cmd --add-port=8080/tcp --permanent      # 确认没问题再存盘

    或先在运行时反复试,满意后一次性 firewall-cmd --runtime-to-permanent。

  3. firewalld 与 iptables-services 只能启用一个。 混用会导致规则被互相覆盖,出现"改了没用/时好时坏"的诡异现象。切换时务必把另一个 stop + disable,必要时 mask 防止被依赖关系拉起。

  4. 不要因为排障方便就关闭 SELinux。 正确顺序是:setenforce 0 确认是否 SELinux → 看 AVC → 按 7.8.5 的优先级修复 → setenforce 1 复验。真正需要长期放宽时,用 semanage permissive -a 某个域 而不是全局关闭。另外,从 disabled 切回 enforcing 必须全盘重打标签(touch /.autorelabel 后重启),大磁盘可能耗时很久,要安排停机窗口。

  5. iptables 只管 IPv4。 服务器启用了 IPv6 时,必须用 ip6tables 配置对应规则,否则攻击者可以直接走 IPv6 绕过全部防护。firewalld 的 --add-port 等命令默认同时处理两族,是它的一个优势;写 rich rule 时则要注意 family="ipv4" / family="ipv6" 需分别声明。

  6. 规则要有注释、有备份、有版本管理。 生产环境的 iptables 规则建议写成脚本(见 4.4 节)纳入版本库,每条规则用 -m comment --comment 注明用途和负责人;每次改动前 iptables-save > /root/iptables-$(date +%F-%H%M).bak。半年后没人记得某条规则是给谁开的,这是运维交接最大的成本。

  7. 端口放行范围要最小化。 能限定源地址就不要对全网开放,尤其是 22、3306、6379、9200、11211、2375(Docker API)这些高危端口。推荐用 rich rule 或 -s 精确限制:

    firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" port port="3306" protocol="tcp" accept'

8.7 常见场景速查

场景 iptables(CentOS 6/7 通用) firewalld(CentOS 7+)
放行 SSH iptables -A INPUT -p tcp --dport 22 -j ACCEPT firewall-cmd --add-service=ssh --permanent
放行 Web iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT firewall-cmd --add-service={http,https} --permanent
只允许某网段访问 MySQL iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 3306 -j ACCEPT rich rule + source address
禁止某个 IP iptables -I INPUT -s 1.2.3.4 -j DROP rich rule + drop
禁 ping iptables -A INPUT -p icmp --icmp-type 8 -j DROP firewall-cmd --add-icmp-block=echo-request --permanent
只允许某 IP ping 先 -A INPUT -s IP -p icmp -j ACCEPT,再 -A INPUT -p icmp -j DROP rich rule accept + --add-icmp-block
放行本地回环 iptables -A INPUT -i lo -j ACCEPT trusted zone 绑定 lo(默认已放行)
放行已建立连接 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT firewalld 自动处理
端口转发 80→8080 iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 firewall-cmd --add-forward-port=port=80:proto=tcp:toport=8080
共享上网(SNAT) iptables -t nat -A POSTROUTING -s 内网段 -o eth0 -j MASQUERADE firewall-cmd --add-masquerade --permanent
发布内网服务(DNAT) iptables -t nat -A PREROUTING -d 公网IP -p tcp --dport 80 -j DNAT --to-destination 内网IP:80 firewall-cmd --add-forward-port=port=80:proto=tcp:toaddr=内网IP
限速防爆破 -m recent 或 -m limit rich rule + limit value="3/m"
限制并发连接数 -m connlimit --connlimit-above 20 -j REJECT --direct 下发同样规则
记录被拒日志 iptables -A INPUT -j LOG --log-prefix "DROP: " rich rule + log prefix= 或 LogDenied=all
应急封锁全部流量 iptables -P INPUT DROP(危险) firewall-cmd --panic-on
保存规则 service iptables save / iptables-save > 文件 --permanent 或 --runtime-to-permanent

练习

以下练习建议在虚拟机中完成,不要在生产服务器上做。每次实验前先备份规则,实验后按 4.3.7 的方法清理环境。

练习一:只允许指定 IP 访问 Web 服务

题目:本机运行 httpd,监听 80 端口。要求只允许 192.168.10.20 这台机器访问 80 端口,其他所有主机的访问都被拒绝。

思路:与"只允许某个 IP ping"完全同构——先写白名单 ACCEPT,再写黑名单 DROP。因为 iptables 是首次匹配即生效,顺序反了白名单就永远不会被检查到。

答案:

# 第 1 步:放行指定主机
iptables -A INPUT -s 192.168.10.20 -p tcp --dport 80 -j ACCEPT

# 第 2 步:拒绝其他所有主机访问 80
iptables -A INPUT -p tcp --dport 80 -j DROP

# 验证顺序
iptables -L INPUT -n -v --line-numbers

对照参考:22 端口的写法完全一样,只是把 --dport 80 换成 --dport 22:

iptables -A INPUT -s 192.168.10.20 -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

常见错误:

# 错误:顺序颠倒,DROP 在前,白名单永远不生效
iptables -A INPUT -p tcp --dport 80 -j DROP
iptables -A INPUT -s 192.168.10.20 -p tcp --dport 80 -j ACCEPT

若已经写错,用 -I 把 ACCEPT 插到第 1 行修正,或用 -D 删掉重写:

iptables -I INPUT 1 -s 192.168.10.20 -p tcp --dport 80 -j ACCEPT

验证方法:

# 在 192.168.10.20 上
curl -I http://服务器IP/            # 应返回 200

# 在其他主机上
curl -I --connect-timeout 3 http://服务器IP/     # 应超时(DROP 的特征)

# 在服务器上看计数器,确认是哪条规则命中
iptables -Z                          # 清零
# 再让两边各访问一次
iptables -L INPUT -n -v --line-numbers

firewalld 版本的等价实现:

# 方式一:富规则(推荐)
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.20" port port="80" protocol="tcp" accept'
firewall-cmd --reload
# 注意:不要 --add-service=http,否则等于对全网放行

# 方式二:用 zone 隔离
firewall-cmd --permanent --zone=trusted --add-source=192.168.10.20
firewall-cmd --permanent --zone=trusted --add-port=80/tcp
firewall-cmd --reload

练习二:Web + FTP + SSH + ICMP 综合防火墙

题目:一台对外服务器,要求满足以下全部条件:

  1. 默认策略:INPUT 拒绝,OUTPUT 和 FORWARD 允许。
  2. 允许任意主机访问 HTTP(80)和 HTTPS(443)。
  3. 允许任意主机使用 FTP(21 命令端口 + 被动模式数据端口 3000-3005)。
  4. 只允许 192.168.10.0/24 网段通过 SSH(22)登录。
  5. 只允许 192.168.10.20 ping 本机,其他主机 ping 不通。
  6. 本机自身的回环通信不受限制。
  7. 本机主动发出的请求(如 yum、curl)的响应包能正常返回。
  8. 所有被拒绝的包记录日志,日志前缀 IPTABLES-DROP:,并限速防止日志爆盘。
  9. 规则重启后依然有效。

环境准备

# 安装并启动服务
yum install -y httpd vsftpd
systemctl start httpd vsftpd          # CentOS 7
systemctl enable httpd vsftpd
# CentOS 6
# service httpd start; service vsftpd start
# chkconfig httpd on; chkconfig vsftpd on

# 准备测试页面
echo "firewall lab page" > /var/www/html/index.html

# 确认监听
ss -lntp | grep -E ':80|:443|:21|:22'

固定 vsftpd 的被动模式端口范围:

# /etc/vsftpd/vsftpd.conf
pasv_enable=YES
pasv_min_port=3000
pasv_max_port=3005
pasv_address=服务器公网IP        # 有 NAT 时必须指定,否则客户端拿到内网地址
anonymous_enable=YES
local_enable=YES
write_enable=YES
systemctl restart vsftpd
# SELinux 相关(如启用)
setsebool -P ftpd_full_access on
semanage port -l | grep ftp_port_t

答案(逐条书写)

注意书写顺序:先放行、最后才改默认策略,否则远程操作会立刻断线。

# ---------- 第 0 步:备份现有规则并挂上回滚保险 ----------
iptables-save > /root/iptables-before-lab.bak
echo "iptables -P INPUT ACCEPT; iptables -F" | at now + 10 minutes

# ---------- 第 1 步:清空旧规则,从干净状态开始 ----------
iptables -P INPUT ACCEPT          # 先改成 ACCEPT 再清,避免锁死
iptables -F
iptables -X
iptables -Z
iptables -t nat -F

# ---------- 第 2 步:放行回环(条件 6) ----------
iptables -A INPUT -i lo -j ACCEPT

# ---------- 第 3 步:放行已建立与相关连接(条件 7,同时也让 FTP 数据连接被识别) ----------
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# ---------- 第 4 步:丢弃非法包(可选但推荐) ----------
iptables -A INPUT -m state --state INVALID -j DROP

# ---------- 第 5 步:放行 SSH,仅限管理网段(条件 4) ----------
iptables -A INPUT -s 192.168.10.0/24 -p tcp --dport 22 -m state --state NEW -j ACCEPT

# ---------- 第 6 步:放行 HTTP / HTTPS(条件 2) ----------
iptables -A INPUT -p tcp -m multiport --dports 80,443 -m state --state NEW -j ACCEPT

# ---------- 第 7 步:放行 FTP 命令端口与被动数据端口(条件 3) ----------
iptables -A INPUT -p tcp --dport 21 -m state --state NEW -j ACCEPT
iptables -A INPUT -p tcp --dport 3000:3005 -m state --state NEW -j ACCEPT

# ---------- 第 8 步:ICMP,只允许指定主机(条件 5) ----------
iptables -A INPUT -s 192.168.10.20 -p icmp --icmp-type 8 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type 8 -j DROP

# ---------- 第 9 步:记录被拒绝的包(条件 8) ----------
iptables -A INPUT -m limit --limit 5/min --limit-burst 10 \
         -j LOG --log-prefix "IPTABLES-DROP: " --log-level 4

# ---------- 第 10 步:最后设置默认策略(条件 1) ----------
iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT

# ---------- 第 11 步:确认无误后撤销回滚任务并保存(条件 9) ----------
atq && atrm 任务号

# CentOS 6
service iptables save
# CentOS 7(需已安装 iptables-services)
iptables-save > /etc/sysconfig/iptables
systemctl enable iptables

逐条对照检查表:

条件 对应规则/命令 关键点
1 默认策略 -P INPUT DROP 等 3 条 必须最后执行
2 HTTP/HTTPS multiport 80,443 也可写两条独立规则
3 FTP 21 + 3000:3005 端口范围用冒号;必须同时固定 vsftpd 配置
4 SSH 限网段 -s 192.168.10.0/24 --dport 22 掩码写对,/24 不是 /32
5 ICMP 限单机 ACCEPT 在前、DROP 在后 顺序是本题最大的坑
6 回环 -i lo -j ACCEPT 漏了会导致本机很多服务异常
7 出站响应 ESTABLISHED,RELATED 漏了会导致 yum、DNS 全部不通
8 日志 -m limit + LOG LOG 不是终点,包会继续走到默认策略被丢
9 持久化 save / iptables-save CentOS 7 需先装 iptables-services

答案(综合书写,精简版)

熟练之后可以合并成更紧凑的写法。核心思路是"白名单 + 状态放行 + 默认拒绝":

iptables -F && iptables -X && iptables -Z
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 80,443,21,3000:3005 -j ACCEPT
iptables -A INPUT -s 192.168.10.0/24 -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -s 192.168.10.20 -p icmp --icmp-type 8 -j ACCEPT
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPTABLES-DROP: "
iptables -P INPUT DROP

只用 5 条核心规则的最简形态(不含日志和 ICMP 白名单):

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 21,80,443,3000:3005 -j ACCEPT
iptables -A INPUT -s 192.168.10.0/24 -p tcp --dport 22 -j ACCEPT
iptables -P INPUT DROP

说明:multiport 一次最多支持 15 个端口,端口范围 3000:3005 算作 2 个。超过 15 个时需拆成多条规则,或改用 ipset。

验证

# 1) 先看规则总览与顺序
iptables -L -n -v --line-numbers
iptables -S

# 2) 计数器归零,便于观察哪条规则命中
iptables -Z

在客户端逐项测试:

# HTTP(任意主机都应成功)
curl -I http://服务器IP/
# HTTPS
curl -kI https://服务器IP/

# FTP 被动模式(关键测试:能登录且 ls 不卡住)
ftp 服务器IP
#   ftp> passive          切换到被动模式
#   ftp> ls               能列出目录 → 数据端口放行成功
#   ftp> bye
# 或用 lftp 明确指定被动模式
lftp -u anonymous, 服务器IP -e "set ftp:passive-mode on; ls; bye"

# SSH(192.168.10.0/24 内的主机应成功)
ssh user@服务器IP
# 网段外的主机应超时
ssh -o ConnectTimeout=3 user@服务器IP

# ICMP(只有 192.168.10.20 能 ping 通)
ping -c 2 服务器IP

在服务器上核对结果:

# 看命中计数,确认走的是预期规则
iptables -L INPUT -n -v --line-numbers

# 看被拒日志
tail -f /var/log/messages | grep "IPTABLES-DROP"
journalctl -k -f | grep IPTABLES-DROP     # CentOS 7
dmesg | tail -20

# 确认 FTP 的 RELATED 连接被正确识别
lsmod | grep conntrack
cat /proc/net/nf_conntrack | grep -c 21

预期结果表:

测试项 来源 预期 若不符合,检查
curl http:// 任意 200 80 规则、httpd 是否运行、SELinux
ftp 登录 任意 成功 21 端口规则
ftp 下的 ls 任意 列出目录 3000-3005 规则、pasv_min/max_port、pasv_address
ssh 10.0/24 内 成功 源地址掩码是否写对
ssh 网段外 超时 是否被前面的宽松规则提前放行
ping 192.168.10.20 有回复 ACCEPT 是否在 DROP 之前
ping 其他主机 100% 丢包 --icmp-type 8 是否写对
本机 yum makecache 服务器自身 成功 ESTABLISHED,RELATED 是否放行
重启后规则仍在 — 存在 是否 save、iptables 服务是否 enable

firewalld 版本的等价实现

# 条件 2:HTTP / HTTPS
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https

# 条件 3:FTP + 被动端口
firewall-cmd --permanent --add-service=ftp
firewall-cmd --permanent --add-port=3000-3005/tcp

# 条件 4:SSH 仅限管理网段
#   先移除默认放行的 ssh,再用富规则限定来源
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" service name="ssh" accept'

# 条件 5:只允许一台机器 ping
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.20" protocol value="icmp" accept'
firewall-cmd --permanent --add-icmp-block=echo-request

# 条件 8:记录被拒的包
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" log prefix="FWD-DROP: " level="warning" limit value="5/m" drop'
#   或者更省事:在 /etc/firewalld/firewalld.conf 里设 LogDenied=all

# 条件 1、6、7、9:firewalld 默认就是"未放行即拒绝",
#   回环放行与连接跟踪由 firewalld 自动处理,--permanent 天然持久化
firewall-cmd --reload
firewall-cmd --list-all

对比结论:同样的需求,firewalld 版本不需要显式写回环放行、连接状态放行和默认策略——这三件事它已经内置。代价是"顺序不可控",需要精细控制匹配顺序时仍要回到 iptables 或 --direct。

练习三:NAT 网关(共享上网 + 端口映射)

题目:一台双网卡服务器作为网关。eth0 接外网(IP 10.0.0.10,网关 10.0.0.1),eth1 接内网(IP 192.168.100.1/24)。内网有一台 Web 服务器 192.168.100.20,监听 80 端口。要求:

  1. 内网 192.168.100.0/24 所有主机都能通过该网关访问外网。
  2. 外网主机访问 10.0.0.10:8080 时,请求被转发到 192.168.100.20:80。
  3. FORWARD 链默认拒绝,只放行必要流量。
  4. 配置重启后依然有效。

答案:

# ---------- 第 1 步:开启内核转发(条件 1 的前提,最容易漏) ----------
echo 1 > /proc/sys/net/ipv4/ip_forward          # 临时
cat >> /etc/sysctl.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
sysctl -p                                        # CentOS 6/7 通用
# CentOS 7 推荐写成独立文件
# echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-router.conf && sysctl --system
sysctl net.ipv4.ip_forward                       # 确认为 1

# ---------- 第 2 步:SNAT / MASQUERADE,让内网能出去(条件 1) ----------
# 外网 IP 固定,用 SNAT(性能更好)
iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o eth0 -j SNAT --to-source 10.0.0.10
# 若外网 IP 是 DHCP/PPPoE 动态获取,改用 MASQUERADE
# iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o eth0 -j MASQUERADE

# ---------- 第 3 步:DNAT,发布内网 Web 服务(条件 2) ----------
iptables -t nat -A PREROUTING -d 10.0.0.10 -p tcp --dport 8080 \
         -j DNAT --to-destination 192.168.100.20:80

# ---------- 第 4 步:FORWARD 链放行(条件 3) ----------
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
# 内网出外网
iptables -A FORWARD -s 192.168.100.0/24 -i eth1 -o eth0 -j ACCEPT
# 外网访问内网 Web —— 注意目标地址写【转换后】的内网 IP 和内网端口
iptables -A FORWARD -d 192.168.100.20 -p tcp --dport 80 -i eth0 -o eth1 -j ACCEPT
iptables -P FORWARD DROP

# ---------- 第 5 步:持久化(条件 4) ----------
service iptables save                            # CentOS 6
iptables-save > /etc/sysconfig/iptables          # CentOS 7

关键点解析:

易错点 错误写法 正确写法 原因
忘开转发 只写 NAT 规则 先 ip_forward=1 内核不转发,包直接丢弃
只临时开转发 echo 1 > /proc/... 同时写 sysctl.conf 重启后失效
FORWARD 目标地址 -d 10.0.0.10 --dport 8080 -d 192.168.100.20 --dport 80 DNAT 在 PREROUTING 已完成,FORWARD 看到的是转换后的地址
漏 ESTABLISHED 只放行单向 加 ESTABLISHED,RELATED 回程包被 FORWARD DROP
内网机器网关 指向自己或错误地址 网关设为 192.168.100.1 包根本不会送到网关
内网机器 DNS 未配置 配置可用 DNS 能 ping IP 但域名不通

验证:

# 网关上
iptables -t nat -L -n -v                         # 看 SNAT/DNAT 命中计数
cat /proc/net/nf_conntrack | head                # 看转换记录
tcpdump -i eth0 -nn host 10.0.0.10 and port 8080

# 内网主机上(网关设为 192.168.100.1)
ping -c 2 10.0.0.1
curl -I http://外网站点/
ip r                                              # 确认默认路由指向网关

# 外网主机上
curl -I http://10.0.0.10:8080/                   # 应返回内网 Web 的响应

# 在内网 Web 服务器上看访问日志,源 IP 应是外网客户端 IP(未做 full NAT 时)
tail -f /var/log/httpd/access_log

firewalld 版本:

# eth0 归 external(自带 masquerade),eth1 归 internal
firewall-cmd --permanent --zone=external --change-interface=eth0
firewall-cmd --permanent --zone=internal --change-interface=eth1
firewall-cmd --permanent --zone=external --add-masquerade
firewall-cmd --permanent --zone=external \
  --add-forward-port=port=8080:proto=tcp:toport=80:toaddr=192.168.100.20
firewall-cmd --reload
firewall-cmd --zone=external --list-all

提示:firewalld 的 --add-forward-port 带 toaddr 时必须同时开启 masquerade,否则回程包不会经过网关。

练习四:SELinux 自定义站点目录

题目:Apache 的站点根目录改为 /data/website,其中 /data/website/uploads 需要 Apache 可写。同时 Apache 需要额外监听 8000 端口,且要能反向代理到后端 192.168.1.50:9000。请在 SELinux 保持 enforcing 的前提下让一切正常工作。

答案:

# ---------- 第 1 步:目录与文件上下文 ----------
mkdir -p /data/website/uploads
echo "hello" > /data/website/index.html
chown -R apache:apache /data/website              # DAC 权限仍然必须正确
chmod -R 755 /data/website

ls -Zd /data/website                              # 确认当前是 default_t

# 永久登记默认上下文(不要用 chcon)
semanage fcontext -a -t httpd_sys_content_t "/data/website(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t "/data/website/uploads(/.*)?"
restorecon -Rv /data/website/
ls -Zd /data/website /data/website/uploads        # 两个目录类型应不同

# ---------- 第 2 步:端口标签 ----------
semanage port -l | grep http_port_t               # 看 8000 是否已在列表中
semanage port -a -t http_port_t -p tcp 8000

# ---------- 第 3 步:布尔值(反向代理需要对外连接) ----------
setsebool -P httpd_can_network_connect on
getsebool httpd_can_network_connect               # 确认为 on

# ---------- 第 4 步:防火墙 ----------
firewall-cmd --permanent --add-port=8000/tcp
firewall-cmd --reload

# ---------- 第 5 步:验证 ----------
systemctl restart httpd
ss -lntp | grep 8000
curl -I http://127.0.0.1:8000/
getenforce                                        # 必须仍是 Enforcing
ausearch -m avc -ts recent -i                     # 应无新的 AVC 记录

三个层次各对应哪一步:

需求 SELinux 手段 若漏掉的症状
读站点文件 fcontext + restorecon 403 Forbidden
写上传目录 httpd_sys_rw_content_t 上传失败、写入 Permission denied
监听 8000 semanage port -a httpd 启动失败,Bind failed
连接后端 9000 setsebool -P httpd_can_network_connect on 502 Bad Gateway、(13: Permission denied)
外部能访问 防火墙放行 8000 本机通、外部超时

练习五:SSH 防爆破与连接数限制

题目:对外的 SSH 服务频繁被暴力破解。要求:同一个源 IP 在 60 秒内新建 SSH 连接超过 4 次就封禁它 300 秒,同时限制单 IP 的 SSH 并发连接数不超过 3 个,被封禁的尝试要记录日志。

答案(用 recent + connlimit,配合自定义链):

# 创建专用链,便于管理和查看
iptables -N SSH_GUARD

# 并发连接数限制
iptables -A INPUT -p tcp --dport 22 -m connlimit --connlimit-above 3 \
         -j REJECT --reject-with tcp-reset

# 新建 SSH 连接进入 SSH_GUARD 链检查
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j SSH_GUARD

# 在 SSH_GUARD 链中:先判断是否已在封禁名单且仍在攻击
iptables -A SSH_GUARD -m recent --name SSHBRUTE --update --seconds 300 --hitcount 5 \
         -m limit --limit 2/min \
         -j LOG --log-prefix "SSH-BRUTE-BLOCK: " --log-level 4
iptables -A SSH_GUARD -m recent --name SSHBRUTE --update --seconds 300 --hitcount 5 -j DROP

# 未超限则记录本次访问并放行
iptables -A SSH_GUARD -m recent --name SSHBRUTE --set -j ACCEPT

更紧凑的两条式写法(经典模板):

iptables -A INPUT -p tcp --dport 22 -m state --state NEW \
         -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m state --state NEW \
         -m recent --update --seconds 60 --hitcount 5 --name SSH -j DROP

参数含义:

参数 作用
--set 把源 IP 记入名单并刷新时间戳
--update 检查源 IP 是否在名单中,并刷新时间戳(所以持续攻击会一直被封)
--rcheck 只检查不刷新时间戳(封禁到期就自动解除)
--seconds 300 时间窗口
--hitcount 5 窗口内出现次数达到该值即匹配
--name SSHBRUTE 名单名称,对应 /proc/net/xt_recent/SSHBRUTE
--remove 从名单中移除

查看与手工解封:

cat /proc/net/xt_recent/SSHBRUTE                       # 查看被记录的 IP
echo "-1.2.3.4" > /proc/net/xt_recent/SSHBRUTE          # 手工移除某 IP
echo "/" > /proc/net/xt_recent/SSHBRUTE                 # 清空整个名单
echo "+5.6.7.8" > /proc/net/xt_recent/SSHBRUTE          # 手工加入
iptables -L SSH_GUARD -n -v                             # 看封禁命中次数
grep "SSH-BRUTE-BLOCK" /var/log/messages

firewalld 版本:

firewall-cmd --permanent --add-rich-rule='rule service name="ssh" limit value="4/m" accept'
firewall-cmd --permanent --add-rich-rule='rule service name="ssh" log prefix="SSH-TRY: " level="notice" limit value="2/m"'
firewall-cmd --reload

注意:firewalld 的 limit 是全局速率限制,不区分源 IP,效果弱于 iptables 的 recent。需要按源 IP 封禁时,用 --direct 下发 recent 规则,或使用 fail2ban。

生产环境更推荐的组合是:SSH 改非标准端口 + 禁用密码登录只用密钥 + fail2ban + 只对管理网段放行。防火墙规则是最后一道,不是唯一一道。

练习六:故障场景分析(思考题)

对下列每个场景,写出你的诊断顺序和最可能的原因,再对照答案。

场景 1:新部署的 Nginx,本机 curl 127.0.0.1 返回 200,同事的电脑访问超时。防火墙已 --add-service=http --permanent。

答案:--permanent 后忘了 --reload。运行时规则里并没有 http。先 firewall-cmd --list-all 对比 firewall-cmd --permanent --list-all,两者不一致即可确认。执行 firewall-cmd --reload。

场景 2:iptables -A INPUT -s 1.2.3.4 -j DROP 加完后,1.2.3.4 仍能正常访问 80。

答案:顺序问题。INPUT 链前面已有 -p tcp --dport 80 -j ACCEPT,首次匹配即生效,DROP 永远走不到。用 iptables -L INPUT -n -v --line-numbers 看计数器验证,然后 iptables -I INPUT 1 -s 1.2.3.4 -j DROP 插到最前面。

场景 3:设置 iptables -P INPUT DROP 后,服务器能被外部访问,但自己 yum install 卡住不动,ping 外网也不通。

答案:缺少 ESTABLISHED,RELATED 放行。出站包能出去(OUTPUT 是 ACCEPT),但响应包回到 INPUT 时无匹配规则,被默认策略丢弃。补一条 iptables -I INPUT 1 -m state --state ESTABLISHED,RELATED -j ACCEPT。

场景 4:MySQL 数据目录从 /var/lib/mysql 迁到 /data/mysql 后启动失败,权限已改成 mysql:mysql,getenforce 是 Enforcing。

答案:SELinux 上下文未迁移。ls -Zd /data/mysql 会看到 default_t。执行 semanage fcontext -a -e /var/lib/mysql /data/mysql && restorecon -Rv /data/mysql/。

场景 5:改完 sshd_config 的 Port 2222 并重启 sshd,服务启动失败,日志是 Bind to port 2222 failed: Permission denied。

答案:root 进程却报 Permission denied,是 MAC 拦截的典型特征。semanage port -a -t ssh_port_t -p tcp 2222,然后重启 sshd 并放行防火墙。

场景 6:装了 Docker 之后,之前写好的 iptables -A INPUT -s 恶意IP -j DROP 对容器暴露的端口不起作用。

答案:Docker 通过 nat 表 PREROUTING 做 DNAT,流量走 FORWARD 而非 INPUT,因此 INPUT 规则管不到容器端口。应写入 DOCKER-USER 链:iptables -I DOCKER-USER -s 恶意IP -j DROP。详见 13-01-Docker基础与进阶.md。

场景 7:网关上 SNAT 已配好、ip_forward 也是 1,内网机器能 ping 通网关但上不了外网。

答案:优先检查内网机器的默认路由和 DNS(ip r、cat /etc/resolv.conf),其次检查网关的 FORWARD 链默认策略是否为 DROP 且未放行内网出站。在网关用 tcpdump -i eth0 -nn host 内网IP 判断包是否出去了。

场景 8:firewall-cmd --zone=public --add-port=3306/tcp 加了却不生效,--list-all 里确实有这条。

答案:网卡实际绑定的不是 public。用 firewall-cmd --get-active-zones 确认;若配置过 source zone,注意source 绑定优先级高于 interface 绑定,流量走的是另一个 zone。

练习七:概念自测

不查资料回答下列问题,答不上来的回到对应章节复习。

  1. 四表五链分别是什么?数据包从外部进入本机的处理路径经过哪些链?(见 2.2、2.3)
  2. -A 和 -I 的区别是什么?什么情况下必须用 -I?(见 3.2.3、3.2.4)
  3. DROP 和 REJECT 的区别?客户端各自看到什么现象?(见 3.4.1)
  4. 为什么"先设 -P INPUT DROP 再放行 SSH"是危险操作?(见 3.2.9)
  5. 默认策略是 DROP 时执行 iptables -F 会发生什么?正确的清理顺序是什么?(见 3.2.7)
  6. -m state 的四种状态分别代表什么?为什么几乎所有规则集都要放行 ESTABLISHED,RELATED?(见 4.2.4)
  7. FTP 被动模式为什么需要 nf_conntrack_ftp 模块?(见 3.7、4.2.4)
  8. SNAT 和 MASQUERADE 的区别?各自适用什么场景?(见 5.2)
  9. DNAT 之后,FORWARD 链里的 -d 应该写公网 IP 还是内网 IP?为什么?(见 5.6)
  10. 做 NAT 之前必须先做什么内核层面的配置?如何让它永久生效?(见 5.1)
  11. firewalld 的 runtime 和 permanent 有什么区别?三种让永久配置生效的方式是什么?(见 6.4)
  12. 一个数据包同时匹配 source zone 和 interface zone 时,走哪个 zone?(见 6.3.5)
  13. firewall-cmd --reload 和 --complete-reload 的区别?(见 6.2)
  14. rich rule 相比 --add-port 多了哪些能力?(见 6.7)
  15. DAC 和 MAC 的检查顺序是怎样的?为什么 chmod 777 有时也解决不了问题?(见 7.1)
  16. chcon 和 semanage fcontext 的本质区别?为什么推荐后者?(见 7.5)
  17. cp 和 mv 一个文件到新目录,上下文分别会怎样变化?(见 7.5.2)
  18. setsebool 忘加 -P 会有什么后果?(见 7.6)
  19. 服务改到非标准端口时,需要同时做哪三件事?(见 7.7.1)
  20. 怀疑是 SELinux 导致的故障,标准诊断流程是什么?为什么 setenforce 0 不算修复?(见 7.8)
  21. 一次访问失败为什么可能产生多条 AVC 记录?(见 7.8.6)
  22. 客户端连接"超时"和"立刻被拒绝"分别提示什么原因?(见 8.1)
  23. 为什么不能用"关掉防火墙"来解决问题?正确的临时验证方式是什么?(见 8.3)
  24. iptables 和 firewalld 为什么不能同时启用?(见 6.11、8.4)
  25. 装了 Docker 之后,为什么要把限制规则写到 DOCKER-USER 链?(见 8.4)

实验环境清理

做完练习后恢复到干净状态,避免影响后续章节的实验。

# ---------- iptables ----------
iptables -P INPUT ACCEPT               # 先放开默认策略
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
for t in filter nat mangle raw; do
    iptables -t $t -F                  # 清空规则
    iptables -t $t -X                  # 删除自定义链
    iptables -t $t -Z                  # 计数器归零
done
iptables -L -n -v                      # 确认已清空
iptables -t nat -L -n -v

# 恢复实验前的备份
iptables-restore < /root/iptables-before-lab.bak
service iptables save                  # CentOS 6
iptables-save > /etc/sysconfig/iptables # CentOS 7

# ---------- 内核参数 ----------
echo 0 > /proc/sys/net/ipv4/ip_forward
sed -i '/net.ipv4.ip_forward/d' /etc/sysctl.conf
rm -f /etc/sysctl.d/99-router.conf
sysctl --system

# ---------- recent 名单 ----------
echo "/" > /proc/net/xt_recent/SSHBRUTE 2>/dev/null

# ---------- firewalld ----------
firewall-cmd --permanent --remove-port=3000-3005/tcp
firewall-cmd --permanent --remove-port=8000/tcp
firewall-cmd --permanent --remove-service=ftp
firewall-cmd --permanent --remove-masquerade
firewall-cmd --permanent --remove-rich-rule='上面添加过的规则原文'
firewall-cmd --permanent --zone=public --add-service=ssh    # 确保 SSH 仍可用
firewall-cmd --reload
firewall-cmd --list-all

# 或者直接恢复默认配置(会丢失所有自定义 zone 设置)
rm -f /etc/firewalld/zones/*.xml
firewall-cmd --complete-reload

# ---------- SELinux ----------
semanage fcontext -l -C                # 看本地添加了哪些
semanage fcontext -d "/data/website(/.*)?"
semanage fcontext -d "/data/website/uploads(/.*)?"
semanage port -l -C                    # 看本地添加的端口
semanage port -d -t http_port_t -p tcp 8000
semanage boolean -l -C                 # 看被改过的布尔值
setsebool -P httpd_can_network_connect off
getenforce                             # 确认仍是 Enforcing

# ---------- 服务与目录 ----------
systemctl stop httpd vsftpd
systemctl disable httpd vsftpd
rm -rf /data/website
atq                                    # 确认没有遗留的 at 回滚任务

提示:semanage fcontext -l -C、semanage port -l -C、semanage boolean -l -C 这三条"只看本地修改"的命令,是清理环境和接手陌生服务器时最有价值的工具——它们能一眼看出前任在 SELinux 上动了哪些手脚。

本章小结

主题 核心结论
架构 防火墙的真正实现在内核 netfilter,iptables / firewall-cmd 都只是前端工具
表链 四表五链决定"在哪个位置能做什么事";filter 做过滤、nat 做地址转换
顺序 iptables 首次匹配即生效,白名单必须在黑名单之前
状态 ESTABLISHED,RELATED 是有状态防火墙的基石,几乎每个规则集都需要
安全 远程改防火墙前先备份、先放行 SSH、挂 at 回滚任务
持久化 iptables 靠 save,firewalld 靠 --permanent + --reload
firewalld zone 模型 + 动态生效;runtime/permanent 双份是最大的坑
二选一 firewalld 与 iptables-services 只能启用一个
SELinux DAC 通过后还要过 MAC;三大手段是上下文、布尔值、端口标签
SELinux 持久 chcon 会被冲掉,用 semanage fcontext + restorecon;setsebool 必须加 -P
排错 分层定位:监听 → 本机访问 → 防火墙 → SELinux → 路由 → 对端
排错工具 iptables -L -n -v 看计数器、ausearch -m avc 看拒绝、tcpdump 看包
禁忌 不要关防火墙、不要关 SELinux、不要盲目 audit2allow
最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏