流量转发实战

遇到一个有趣的网络转发课题,整理出来分享

实战场景

  • 如上图所示,FW/NAT 服务器上有四张网卡
    • eth0 互联内网交换机,网段10.222.0.1/16,有4个设备需要访问外网
    • eth1 互联电信网络,2个出口地址 6.6.1.5/24,6.6.1.6/24;(网关 6.6.1.1 主路由)
    • eth2 互联联通网段,1个出口地址 6.6.2.5/24 (网关 6.6.2.1)
    • eth3 互联移动网段,1个出口地址 6.6.3.5/24 (网关 6.6.3.1)
  • 需求是定制一套可配置的流量调度器,实现
    • 10.222.0.11 网络流量出口 eth1-6.6.1.5
    • 10.222.1.11 网络流量出口 eth1-6.6.1.6
    • 10.222.2.11 网络流量出口 eth2-6.6.2.5
    • 10.222.3.11 网络流量出口 eth3-6.6.3.5
  • 要求配置独立 (即访问相同的ip地址,不同的内外设备出口ip不同),也同时可低成本动态修改 (提供api等方式)

解决思路

  1. 涉及到网络出口转发,那么就得用到snat,而snat最低成本的方式莫过于netfilter体系下的 nat
  2. 多网卡多网关,而且要求根据来源设备来区分网络出口,那么必然需要 路由表+策略路由,通过来源IP来定义流量转发所匹配的路由规则

解决方案

综上分析,借助 nat + 路由表+策略路由,有以下2个可行的解决方案

  • 动态更新策略路由表+nat表
  • 固化策略路由表 + 动态更新nat表

1. 动态更新策略路由表+nat表

1.1 除了主路由(ctcc), 为另外2个互联网卡创建独立路由表 (1002/1003)

# eth2
/sbin/ip route flush table 1002
/sbin/ip route add default via 6.6.2.1 dev eth2 table 1002

# eth3
/sbin/ip route flush table 1003
/sbin/ip route add default via 6.6.3.1 dev eth3 table 1003

1.2 定义策略路由

# 10.222.2.11 
/sbin/ip rule add from 10.222.2.11 lookup 1002

# 10.222.3.11 
/sbin/ip rule add from 10.222.3.11 lookup 1003

由于eth1 为主路由,所以无需为 10.222.0.11, 10.222.1.11 额外定义策略

1.3 开启forward

sysctl -w net.ipv4.ip_forward=1

也 可考虑 如下方法固化修改

cat > /etc/sysctl.d/999-custom.conf <<EOF
net.ipv4.ip_forward=1
net.ipv4.tcp_syncookies=1
net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.all.rp_filter=0
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.tcp_tw_reuse=0
net.ipv4.tcp_tw_recycle=0
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216

# net.netfilter
net.netfilter.nf_conntrack_max=1048576
net.netfilter.nf_conntrack_buckets=262144
net.netfilter.nf_conntrack_tcp_loose=0
EOF

1.4 生成nat规则 (注意多出口ip需要额外定义)

iptables -t nat -A POSTROUTING -s 10.222.0.11 -o eth0 -j SNAT --to 6.6.1.5
iptables -t nat -A POSTROUTING -s 10.222.1.11 -o eth0 -j SNAT --to 6.6.1.6

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE

1.5 动态调度

  • 指定网卡出口 需要更新策略路由 (ip rule delete/add)
  • 指定网卡附加IP 出口,需要更新 iptables 防火墙 (iptables -D)

2. 固化策略路由表 + 动态更新nat表

2.1 除了主路由(ctcc), 为另外2个互联网卡创建独立路由表 (1002/1003)

# eth2
/sbin/ip route flush table 1002
/sbin/ip route add default via 6.6.2.1 dev eth2 table 1002

# eth3
/sbin/ip route flush table 1003
/sbin/ip route add default via 6.6.3.1 dev eth3 table 1003

2.2 定义策略路由 (打标签)

# 非主路由的网卡有多少就定义多少个标签
# 0x00001002-eth2
/sbin/ip rule add fwmark 0x00001002 table 1002

# 0x00001003-eth3
/sbin/ip rule add from 0x00001003 lookup 1003

2.3 开启forward

sysctl -w net.ipv4.ip_forward=1

也 可考虑 如下方法固化修改

cat > /etc/sysctl.d/999-custom.conf <<EOF
net.ipv4.ip_forward=1
net.ipv4.tcp_syncookies=1
net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.all.rp_filter=0
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.tcp_tw_reuse=0
net.ipv4.tcp_tw_recycle=0
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216

# net.netfilter
net.netfilter.nf_conntrack_max=1048576
net.netfilter.nf_conntrack_buckets=262144
net.netfilter.nf_conntrack_tcp_loose=0
EOF

2.4 生成nat规则 (使用nftables)

  • 针对需要从特定网卡出去的流量添加meta mark标签,策略路由只需识别固定的标签即可选择正确的路由表
  • 针对特定的snat出口的地址进行map数据结构管理
table ip nat {
	set outbond_eth2 {
		type ipv4_addr
		flags interval
		elements = { 10.222.2.11 }
	}

	set outbond_eth3 {
		type ipv4_addr
		flags interval
		elements = { 10.222.3.11 }
	}

	map outbond_ip {
		type ipv4_addr : ipv4_addr
		flags interval
		elements = { 10.222.0.11 : 6.6.1.5, 10.222.1.11 : 6.6.1.6 }
	}

	chain nat_prerouting {
		type nat hook prerouting priority 100; policy accept;
		ip saddr @outbond_eth2 meta mark set 0x00001002 ct mark set 0x00001002
		ip saddr @outbond_eth3 meta mark set 0x00001003 ct mark set 0x00001003
	}

	chain filter_prerouting {
		type filter hook prerouting priority filter; policy accept;
		ct status snat ct direction original ct mark != 0x00000000 meta mark set ct mark
	}

	chain postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		oifname { "eth1", "eth2", "eth3" } snat to ip saddr map @outbond_ip
		oifname { "eth1", "eth2", "eth3" } masquerade
	}
}

2.5 动态调度

  • 指定网卡出口 , 需修改出口set数据 (outbond_eth2, outbond_eth3)

    • 查询 nft list set ip nat outbond_eth2
    • 增加 nft add element ip nat outbond_eth2 \{ 10.222.2.12 \}
    • 删除 nft delete element ip nat outbond_eth2 \{ 10.222.2.12 \}
  • 指定网卡附加IP 出口,需修改出口map数据 ( outbond_ip )

    • 查询 nft list map ip nat outbond_ip
    • 增加 nft add element ip nat outbond_ip \{ 10.222.1.12: 6.6.1.6 \}
    • 删除 nft delete element ip nat outbond_ip \{ 10.222.1.12: 6.6.1.6 \}

方案分析

  1. 方案1需要动态修改策略路由表,而策略路由表是按顺序自上而下(1-32767)遍历匹配,随着调度设备的增多,策略路由查询会出现明显的cpu消耗
  2. 方案2借助了 nftables的 set、map数据结构来打标签、指定snat,可拓展性要更高,同时CPU消耗低,性能也更好。

扩展思考

另外一个思路是基于nftables的 flowtables ,可以提高转发性能。但随着ebpf的兴起,针对当前的场景可直接使用 ebpf 绕过系统内核

  • 定义转发表
    • eth0 10.222.0.11 =SNAT= eth1 6.6.1.5
    • eth0 10.222.1.11 =SNAT= eth1 6.6.2.5
    • eth0 10.222.2.11 =SNAT= eth2 6.6.2.5
    • eth0 10.222.3.11 =SNAT= eth3 6.6.3.5
    • eth1 10.222.0.0/16 =FW= eth0
    • eth2 10.222.0.0/16 =FW= eth0
    • eth3 10.222.0.0/16 =FW= eth0
  • 监听 eth0 的数据包
    • 如果命中规则,则进行snat转换,投递到对应的网络出口 (记录状态,实现端口分配与回收)
  • 监听 eth1/eth2/eth3 的数据包
    • 如果命中规则,则直接转发到 eth0网口

ebpf的难点在于实现类似conntrack的连接记录(可简单暴力超时回收例如udp;也可以往复杂里做按传输层的连接状态,例如tcp)

引申场景

如果要实现

  • 内网的客户端 (10.222.0.11) 访问 8.8.8.8 走 eth2(cucc)
  • 内网的客户端 (10.222.0.12) 访问 8.8.8.8 走 eth3(ctcc)
  • 内网的客户端 (10.222.1.11) 访问 8.8.8.8 走 eth1(ctcc) 6.6.2.6

要如何配置?