veth挂载的XDP无法处理非直连流量问题咨询
实验拓扑

问题现象
在veth0上执行ping 192.168.50.3时,veth-xdp的DNAT功能正常,能将目的IP修改为172.10.1.2,数据包可成功到达ns1。但ICMP回复包从ns1返回至veth0时,SNAT未生效,终端提示如下:
所用XDP程序
SEC("xdp") int xdp_redirect_func(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; int action = XDP_PASS; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) { action = XDP_DROP; goto out; } if (eth->h_proto != htons(ETH_P_IP)) goto out; struct iphdr *ip = data + sizeof(struct ethhdr); if ((void *)(ip + 1) > data_end) { action = XDP_DROP; goto out; } __u8 protocol = ip->protocol; if (protocol != IPPROTO_TCP && protocol != IPPROTO_ICMP && protocol != IPPROTO_UDP) { goto out; } __u32 old_src_ip = ip->saddr; __u32 old_dst_ip = ip->daddr; __u16 original_checksum = ip->check; __u32 *new_dst_ip = bpf_map_lookup_elem(&dnat_map, &old_dst_ip); if (new_dst_ip) { ip->daddr = *new_dst_ip; ip->check = update_checksum(original_checksum, (__u16)(old_dst_ip >> 16), (__u16)(*new_dst_ip >> 16)); ip->check = update_checksum(ip->check, (__u16)(old_dst_ip & 0xFFFF), (__u16)(*new_dst_ip & 0xFFFF)); goto out; } __u32 *new_src_ip = bpf_map_lookup_elem(&snat_map, &old_src_ip); if (new_src_ip) { ip->saddr = *new_src_ip; ip->check = update_checksum(original_checksum, (__u16)(old_src_ip >> 16), (__u16)(*new_src_ip >> 16)); ip->check = update_checksum(ip->check, (__u16)(old_src_ip & 0xFFFF), (__u16)(*new_src_ip & 0xFFFF)); } out: return xdp_stats_record_action(ctx, action); }
疑问
- 挂载在veth上的XDP程序是否仅能处理直连流量?
- 如何让veth-xdp正确处理其他veth接口转发的流量?
关于veth上XDP的流量处理范围
veth上的XDP程序不是仅处理直连流量,它能处理所有经过该veth接口的数据包——不管是来自直连端点的流量,还是内核转发过来的流量。但要注意:XDP运行在网络栈的最底层(虚拟网卡入口处),数据包到达XDP程序的时机早于内核路由、iptables等逻辑,所以需要确保流量确实会经过挂载XDP的veth接口。
SNAT未生效的原因及修复方案
从代码和现象来看,SNAT未生效的核心问题是匹配逻辑错误:
当ICMP回复包从ns1返回时,数据包的源IP是172.10.1.2,目的IP是veth0的IP(比如192.168.50.1)。现有代码用old_src_ip(即172.10.1.2)查询snat_map,但你的映射表中大概率没有这个条目——实际需要的是反向会话匹配:根据回复包的目的IP(原请求的源IP)和源IP(原请求DNAT后的目的IP),找到要替换的源IP(原请求的目的IP192.168.50.3)。
具体修复步骤:
改用双向会话表替代独立的DNAT/SNAT表
定义包含原IP和转换后IP的会话结构体,用哈希表存储正向(请求)和反向(回复)的会话映射,确保两种方向的流量都能匹配到对应的NAT规则。调整流量处理顺序
先检查是否是回复包(通过反向IP组合查询会话表),再处理请求包的DNAT逻辑,避免处理完DNAT就直接跳出导致回复包未被处理。补充ICMP校验和更新
ICMP包的校验和依赖IP伪首部,修改IP源/目的IP后必须同步更新ICMP校验和,否则回复包会被内核或端点丢弃。确认流量路径正确性
检查ns1的路由表,确保回复包的默认路由指向挂载XDP的veth端点;用tcpdump在veth两端抓包,确认回复包确实到达了XDP程序所在的接口。
示例代码调整思路
// 定义NAT会话结构体 struct nat_session { __u32 orig_src; __u32 orig_dst; __u32 new_src; __u32 new_dst; }; // 定义会话表哈希map(键为{源IP, 目的IP, 协议},值为struct nat_session) struct bpf_map_def SEC("maps") nat_session_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(struct {__u32 src; __u32 dst; __u8 proto;}), .value_size = sizeof(struct nat_session), .max_entries = 1024, }; // 原DNAT规则表 struct bpf_map_def SEC("maps") dnat_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(__u32), .value_size = sizeof(__u32), .max_entries = 64, }; SEC("xdp") int xdp_nat_func(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; int action = XDP_PASS; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) { action = XDP_DROP; goto out; } if (eth->h_proto != htons(ETH_P_IP)) goto out; struct iphdr *ip = data + sizeof(struct ethhdr); if ((void *)(ip + 1) > data_end) { action = XDP_DROP; goto out; } __u8 protocol = ip->protocol; if (protocol != IPPROTO_TCP && protocol != IPPROTO_ICMP && protocol != IPPROTO_UDP) goto out; __u32 src = ip->saddr; __u32 dst = ip->daddr; __u16 orig_ip_check = ip->check; // 1. 处理回复包:反向查询会话表 struct {__u32 src; __u32 dst; __u8 proto;} rev_key = {dst, src, protocol}; struct nat_session *rev_sess = bpf_map_lookup_elem(&nat_session_map, &rev_key); if (rev_sess) { // 执行SNAT:将源IP替换为原请求的目的IP ip->saddr = rev_sess->orig_dst; // 更新IP校验和 ip->check = update_checksum(orig_ip_check, (__u16)(src >> 16), (__u16)(rev_sess->orig_dst >> 16)); ip->check = update_checksum(ip->check, (__u16)(src & 0xFFFF), (__u16)(rev_sess->orig_dst & 0xFFFF)); // 更新ICMP校验和(如果是ICMP包) if (protocol == IPPROTO_ICMP) { struct icmphdr *icmp = (void*)ip + sizeof(struct iphdr); if ((void*)icmp + 1 > data_end) goto out; __u16 orig_icmp_check = icmp->checksum; icmp->checksum = update_checksum(orig_icmp_check, (__u16)(src >> 16), (__u16)(rev_sess->orig_dst >> 16)); icmp->checksum = update_checksum(icmp->checksum, (__u16)(src & 0xFFFF), (__u16)(rev_sess->orig_dst & 0xFFFF)); } goto out; } // 2. 处理请求包:查询DNAT规则并创建会话 struct {__u32 src; __u32 dst; __u8 proto;} req_key = {src, dst, protocol}; struct nat_session *req_sess = bpf_map_lookup_elem(&nat_session_map, &req_key); if (!req_sess) { __u32 *new_dst = bpf_map_lookup_elem(&dnat_map, &dst); if (new_dst) { struct nat_session new_sess = { .orig_src = src, .orig_dst = dst, .new_src = src, // 如需固定SNAT IP,替换为对应值 .new_dst = *new_dst }; // 存入正向和反向会话 bpf_map_update_elem(&nat_session_map, &req_key, &new_sess, BPF_NOEXIST); struct {__u32 src; __u32 dst; __u8 proto;} rev_key2 = {*new_dst, src, protocol}; bpf_map_update_elem(&nat_session_map, &rev_key2, &new_sess, BPF_NOEXIST); // 执行DNAT ip->daddr = *new_dst; ip->check = update_checksum(orig_ip_check, (__u16)(dst >> 16), (__u16)(*new_dst >> 16)); ip->check = update_checksum(ip->check, (__u16)(dst & 0xFFFF), (__u16)(*new_dst & 0xFFFF)); // 更新ICMP校验和 if (protocol == IPPROTO_ICMP) { struct icmphdr *icmp = (void*)ip + sizeof(struct iphdr); if ((void*)icmp + 1 > data_end) goto out; __u16 orig_icmp_check = icmp->checksum; icmp->checksum = update_checksum(orig_icmp_check, (__u16)(dst >> 16), (__u16)(*new_dst >> 16)); icmp->checksum = update_checksum(icmp->checksum, (__u16)(dst & 0xFFFF), (__u16)(*new_dst & 0xFFFF)); } } } out: return xdp_stats_record_action(ctx, action); }
额外注意事项
- 确保内核版本支持veth的native XDP(Linux 4.18+),低版本内核可使用generic模式,但性能会下降。
- 用
bpftool map dump命令查看会话表和DNAT表的条目,确认映射是否正确创建。 - 抓包时同时监控veth的两端,对比数据包经过XDP前后的IP变化,快速定位问题环节。
内容的提问来源于stack exchange,提问作者Heason H

