使用XDP转发lo网卡127.0.0.1:9999到8080失败,请求排查问题
我尝试用XDP把lo网卡上发往127.0.0.1:9999的流量转发到127.0.0.1:8080,以此访问启动的Golang Web服务,但始终无法建立连接。
执行命令cat /sys/kernel/debug/tracing/trace_pipe得到输出:
bpf_trace_printk: source3879 target 36895
以下是我的XDP代码:
//+build ignore #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_endian.h> #define MAX_CPU 128 #define ETH_P_IP 0x0800 struct pkt_meta { union { __be32 src; __be32 srcv6[4]; }; union { __be32 dst; __be32 dstv6[4]; }; __u16 port16[2]; __u16 l3_proto; __u16 l4_proto; __u16 data_len; __u16 pkt_len; __u32 seq; }; static __always_inline bool parse_tcp(struct tcphdr *tcp, struct pkt_meta *pkt) { pkt->port16[0] = tcp->source; pkt->port16[1] = tcp->dest; pkt->seq = tcp->seq; return true; } static __always_inline bool parse_ip4(struct iphdr *iph, struct pkt_meta *pkt) { if (iph->ihl != 5) return false; pkt->src = iph->saddr; pkt->dst = iph->daddr; pkt->l4_proto = iph->protocol; return true; } SEC("xdp") int process_packet(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; struct tcphdr *tcp; struct iphdr *iph; struct pkt_meta pkt = {}; __u32 off; /* parse packet for IP Addresses and Ports */ off = sizeof(struct ethhdr); if (data + off > data_end) return XDP_PASS; pkt.l3_proto = bpf_htons(eth->h_proto); if (pkt.l3_proto == ETH_P_IP) { iph = data + off; if ((void *)(iph + 1) > data_end) return false; if (!parse_ip4(iph, &pkt)) return XDP_PASS; off += sizeof(struct iphdr); } if (data + off > data_end) return XDP_PASS; if (pkt.l4_proto == IPPROTO_TCP) { tcp = data + off; if ((void *)(tcp + 1) > data_end) return false; off += sizeof(struct tcphdr); if (pkt.port16[1] == bpf_htons(9999) && pkt.dst == bpf_htonl(0x7f000001)) { tcp->dest = bpf_htons(8080); iph->daddr = bpf_htonl(0x7f000001); bpf_printk("source%d target %d\n", pkt.port16[1], tcp->dest); return XDP_TX; } } return XDP_PASS; } char LICENSE[] SEC("license") = "GPL";
核心问题分析
字节序打印误导
打印输出里的3879是9999的网络字节序转主机字节序前的数值(9999十六进制为0x270F,网络大端存储为0x0F27,对应十进制3879),36895是8080的网络字节序数值,说明端口修改逻辑确实触发了,但连接失败另有原因。TCP校验和未更新
修改TCP目标端口后,TCP头部的校验和会失效,内核后续处理时会直接丢弃该包,这是连接失败的核心原因。错误的XDP返回值
代码中存在返回false的逻辑,而XDP程序的返回值必须是XDP_*系列常量(如XDP_PASS、XDP_DROP),返回false会被内核解析为XDP_ABORTED,可能导致包被意外丢弃。TCP端口解析逻辑缺失
在TCP处理分支中未调用parse_tcp函数,pkt.port16[1]为初始化的0值,理论上不会触发后续的端口匹配逻辑——若实际触发了,说明运行的代码与贴出的代码存在差异,但仍需补全该逻辑保证正确性。
具体修复步骤
1. 补全TCP端口解析逻辑
在TCP处理分支中调用parse_tcp,确保pkt.port16被正确赋值:
if (pkt.l4_proto == IPPROTO_TCP) { tcp = data + off; if ((void *)(tcp + 1) > data_end) return XDP_PASS; // 替换原返回false的逻辑 parse_tcp(tcp, &pkt); // 新增调用,解析TCP端口到pkt结构体 off += sizeof(struct tcphdr); // ... 后续匹配逻辑 }
2. 重新计算TCP校验和
修改端口后,使用bpf_l4_csum_replace辅助函数更新TCP校验和:
if (pkt.port16[1] == bpf_htons(9999) && pkt.dst == bpf_htonl(0x7f000001)) { __u16 old_port = bpf_htons(9999); __u16 new_port = bpf_htons(8080); tcp->dest = new_port; // 重新计算TCP校验和 bpf_l4_csum_replace(ctx, &tcp->check, old_port, new_port, sizeof(old_port)); // ... 打印与返回逻辑 }
3. 修正错误返回值
将所有返回false的位置替换为XDP_PASS:
if ((void *)(iph + 1) > data_end) return XDP_PASS;
4. 优化打印逻辑(可选)
将网络字节序端口转为主机字节序后打印,避免数值混淆:
bpf_printk("source%d target %d\n", bpf_ntohs(pkt.port16[1]), bpf_ntohs(tcp->dest));
5. 确认XDP挂载方式
确保程序挂载到lo网卡的通用XDP模式(lo网卡不支持驱动XDP),挂载命令示例:
ip link set dev lo xdp obj your_program.o sec xdp
//+build ignore #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_endian.h> #define MAX_CPU 128 #define ETH_P_IP 0x0800 struct pkt_meta { union { __be32 src; __be32 srcv6[4]; }; union { __be32 dst; __be32 dstv6[4]; }; __u16 port16[2]; __u16 l3_proto; __u16 l4_proto; __u16 data_len; __u16 pkt_len; __u32 seq; }; static __always_inline bool parse_tcp(struct tcphdr *tcp, struct pkt_meta *pkt) { pkt->port16[0] = tcp->source; pkt->port16[1] = tcp->dest; pkt->seq = tcp->seq; return true; } static __always_inline bool parse_ip4(struct iphdr *iph, struct pkt_meta *pkt) { if (iph->ihl != 5) return false; pkt->src = iph->saddr; pkt->dst = iph->daddr; pkt->l4_proto = iph->protocol; return true; } SEC("xdp") int process_packet(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; struct tcphdr *tcp; struct iphdr *iph; struct pkt_meta pkt = {}; __u32 off; /* parse packet for IP Addresses and Ports */ off = sizeof(struct ethhdr); if (data + off > data_end) return XDP_PASS; pkt.l3_proto = bpf_htons(eth->h_proto); if (pkt.l3_proto == ETH_P_IP) { iph = data + off; if ((void *)(iph + 1) > data_end) return XDP_PASS; if (!parse_ip4(iph, &pkt)) return XDP_PASS; off += sizeof(struct iphdr); } if (data + off > data_end) return XDP_PASS; if (pkt.l4_proto == IPPROTO_TCP) { tcp = data + off; if ((void *)(tcp + 1) > data_end) return XDP_PASS; parse_tcp(tcp, &pkt); off += sizeof(struct tcphdr); if (pkt.port16[1] == bpf_htons(9999) && pkt.dst == bpf_htonl(0x7f000001)) { __u16 old_port = bpf_htons(9999); __u16 new_port = bpf_htons(8080); tcp->dest = new_port; bpf_l4_csum_replace(ctx, &tcp->check, old_port, new_port, sizeof(old_port)); bpf_printk("source%d target %d\n", bpf_ntohs(pkt.port16[1]), bpf_ntohs(tcp->dest)); return XDP_TX; } } return XDP_PASS; } char LICENSE[] SEC("license") = "GPL";
内容的提问来源于stack exchange,提问作者nexus

