Auto Last Hop 快速实践之路
起
本文尝试以现实需求为背景,通过其实现方案的逐步变更优化过程,简单说明一些 Linux 网络协议栈转发规则。
承
事件背景
某客户在 WAF 前后分别有一个动态增减的 F5 集群池,流量需要经过 F5 猜能到达 客户端 与 服务器,WAF 旁挂在核心交换机。客户在 F5 上开启了 `Auto Last Hop` 功能,同时 F5 维护连接会话,即需要保持源进源出(请求从哪台 F5 发出,响应发回到哪台 F5)。
关于 Auto Last Hop
细节信息可以参考 官方文档
回归到本文的事件,需要关注的事情有以下几点:
- `Auto Last Hop` 使经过设备的以太网包的二层上源 MAC 透传,同时需要回包时目的 MAC 维持对应来包的源 MAC
- F5 会透传客户端源 IP (公网 IP)
- 由于客户其他需求,Waf 作为代理服务器,同时使用 `路由代理` 模式
转
方案一:默认网关
虽然 Waf 与 F5 之间没有一般意义上的网关,但是可以也可以利用 Linux 对于网关的处理逻辑 —— 将目的 MAC 指定为网关,并让全网段的数据包均经过该设备向外发出 —— 从而做到指定所有数据包发向 F5 来使目的 MAC 变为为 F5,同时不需要一个网关来处理发往公网 IP 的响应包。
再来看网络拓扑,Waf 与 F5 存在同一网段的内网 IP,Waf 可以通过 ARP 获得目标 F5 的 MAC,那么将 默认网关 指定为该内网 IP 便能达成设置 MAC 的目标。
遗留问题
但这样的方案存在一个问题:只能指定一个 F5 作为默认网关。如果客户在 Waf 前使用多台 F5 组成一个可以负载均衡的池子,并且每台 F5 单独进行会话保持,即需要同一 TCP 连接中的数据包经过同一台 F5,那么就不能将回包的目标固定为单一的 默认网关。
方案二:默认网关 + 策略路由
在方案一中我们已经可以通过路由(默认网关)达成目的 MAC 的控制,那么对于更多的目标 F5 设备,可以使用 策略路由 的方式,让不同条件的数据包使用不同的 “默认网关”。
方案基础
- 使用 iptables 与 conntrack,在 TCP 连接层面根据源 MAC 维护一个 MARK,根据 MARK 在发包时使用不同的 “网关”
- iptables 对每个 “包” 进行单独处理,所以需要 conntrack 将同一连接的包关联起来
- conntrack 根据源目的 IP 端口对包进行连接的划分,不是严格意义上的 TCP 连接,不过此处无伤大雅
- iptables 不能根据 源 MAC 动态设置 MARK,所以需要提前收集 F5 的 内网 IP 与 MAC
- ip rule 可以使用 iptables 设置的 MARK 来进入不通的路由表
- 每个路由表中可以使用默认网关来使任意 IP 包在二层使用对应 MAC 直接发向目标 F5
iptables 规则
1. 对 TCP 连接的 SYN 包,创建 MAC_MARK Chain 进行 MARK 设置工作,在 PREROUTING 通过 conntrack 设置 MARK 到连接上;对于其他包,在 PREROUTING Chain 与 OUTPU Chain 将设置的 MARK 从 连接上 restore 到当前包
iptables -t mangle -N MAC_MARK
iptables -t mangle -A MAC_MARK -j CONNMARK --save-mark
iptables -t mangle -I PREROUTING -j MAC_MARK
iptables -t mangle -I PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark
2. 增加规则使对应源 mac 的 tcp 连接搭上唯一 MARK,**增加 F5 需要增加该命令**
iptables -t mangle -I MAC_MARK -p tcp -m tcp
--tcp-flags ALL SYN -m mac --mac-source <F5 MAC>
-j MARK --or-mark 0x10
route table
1. 为单台 F5 创建新的路由表
echo "<table id> <route table>" >> /etc/iproute2/rt_tables
2. 在路由标种增加路由规则,使进入该表的所有包直接发向 <F5 IP> 所对应 MAC
ip route add default via <F5 IP> table <route table>
ip route add <F5 IP> dev <interface port> table <route table>
3. 增加路由策略,根据 MARK 进入不同路由表 `ip rule add fwmark 0x10/0xff0 table <route table>`
遗留问题
因为无法在 iptables 动态维护 MARK,所以需要提前收集 F5 的内网 IP 与 MAC 用于创建规则,但一个可增减的 F5 池客户很难提供并维护正确的 IP 与 MAC 关系。
方案三:eBPF 自动维护 MAC 与 MARK 关系
eBPF 是最新 Linux 内核提供的强大的可编程机制,能够提供更加稳定且高效的二、三层流量控制。基于最新的 Linux 内核网络技术 eBPF,结合 Linux 自身成熟的 netfilter、tc (traffic control) 以及 conntrack 机制,可以通过自动化的来源 MAC 地址学习、外加根据来源 MAC 地址将反向数据包回源到二层链路的上一跳,从而实现 Auto-Last-Hop 功能。
简单来讲,eBPF 提供了一种能力,让使用者可以在一些固定内核 hook 点上增加一些处理逻辑,结合 eBPF 提供的一些额外功能来完成一些功能。
具体 eBPF 的细节不在此介绍,可以参考 官方文档 和 随便搜的文档 。
方案基础
1. 网卡流量会经过 tc (Traffic Control),同时 tc 支持在 ingress 与 egress 处插入 eBPF
2. 此处可以操作的对象为 `*__sk_buff` 如下所示(4.15 内核),其结构支持我们 “获取源 MAC” “设置 MARK” “设置目的 MAC”
/* user accessible mirror of in-kernel sk_buff.
* new fields can only be added to the end of this structure
*/
struct __sk_buff {
__u32 len;
__u32 pkt_type;
__u32 mark;
__u32 queue_mapping;
__u32 protocol;
__u32 vlan_present;
__u32 vlan_tci;
__u32 vlan_proto;
__u32 priority;
__u32 ingress_ifindex;
__u32 ifindex;
__u32 tc_index;
__u32 cb[5];
__u32 hash;
__u32 tc_classid;
__u32 data;
__u32 data_end;
__u32 napi_id;
/* Accessed by BPF_PROG_TYPE_sk_skb types from here to ... */
__u32 family;
__u32 remote_ip4; /* Stored in network byte order */
__u32 local_ip4; /* Stored in network byte order */
__u32 remote_ip6[4]; /* Stored in network byte order */
__u32 local_ip6[4]; /* Stored in network byte order */
__u32 remote_port; /* Stored in network byte order */
__u32 local_port; /* stored in host byte order */
/* ... here. */
__u32 data_meta;
};
3. eBPF 提供可以被多个 eBPF 程序共享的 map 缓存
流程设计
从上面已经知道,可以在 ingress 与 egress 过程插入 eBPF 程序,分别处理 MARK 的生成与目的 MAC 的赋值,中间使用 map 进行数据同步。
此处为减少 eBPF 的处理逻辑,我们增加了一个用户态进程对 map 进行维护。除了向内核插入代码,“和用户态进程交互” 也是 eBPF 比较重要的特性,能让编写者做到更多更灵活的事情。
ingress 处的 eBPF 用于将 syn 包的源 MAC 和 map 中的缓存数据进行比较,如果发现已经创建过对应 MARK 则将 `__uint32 mark` 设置为对应值,否则就通过 `BPF_MAP_TYPE_PERF_EVENT_ARRAY` 向用户态进程新的 MAC,用户态进程选择一个未使用的 MARK,将 MAC - MARK 关系写入 map 进行缓存用于后续比较。
egress 处的 eBPF 根据 `__sk_buff` 的 mark 从 map 缓存中获取对应的 MAC,并将目的 MAC 设置为对应值。
虽然能够通过 eBPF 获取 MAC 创建 MARK 并回包时设置 MAC 了,但是 ingress 与 egress 之间仍然需要经过 iptables 等内核协议栈,另外同一连接中数据包的 MARK 仍需要 conntrack 来赋值,所以仍需要增加规则来完成如下工作,具体规则可以参考上述方案:
- 物理网口的源进源出:如果不做处理,数据包会因为目的 IP 是公网 IP 导致转发到默认网关所在网口(一般为管理口而非数据口)
- `conntrack` 将连接上双向的数据包赋上对应 MARK
遗留问题
未知源 MAC 设备发来的 syn 包,在 ingress 处没能得到 MARK,我们也确实没有让 eBPF 去轮询等待 MARK,主要有两个原因:
- 虽然第一个 syn 会失败,但后续重传的 syn 可以成功,整个初始化流程不会对请求造成很大延迟
- 源 MAC 来自客户的 F5 设备,数量有限且预估在一千以下,需要配置 MARK 的源 MAC 数量有限,需要 syn 重传的情况并不会一直持续
合
没能找到合适的位置放入这张图就放在最后了,这张图描述了 Linux 网络协议栈内的流量走向,也可以帮助理解上文的内容。