介绍

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

  1. // 创建索引表
  2. array_fd = bpf_obj_get(pinned_file);
  3. if (array_fd < 0) {
  4. fprintf(stderr, "bpf_obj_get(%s): %s(%d)\n", pinned_file, strerror(errno), errno);
  5. goto out;
  6. }
  7. // 把 ifindex 配置索引表key:0 value上
  8. /* bpf_tunnel_key.remote_ipv4 expects host byte orders */
  9. ret = bpf_map_update_elem(array_fd, &array_key, &ifindex, 0);
  10. if (ret) {
  11. perror("bpf_map_update_elem");
  12. goto out;
  13. }

内核获取用户空间配置

tc_l2_redirect_kern.c, 通过bpf_map_lookup_elem函数去获取

  1. int key = 0, *ifindex;
  2. // ...
  3. ifindex = bpf_map_lookup_elem(&tun_iface, &key);

Tc ingress 数据传输流程

TC Ingress 在命名空间ns1ping 10.10.1.102 地址,数据发送,转化过程。Ingress 代表在TC流代表数据流入流表前事件嵌入bpf代码。
发送过程如图1。
tc_l2_redirect-l2_to_ipip_ingress-ping-send.jpg
根据标本初始化空间bpf配置,转化上图图表表示。

发送起点

Ingress 发送测试在命名空间ns1执行ping
tc_l2_redirect.sh:

  1. [[ -z $IP ]] && IP='ip'
  2. ....
  3. $IP netns exec ns1 ping -c1 10.10.1.102 >& /dev/null

匹配路由表默认路由配置:

  1. 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配置在ve1tc 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_redirecthandler处理

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出去。整个数据包结构如下:
tc_l2_redirect-ipip-tun-send-revc-pkg.jpg
有上图ICMP协议经过ipip隧道会有两层IP数据包。IPIP接口虚拟的,最终通过真实接口/veth接口发送数据,接收端物理接口、veth接口接收以后,发现ipip flag,自动有IPIP接口解包。bpf跳转代码是

return bpf_redirect(*ifindex, 0);

所以不好进入接口ingress 路由,直接egress发送出去,所以不会进入ve2 ingress bpf 处理代码里面。

隧道接口接收数据

隧道接收数据以后,数据包还原不同ping数据包,根据ICMP目标就会到达lo接口。这里不详细说明。

数据包返回

tc_l2_redirect-l2_to_ipip_ingress-ping-recv.jpg
由上图看到返回主要不同有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 在命名空间ns1ping 10.10.1.102 地址,数据发送,转化过程。Engress 代表在TC流表在数据在流出几口事件嵌入。如图:
tc_l2_redirect-l2_to_ipip_engress-ping-send.jpg

发送起点

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_redirecthandler处理,因为两次被截获,怎么判断是转发到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进行数据包跳转