eBPF Talk: XDP 支持 traceroute
不管 traceroute 具體的工作原理是什麼,只需要抓住一點:如果當前的包 IP 頭裏的 TTL 是 1,那麼就可以回一個 ICMP TtlExceeded 包,這樣就可以支持 traceroute 和 mtr 了。
TL;DR XDP 程序裏的處理邏輯比較簡單:
-
判斷 IP 頭裏的 TTL 是否爲 1。
-
幹掉當前包裏的 payload。
-
擴充包頭部空間,加上 IP 頭和 ICMP 頭的空間大小。
-
填充 IP 頭並計算校驗和。
-
填充 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
生成的,其他的包是內核協議棧生成的。
- 判斷 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;
// ...
}
- 幹掉當前包裏的 payload
這裏的 payload 指的是 TCP/UDP/ICMP 等協議的數據部分,這裏的處理邏輯複雜一些:
-
通過
ctx->data_end - ctx->data
計算出當前包的大小。 -
通過 IP 頭裏的
ihl
字段計算出 IP 頭的大小 通過 IP 頭裏的protocol
字段判斷 TCP/UDP/ICMP。 -
通過當前包的大小減去 ETH 頭、IP 頭和 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;
}
- 擴充包頭部空間,加上 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));
}
- 填充 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);
}
- 填充 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,主要的處理邏輯是:
-
判斷 IP 頭裏的 TTL 是否爲 1。
-
幹掉當前包裏的 payload。
-
擴充包頭部空間,加上 IP 頭和 ICMP 頭的空間大小。
-
填充 IP 頭並計算校驗和。
-
填充 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