不管 traceroute 具体的工作原理是什么,只需要抓住一点:如果当前的包 IP 头里的 TTL 是 1,那么就可以回一个 ICMP TtlExceeded 包,这样就可以支持 traceroute 和 mtr 了。
TL;DR XDP 程序里的处理逻辑比较简单:
# ./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
生成的,其他的包是内核协议栈生成的。
该判断逻辑比较简单,如下:
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;
// ...
}
这里的 payload 指的是 TCP/UDP/ICMP 等协议的数据部分,这里的处理逻辑复杂一些:
ctx->data_end - ctx->data
计算出当前包的大小。ihl
字段计算出 IP 头的大小 通过 IP 头里的 protocol
字段判断 TCP/UDP/ICMP。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;
}
这里的处理逻辑也比较简单,通过 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 头的时候,需要把源 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));
}
这里的处理逻辑也比较简单,就是填充 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);
}
这里的处理逻辑也比较简单,就是填充 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);
}
完整源代码请查看:GitHub xdp-traceroute[1]。
“阅读原文”亦可查看。
本文介绍了如何使用 XDP 来支持 traceroute 和 mtr,主要的处理逻辑是:
其中需要注意的地方是:计算校验和的时候,需要明确地知道用于计算校验和的包范围。
GitHub xdp-traceroute: https://github.com/Asphaltt/learn-by-example/tree/main/ebpf/xdp-traceroute