eBPF Talk: XDP 支持 traceroute

不管 traceroute 具體的工作原理是什麼,只需要抓住一點:如果當前的包 IP 頭裏的 TTL 是 1,那麼就可以回一個 ICMP TtlExceeded 包,這樣就可以支持 traceroute 和 mtr 了。

TL;DR XDP 程序裏的處理邏輯比較簡單:

  1. 判斷 IP 頭裏的 TTL 是否爲 1。

  2. 幹掉當前包裏的 payload。

  3. 擴充包頭部空間,加上 IP 頭和 ICMP 頭的空間大小。

  4. 填充 IP 頭並計算校驗和。

  5. 填充 ICMP 頭並計算校驗和。

demo 效果

# ./xdp-traceroute --dev enp0s1
2023/12/17 06:20:36 traceroute is running on enp0s1

P.S. 下圖中標出的 DSCP 0x2b 是因爲在 xdp-traceroute 裏把 IP 頭裏的 tos 字段設置爲了 0x2b,這樣可以方便在 wireshark 裏過濾出來。

下圖中,Time to live exceeded in Transit 的包是 xdp-traceroute 生成的,其他的包是內核協議棧生成的。

  1. 判斷 IP 頭裏的 TTL 是否爲 1

該判斷邏輯比較簡單,如下:

SEC("xdp")
int traceroute(struct xdp_md *ctx)
{
    struct ethhdr *eth = (struct ethhdr *)((void *)(__u64) ctx->data), copied;
    struct iphdr *iph = (struct iphdr *)(eth + 1);
    // ...

    if ((void *)(__u64) (iph + 1) > (void *)(__u64) ctx->data_end)
        return XDP_PASS;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    if (iph->ttl > 1)
        return XDP_PASS;

    // ...
}
  1. 幹掉當前包裏的 payload

這裏的 payload 指的是 TCP/UDP/ICMP 等協議的數據部分,這裏的處理邏輯複雜一些:

  1. 通過 ctx->data_end - ctx->data 計算出當前包的大小。

  2. 通過 IP 頭裏的 ihl 字段計算出 IP 頭的大小 通過 IP 頭裏的 protocol 字段判斷 TCP/UDP/ICMP。

  3. 通過當前包的大小減去 ETH 頭、IP 頭和 TCP/UDP/ICMP 頭的大小,得到需要幹掉的包大小。

  4. 調用 bpf_xdp_adjust_tail() 來幹掉 payload。

P.S. 幹掉 payload 的同時,計算出 TtlExceeded 包裏 ICMP 頭的 payload 大小,方便後面計算 ICMP 頭的校驗和。

static __always_inline int
__trim_payload(struct xdp_md *ctx, struct ethhdr *eth, struct iphdr *iph,
               __u64 *icmp_payload)
{
    int pkt_len = ctx->data_end - ctx->data, trim_size;
    int payload_len, iph_len = sizeof(*iph);
    // ...

    switch (iph->protocol) {
    case IPPROTO_TCP:
        payload_len = iph_len + sizeof(struct tcphdr);
        // ...
        break;

    case IPPROTO_UDP:
        payload_len = iph_len + sizeof(struct udphdr);
        // ...
        break;

    case IPPROTO_ICMP:
        payload_len = iph_len + sizeof(struct icmphdr);
        // ...
        break;

    default:
        return XDP_PASS;
    }

    *icmp_payload = payload_len;
    trim_size = pkt_len - sizeof(*eth) - payload_len;
    if (trim_size < 0)
        return XDP_PASS;

    if (trim_size > 0 && bpf_xdp_adjust_tail(ctx, -trim_size))
        return XDP_PASS;

    return 0;
}
  1. 擴充包頭部空間,加上 IP 頭和 ICMP 頭的空間大小

這裏的處理邏輯也比較簡單,通過 bpf_xdp_adjust_head() 來擴充包頭部空間:

static __always_inline int
__expand_icmp_headroom(struct xdp_md *ctx)
{
    const int siz = (sizeof(struct iphdr) + sizeof(struct icmphdr));

    return bpf_xdp_adjust_head(ctx, -siz);
}

填充 ETH 頭

這裏只需要注意一點,就是填充 ETH 頭的時候,需要把源 MAC 和目的 MAC 交換一下:

static __always_inline int
__encode_icmp_packet(struct xdp_md *ctx, struct ethhdr *org_eth,
                     __u64 icmp_payload, __u32 sip, __u16 id)
{
    struct ethhdr *eth = (struct ethhdr *)((void *)(__u64) ctx->data);
    // ...

    if ((void *)(__u64) (icmph + 1) + icmp_payload > (void *)(__u64) ctx->data_end)
        return XDP_PASS;

    __builtin_memcpy(eth->h_dest, org_eth->h_source, ETH_ALEN);
    __builtin_memcpy(eth->h_source, org_eth->h_dest, ETH_ALEN);
    eth->h_proto = bpf_htons(ETH_P_IP);

    // ...

    return XDP_TX;
}

SEC("xdp")
int traceroute(struct xdp_md *ctx)
{
    struct ethhdr *eth = (struct ethhdr *)((void *)(__u64) ctx->data), copied;
    // ...

    __builtin_memcpy(&copied, eth, sizeof(copied));

    // ...

    return __encode_icmp_packet(ctx, &copied, icmp_payload, sip, id);
}

計算校驗和

在 XDP 裏計算校驗和並不是一件容易的事情,因爲 XDP 裏在計算校驗和之前,需要計算出包的大小;而如果使用 ctx->data_end - ctx->data 來計算包的大小,那麼 bpf verifier 會報錯:

    ; size = ctx_ptr(ctx, data_end) - (void *)(__u64) icmph;
    205: (1c) w4 -= w8                    ; R4_w=scalar() R8_w=pkt(off=34,r=82,imm=0)
    ; sum = bpf_csum_diff(0, 0, data_start, data_size, 0);
    206: (b7) r1 = 0                      ; R1_w=0
    207: (b4) w2 = 0                      ; R2_w=0
    208: (bf) r3 = r8                     ; R3_w=pkt(off=34,r=82,imm=0) R8_w=pkt(off=34,r=82,imm=0)
    209: (b4) w5 = 0                      ; R5_w=0
    210: (85) call bpf_csum_diff#28
    R4 min value is negative, either use unsigned or 'var &= const'

而 demo 裏採用的校驗和計算方法是:

static __always_inline __u16 csum_fold_helper(__wsum sum)
{
    sum = (sum & 0xffff) + (sum >> 16);
    return ~((sum & 0xffff) + (sum >> 16));
}

static __always_inline __u16
ipv4_csum(void *data_start, int data_size)
{
    __wsum sum = 0;
    sum = bpf_csum_diff(0, 0, data_start, data_size, 0);
    return csum_fold_helper(sum);
}

static __always_inline void
__update_icmp_checksum(struct icmphdr *icmph, int size)
{
    icmph->checksum = 0;
    icmph->checksum = ipv4_csum(icmph, size);
}

static __always_inline void
__update_ip_checksum(struct iphdr *iph)
{
    iph->check = 0;
    iph->check = ipv4_csum(iph, sizeof(*iph));
}
  1. 填充 IP 頭並計算校驗和

這裏的處理邏輯也比較簡單,就是填充 IP 頭並計算校驗和:

static __always_inline int
__encode_icmp_packet(struct xdp_md *ctx, struct ethhdr *org_eth,
                     __u64 icmp_payload, __u32 sip, __u16 id)
{
    struct ethhdr *eth = (struct ethhdr *) ctx_ptr(ctx, data);
    struct iphdr *iph = (struct iphdr *)(eth + 1);
    // ...

    if ((void *)(__u64) (icmph + 1) + icmp_payload > ctx_ptr(ctx, data_end))
        return XDP_PASS;

    // ...

    iph->version = 4;
    iph->ihl = sizeof(*iph) >> 2;
    iph->tos = 0x2b; // Custom TOS to identify the packet.
    iph->tot_len = bpf_htons(sizeof(*iph) + sizeof(*icmph) + icmp_payload);
    iph->id = id;
    iph->frag_off = 0;
    iph->ttl = 64;
    iph->protocol = IPPROTO_ICMP;
    iph->saddr = MY_ADDR; // Custom IP address by Go RewriteContants().
    iph->daddr = sip;
    __update_ip_checksum(iph);

    // ...

    return XDP_TX;
}

SEC("xdp")
int traceroute(struct xdp_md *ctx)
{
    struct ethhdr *eth = (struct ethhdr *) ctx_ptr(ctx, data), copied;
    struct iphdr *iph = (struct iphdr *)(eth + 1);
    __u64 icmp_payload;
    __u32 sip;
    __u16 id;

    // ...

    sip = iph->saddr;
    id = iph->id;

    // ...

    return __encode_icmp_packet(ctx, &copied, icmp_payload, sip, id);
}
  1. 填充 ICMP 頭並計算校驗和

這裏的處理邏輯也比較簡單,就是填充 ICMP 頭並計算校驗和:

static __always_inline int
__encode_icmp_packet(struct xdp_md *ctx, struct ethhdr *org_eth,
                     __u64 icmp_payload, __u32 sip, __u16 id)
{
    struct ethhdr *eth = (struct ethhdr *) ctx_ptr(ctx, data);
    struct iphdr *iph = (struct iphdr *)(eth + 1);
    struct icmphdr *icmph = (struct icmphdr *)(iph + 1);

    if ((void *)(__u64) (icmph + 1) + icmp_payload > ctx_ptr(ctx, data_end))
        return XDP_PASS;

    // ...

    icmph->type = ICMP_TIME_EXCEEDED;
    icmph->code = ICMP_EXC_TTL;
    icmph->un.gateway = 0;
    __update_icmp_checksum(icmph, sizeof(*icmph) + icmp_payload);

    return XDP_TX;
}

SEC("xdp")
int traceroute(struct xdp_md *ctx)
{
    struct ethhdr *eth = (struct ethhdr *) ctx_ptr(ctx, data), copied;
    struct iphdr *iph = (struct iphdr *)(eth + 1);
    // ...

    if (__trim_payload(ctx, eth, iph, &icmp_payload))
        return XDP_PASS;

    // ...

    return __encode_icmp_packet(ctx, &copied, icmp_payload, sip, id);
}

demo 源代碼

完整源代碼請查看:GitHub xdp-traceroute[1]。

小結

本文介紹瞭如何使用 XDP 來支持 traceroute 和 mtr,主要的處理邏輯是:

  1. 判斷 IP 頭裏的 TTL 是否爲 1。

  2. 幹掉當前包裏的 payload。

  3. 擴充包頭部空間,加上 IP 頭和 ICMP 頭的空間大小。

  4. 填充 IP 頭並計算校驗和。

  5. 填充 ICMP 頭並計算校驗和。

其中需要注意的地方是:計算校驗和的時候,需要明確地知道用於計算校驗和的包範圍。

參考資料

[1]

GitHub xdp-traceroute: https://github.com/Asphaltt/learn-by-example/tree/main/ebpf/xdp-traceroute

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/z5eWM6tZYuV8b9tXfm_sAA