介绍
tc_l2_redirect 代码位于内核/kernel-src/sample/bpf
目录, 有下面三个代码组成
- tc_l2_redirect_kern.c: 具体实现
- tc_l2_redirect_user.c: 通过命令行配置映射设备index id
- tc_l2_redirect.sh: 测试脚本
代码主要通过tc流表, ingress/egress 入口看嵌入bpf代码,通过bpf_redirect函数实现网络数据包重新定向制定index设备上。
源码分析
源码分析主要介绍ipip ingress, egress代码数据传输路线,以及bpf嵌入bpf代码功能
数据传递
用户空间和内核bpf间数据传递
由于bpf_redirect
数据重定向需要制定目的的接口index
, 这个接口索引在脚本创建成功后才可以获取,脚本和bpf程序之间怎么传递数据
传递数据代码
tc_l2_redirect_user.c
, 这个代码编译成工具,接收用户配置参数,把接口index配置绑传递到kern上,那么怎么实现,是通过bpf_map_update_elem
// 创建索引表
array_fd = bpf_obj_get(pinned_file);
if (array_fd < 0) {
fprintf(stderr, "bpf_obj_get(%s): %s(%d)\n", pinned_file, strerror(errno), errno);
goto out;
}
// 把 ifindex 配置索引表key:0 value上
/* bpf_tunnel_key.remote_ipv4 expects host byte orders */
ret = bpf_map_update_elem(array_fd, &array_key, &ifindex, 0);
if (ret) {
perror("bpf_map_update_elem");
goto out;
}
内核获取用户空间配置
tc_l2_redirect_kern.c
, 通过bpf_map_lookup_elem函数去获取
int key = 0, *ifindex;
// ...
ifindex = bpf_map_lookup_elem(&tun_iface, &key);
Tc ingress 数据传输流程
TC Ingress 在命名空间ns1
ping 10.10.1.102 地址,数据发送,转化过程。Ingress 代表在TC流代表数据流入流表前事件嵌入bpf代码。
发送过程如图1。
根据标本初始化空间bpf配置,转化上图图表表示。
发送起点
Ingress 发送测试在命名空间ns1
执行ping
tc_l2_redirect.sh:
[[ -z $IP ]] && IP='ip'
....
$IP netns exec ns1 ping -c1 10.10.1.102 >& /dev/null
匹配路由表默认路由配置:
default via 10.1.1.1 dev vens1
路由说明由vens1设备发送,目标地址10.1.1.1/24,包MAC源地址是vens1 MAC, MAC目的地址是:ve1 MAC
BPF 重定向隧道
在图表1, 2, 可以看到,数据进入接口ve1
ingress,被bpf的l2_to_iptun_ingress_redirect
处理函数截获
脚本tc_l2_redirect.sh
配置在ve1
tc ingress 注册bpf处理函数如下:
$TC qdisc add dev ve1 clsact
$TC filter add dev ve1 ingress bpf da obj $REDIRECT_BPF sec l2_to_iptun_ingress_redirect
处理时候,当数据包进入ve1前就内核bpf截获事件,由l2_to_iptun_ingress_redirect
handler处理
SEC("l2_to_iptun_ingress_redirect")
int _l2_to_iptun_ingress_redirect(struct __sk_buff *skb)
{
struct bpf_tunnel_key tkey = {};
void *data = (void *)(long)skb->data;
struct eth_hdr *eth = data;
void *data_end = (void *)(long)skb->data_end;
int key = 0, *ifindex;
int ret;
if (data + sizeof(*eth) > data_end)
return TC_ACT_OK;
// 获取脚本创建root namaspace ipt ipip external 接口index
ifindex = bpf_map_lookup_elem(&tun_iface, &key);
if (!ifindex)
return TC_ACT_OK;
if (eth->h_proto == htons(ETH_P_IP)) {
char fmt4[] = "e/ingress redirect daddr4:%x to ifindex:%d\n";
struct iphdr *iph = data + sizeof(*eth);
__be32 daddr = iph->daddr;
if (data + sizeof(*eth) + sizeof(*iph) > data_end)
return TC_ACT_OK;
// 处理egress时候,如果已经ipip隧道,在此就放行,不在进行重定向了
if (!is_vip_addr(eth->h_proto, daddr))
return TC_ACT_OK;
bpf_trace_printk(fmt4, sizeof(fmt4), _htonl(daddr), *ifindex);
} else {
return TC_ACT_OK;
}
// 配置ipip隧道pkg目标地址,在根分区ve2 -> ven2 这段走ipip隧道协议
tkey.tunnel_id = 10000;
tkey.tunnel_ttl = 64;
tkey.remote_ipv4 = 0x0a020166; /* 10.2.1.102 */
bpf_skb_set_tunnel_key(skb, &tkey, sizeof(tkey), 0);
return bpf_redirect(*ifindex, 0);
}
隧道接口发送数据
上面已经配置ipip对到pkg目标地址,那么ipip隧道协议源地址怎么决定能,可以其实可以根据root namespace路由表表决定。
ipip 包目标地址是: 10.2.1.102 匹配下面路由配置
10.2.1.0/24 dev ve2 via 10.2.1.1
tcp/ip 堆栈里面数据包,准备由接口ve2
出去。整个数据包结构如下:
有上图ICMP协议经过ipip隧道会有两层IP数据包。IPIP接口虚拟的,最终通过真实接口/veth接口发送数据,接收端物理接口、veth接口接收以后,发现ipip flag,自动有IPIP接口解包。bpf跳转代码是
return bpf_redirect(*ifindex, 0);
所以不好进入接口ingress 路由,直接egress发送出去,所以不会进入ve2 ingress bpf 处理代码里面。
隧道接口接收数据
隧道接收数据以后,数据包还原不同ping数据包,根据ICMP目标就会到达lo接口。这里不详细说明。
数据包返回
由上图看到返回主要不同有bpf处理函数l2_to_iptun_ingress_forwawd进行数据包跳转。返回的时候,由于IPIP接口ipt
没有和ve2进行绑定,ve2接口ipip 数据包以后,需要ipip解包才可以还原。所以需要bpf截获以后重新定向接口ipt
上才可以。由于ns2空间里面,ipt2
和接口vens2
本来有对应关系所以不用bpf重定向。数据包在到达接口ve2前就被l2_to_iptun_ingress_forward截获,代码如下:
....
ifindex = bpf_map_lookup_elem(&tun_iface, &key);
if (!ifindex)
return TC_ACT_OK;
// IPV4
if (eth->h_proto == htons(ETH_P_IP)) {
char fmt4[] = "ingress forward to ifindex:%d daddr4:%x\n";
struct iphdr *iph = data + sizeof(*eth);
if (data + sizeof(*eth) + sizeof(*iph) > data_end)
return TC_ACT_OK;
// 非ipip协议不进行重定向
if (iph->protocol != IPPROTO_IPIP)
return TC_ACT_OK;
bpf_trace_printk(fmt4, sizeof(fmt4), *ifindex,
_htonl(iph->daddr));
// ipip 协议进入ipt接口ingress时间,如果tag:0就直接跳过ipt接口,不能进行解包了
return bpf_redirect(*ifindex, BPF_F_INGRESS);
}
....
接口ipt
解包以后,目标地址10.1.1.101
匹配路由地址
10.1.1.0/24 via dev ve1 src 10.1.1.1
数据包有接口ve1
离开root namespace, 进入命名空间ns1, vens1接口。
Tc egress 数据传输流程
TC Egress 在命名空间ns1
ping 10.10.1.102 地址,数据发送,转化过程。Engress 代表在TC流表在数据在流出几口事件嵌入。如图:
发送起点
Ingress 发送测试在命名空间ns1
执行ping
tc_l2_redirect.sh:
[[ -z $IP ]] && IP='ip'
....
$IP netns exec ns1 ping -c1 10.10.1.102 >& /dev/null
匹配路由表默认路由配置:
default via 10.1.1.1 dev vens1
路由说明由vens1设备发送,目标地址10.1.1.1/24,包MAC源地址是vens1 MAC, MAC目的地址是:ve1 MAC
根分区IP路由
当ns1
ping 数据包到达根网络命名空间接口ve1
以后,进入根网络空间TCP/IP栈。这次和tc ingress区别是,在根网络空间两次匹配路由到达到接口ve2
, 并且进入此接口egress bpf处理函数l2_to_iptun_ingress_redirect
里面。
1) 首先ping 目的地址是10.10.1.102, 匹配下面路由,此时数据包没有经过IPIP隧道封装:
10.10.1.0/24 dev ve2 via 10.2.1.102
2)根据此路由,ping数据包由vens2发送,下一跳路由器目标地址是10.2.1.102, 由接口发送到出去时候,被bpf处理函数重定向到接口ipt
上,IPIP封装以后, 目标地址10.2.1.102, 再次匹配根网络空间路由表下面路由.
10.2.1.0/24 dev ve2 src 10.2.1.1
3)当ve2发出数据包时候,再次被bpf处理函数l2_to_iptun_ingress_redirect
截获,这次有bpf 放行进入ns2
空间接口vens2
BPF重定向/放行
当数据包由ve2
发出的时候,就内核bpf截获事件,由l2_to_iptun_ingress_redirect
handler处理,因为两次被截获,怎么判断是转发到ipt
接口,还是放行呢,是通过数据包目标地址去判断,代码如下:
static __always_inline bool is_vip_addr(__be16 eth_proto, __be32 daddr)
{
if (eth_proto == htons(ETH_P_IP))
return (_htonl(0xffffff00) & daddr) == _htonl(0x0a0a0100); /* NetMask: 255.255.255.0 & ip === 10.10.1.0 */
else if (eth_proto == htons(ETH_P_IPV6))
return (daddr == _htonl(0x2401face));
return false;
}
通过vip函数判断是否放行
SEC("l2_to_iptun_ingress_redirect")
int _l2_to_iptun_ingress_redirect(struct __sk_buff *skb)
{
...
if (eth->h_proto == htons(ETH_P_IP)) {
char fmt4[] = "e/ingress redirect daddr4:%x to ifindex:%d\n";
struct iphdr *iph = data + sizeof(*eth);
__be32 daddr = iph->daddr;
if (data + sizeof(*eth) + sizeof(*iph) > data_end)
return TC_ACT_OK;
// 如果目标地址不是10.10.1.0, 就放行
if (!is_vip_addr(eth->h_proto, daddr))
return TC_ACT_OK;
bpf_trace_printk(fmt4, sizeof(fmt4), _htonl(daddr), *ifindex);
} else {
return TC_ACT_OK;
}
// 如果目标地址是10.10.1.0, 重定向到ipt接口
tkey.tunnel_id = 10000;
tkey.tunnel_ttl = 64;
// ipip 隧道目的地址配置
tkey.remote_ipv4 = 0x0a020166; /* 10.2.1.102 */
bpf_skb_set_tunnel_key(skb, &tkey, sizeof(tkey), 0);
return bpf_redirect(*ifindex, 0);
}
数据包返回
和ingress一致,在ve2接口ingress,有处理函数l2_to_iptun_ingress_forwawd进行数据包跳转