# 防火墙

# netfilter

  • 相关历史:

    • 1998 年,澳大利亚程序员 Rusty Russell 创建了 netfilter 项目,旨在改进 Linux 防火墙软件。
    • 2000 年,Linux 2.4 内核发布,添加了 netfilter 框架,包括内核模块 ip_tables、ip6_tables、arp_tables、eb_tables 等。
  • netfilter 是 Linux 内核中的一个网络框架,提供了数据包过滤(包括无状态包、有状态包)、NAT 等功能。

    • 目前大部分 Linux 发行版的防火墙软件,是基于 netfilter 实现的。
    • 可执行 sysctl -a | grep net.netfilter 查看相关的内核参数。
  • netfilter 的工作原理:

    1. 用户使用 iptables 等命令配置防火墙规则,传给 netfilter 。
    2. netfilter 收到防火墙规则,转换成钩子(hook)函数,注册在 Linux 网络协议栈中。
    3. 当 Linux 内核每次接收、发送网络数据包时,就会触发 hook ,根据 hook 的逻辑来处理网络包,比如丢弃包、修改包。
  • netfilter 工作在内核态,并且提供了 xtables 工具集(它工作在用户态)。

    • xtables 担任防火墙前端,供用户配置防火墙规则。包含以下命令行工具:
      iptables    # 用于过滤 IPv4 协议的数据包
      ip6tables   # 用于过滤 IPv6 协议的数据包
      arptables   # 用于过滤 ARP 协议的数据包
      ebtables    # 用于过滤以太网帧
      
      还有以下命令,用于导入、导出防火墙规则:
      iptables-save
      iptables-restore
      ip6tables-save
      ip6tables-restore
      arptables-save
      arptables-restore
      ebtables-save
      ebtables-restore
      
    • 不同网络协议的架构不同,因此它们的防火墙规则的语法不同,需要用不同的命令来配置。
    • 不管用户配置了 iptables 规则,还是配置了 arptables 等规则,都会被 netfilter 转换成 hook 函数。

# nf_conntrack

  • netfilter 属于有状态防火墙,默认会记录所有数据包的上下文信息,据此过滤数据包。

    • 该功能由 netfilter 中的 conntrack 子模块实现,简称为 ct 。原理如下:
      1. hook 得到数据包。
      2. 识别数据包的状态。比如该数据包属于某个网络连接(包括 TCP、UDP、ICMP 等协议)。
      3. 根据数据包的信息,更新 nf_conntrack 中记录的网络连接。
  • nf_conntrack 采用哈希表结构,每行记录一个网络连接的信息(并不是每行记录一个数据包)。

    • nf_conntrack 存储在 Linux 内核的内存中。
    • 当 nf_conntrack 容量写满时,防火墙会拒绝新的 TCP、UDP 等网络连接。可执行 grep 'nf_conntrack: table full, dropping packet' /var/log/messages 查看已拒绝的日志。
    • 如果一个网络连接一直没有传输数据,则超过 nf_conntrack_xx_timeout_xx 时长之后,会从 nf_conntrack 中删除其记录。
      • 即使网络连接已关闭,也要等超时之后才能删除记录,从而跟踪网络连接的上下文信息。
      • 减少 nf_conntrack_xx_timeout_xx 超时时间,能更快地删除无用的记录,节约 nf_conntrack 的容量。
  • 例:

    • 如果 client 与 server 建立 TCP 连接,则双方都绑定一个 ESTABLISHED 状态的 Socket ,在 nf_conntrack 中记录一些 ESTABLISHED 状态的数据包。
    • 如果 client 主动关闭 TCP 连接,则:
      • client 作为主动关闭方,Socket 会变为 TIME_WAIT 状态,等 2*MSL 时长之后才关闭。而 nf_conntrack 中依然记录该 TCP 连接,停留在 TIME_WAIT 状态,等 nf_conntrack_tcp_timeout_close_wait 超时之后才删除该记录。
      • server 作为被动关闭方,会立即关闭 Socket 。而 nf_conntrack 中依然记录该 TCP 连接,停留在 TIME_WAIT 状态,等超时之后才删除该记录。
    • 如果执行 ping 命令,向目标 IP 发送多个 ICMP 包,则 nf_conntrack 只会记录一条网络连接。
      • 每发送一个新包,就会更新一次 nf_conntrack 中记录的网络连接,重新计算还剩多少秒可删除。
    • 如果执行多次 ping 命令,即使目标 IP 相同, nf_conntrack 也会分别记录一条网络连接。
    • 如果未成功建立网络连接,比如被防火墙拒绝了,则不会被 nf_conntrack 记录。
  • 相关命令:

    cat /proc/net/nf_conntrack                      # 查看 nf_conntrack 的当前内容
    cat /proc/sys/net/netfilter/nf_conntrack_count  # 查看 nf_conntrack 当前记录的连接数
    
  • 例:

    [CentOS ~]# cat /proc/net/nf_conntrack
    ipv4     2 tcp      6  75  TIME_WAIT src=10.0.0.1 dst=169.254.0.4 sport=56586 dport=80 src=169.254.0.4 dst=10.0.0.1 sport=80 dport=56586 [ASSURED] mark=0 zone=0 use=2
    ipv4     2 tcp      6  85  TIME_WAIT src=10.0.0.1 dst=169.254.0.4 sport=56612 dport=80 src=169.254.0.4 dst=10.0.0.1 sport=80 dport=56612 [ASSURED] mark=0 zone=0 use=2
    ipv4     2 udp      17 87  src=10.0.0.1 dst=183.60.83.19 sport=37967 dport=53 src=183.60.83.19 dst=10.0.0.1 sport=53 dport=37967 [ASSURED] mark=0 zone=0 use=2
    ipv4     2 icmp     1  24  src=169.254.128.23 dst=10.0.0.1 type=8 code=0 id=13609 src=10.0.0.1 dst=169.254.128.23 type=0 code=0 id=13609 mark=0 zone=0 use=2
    
    • 第一、二列表示网络层的协议名、协议号,比如 ipv4 的协议号是 2 。
    • 第三、四列表示传输层的协议名、协议号。
    • 第五列表示这行记录还剩多少秒可删除。
    • 上例的第一行记录了一个 TCP 连接。
      • 状态为 TIME_WAIT 。
      • 第一组 src、dst 表示网络请求包的源地址、目标地址。
      • 第二组 src、dst 表示网络回复包的源地址、目标地址。
      • ASSURED 表示已记录请求包、回复包。如果只记录了请求包,还未出现回复包,则记作 UNREPLIED 。
  • nf_conntrack 中记录的数据包,可能添加多种状态(conntrack state ,简称为 ctstate)标记:

    NEW           # 该数据包属于一个新的网络连接,不属于已有的网络连接。例如 TCP、UDP、ICMP 通信时的第一个数据包
    ESTABLISHED   # 该数据包属于一个已有的网络连接。例如 TCP、UDP、ICMP 通信时的回复包,特点是数据包中的 src_address 与 dst_address 互换了
    RELATED       # 该数据包属于一个新的网络连接,并且关联已有的网络连接。这是因为某些网络协议,需要创建多个连接来通信
    INVALID       # 不能识别数据包的状态
    UNTRACKED     # 不跟踪该数据包的状态
    SNAT          # 该数据包的 src_address 被改了,因此收到回复包时,需要将其中的 dst_address 改回原值
    DNAT
    
    • 这些标记是数据包外部附加的信息,不会修改数据包的原本内容,不会传递到其它主机。

# iptables

  • iptables 配置的防火墙规则,数据结构像表格。
    • 总共内置了 5 个规则表(rule table)。
    • 每个规则表包含多个规则链(rule chain)。
    • 每个规则链包含多行规则。

# rule table

  • iptables 内置了 5 个规则表,用于让防火墙对数据包执行不同操作:

    • raw
      • :用于给数据包添加 UNTRACKED 标记,从而不被 netfilter 跟踪。
      • 内置规则链:PREROUTING、OUTPUT
    • mangle
      • :用于修改数据包中的元数据,比如 TTL、MTU 。
      • 内置规则链:全部 5 种
    • nat
      • :用于修改数据包中的 src_ip、src_port、dst_ip、dst_port 等元数据,从而实现端口转发、NAT 地址转换。
      • DNAT 时的内置规则链:PREROUTING、OUTPUT
      • SNAT 时的内置规则链:INPUT、POSTROUTING
    • filter
      • :用于过滤数据包,比如丢弃哪些数据包、放行哪些数据包。
      • 内置规则链:INPUT、FORWARD、OUTPUT
    • security
      • :用于执行 SELinux 等强制访问控制(Mandatory Access Control,MAC)规则。
      • 内置规则链:INPUT、FORWARD、OUTPUT
  • 5 个规则表,会按照 raw > mangle > nat > filter > security 的优先级顺序,依次执行。

    • 大部分用户只会用到 nat、filter 两个表。

# rule chain

  • iptables 内置了 5 种规则链,用于在不同时间点处理数据包:

    INPUT         # 当网口接收数据包时
    OUTPUT        # 当网口发送数据包时
    FORWARD       # 数据包的源地址、目标地址都不是当前网口,说明需要路由转发到其它网口、其它主机
    
    PREROUTING    # 接收数据包时,并且尚未执行 IP 路由、INPUT 链
    POSTROUTING   # 发送数据包时,并且已执行 IP 路由、OUTPUT 链、FORWARD 链
    
    • 数据包到达 INPUT、OUTPUT 或 FORWARD 链之前,都需要执行 IP 路由,决定将数据包发到何处。而 IP 路由之前,都需要执行 PREROUTING 链。
    • 网口发送数据包时,可能发送到当前网口、本机的其它网口、其它主机。
      • 如果数据包是本机生成的,则源地址是当前网口。
      • 如果数据包是其它主机发送、由本机转发的,则源地址、目标地址都不是当前网口,因此不会执行 OUTPUT 链,而是执行 FORWARD 链。
      • 即使网口发送数据包给自己,没有经过网络传输,也需要走一遍发送数据包、接收数据包的完整流程。
  • 例:

    • 本机生成并发送数据包时,流程如下:
      1. 本机进程将数据包写入 Socket 文件。
      2. 网口从 Socket 读取到数据包,依次执行 IP 路由、OUTPUT 链、POSTROUTING 链。
      3. 网口将数据包发出去。
    • 本机接收数据包时,先执行 PREROUTING 链。然后执行 IP 路由:
      • 如果数据包发向当前网口,则:
        1. 执行 INPUT 链。
        2. 正式接收数据包。例如将数据包写入 Socket 文件,供本机进程读取。
      • 如果数据包发向其它网口、其它主机,则:
        1. 依次执行 FORWARD 链、POSTROUTING 链。
        2. 网口将数据包发出去。
    • 执行命令 ping localhost 时,流程如下:
      • 首先网口发送 ICMP 请求包:依次执行 OUTPUT 链、IP 路由、POSTROUTING 链。
      • 然后网口接收 ICMP 请求包:依次执行 PREROUTING 链、IP 路由、INPUT 链。
  • netfilter 只支持在上述 5 个时间点创建 hook 函数,因此只内置了 5 种规则链。

    • 用户不能创建新的规则表,但允许在内置规则表中创建新的规则链。
    • 用户创建的 chain 没有绑定时间点,需要被内置 chain 引用,作为子链,才会生效。
      • 例如在 INPUT 链中添加一行 rule ,其 target 为用户创建的 chain1 链。还可在 chain1 链中添加一行 rule ,其 target 为用户创建的 chain2 链。

# rule

  • 每个 chain 默认包含 0 行规则(rule)。

    • 用户可以在每个 chain 中添加任意行 rule 。
    • 用户可以在一个 chain 中添加相同几行相同的 rule ,iptbales 不会自动去重。
    • 每行 rule 的语法为 <match> <target> ,表示如果一个数据包匹配某条件,则执行某动作,又称为规则目标(rule target)。
  • iptables 内置了几种动作:

    • ACCEPT
      • :允许数据包通过。
      • 不过数据包会经过多个规则表、多个 chain 处理,即使前面的环节用 ACCEPT 放行了数据包,后面的环节也可能拒绝数据包。
    • REJECT
      • :丢弃数据包,并回复一个拒绝访问的数据包给发送方。
      • 不能用作 chain 的默认动作。
      • 只允许用于 INPUT、FORWARD、OUTPUT 链及其子链。
    • DROP
      • :丢弃数据包,并且不作出回复。
      • 优点:在一定程度上防御恶意的访问者。
        • 如果其它主机对本机进行端口扫描,尝试访问每个端口。则 REJECT 动作会让对方立即发现端口的连通性,而 DROP 动作会导致不能立即判断端口的连通性。
        • 如果发生 DDOS 攻击,则 REJECT 动作需要回复大量拒绝信息,消耗本机的资源,而 DROP 动作不消耗。
        • 如果本机 DROP 掉所有 IP 发来的数据包,只允许被白名单 IP 访问,则可以对外隐藏本机的存在。
      • 缺点:可能误导正常的访问者,以为网络不连通,或者丢包了,还要多次重试访问。
    • RETURN
      • :结束遍历当前 chain 。
      • 如果当前 chain 是以 -j <target> 方式执行的子链,则 RETURN 之后会执行父链。
      • 如果当前 chain 是内置 chain ,则 RETURN 之后会执行当前 chain 的默认动作,然后执行下一个规则表中的同类型 chain 。
    • DNAT
      • :修改数据包的目标 ip、port 。
      • 只允许用于 nat 表的 PREROUTING、OUTPUT 链及其子链。
    • SNAT
      • :修改数据包的源 ip、port 。
      • 只允许用于 nat 表的 INPUT、POSTROUTING 链及其子链。
    • REDIRECT
      • :将数据包重定向到当前网口的某个端口。
      • 用途与 DNAT 相似,会将数据包的目标 ip、port 改成当前网口的 ip、port 。如果是本机生成的数据包,则将目标 ip 改成 127.0.0.1 。
    • MASQUERADE
      • :用途与 SNAT 相似,会将数据包的源 ip 改成通过 DHCP 等方式动态获取的 ip 。
      • 如果不能动态获取 ip ,则应该使用 SNAT 动作。
    • LOG
      • :放行这个数据包,并记录一行日志。
      • 建议将记录日志的 rule 放在其它 rule 之前执行,避免其它 rule 执行了 terminating target ,没来得及记录日志。
    • MARK
      • :给数据包添加 netfilter 标记。
      • 只允许用于 mangle 表。
  • 网口每次接收、发送一个数据包时,会按以下流程处理:

    1. 执行符合当前时间点的那个 chain 。
      • 例如网口接收数据包时,会依次执行 PREROUTING 链、IP 路由、INPUT 链。
      • 多个规则表中可能存在同一类型的 chain ,此时会按照各个表的优先级顺序,依次执行各个表中的同类型 chain 。
        • 例如执行 OUTPUT 链时,先执行 raw 表中的 OUTPUT 链,然后执行 mangle 表中的 OUTPUT 链,以此类推。
    2. 执行每个 chain 时,会按从上到下的顺序读取每行 rule ,即逐行遍历。
      • 如果某行 rule 匹配数据包,则执行该 rule 的 target 。然后:
        • 如果该 rule 的 target 属于终止式目标(terminating target),包括 ACCEPT、REJECT、DROP、RETURN ,则结束遍历当前 chain ,然后执行父链或下一个规则表中的同类型 chain 。
        • 如果该 rule 的 target 不属于 terminating target ,则继续读取下一行 rule 。
      • 如果某行 rule 不匹配数据包,则继续读取下一行 rule 。
      • 如果该 chain 中的所有 rule 都不匹配数据包,则执行该 chain 的默认动作。
        • 用户创建的 chain 不支持设置默认动作。
        • 默认动作是 chain 的元数据。每个 chain 的默认动作一般为 ACCEPT 。
        • 用户也可以在 chain 最下方设置一行 rule ,匹配所有数据包并执行某动作,从而实现默认动作的效果。

# 命令

iptables
        # 关于 table
        -t filter           # 指定规则表,默认指定 filter 表

        # 关于 chain
        -L [chain]              # --list ,显示 chain 及其中所有 rule ,不指定 chain 则显示当前表的所有 chain
            -n                  # 将 domain 显示成 ip 格式,将 port 从服务名显示成数字格式。否则默认会显示前一种格式
            -v                  # 显示详细信息,比如 pkts、bytes
            -x                  # 显示 pkts、bytes 时精确到个位。否则默认会显示成 K、M、G 等单位
            --line-numbers      # 显示行号(每个 chain 中,每行 rule 有一个行号,从 1 开始递增)
        -S [chain]              # 显示 chain 及其中所有 rule 的创建命令,相当于导出 iptables 配置
        -N <chain>              # --new-chain
        -X [chain]              # --delete-chain ,删除 chain ,但不允许删除内置 chain 、被其它 chain 引用的 chain 、包含 rule 的 chain
        -E <chain> <new_chain>  # --rename-chain ,重命名 chain 。引用该 chain 的其它 rule 会同步到新的 chain 名称
        -F [chain]              # --flush ,清空某个 chain 中的 rule ,不指定 chain 则清空所有 chain
        -P <chain> <target>     # --policy ,设置某个 chain 的默认动作
        -Z [chain] [n]          # 将 chain 中第 n 行 rule 的 pkts、bytes 计数器重置为 0 。不指定 n 则重置整个 chain ,不指定 chain 则重置整个 table

        # 关于 chain 中的 rule
        -A <chain> <rule>       # --append ,附加一行 rule 到最后一行
        -I <chain> [n] <rule>   # --insert ,插入一行 rule 到第 n 行,而原来的第 n 行 rule 会变成第 n+1 行。不指定 n 则插入到第一行
        -R <chain> <n> <rule>   # --replace ,插入一行 rule ,替换原来的第 n 行 rule
        -D <chain> <n>          # --delete ,删除第 n 行 rule
        -D <chain> <rule>       # 删除指定内容的 rule 。这会按 --check 的方式查找 rule ,只会删除第一个找到的 rule 。没找到则报错 iptables: No chain/target/match by that name.
        -C <chain> <rule>       # --check ,检查 chain 中是否存在指定内容的 rule
                                # 这会在 chain 中逐行遍历 rule ,如果某行 rule 与指定 rule 的 match、target 都相同,则命令返回码为 0
                                # 如果没找到相同的 rule ,则命令返回码为 1 ,并报错 iptables: No chain/target/match by that name

        # 关于 rule 中的匹配条件。每个匹配条件之前可加 ! 表示反向匹配
        -p <protocal>       # 数据包采用的网络协议,可以填 tcp、udp、icmp 等。默认值为 all ,表示匹配所有网络协议
        -s <ip>             # --source ,源地址。可以填 ip(可选带掩码)或 domain 格式。如果为 domain 格式,则会解析成 ip 再传给 netfilter ,不会动态 DNS 解析
        -d <ip>             # --destination ,目标地址
        --sport <n>         # 源端口
        --dport <n>         # 目的端口
        -i <name>           # --in-interface ,数据包从哪个网口接收。默认匹配所有网口。可以加 + 后缀作为通配符
        -o <name>           # --out-interface ,数据包发送到哪个网口。默认匹配所有网口
        -m <match>          # 指定一个扩展的匹配条件

        # 关于 rule 中的动作
        -j <target>         # --jump ,表示 rule 对于符合匹配条件的数据包执行什么动作
                            # 可以填内置动作(built-in target)。
                            # 可以填用户创建的 chain 名称,作为子链。这会遍历子链中的每行规则,如果匹配当前数据包则执行,然后回到父链
                            # 可以省略该选项,此时 rule 不会对数据包产生任何影响,但依然会累计 pkts、bytes
        -g <chain>          # --goto ,跳转到子链,且不会回到父链

        # 其它
        -w [n]              # --wait 。修改 iptables 配置之前会自动获取 xtables 锁,如果获取不到,则默认结束命令。使用该选项可阻塞等待 n 秒,不指定 n 则无限等待
  • 例:查看 INPUT 链

    [root@CentOS ~]# iptables -nvL INPUT --line-numbers
    Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
    num   pkts bytes target     prot opt in     out     source               destination
    1     2819  446K ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80
    2    50557  383M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    3        0     0 ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0
    4       53  4960 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate INVALID
    5      484 26644 REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-port-unreachable
    

    各字段含义:

    pkts          # 该 rule 累计匹配的数据包数
    bytes         # 该 rule 累计匹配的数据包总大小
    target        # 该 rule 的动作
    prot          # 该 rule 匹配的网络协议
    opt           # 该 rule 的选项
    in            # 该 rule 匹配哪个网口接收的数据包
    out           # 该 rule 匹配哪个网口发送的数据包
    source        # 该 rule 匹配哪个源地址的数据包
    destination   # 该 rule 匹配哪个目标地址的数据包
    
  • 例:修改 rule

    iptables -N chain1     # 创建一个链
    iptables -X chain1     # 删除一个链
    
    iptables -P chain1 DROP                                           # 设置 chain1 链的默认动作为 DROP
    iptables -A chain1 -p icmp -j REJECT                              # 在 chain1 链尾部附加一行规则:如果数据包采用 ICMP 协议,则拒绝
                                  --reject-with icmp-port-unreachable # REJECT 动作默认会回复一个表示拒绝访问的 ICMP 数据包
                                  --reject-with tcp-reset             # 也可回复其它类型的数据包
    iptables -A chain1 -p tcp --dport 80 -j ACCEPT                    # 如果数据包采用 TCP 协议,且目标端口为 80 ,则放行
    iptables -I chain1 -p tcp -s 10.0.0.0/24 --dport 22 -j ACCEPT     # 在 chain1 链头部插入一行规则:如果数据包采用 TCP 协议,且源地址、目标端口符合条件,则放行
    iptables -I chain1 -m state --state RELATED,ESTABLISHED -j ACCEPT # 如果数据包关联、属于已有的网络连接,则放行。这是有状态防火墙的常见功能
    
    • 指定多个端口的语法:
      --dport 80:90                     # 指定一组连续的端口
      --match multiport --dport 80,443  # 指定多个端口,用逗号分隔
      
  • 用 iptables 命令修改的防火墙规则,在主机重启时会丢失。

    • 为了持久保存配置,可以将 iptables 的当前配置保存到 /etc/sysconfig/iptables 文件中,每次主机重启时会自动导入该文件。
      iptables-save    > /etc/sysconfig/iptables    # 导出
                    -c    # 增加导出每行规则的 pkts、bytes
      iptables-restore < /etc/sysconfig/iptables    # 导入
      
    • iptables-save 导出的配置采用特定格式,只能被 iptables-restore 识别。如果想导出可执行的命令,可执行 iptables -t filter -S

# nat

  • 例:给 nat 表添加规则
    # 采用 REDIRECT 动作。如果网口发送的数据包指向该 ip、port ,则重定向到 80 端口
    iptables -t nat -A OUTPUT     -p tcp -d 10.0.0.0/24 --dport 80 -j REDIRECT --to-ports 80
    # 在 OUTPUT 链执行 DNAT 动作,影响本机生成并发送的数据包
    iptables -t nat -A OUTPUT     -p tcp -d 10.0.0.0/24 --dport 80 -j DNAT --to-destination 10.0.0.1:80
    # 在 PREROUTING 链执行 DNAT 动作,影响本机接收的数据包
    iptables -t nat -A PREROUTING -p tcp -d 10.0.0.0/24 --dport 80 -j DNAT --to-destination 10.0.0.1:80
    
    # 以下规则是进行负载均衡:将 50% 的数据包转发给 10.0.0.1:80 ,剩下的数据包转发给 10.0.0.2:80
    iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.0.0.1:80 -m statistic --mode random --probability 0.5
    iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.0.0.2:80
    
    # 在 POSTROUTING 链执行 SNAT 动作
    iptables -t nat -A POSTROUTING -p tcp -s 10.0.0.1 -j SNAT --to-source 10.0.0.2
    

# log

  • 例:对数据包记录日志
    iptables -t mangle -I OUTPUT -p icmp -d 127.0.0.1 -j LOG --log-level info --log-prefix '<iptables log mangle.OUTPUT> '
    iptables -t mangle -I INPUT  -p icmp -d 127.0.0.1 -j LOG --log-level info --log-prefix '<iptables log mangle.INPUT > '
    
    查看日志:
    [root@CentOS ~]# ping 127.0.0.1 -c 1
    [root@CentOS ~]# tail -f /var/log/messages | grep '<iptables log'
    Dec 12 12:00:01 [localhost] kernel: <iptables log mangle.OUTPUT> IN= OUT=lo SRC=127.0.0.1 DST=127.0.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=62883 DF PROTO=ICMP TYPE=8 CODE=0 ID=10362 SEQ=1
    Dec 12 12:00:01 [localhost] kernel: <iptables log mangle.INPUT > IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=62883 DF PROTO=ICMP TYPE=8 CODE=0 ID=10362 SEQ=1
    Dec 12 12:00:01 [localhost] kernel: <iptables log mangle.OUTPUT> IN= OUT=lo SRC=127.0.0.1 DST=127.0.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=62884 PROTO=ICMP TYPE=0 CODE=0 ID=10362 SEQ=1
    Dec 12 12:00:01 [localhost] kernel: <iptables log mangle.INPUT > IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=62884 PROTO=ICMP TYPE=0 CODE=0 ID=10362 SEQ=1
    
    • 前两行日志表示 lo 网口发出一个 ICMP 请求包,然后被自己接收,数据包 ID 为 62883 。
    • 后两行日志表示 lo 网口发出一个 ICMP 响应包,然后被自己接收,数据包 ID 为 62884 。
    • 各字段的含义:
      IN    # 该数据包从哪个网口接收。如果数据包是本机生成并发送的,则该字段为空
      OUT   # 该数据包从哪个网口发出。如果数据包是被本机接收的,则该字段为空
      MAC   # 一串十六进制数,由三个字段拼凑而成:数据包的源 MAC 地址(12 字节)、目标 MAC 地址(12 字节)、以太网帧的类型(4 字节)
      SRC   # 数据包的源 IP 地址
      DST   # 数据包的目标 IP 地址
      LEN   # 数据包的长度,单位为字节
      TOS   # Type Of Service ,服务类型
      PREC  # Precedence ,数据包的优先级
      TTL   # 数据包剩下的生存时间
      ID    # 数据包的 ID 。如果一个 IP 数据包分成多个片段(fragment),则这些片段拥有相同的 ID
      DF    # Don't Fragment ,表示该数据包没有分成多个片段
      MF    # More Fragments following ,表示该数据包分成了多个片段
      SEQ   # 数据包的序列号。例如 ping 命令发送多个 ICMP 包时,序列号从 1 开始递增
      

# ipset

  • 如果需要对多个 ip 执行相同的防火墙规则,例如:

    iptables -I INPUT -p tcp -s 10.0.0.1 -j ACCEPT
    iptables -I INPUT -p tcp -s 10.0.0.2 -j ACCEPT
    iptables -I INPUT -p tcp -s 10.0.0.3 -j ACCEPT
    

    则可用 ipset 命令创建一个哈希集合,包含多个 ip :

    ipset create ipset1 hash:net
    ipset add ipset1 10.0.0.1
    ipset add ipset1 10.0.0.2
    ipset add ipset1 10.0.0.3
    
    iptables -I INPUT -p tcp -m set --match-set ipset1 src -j ACCEPT
    

    优点:

    • 方便管理多个 ip ,简化防火墙规则。
    • 使用哈希集合时,比线性查找的效果更高。
  • ipset 命令的语法:

    ipset list [set]          # 列出所有集合的信息,或者指定一个集合
    ipset create <set> <type> # 创建一个集合
    ipset destroy <set>       # 删除一个集合
    
    ipset add <set> <member>  # 在集合中添加元素
    ipset del <set> <member>  # 在集合中删除元素
    
    ipset save       > /etc/ipset.conf  # 导出配置
    ipset restore -! < /etc/ipset.conf  # 导入配置,并忽略重复项
    

# nftables

  • 相关历史:

    • 2008 年,netfilter 开发团队公布了 nftables 项目,它是一个基于 netfilter 的网络框架,旨在代替 xtables 工具集与 netfilter 交互,从而提高效率。
      • 之前的 Linux 中,防火墙前端为 xtables 工具集,后端为 netfilter 。
      • 改用 nftables 时,前端为 nft 命令,后端为 nftables 。而 nftables 的后端为 netfilter 。
      • 不过大部分用户依然更习惯使用 xtables 工具集。
    • 2014 年,Linux 3.13 内核发布,添加了 nftables 框架,包括内核模块 nf_tables 等。
  • 与 xtables 相比,nftables 的优点:

    • 命令语法简单了些,但依然复杂。

    • 采用链表结构,因此修改更快。

      • xtables 命令每次修改防火墙规则时,需要从内核获取全部规则,是一块二进制 blob 。在用户态修改之后,再全部传给内核。因此规则越多,耗时越久,时间复杂度为 O(n) 。
      • 而 nftables 采用链表结构存储所有规则,修改规则的时间复杂度为 O(1) 。
      • 例如 k8s 中用 iptables 配置了大量防火墙规则,还需要经常修改,明显耗时更久。
    • 不需要获取锁,因此修改更快。

      • 为了避免同时执行多个 xtables 进程时发生冲突。每个 xtables 进程在修改防火墙规则之前,需要先获取 /run/xtables.lock 这个文件锁。如果未获取到锁,则阻塞等待。
      • 而 nftables 不需要获取锁。
    • 减少了规则表、规则链,因此查找更快。

      • iptables 内置了 5 个规则表,每个表中内置了多个规则链。即使这些规则链为空,处理数据包时也需要读取这些规则链,增加了轻微耗时。
      • 而 nftables 只内置了 filter 表。
    • 原生支持创建哈希集合,使得部分查找更快。

      • iptables 原生不支持创建哈希集合。可用 ipset 命令创建 ip 的集合,但不支持创建 port 的集合。
      • 而 nftables 原生支持创建哈希集合,例如:
        # 创建一个集合,包含多个 ip
        nft add element ip filter ipset1 {10.0.0.1,10.0.0.2}
        nft add rule ip filter output ip daddr != @ipset1 accept
        
        # 创建一个集合,包含多个 port
        nft add rule ip filter input tcp dport {80,443} accept
        
      • nftables 还支持创建 map 映射表,例如:
        # 将访问 81、82 端口的数据包,分别 DNAT 转发到不同 ip
        nft add rule ip nat prerouting dnat to tcp dport map {81:10.0.0.1,82:10.0.0.2}
        
      • 总之:
        • xtables 每次处理数据包时需要遍历所有防火墙规则,找到与当前数据包匹配的规则,属于线性查找,时间复杂度为 O(n) 。规则越多,查找的耗时越久,单位时间内收发数据包的吞吐量越低。
        • nftables 基本上也是线性查找,但部分查找可用哈希集合加速,让时间复杂度变为 O(1) 。
        • 例如 k8s 中,经常会对多个 ip 执行相同防火墙规则,因此用哈希集合能大幅提高查找速度。
    • 默认未启用计数器,降低了少量开销。

      • iptables 对每个 chain 和 rule 启用了 pkts、bytes 计数器,且不能禁用。
      • 而 nftables 默认未启用计数器,需要用户主动启用。
    • 配置针对 ipv6 的规则时,更方便。

      • iptables 命令只能处理 ipv4 流量,用户需要将 iptables 配置的防火墙规则同步到 ip6tables 命令,才能处理 ipv6 流量。
      • 而 nft 命令可以创建一个 family 为 inet 的 table ,同时处理 ipv4、ipv6 流量。
    • 升级软件版本更容易。

      • xtables 的大部分代码集成在 Linux 内核中,修改代码时需要重新编译内核,因此难以修改。
      • 而 nftables 的大部分代码在 Linux 内核之外,工作在用户态,因此便于升级 nftables 的软件版本。
  • 用户可以同时使用 iptables (后端为 netfilter )和 firewall-cmd (后端为 nftables )两个命令来配置防火墙规则,但它们配置的规则冲突时,优先级不明。因此建议不要同时使用,而是采用以下做法之一:

    • 只用 iptables 命令配置防火墙,停止或卸载 firewalld 进程。
    • 只用 firewalld-cmd 命令配置防火墙,使用 iptables 命令时仅涉及读取操作。

# 命令

  • nftables 提供了一个 nft 命令来配置防火墙规则。命令语法:
    nft list   tables                   # 显示所有 table
    nft list   table [family] <table>   # 只显示指定的 table
    nft add    table [family] <table>
    nft delete table [family] <table>
    
    nft list   chains                   # 显示所有 table ,以及每个 table 中的所有 chain ,每个 chain 中的所有 rule
    nft list   chain [family] <table> <chain>
    nft add    chain [family] <table> <chain> [priority]
    nft delete chain [family] <table> <chain>
    
    nft add    rule  [family] <table> <chain> statement [comment]
    nft delete rule  [family] <table> <chain> statement
    
    • family 是指网络协议栈,可以取值为 ip、ip6、inet、arp 等。
      • 不指定 family 时,默认值为 ip ,表示 ipv4 。
      • inet 是指 ipv4、ipv6 的合集。
      • 每个 table 只能处理一种 family 的流量。
    • priority 是指优先级,从高到低排列如下:
      raw
      mangle
      dstnat
      filter
      security
      srcnat
      
    • 例如:
      nft add rule ip filter output ip daddr 1.2.3.4 drop
      
      相当于:
      iptables -A OUTPUT -d 1.2.3.4 -j DROP
      

# 迁移

  • 为了方便用户从 xtables 迁移到 nftables ,nftables 提供了 xtables-nft 工具集,包含多个命令行工具:
    iptables-nft
    iptables-nft-save
    iptables-nft-restore
    ip6tables-nft
    ip6tables-nft-save
    ip6tables-nft-restore
    arptables-nft
    ebtables-nft
    
    • 这些命令的语法兼容 xtables 工具集,但配置的防火墙规则会传给 nftables ,而不是 netfilter 。
  • nftables 还提供了以下命令,用于将 xtables 语法的命令,转换成 nft 语法的命令。
    iptables-translate
    ip6tables-translate
    iptables-restore-translate -f <file>    # 读取 iptables 导出的配置文件,翻译并打印到终端
    ip6tables-restore-translate -f <file>
    
    例:
    [root@CentOS ~]# iptables-translate -t filter -A chain1 -p tcp --dport 80 -j ACCEPT
    nft add rule ip filter chain1 tcp dport 80 counter accept
    
  • 用户可执行 xtables-nft-multi 命令,以子命令的方式调用上述命令,例如:
    xtables-nft-multi iptables-nft -L
    
  • RHEL 8 发行版弃用了 xtables 工具集。当用户执行 iptables 命令时,实际上是执行 iptables-nft 命令。用户可执行以下命令,检查后端是否为 nf_tables :
    [root@CentOS ~]# iptables -V
    iptables v1.8.4 (nf_tables)
    

# firewalld

  • firewalld 是 Red Hat 公司开发的一款防火墙软件。

    • GitHub (opens new window)
    • 于 2011 年发布,采用 Python 语言开发。
    • firewalld 防火墙的前端命令为 firewalld-cmd 。后端默认为 nftables ,也可改为 netfilter 。
    • RHEL 7 发行版中,默认的防火墙软件从 iptables 改为了 firewalld 。
  • 优点:

    • 简化了配置防火墙的工作量。
  • 缺点:

    • 只是擅长配置一些常用的防火墙规则,不如 nft、iptables 的功能多。
  • 使用 firewalld 的步骤:

    1. 安装并运行 firewalld 进程。
      yum install firewalld
      systemctl start firewalld
      systemctl enable firewalld
      
    2. 用 firewalld-cmd 命令配置防火墙规则。

# zone

  • firewalld 引入了区域(zone)的设定,每个 zone 包含一组防火墙规则。

    • 同时只能启用一个 zone 。
    • 用户可以在各个 zone 中配置不同的防火墙规则,然后通过切换 zone 来快速切换防火墙规则。
    • 各个 zone 的配置文件保存在 /etc/firewalld/zones/ 目录下,采用 XML 文件格式。
  • 常用的 zone 举例:

    • trusted :允许接收所有数据包。
    • home 、internal :只允许接收 ssh、mdns、samba-client、dhcpv6-client 服务的数据包。
    • work 、public :只允许接收 ssh、dhcpv6-client 服务的数据包。
    • external 、dmz :只允许接收 ssh 服务的数据包。
    • block 、drop :不接收外部发来的数据包。
  • 所有的 zone 默认都允许本机发送数据包到任何目标地址,允许接收本机发送的数据包,只是不一定接收外部发送的数据包。

# 命令

firewall-cmd
            --get-zones                 # 显示所有 zone 的名称
            --get-active-zones          # 显示当前启用的 zone
            --get-default-zone          # 显示默认的 zone
            --set-default-zone=public   # 设置默认的 zone

            --get-zone-of-interface=eth0            # 显示指定网口采用的 zone
            --change-interface=eth0 --zone=public   # 修改指定网口采用的 zone

            [--zone=public]                         # 指定一个 zone 。不指定则选择默认的 zone
                          --list-all                # 显示 zone 内的防火墙规则
                          --add-service=http        # 启用 http 服务,即接收发向本机 TCP 80 端口的数据包
                          --remove-service=http     # 禁用 https 服务,即拒绝发向本机 TCP 443 端口的数据包
                          --add-port=80/tcp         # 接收发向本机 TCP 80 端口的数据包
                          --add-port=80-82/tcp      # 开放一组端口
                          --remove-port=80/tcp      # 拒绝接收

            --permanent                             # 将配置写入配置文件(从而会永久保存,但是需要 reload 或重启才会影响运行时配置)
            --runtime-to-permanent                  # 将运行时的全部配置写入配置文件
            --reload                                # 重新加载配置文件,这会覆盖运行时的全部配置
            --complete-reload                       # 完全重新加载,这会重置 nf_conntrack 状态信息,中断所有网络连接

            --panic-on                              # 开启 panic 模式,拒绝接收所有数据包
            --panic-off                             # 关闭 panic 模式
            --query-panic                           # 检查是否开启了 panic 模式
  • 使用 --add-service 的效果与 --add-port 一样,只是标明了开放的端口是被哪个服务使用。

  • 例:

    [root@CentOS ~]# firewall-cmd --list-all
    public (active)
      target: default                                   # 处理数据包的默认动作
      icmp-block-inversion: no
      interfaces: eth0                                  # 只处理该网口收发的数据包
      sources:
      services: ssh dhcpv6-client                       # 允许接收哪些服务的数据包
      ports: 3000/tcp
      protocols:
      masquerade: yes
      forward-ports: port=80:proto=tcp:toaddr=10.0.0.2  # 端口转发的规则
      source-ports:
      icmp-blocks:
      rich rules:
    
  • 通过 rich-rule 可以配置功能更丰富的防火墙规则,如下:

    firewall-cmd [--zone=public]
                    --add-rich-rule='rule family=ipv4 port port=22 protocol=tcp accept'
                    --add-rich-rule='rule family=ipv4 source address=10.0.0.2 port port=80 protocol=tcp accept'
                    --remove-rich-rule='...'
    
    • 指定 IP 地址时,可以加上子网掩码,比如 "10.0.0.1/32" 。

# 端口转发

  • 端口转发(port forwarding)是一种高级的 NAT 功能,将本机某个端口接收的数据包转发到指定 ip、port ,相当于 Nginx 的反向代理。

    • 假设让本机的某个端口反向代理主机 A 的某个端口,供主机 B 访问。工作原理如下:
      • 当主机 B 发送数据包到本机端口时,会执行 DNAT ,将数据包的 dst_ip 改为主机 A 、src_ip 改为本机。然后将数据包转发给主机 A 。
        • 如果只执行 DNAT 修改数据包的 dst_ip ,则数据包的 src_ip 依然是主机 B ,因此主机 A 回复数据包时会发送给主机 B ,而不是本机。
      • 当主机 A 回复数据包到本机时,会执行 SNAT ,将数据包的 dst_ip 改为主机 B 、src_ip 改为本机。然后将数据包转发给主机 B 。
    • 端口转发是防火墙的操作,发生在 Socket 接收数据包之前。假设一个进程监听 TCP 80 端口,则可以将 80 端口收到的所有数据包转发到别处,不传给该进程。
    • 端口转发的 dst_ip ,可以是当前网口,或者其它网口、其它主机。但后者需要满足以下条件:
      • 开启内核参数,允许跨网口转发数据包:
        sysctl net.ipv4.ip_forward=1
        echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
        
      • 用 firewall-cmd 时,需要开启 masquerade 伪装功能,在转发数据包时保留 src_ip ,否则会使用本机 IP 作为 src_ip 。该功能与 SNAT 相似。
  • 如果用 iptables 命令实现端口转发功能,则需要配置多条规则。例如:

    # 将本机 10.0.0.1:80 端口接收的数据包,转发到 10.0.0.2:22
    iptables -t nat -A PREROUTING   -p tcp -d 10.0.0.1 --dport 80 -j DNAT --to-destination 10.0.0.2:22
    # PREROUTING 链只影响本机接收的数据包,而 OUTPUT 链影响本机生成并发送的数据包
    iptables -t nat -A OUTPUT       -p tcp -d 10.0.0.1 --dport 80 -j DNAT --to-destination 10.0.0.2:22
    # 转发的同时,将数据包的 src_ip 改为本机 ip
    iptables -t nat -A POSTROUTING  -p tcp -d 10.0.0.2 --dport 22 -j SNAT --to-source 10.0.0.1
    
  • 如果用 firewall-cmd 实现端口转发功能,则只需配置一条规则。例如:

    firewall-cmd [--zone=public]
                  --add-forward-port=port=80:proto=tcp:toport=22                  # 将本机 TCP 80 端口收到的数据包转发到 22 端口
                  --add-forward-port=port=80:proto=tcp:toaddr=10.0.0.2            # 将 TCP 80 端口收到的数据包转发到另一个主机的相同端口
                  --add-forward-port=port=80:proto=tcp:toport=80:toaddr=10.0.0.2  # 将 TCP 80 端口收到的数据包转发到另一个主机的 80 端口
                  --remove-forward-port=...                                       # 取消端口转发
    
                  --add-masquerade     # 开启伪装
                  --remove-masquerade  # 取消伪装
    
                  --add-rich-rule="rule family=ipv4 source address=10.0.0.3 forward-port port=80 protocol=tcp to-port=80 to-addr=10.0.0.2"  # 允许接收数据包,并进行端口转发
    
    • 缺点:只能影响本机接收的数据包,不能影响本机生成并发送的数据包。因为 firewall-cmd 的端口转发,仅相当于 iptables 的 nat 表的 PREROUTING 链,没有实现 OUTPUT 链的功能。为了解决该问题,可添加一条 iptables 语法的规则:
      firewall-cmd --direct --add-rule ipv4 nat OUTPUT 0 -p tcp -d 10.0.0.1 --dport 80 -j DNAT --to-destination 10.0.0.2:22