不管 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 效果

1
2
# ./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 生成的,其他的包是内核协议栈生成的。

xdp-traceroute

1. 判断 IP 头里的 TTL 是否为 1

该判断逻辑比较简单,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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;

    // ...
}

2. 干掉当前包里的 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 头的校验和。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
}

3. 扩充包头部空间,加上 IP 头和 ICMP 头的空间大小

这里的处理逻辑也比较简单,通过 bpf_xdp_adjust_head() 来扩充包头部空间:

1
2
3
4
5
6
7
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 交换一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 会报错:

1
2
3
4
5
6
7
8
9
    ; 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 里采用的校验和计算方法是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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));
}

4. 填充 IP 头并计算校验和

这里的处理逻辑也比较简单,就是填充 IP 头并计算校验和:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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);
}

5. 填充 ICMP 头并计算校验和

这里的处理逻辑也比较简单,就是填充 ICMP 头并计算校验和:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

小结

本文介绍了如何使用 XDP 来支持 traceroute 和 mtr,主要的处理逻辑是:

  1. 判断 IP 头里的 TTL 是否为 1。
  2. 干掉当前包里的 payload。
  3. 扩充包头部空间,加上 IP 头和 ICMP 头的空间大小。
  4. 填充 IP 头并计算校验和。
  5. 填充 ICMP 头并计算校验和。

其中需要注意的地方是:计算校验和的时候,需要明确地知道用于计算校验和的包范围。