背景
Linux Bridge 是内核中通过软件模拟交换机的技术,在实际应用中,我们可以通过分析和修改 Bridge 的源代码,来对这台“交换机”做自定义的配置,以满足特殊的场景需求。
三台服务器 host1、host2、host3 依次相连。网络拓扑结构如下所示,host1 有两块网卡 eth0、eth1,分别与 host2、host3 相连。host1 上将两块网卡聚合成一个网桥 br1,host2 和 host3 之间可以借由 br1 进行通信。
实际运行时,host2 与 host1 均有大量数据发送至 host3。
问题描述
因项目需求,需要对 host2、host1 到 host3 的数据流进行优先级划分,保障部分数据流优先占用网络带宽。
初步方案:
根据网络拓扑可知,发送至 host3 的数据都需要经过 host1 的 eth0 网卡,因此对 eth0 配置 qdisc 规则 prio,prio 会根据 IP 首部 TOS 字段,来将数据包放入不同的队列(band)中,在发送时则会优先发送高优 band 的数据。因此,理论上通过设置不同数据流的 TOS 字段,可以实现数据流的优先级划分。
通过 man tc-prio 查看 TOS 与 band 的对应关系,共有3个 band,band0-band2,发送优先级依次降低。
TOS Bits Means Linux Priority Band
------------------------------------------------------------
0x0 0 Normal Service 0 Best Effort 1
0x2 1 Minimize Monetary Cost 0 Best Effort 1
0x4 2 Maximize Reliability 0 Best Effort 1
0x6 3 mmc+mr 0 Best Effort 1
0x8 4 Maximize Throughput 2 Bulk 2
0xa 5 mmc+mt 2 Bulk 2
0xc 6 mr+mt 2 Bulk 2
0xe 7 mmc+mr+mt 2 Bulk 2
0x10 8 Minimize Delay 6 Interactive 0
0x12 9 mmc+md 6 Interactive 0
0x14 10 mr+md 6 Interactive 0
0x16 11 mmc+mr+md 6 Interactive 0
0x18 12 mt+md 4 Int. Bulk 1
0x1a 13 mmc+mt+md 4 Int. Bulk 1
0x1c 14 mr+mt+md 4 Int. Bulk 1
0x1e 15 mmc+mr+mt+md 4 Int. Bulk 1
方案测试:
host3 上启动 iperf3 server,host1 和 host2 分别启动 iperf3 client 向 host3 发送数据,将 TOS 字段设置为0x10,通过 tc 打印各个 band 的统计,看数据是否从 band0 被发出。
打印各个 band 统计的命令
tc -s qdisc ls dev eth0
问题现象:
从 host1 上向 host3 发送 TOS 为0x10的数据,能够看到数据均从 band0 发出。但从 host2 向 host3 发送相同的数据,却看到数据是从 band1 发出的,与预期不符。
原因分析:
思路如下:
- 分析 prio 源码,看代码具体是根据什么信息,如何将数据包加入不同队列的。
- 分析 bridge 中数据转发的代码,看是否会对数据包信息做改动。
基于内核版本:6.0
prio 入队
prio 数据包入队代码
static struct Qdisc *
prio_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
{
struct prio_sched_data *q = qdisc_priv(sch);
u32 band = skb->priority; /* 后续根据band来确定返回的queue */
struct tcf_result res;
struct tcf_proto *fl;
int err;
*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
if (TC_H_MAJ(skb->priority) != sch->handle) {
fl = rcu_dereference_bh(q->filter_list);
err = tcf_classify(skb, NULL, fl, &res, false);
......
if (!fl || err < 0) {
if (TC_H_MAJ(band))
band = 0;
return q->queues[q->prio2band[band & TC_PRIO_MAX]];
}
band = res.classid;
}
band = TC_H_MIN(band) - 1;
if (band >= q->bands)
return q->queues[q->prio2band[0]];
return q->queues[band];
}
/* 数据包入队 */
static int
prio_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free)
{
unsigned int len = qdisc_pkt_len(skb);
struct Qdisc *qdisc;
int ret;
/* prio_classify选择要入的队列 */
qdisc = prio_classify(skb, sch, &ret);
......
}
根据 prio 源码可知,prio 是根据 skb->priority 来确定要进入的队列的,因此我们需要查看 skb->priority 是在哪里被设置,以及在 bridge 转发过程中是否设置了该值。
skb->priority 设置
梳理 ip 数据发送流程,在 ip 层发送函数 ip_queue_xmit 中,skb->priority 被赋值为 sk->priority。
/* Note: skb->sk can be different from sk, in case of tunnels */
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,
__u8 tos)
{
......
/* TODO : should we use skb->sk here instead of sk ? */
skb->priority = sk->sk_priority;
接下来再看 sk->priority 是在哪里被赋值的。
用户态通过 setsockopt 设置发送数据的 TOS 字段,在内核中的实现如下:
case IP_TOS: /* This sets both TOS and Precedence */
if (sk->sk_type == SOCK_STREAM) {
val &= ~INET_ECN_MASK;
val |= inet->tos & INET_ECN_MASK;
}
if (inet->tos != val) {
inet->tos = val;
sk->sk_priority = rt_tos2priority(val);
sk_dst_reset(sk);
}
break;
可以看到,用户态设置 TOS 字段,内核中实际转化为了
sk->priority,也就是说,用户态通过设置 socket 选项来确定属于当前 socket 的数据包的优先级。
网桥转发过程
梳理内核代码,网桥数据转发流程如下:
__netif_receive_skb_core->rx_handler(br_handle_frame)->br_handle_frame_finish->br_pass_frame_up->br_forward->dev_queue_xmit(发送数据)
转发函数中并没有针对 skb->priority 做赋值操作,因此 skb->priority 为0,根据 prio 的 priomap,被分入 band1中。
解决方案:
梳理网桥转发代码,看到转发函数 __br_forward 的结尾处通过 NF_HOOK 插入了一个 netfilter 钩子点,意味着我们可以通过 netfiler 钩子函数来动态改变转发数据的 skb->priority。
static void __br_forward(const struct net_bridge_port *to,
struct sk_buff *skb, bool local_orig)
{
......
NF_HOOK(NFPROTO_BRIDGE, br_hook,
net, NULL, skb, indev, skb->dev,
br_forward_finish);
}
netfiler 示例代码,我们需要将 TOS 字段0x10的数据包的优先级改为6(根据 man tc-prio,优先级为6的数据包会被放入 band0)。
#include <linux/netfilter.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/netfilter_bridge.h>
#include <linux/ip.h>
#include <linux/inet.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/tcp.h>
#include <linux/byteorder/generic.h>
/**
* __br_forward处被调用的hook函数.
* 根据TOS修改数据包的优先级.
*/
unsigned int modify_tos_hookfn(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct ethhdr *ethh;
struct iphdr *iph;
ethh = eth_hdr(skb);
/* exclude packets of whose net protocol is not ip */
if (ethh->h_proto != ntohs(ETH_P_IP))
goto out;
iph = ip_hdr(skb);
if (iph->tos == 0x10) {
skb->priority = 6;
trace_printk("Change tos, dstip: 0x%x, tos: 0x%x, priority: %d\n",
iph->daddr, iph->tos, skb->priority);
}
out:
return NF_ACCEPT;
}
/* A netfilter instance to use */
static struct nf_hook_ops br_hook = {
.hook = (nf_hookfn *)modify_tos_hookfn,
.pf = NFPROTO_BRIDGE,
.hooknum = NF_BR_FORWARD,
.priority = NF_BR_PRI_FIRST,
};
static int __init nf_init(void)
{
if (nf_register_net_hook(&init_net, &br_hook)) {
printk(KERN_ERR"nf_register_hook() failed\n");
return -1;
}
return 0;
}
static void __exit nf_exit(void)
{
nf_unregister_net_hook(&init_net, &br_hook);
}
module_init(nf_init);
module_exit(nf_exit);
MODULE_LICENSE("GPL");
编译该模块,插入 host1 的内核中,再次通过 iperf3 进行相同的测试。通过 tc 打印各个 band 的统计,就可以看到从 host2 向 host3 发送的 TOS = 0x10 的数据,是从 band0 发出的了。