玩转 NFTables

2021年3月17日 | Lars Vogdt 和 Darix | CC-BY-SA-3.0

Playing along with NFTables

默认情况下,openSUSE Leap 15.x 使用 firewalld 防火墙实现(并且 firewalld 后端在底层使用 iptables)。

但是,openSUSE 已经有一段时间支持 nftables 了——但 YaST 或其他专用工具目前尚未配置为直接支持它。但我们有一些机器在我们的基础设施中,它们既不是直接的桌面机器,也不是大部分时间处于空闲状态。所以让我们尝试一下我们有多好,可以尝试和测试新事物,并使用我们中央管理机器之一:VPN 网关,它使所有 openSUSE 英雄都可以访问 openSUSE 基础设施的内部世界。

这台机器已经有点特殊了

  • “外部”接口持有与互联网的连接
  • “私有”接口位于 openSUSE 英雄私有网络内部
  • 我们运行 openVPN,使用 tun 设备(一个用于 udp,一个用于 tcp),以允许 openSUSE 英雄通过个人证书 + 他们的用户凭据连接
  • 此外,我们运行 wireguard 以连接普罗沃和纽伦堡(在我们的赞助商处)的私有网络
  • 而且在我们忘记之前:我们的 VPN 网关不仅是 VPN 网关:它还被用作所有内部机器的互联网网关,只允许“预知流量”目的地

所有这些都使防火墙设置变得更加复杂。

顺便说一下:通过给接口命名,例如在我们的示例中“external”或“private”,如果你使用服务或防火墙,这将带来巨大的好处。只需查看一下 /etc/udev/rules.d/70-persistent-net.rules 你的设备启动后,并根据你的需要重命名它们(你也可以使用 YaST 来完成此操作)。但请记住也要检查/重命名 */etc/sysconfig/network/ifcfg-** 中的接口,以使用相同的名称,然后再重新启动机器。否则你将陷入无法工作的网络设置。

让我们简要了解一下我们所说的领域

{width: 80%}openSUSE Heroes gateway

正如你可能注意到的,社区侧面的任何服务都不会受到影响。我们拥有基于标准(iptables)的防火墙,并使用代理将用户请求转发到正确的服务器。

在 openSUSE 英雄侧,我们用基于 nftables 的新设置取代了旧的 SuSEfirewall2 设置。

有几个原因影响了我们切换到 nftables

  • 旧的 SuSEfirewall2 正常工作,但在我们的问题机器上生成了一个巨大的 iptables 列表
  • 使用 ipsets 或变量与 SuSEfirewall2 是可行的,但不是一件容易的任务
  • 我们在使用 firewalld 作为前端时遇到了一些 NAT 和 Masquerading 问题
  • Salt 是另一个有趣的领域
    • 通过在机器上部署一些文件来 Salt'ing SuSEfirewall2 总是可能的,但并不是很直接
    • 没有适用于 SuSEfirewall2 的 Salt 模块(可能永远也不会有)
    • 有适用于 firewalld 和 nftables 的 Salt 模块,两者水平几乎相同
  • nftables 已经集成到内核中一段时间了,应该长期取代所有 *tables 模块。所以为什么不直接跳转到它,因为(作为管理员)我们反正不使用 YaST 或 firewalld-gui 等 GUI 工具?

那么主要优势是什么?

  1. 集合是核心功能的一部分。你可以拥有端口、接口名称和地址范围的集合。不再需要 ipset。不再需要 multiport。 ip daddr { 1.1.1.1, 1.0.0.1 } tcp dport { dns, https } oifname { "external", "wg_vpn1" } accept; 这意味着你可以用几个规则覆盖大量的防火墙集合。
  2. 不再需要额外的规则来记录。只需在需要时打开计数器。 counter log prefix "[nftables] forward reject " reject
  3. 当使用 table inet 时,你可以用单个规则集覆盖 IPv4 和 IPv6,但你也可以拥有每个 IP 协议的表。有时甚至需要它们,例如用于后路由。

从头开始

一个非常基本的 /etc/nftables.conf 看起来像这样

#!/usr/sbin/nft -f

flush ruleset

# This matches IPv4 and IPv6
table inet filter {
    # chain names are up to you.
    # what part of the traffic they cover, 
    # depends on the type line.
	chain input {
		type filter hook input priority 0; policy accept;
	}
	chain forward {
		type filter hook forward priority 0; policy accept;
	}
	chain output {
		type filter hook output priority 0; policy accept;
	}
}

但到目前为止,我们还没有停止或允许任何流量。好吧,实际上我们现在允许所有流量进出,因为所有链都具有接受策略。

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain base_checks {
        ## another set, this time for connection tracking states.
        # allow established/related connections
        ct state {established, related} accept;

        # early drop of invalid connections
        ct state invalid drop;
    }

   	chain input {
		type filter hook input priority 0; policy drop;
        
        # allow from loopback
        iif "lo" accept;

        jump base_checks;

        # allow icmp and igmp
        ip6 nexthdr icmpv6 icmpv6 type { echo-request, echo-reply, packet-too-big, time-exceeded, parameter-problem, destination-unreachable, packet-too-big, mld-listener-query, mld-listener-report, mld-listener-reduction, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept;
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } accept;
        ip protocol igmp accept;
        
        # for testing reject with logging
        counter log prefix "[nftables] input reject " reject;
	}
    chain forward {
		type filter hook forward priority 0; policy accept;
	}
	chain output {
		type filter hook output priority 0; policy accept;
	}
}

你可以使用 nft --file nftables.conf 激活配置,但不要在远程机器上这样做。在实际加载文件之前运行 nft --check --file nftables.conf 以捕获语法错误也是一个好习惯。

那么我们改变了什么?

  1. 最重要的是,我们将链的策略更改为 drop,并在末尾添加了一个 reject 规则。所以现在没有任何东西可以进入。
  2. 我们允许所有流量在 localhost 接口上。
  3. base_checks 链处理与已建立连接相关的所有数据包。这确保了来自传出连接的传入数据包能够通过。
  4. 我们允许重要的 ICMP/IGMP 数据包。同样,这是使用集合和类型名称而不是一些神秘的数字。为可读性欢呼。

现在,如果有人尝试通过 ssh 连接到我们的机器,我们会看到

[nftables] input reject IN=enp1s0 OUT= MAC=52:54:00:4c:51:6c:52:54:00:73:a1:57:08:00 SRC=172.16.16.2 DST=172.16.16.30 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=22652 DF PROTO=TCP SPT=55574 DPT=22 WINDOW=64240 RES=0x00 SYN URGP=0

并且 nft list ruleset 将向我们显示

counter packets 1 bytes 60 log prefix "[nftables] input reject " reject

所以我们现在是安全的。虽然也许允许 SSH 重新进入会很好。你知道,以防万一。我们现在有两个选择。选项 1 将是在我们的 reject 行之前插入以下行。

tcp dport 22 accept;

但是我们已经提到过我们有集合,它们很棒了吗?特别是当我们需要在多个地方使用相同的端口/IP 范围/接口名称时?

我们有两种定义集合的方式

define wanted_tcp_ports {
  22,
}

是的,尾随逗号是可以的。并且它使向列表中添加元素更容易。所以我们一直这样做。这将改变我们上面的规则为

tcp dport $wanted_tcp_ports accept;

如果加载配置文件并运行 nft list ruleset,我们会看到

tcp dport { 22 } accept

但实际上有一种稍微更好的方法来做到这一点

    set wanted_tcp_ports {
        type inet_service; flags interval;
        elements = {
           ssh
        }
    }

这样我们的防火墙规则就变成了

tcp dport @wanted_tcp_ports accept;

如果我们使用 nft list ruleset 之后转储防火墙,它仍然会显示为 @wanted_tcp_ports,并且不会用值替换变量。虽然这已经很棒了,但第二种语法实际上还有一个优势。

$ nft add element inet filter wanted_tcp_ports \{ 443 \}

现在我们的 wanted_tcp_ports 列表将允许端口 22 和 443。这当然更有效,如果我们用它来使用 IP 地址。

    set fail2ban_hosts {
        type ipv4_addr; flags interval;
        elements = {
           192.168.0.0/24
        }       
    }

让我们向该集合添加一些元素

$ nft add element inet filter fail2ban_hosts \{ 192.168.254.255, 192.168.253.0/24 \}
$ nft list ruleset

… 并且我们得到…

        set fail2ban_hosts {
                type ipv4_addr
                flags interval
                elements = { 192.168.0.0/24, 192.168.253.0/24,
                             192.168.254.255 }
        }

现在我们可以更改 fail2ban 以向集合中添加元素,而不是为它想要阻止的每个新机器创建一个新规则。更少的规则。更快的处理。

但是,在重新加载防火墙时,我们从端口列表中删除了端口 443。糟糕。不过……如果你对规则感到满意。你只需要运行

$ nft list ruleset > nftables.conf

当你使用所有集合而不是变量时,所有的防火墙规则仍然会看起来不错。

我们的完整防火墙看起来像

table inet filter {
        set wanted_tcp_ports {
                type inet_service
                flags interval
                elements = { 22, 443 }
        }

        set fail2ban_hosts {
                type ipv4_addr
                flags interval
                elements = { 192.168.0.0/24, 192.168.253.0/24,
                             192.168.254.255 }
        }

        chain base_checks {
                ct state { established, related } accept
                ct state invalid drop
        }

        chain input {
                type filter hook input priority filter; policy drop;
                iif "lo" accept
                jump base_checks
                ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept
                ip protocol icmp icmp type { echo-reply, destination-unreachable, echo-request, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept
                ip protocol igmp accept
                tcp dport @wanted_tcp_ports accept
                counter packets 12 bytes 828 log prefix "[nftables] input reject " reject
        }

        chain forward {
                type filter hook forward priority filter; policy accept;
        }

        chain output {
                type filter hook output priority filter; policy accept;
        }
}

更多信息请参见 nftables wiki

分享此帖子