动态加速 — Nftables 研究

网络加速有两种途径

  • 一种是从传输带宽入手, 即对传输协议本身进行挖掘, 比如单边加速BBR, Serverspeeder, 以及双边加速KCP, FinalSpeed. 这些对于带宽资源丰富的通讯双方, 效果还是比较明显的.
  • 另外一种, 是从传输延迟, 也就是链路路径上入手. 通过自定义链路中转点而寻找最近路径. 比如大陆到欧洲, 可以通过沿海光缆, 也可以用通过北京到德国 电信CN2 节点, 而后面一种明显存在很大的延差空间.

本文依赖于第二种思路, 寻找到最佳链路后, 通过 Nftables的端口转发技术, 将流量进行跨国传输. 关于NFtables可以参考官方的 wiki 以及本站在去年撰写的 [Nftables初探]1. 在这篇文章里, 会着重介绍 Nftables 在NAT方面使用的方法。

Netfilter 架构

说到 nftables/Iptables 避不开 Netfilter 数据包处理的架构。

                               XXXXXXXXXXXXXXXXXX
                             XXX     Network    XXX
                               XXXXXXXXXXXXXXXXXX
                                       +
                                       |
                                       v
 +-------------+              +------------------+
 |table: filter| <---+        | table: nat       |
 |chain: INPUT |     |        | chain: PREROUTING|
 +-----+-------+     |        +--------+---------+
       |             |                 |
       v             |                 v
 [local process]     |           ****************          +--------------+
       |             +---------+ Routing decision +------> |table: filter |
       v                         ****************          |chain: FORWARD|
****************                                           +------+-------+
Routing decision                                                  |
****************                                                  |
       |                                                          |
       v                        ****************                  |
+-------------+       +------>  Routing decision  <---------------+
|table: nat   |       |         ****************
|chain: OUTPUT|       |               +
+-----+-------+       |               |
      |               |               v
      v               |      +-------------------+
+--------------+      |      | table: nat        |
|table: filter | +----+      | chain: POSTROUTING|
|chain: OUTPUT |             +--------+----------+
+--------------+                      |
                                      v
                               XXXXXXXXXXXXXXXXXX
                             XXX    Network     XXX
                               XXXXXXXXXXXXXXXXXX

# Reference: https://wiki.archlinux.org/index.php/Iptables

对于NAT端口映射来说, 我们需要关心的链路主要是

  • PREROUTING (注意所有进入系统的数据包都会经过这里的匹配处理逻辑)
  • FORWARD
  • POSTROUTING (注意所有系统出去的数据包都会经过这里的匹配处理逻辑)

对比 iptables, nftables增加了 map/set 数据结构, 使用起来就类似 iptables + ipset.

Nftables-Nat 笔记要点

Chain优先级

在iptables里, 固定有五个表 raw, filter, nat, mangle, security, 在nftables放开了这个限制, 表只是作为存放chain 的容器, chain 的类型以及钩子(hook) 决定了规则的适应范围, 而且在不同的table里面, chain的名字是可以相同的。关于多个chain, 如果类型以及hook相同, 优先级有以下规律

  • filter 中的input/ouput/forward链, 根据 priority 以及添加顺序 叠加, 越低越先处理。

    The priority is important since it determines the ordering of the chains, thus, if you have several chains in the input hook, you can decide which one sees packets before another.

  • 其他chain, priority 越低优先级越高, 且直接覆盖, 不叠加。

    也就是说, 除了filter类型的chain,规则都是没有叠加的。

RTT重置

对于链路中转节点来说, 数据包经过nftables转发后, rtt 会出现不足而在公网上被丢弃。因此重置转发数据包的rtt 也是必须的

	chain forward {
		type filter hook forward priority 0; policy accept;
		ip ttl set 128
	}

多端口汇聚

在 iptables-nat上, 我们创建 prerouting 规则, 会这么写

-A PREROUTING -p tcp --dport 10000 -j DNAT --to-destination 8.8.8.8:80
-A PREROUTING -p tcp --dport 20000 -j DNAT --to-destination 9.9.9.9:80
....

当然 Nftables 也可以这么用, 但是使用map 写法会更加灵活

table ip nat {
	map sport_po {
		type inet_service : inet_service
		elements = { 10000 : 80, 20000 : 80}
	},
	map sport_ip {
		type inet_service : ipv4_addr
		elements = { 10000 : 8.8.8.8, 20000 : 9.9.9.9}
	},
	chain prerouting {
		type nat hook prerouting priority 100; policy accept;
		dnat to udp dport map @sport_ip:udp dport map @sport_po
		dnat to tcp dport map @sport_ip:tcp dport map @sport_po
	}
}

如果需要增加删除端口规则,只需修改 @sport_po, @sport_ip 这两个映射map就可以了。

DNat 负载均衡

使用map 方便了端口转发的管理, 但是由于map数据结构中 key 的唯一性, 如果需要实现多链路均衡转发,则必须另辟蹊径。下面用dnat作为例子。

  • 在 Linux kernel 4.14 之前的版本, 可以通过 数据包轮询(Round-Robin)匹配规则来实现均衡。注意这里的规则是对新请求(未记录conntrack)才生效, 如果已经建立了连接,通讯双方的数据包会根据conntrack规则进行转发。

    tables ip nat {
       chain prerouting {
        type nat hook prerouting priority 100; policy accept;
        udp dport 10000 dnat to numgen inc mod 2 map { 0 : 8.8.8.8, 1 : 9.9.9.9 }:80 # 1:1 均衡
        udp dport 11000 dnat to numgen inc mod 10 map { 0-7 : 8.8.8.8, 8-9 : 9.9.9.9 }:80 # 按比重均衡
        udp dport 11000 dnat to 8.8.8.8 : numgen inc mod 10 map { 0-7 : 80, 8-9 : 90 } # 按端口比重均衡
        udp dport 11001 dnat to 8.8.8.8 : numgen inc mod 10 offset 10 map { 10-17 : 80, 18-19 : 90 } # 带offset 端口比重均衡
    
        dnat to numgen inc mod 2 map { 0 : 8.8.8.8, 1 : 9.9.9.9 }
       }
    }
    
  • 在 Linux kernel 4.14 以及之后的内核版本, 可以通过 jhash 一致性哈希方法来实现, 优点是固定流量方向

    tables ip nat {
       chain prerouting {
        type nat hook prerouting priority 100; policy accept;
        udp dport 10000 dnat to jhash ip saddr . udp sport mod 2 map { 0 : 8.8.8.8, 1 : 9.9.9.9 }:80
       }
    }
    

    一般来说, 进行Dnat的节点会同时进行Snat,充当转发地址的 “网关”。但是如果为了获取更高性能,可以使用 DSR(Direct Server Return), 也就是说 Dnat的节点只负责均衡流量到服务节点, 服务节点收到请求后自行回复客户端。架构如下

graph TD

A>Client] --> B(GW)
B --> A

B --> C(Nft-Dnat)

C --> E[service_1]
C --> F[service_2]
C --> G[service_n]

E --> H((Direct))
F --> H
G --> H

H --> B

SNAT 匹配出口

当数据包经过 DNat 修改目的地址信息后,在转发到目的节点之前,需要通过 SNat修改 数据包的源地址信息,以便收到目的节点的回包。

  • 如果本地出口的Ip是动态调整的,一般会建议使用 masquerade, 也就是

     tables ip nat {
         chain postrouting {
          type nat hook postrouting priority 100; policy accept;
          oif "eth0" masquerade
         }
      }
    
  • 对于多IP的机器, 如果需要实现多进多出,可以通过ct状态进行匹配

     tables ip nat {
            chain postrouting {
                type nat hook postrouting priority 100; policy accept;
                ip protocol udp mark 0x00000001 snat to ct original ip daddr:10000-65000
                ip protocol tcp mark 0x00000001 snat to ct original ip daddr:10000-65000
           }
      }
    

Mark标记与路由

上面的规则里用到了mark标记, 也就是 meta mark, 使用meta mark 配合iproute策略路由, 可以让数据包走特定的路由表。注意标记之后,如果目的方回复数据包,该数据包并不携带meta mark 标记

还有另外一个ct mark, 也就是 conntrack mark, 这是Netfilter 对于每条连接的标记。如果目的方回复了数据包,ct mark 是依然存在的(还是同一条连接flow)。所以借助ct mark 可以恢复meta mark 从而使用策略路由进行路由。

[案例]: 设定一台机器上存在2个网段 192.168.1.2/24, 192.168.2.2/24, 而192.168.1.1是默认路由。 现在需要配置: 从 192.168.2.2 访问 80 端口,数据包将从 192.168.1.2 出口访问 5.5.5.5:8080.

为了兼容2个网段,设定1个额外的路由表 t2. 设定t2表的网络以及网关后, 需要增加策略路由

ip route flush table t2
ip route add default via 192.168.2.1 dev eth2 src 192.168.2.2 table t2
ip rule add from 192.168.2.0/24 lookup t2

ip rule add fwmark 0x1 table t2

配置 nftables表格

  chain prerouting {
      type nat hook prerouting priority 0; policy accept;
      ip daddr 192.168.2.2 udp dport 80 mark set 0x00000000 ct mark set 0x00000001 dnat to 5.5.5.5:8080
  }
	chain mark_prerouting {
		type filter hook prerouting priority 0; policy accept;
		ct direction reply meta mark set ct mark
	}