如何使用AF_XDP、XDP和eBPF向指定解析器地址发送DNS数据包?
可行方案与问题解析
为什么AF_XDP的sendto()调用失败?
AF_XDP socket的sendto()并非普通TCP/UDP socket的用法,内核不允许通过它直接指定IP/端口发送数据包——AF_XDP设计为用户态提供完整链路层数据包(以太网帧+IP+UDP+应用层数据)后再发送,而非依赖内核封装协议头。你的调用方式不符合AF_XDP的设计逻辑,因此失败。
针对你的需求的三种可行方案
方案1:XDP程序发请求,AF_XDP用户态收响应(推荐新手)
利用XDP的XDP_TX动作直接从网卡发送构造好的DNS请求,同时用AF_XDP绑定同一网卡接收响应。无需复杂的用户态发包逻辑,且能利用XDP的高性能。
关键步骤:
- 编写极简XDP C程序(无需复杂C能力):
只需要实现数据包构造和发送逻辑,用CO-RE编译确保内核兼容:#include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/udp.h> #include <linux/types.h> // 提前定义好resolver的MAC、IP,本机MAC、IP static const __u8 RESOLVER_MAC[ETH_ALEN] = {0x11,0x22,0x33,0x44,0x55,0x66}; static const __be32 RESOLVER_IP = bpf_htonl(0x0a000001); // 示例10.0.0.1 static const __u8 LOCAL_MAC[ETH_ALEN] = {0xaa,0xbb,0xcc,0xdd,0xee,0xff}; static const __be32 LOCAL_IP = bpf_htonl(0x0a000002); // 示例10.0.0.2 SEC("xdp") int xdp_dns_tx(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; // 调整数据包头部空间,用于添加以太网、IP、UDP头 if (bpf_xdp_adjust_head(ctx, -(int)(sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr)))) { return XDP_ABORTED; } data = (void *)(long)ctx->data; data_end = (void *)(long)ctx->data_end; // 构造以太网帧头 struct ethhdr *eth = data; if ((void *)eth + sizeof(*eth) > data_end) return XDP_ABORTED; __builtin_memcpy(eth->h_dest, RESOLVER_MAC, ETH_ALEN); __builtin_memcpy(eth->h_source, LOCAL_MAC, ETH_ALEN); eth->h_proto = bpf_htons(ETH_P_IP); // 构造IPv4头 struct iphdr *ip = (void *)eth + sizeof(*eth); if ((void *)ip + sizeof(*ip) > data_end) return XDP_ABORTED; ip->version = 4; ip->ihl = 5; ip->tos = 0; ip->tot_len = bpf_htons(sizeof(struct iphdr) + sizeof(struct udphdr) + 12); // DNS请求最小长度12字节 ip->id = bpf_htons(0x1234); ip->frag_off = 0; ip->ttl = 64; ip->protocol = IPPROTO_UDP; ip->saddr = LOCAL_IP; ip->daddr = RESOLVER_IP; ip->check = 0; // 内核会自动填充校验和 // 构造UDP头 struct udphdr *udp = (void *)ip + sizeof(*ip); if ((void *)udp + sizeof(*udp) > data_end) return XDP_ABORTED; udp->source = bpf_htons(54321); // 自定义源端口 udp->dest = bpf_htons(53); udp->len = bpf_htons(sizeof(struct udphdr) + 12); udp->check = 0; // 可选关闭校验和 // 构造DNS请求(示例:查询example.com A记录) char dns_request[] = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\x01\x00\x01"; char *dns = (void *)udp + sizeof(*udp); if ((void *)dns + sizeof(dns_request) > data_end) return XDP_ABORTED; __builtin_memcpy(dns, dns_request, sizeof(dns_request)-1); // 发送数据包 return XDP_TX; } char _license[] SEC("license") = "GPL"; - Go侧加载XDP程序并通过AF_XDP收响应:
用cilium/ebpf库加载上述XDP程序到目标网卡,同时创建AF_XDP Socket,过滤目的端口为XDP程序中指定的54321的UDP包,解析DNS响应。
方案2:AF_XDP UMEM模式用户态直接发包收包
放弃sendto(),改用AF_XDP的UMEM(用户态内存池)模式,在Go中构造完整的链路层数据包,通过sendmsg()发送,同时收响应。
关键步骤:
- 用
cilium/ebpf/afxdp包创建UMEM和Socket:import "github.com/cilium/ebpf/afxdp" // 创建UMEM umem, err := afxdp.NewUMEM(afxdp.UMEMOptions{ Size: 4096 * 1024, // 内存池大小 FrameSize: 2048, }) if err != nil { // 处理错误 } defer umem.Close() // 创建AF_XDP Socket sock, err := afxdp.NewSocket(ifindex, afxdp.SocketOptions{ UMEM: umem, QueueID: 0, RxRingSize: 1024, TxRingSize: 1024, }) if err != nil { // 处理错误 } defer sock.Close() - 在Go中构造完整的以太网帧+IP+UDP+DNS请求,拷贝到UMEM帧中,调用
sock.Send()发送。 - 通过
sock.Recv()接收DNS响应,解析数据包。
方案3:XDP仅收响应,用户态用高性能socket发请求
如果你的高吞吐量需求主要在接收响应而非发送请求,可以简化逻辑:
- 用户态用Go的
net包或io_uring发送DNS请求(普通UDP socket即可,性能足够)。 - XDP程序过滤目标IP为本机、目标端口为请求端口的DNS响应,通过
XDP_REDIRECT或bpf_xdp_output转发到AF_XDP用户态,实现快速收包。
注意事项
- 运行程序需要
CAP_NET_ADMIN权限,可通过sudo执行或设置权限位。 - 确保网卡支持XDP(优先用XDP DRV模式,云环境可能需要确认网卡兼容性)。
- 提前获取resolver的MAC地址(可通过
arping命令获取),否则XDP_TX的数据包无法到达目标。
内容的提问来源于stack exchange,提问作者kenjoe41
相关产品推荐
相关产品推荐

