Linux RAW UDP套接字接收额外IP头是否属于Bug?
关于Linux UDP RAW套接字收发数据包格式差异的问题
我创建了基于UDP协议的RAW套接字,发送包含UDP头和数据的数据包,抓包显示数据包正常。但在另一个程序中创建同类型套接字接收数据时,却收到了包含IP头的数据包——即发送的是UDP头+数据,接收的却是IP头+UDP头+数据,必须将指针偏移20字节(IP头长度)才能正常解析。这是Linux套接字的Bug吗?为何相关文档中没有说明此情况?
问题解答
这不是Linux套接字的Bug,是RAW套接字的默认行为,细节如下:
- 发送端逻辑:当你用
SOCK_RAW+IPPROTO_UDP创建套接字时,内核默认会自动帮你封装IP头(除非显式设置IP_HDRINCL选项)。所以你只需要构造UDP头和数据,内核会负责补充IP头字段,抓包看到的完整IP+UDP+数据就是这么来的。 - 接收端逻辑:同类型的RAW套接字接收时,默认会把完整的IP数据包(包含IP头+UDP头+数据)交付给用户空间。这是RAW套接字的设计特性——允许直接访问IP层的完整报文,方便做协议分析、自定义报文处理等场景。
相关文档其实有说明该特性,只是容易被忽略:SOCK_RAW类型的套接字,接收报文包含IP头,发送时未设置IP_HDRINCL则内核自动添加IP头。
优化方案
如果不想硬编码20字节偏移(IP头可能因选项字段变长,长度不固定),可以动态解析IP头长度:
struct iphdr *ip = iov.iov_base; int ip_header_len = ip->ihl * 4; // IP头长度字段以4字节为单位 struct udphdr *udp = (uint8_t*)ip + ip_header_len;
如果希望接收端直接拿到UDP头+数据,可以通过套接字选项修改行为(部分内核版本支持):
int flag = 0; setsockopt(m_socket, IPPROTO_IP, IP_HDRINCL, &flag, sizeof(flag));
发送端代码
#include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <linux/udp.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/ether.h> #include <sys/types.h> #include <sys/socket.h> #define __USE_XOPEN2K #include <netdb.h> #define AF_INETv4 0x02 #define AF_INETv6 0x0A int main(int argc, char **argv) { int m_socket = socket(AF_INETv4, SOCK_RAW, IPPROTO_UDP); struct sockaddr_in bnd; bnd.sin_family = AF_INET; bnd.sin_port = htons(55555); bnd.sin_addr.s_addr = inet_addr("127.0.0.1"); bind(m_socket, &bnd, sizeof(bnd)); perror("bind: "); struct iovec iov; char buffer[1500 - 14 - 20] = {0}; iov.iov_base = &buffer; iov.iov_len = sizeof(buffer); struct udphdr *udp = iov.iov_base; udp->source = htons(55555); udp->dest = htons(22222); udp->len = htons(8); udp->check = 0; struct msghdr msg = {0}; msg.msg_flags = 0; msg.msg_iov = &iov; msg.msg_iovlen = 1; struct sockaddr_in in; in.sin_family = AF_INETv4; in.sin_addr.s_addr = htonl(INADDR_LOOPBACK); in.sin_port = htons(22222); msg.msg_name = ∈ msg.msg_namelen = sizeof(struct sockaddr_in); for(int i = 0; i < 10; i++) { sendmsg(m_socket, &msg, 0); perror(""); } return 0; }
接收端代码
#include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <linux/udp.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/ether.h> #include <sys/types.h> #include <sys/socket.h> #define __USE_XOPEN2K #include <netdb.h> #define AF_INETv4 0x02 #define AF_INETv6 0x0A int main(int argc, char **argv) { int m_socket = socket(AF_INETv4, SOCK_RAW, IPPROTO_UDP); struct sockaddr_in bnd; bnd.sin_family = AF_INET; bnd.sin_port = htons(22222); bnd.sin_addr.s_addr = inet_addr("127.0.0.1"); bind(m_socket, &bnd, sizeof(bnd)); perror("bind: "); struct iovec iov; char buffer[1500 - 14 - 20] = {0}; iov.iov_base = &buffer; iov.iov_len = sizeof(buffer); struct msghdr msg = {0}; msg.msg_flags = 0; msg.msg_iov = &iov; msg.msg_iovlen = 1; struct sockaddr_in in; in.sin_family = AF_INETv4; in.sin_addr.s_addr = 0; in.sin_port = 0; msg.msg_name = ∈ msg.msg_namelen = sizeof(struct sockaddr_in); for(int i = 0; i < 10; i++) { recvmsg(m_socket, &msg, MSG_ZEROCOPY); perror(""); // 动态解析IP头长度,替代硬编码20字节 struct iphdr *ip = iov.iov_base; int ip_header_len = ip->ihl * 4; struct udphdr *udp = (uint8_t*)ip + ip_header_len; uint16_t src = htons(udp->source ); uint16_t dst = htons(udp->dest); uint16_t len = htons(udp->len); uint16_t chk = htons(udp->check); fprintf(stderr, "src: %hu dst: %hu len: %hu chk: %hu\n", src, dst, len, chk); } return 0; }
内容的提问来源于stack exchange,提问作者Penguin
相关产品推荐
相关产品推荐

