在上一篇文章中,我们编写了一个程序来返回ICMP Echo Reply。
虽然实现了返回ICMP Echo Reply的程序,但发现ICMP的校验和计算不正确。
这次,我想写一下如何正确计算ICMP的校验和并返回。
不要懒惰,趁着劲儿连写了两天,请表扬我
ICMP的校验和
- ICMP头结构
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type | Code | Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identifier | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ICMP头包含Checksum,由于我们将ICMP Echo Request的Type修改为ICMP Echo Reply,因此需要重新计算Checksum的值。
ICMP的校验和计算是将头和整个数据部分按16位单位相加,如果发生进位,则将该值再加一次。最后取反(取1的补数)以获得最终的校验和。
关于这方面的计算方法在 RFC 1624 中有详细记载。请注意,参考RFC 1141会导致差分计算失败,因为RFC 1141中的差分计算描述是错误的,因此被RFC 1624所修正。
有过没有意识到更新,反而查看RFC 1141,浪费了无尽的时间的经历。
RFC并不是别人翻译的,一定要阅读原文。。。(自我提醒)
每16位相加后,最后取1的补数进行计算,因此实际上只需对修改部分进行16位单位的差分计算,不需要重新计算所有的数据。
bpf_csum_diff
在eBPF程序中计算校验和时,提供了一个函数bpf_csum_diff
。通过使用这个函数,可以方便地进行校验和计算。
bpf_csum_diff
是一个基于指定内存区域的16位单位的修改来计算校验和变化量的函数。通过这个方法,可以高效地更新ICMP和IP的校验和。
返回ICMP Echo Reply的程序
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <arpa/inet.h>
static __always_inline __u16 csum_fold(__u32 csum)
{
// 将高16位的值加到低16位上
csum = (csum & 0xffff) + (csum >> 16);
// 再次加上进位
csum = (csum & 0xffff) + (csum >> 16);
// 取1的补数,返回最终的校验和
return (__u16)~csum;
}
static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
__u8 tmp[ETH_ALEN];
__builtin_memcpy(tmp, a, ETH_ALEN);
__builtin_memcpy(a, b, ETH_ALEN);
__builtin_memcpy(b, tmp, ETH_ALEN);
}
static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
__u32 tmp = *a;
*a = *b;
*b = tmp;
}
SEC("xdp")
int xdp_echo_reply(struct xdp_md *ctx)
{
void *data_end = (void *)(unsigned long)ctx->data_end;
void *data = (void *)(unsigned long)ctx->data;
// 获取以太网头的指针
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
return XDP_PASS;
// 确认以太网帧包含IP数据包
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
// 获取IP头的指针
struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
return XDP_PASS;
// 确认协议是ICMP
if (ip->protocol != IPPROTO_ICMP)
return XDP_PASS;
// 获取ICMP头的指针
struct icmphdr *icmp = (void *)ip + sizeof(*ip);
if ((void *)icmp + sizeof(*icmp) > data_end)
return XDP_PASS;
// 其他ICMP Echo Request的包继续传递
if (icmp->type != ICMP_ECHO)
return XDP_PASS;
// 互换源和目标的MAC地址
swap_eth_addr(eth->h_dest, eth->h_source);
// 互换源和目标的IP地址
swap_ip_addr(&ip->saddr, &ip->daddr);
// 复制icmp前的头
struct icmphdr icmp_before = *icmp;
// 将ICMP类型修改为ICMP Echo Reply
icmp->type = ICMP_ECHOREPLY;
// 初始化校验和为0
icmp->checksum = 0;
__s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
if (value >= 0)
icmp->checksum = csum_fold(value);
// ICMP Echo Request的包直接丢弃
// return XDP_DROP;
return XDP_TX;
}
char _license[] SEC("license") = "MIT";
static __always_inline __u16 csum_fold(__u32 csum)
{
// 将高16位的值加到低16位上
csum = (csum & 0xffff) + (csum >> 16);
// 再次加上进位
csum = (csum & 0xffff) + (csum >> 16);
// 取1的补数,返回最终的校验和
return (__u16)~csum;
}
csum_fold
函数用于将32位的校验和压缩为16位。bpf_csum_diff() 的返回值为32位,可能会产生进位。因此,首先将高16位的值加到低16位,再进行一次处理。最后通过取1的补数来求得最终的校验和。
static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
__u8 tmp[ETH_ALEN];
__builtin_memcpy(tmp, a, ETH_ALEN);
__builtin_memcpy(a, b, ETH_ALEN);
__builtin_memcpy(b, tmp, ETH_ALEN);
}
swap_eth_addr
函数用于互换以太网头的MAC地址。
static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
__u32 tmp = *a;
*a = *b;
*b = tmp;
}
swap_ip_addr
函数用于互换IP头的IP地址。
由于在eBPF中,函数调用需要内联展开,因此添加了__always_inline
。
// 互换源和目标的MAC地址
swap_eth_addr(eth->h_dest, eth->h_source);
// 互换源和目标的IP地址
swap_ip_addr(&ip->saddr, &ip->daddr);
// 复制ICMP头
struct icmphdr icmp_before = *icmp;
// 将ICMP类型修改为ICMP Echo Reply
icmp->type = ICMP_ECHOREPLY;
// 初始化校验和为0
icmp->checksum = 0;
__s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
if (value >= 0)
icmp->checksum = csum_fold(value);
这一部分与之前不同。swap_eth_addr
和 swap_ip_addr
的功能被封装为函数。
复制icmp
,将icmp->type
修改为ICMP_ECHOREPLY
。
然后计算校验和。
通过icmp->checksum = 0;
先初始化校验和为0,然后使用bpf_csum_diff()
计算差分。之后,bpf_csum_diff的返回值不会产生进位,因此使用csum_fold()进行折叠。
动作确认
从客户端发送ping
$ ping 192.168.XXX.XXX
确认从客户端发送ping时返回ICMP Echo Reply。
$ sudo tcpdump -i eth0 icmp -vvv XX:XX:XX.413478 IP (tos 0x0, ttl 64, id 35523, offset 0, flags [DF], proto ICMP (1), length 84) 192.168.XXX.1 > 192.168.XXX.2: ICMP echo request, id 64346, seq 16, length 64 XX:XX:XX.414359 IP (tos 0x0, ttl 64, id 35523, offset 0, flags [DF], proto ICMP (1), length 84) 192.168.XXX.2 > 192.168.XXX.1: ICMP echo reply, id 64346, seq 16, length 64
我们确认了校验和错误已解决,并成功返回ICMP Echo Reply。
实际上……
实际上,IP头也有校验和,因此也需要重新计算。
- IP头结构
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Version | IHL | DSCP | ECN | Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source IP Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination IP Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options (if any) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
HeaderChecksum字段本来就需要重新计算。
然而,由于我们只是按16位单位互换源IP和目标IP,因此从计算方法上,结果的校验和实际上不会改变。
这是因为“当IP地址按16位单位互换时,作为累加值的1的补数校验和不会改变”的特性。因此,这一次正好没有受到影响,但如果更改其他字段,则必须重新计算IP的校验和。
根据需要,应像ICMP头一样重新计算IP头的校验和。
计算IP头的校验和
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <arpa/inet.h>
static __always_inline __u16 csum_fold(__u32 csum)
{
// 将高16位的值加到低16位上
csum = (csum & 0xffff) + (csum >> 16);
// 再次加上进位
csum = (csum & 0xffff) + (csum >> 16);
// 取1的补数,返回最终的校验和
return (__u16)~csum;
}
static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
__u8 tmp[ETH_ALEN];
__builtin_memcpy(tmp, a, ETH_ALEN);
__builtin_memcpy(a, b, ETH_ALEN);
__builtin_memcpy(b, tmp, ETH_ALEN);
}
static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
__u32 tmp = *a;
*a = *b;
*b = tmp;
}
SEC("xdp")
int xdp_echo_reply(struct xdp_md *ctx)
{
void *data_end = (void *)(unsigned long)ctx->data_end;
void *data = (void *)(unsigned long)ctx->data;
// 获取以太网头的指针
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
return XDP_PASS;
// 确认以太网帧包含IP数据包
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
// 获取IP头的指针
struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
return XDP_PASS;
// 确认协议是ICMP
if (ip->protocol != IPPROTO_ICMP)
return XDP_PASS;
// 获取ICMP头的指针
struct icmphdr *icmp = (void *)ip + sizeof(*ip);
if ((void *)icmp + sizeof(*icmp) > data_end)
return XDP_PASS;
// 其他ICMP Echo Request的包继续传递
if (icmp->type != ICMP_ECHO)
return XDP_PASS;
// 复制IP头
struct iphdr ip_before = *ip;
// 互换源和目标的MAC地址
swap_eth_addr(eth->h_dest, eth->h_source);
// 互换源和目标的IP地址
swap_ip_addr(&ip->saddr, &ip->daddr);
// 复制ICMP头
struct icmphdr icmp_before = *icmp;
// 将ICMP类型修改为ICMP Echo Reply
icmp->type = ICMP_ECHOREPLY;
// 初始化校验和为0
icmp->checksum = 0;
__s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
if (value >= 0)
icmp->checksum = csum_fold(value);
// ip头校验和
ip->check = 0;
value = bpf_csum_diff((void *)&ip_before, sizeof(ip_before), (void *)ip, sizeof(*ip), 0);
if (value >= 0)
ip->check = csum_fold(value);
// ICMP Echo Request的包直接丢弃
// return XDP_DROP;
return XDP_TX;
}
char _license[] SEC("license") = "MIT";
在修改swap_ip_addr
之前,先复制IP头,并通过bpf_csum_diff
计算差分,与ICMP的校验和类似。这样,IP头的校验和也能正确计算。
结论
- 我们实现了一个XDP程序,通过XDP对ICMP Echo Request返回ICMP Echo Reply。
- 介绍了如何计算ICMP的校验和并正确返回。
- 校验和的计算只需对修正部分进行16位单位的差分,不需要重新计算所有数据。
- 使用
bpf_csum_diff()
函数可以轻松地进行校验和计算。