Linux raw_sendmsg原始套接字与IP_HDRINCL控制
Linux raw_sendmsg原始套接字与IP_HDRINCL控制原始套接字AF_INET, SOCK_RAW允许用户空间直接构造和发送IP数据报。raw_sendmsg是内核中处理原始套接字发送的核心函数位于net/ipv4/raw.c。它的关键设计围绕两个核心问题是否由用户填充IP头IP_HDRINCL控制以及如何校验和处理用户输入。一、 IP_HDRINCL选项与路径分岔用户通过 setsockopt(IP_HDRINCL) 控制IP头填充责任的归属。当 IP_HDRINCL 为1时用户必须在sendmsg的缓冲区中包含完整的IP头当 IP_HDRINCL 为0默认时内核自动构造IP头。raw_sendmsg的入口首先要检查此选项static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len){struct inet_sock *inet inet_sk(sk);struct ipcm_cookie ipc;struct rtable *rt NULL;int hdrincl;int err;struct flowi4 fl4;int free_len;hdrincl inet-hdrincl;if (hdrincl) {/* 用户提供了IP头len必须至少20字节 */if (len sizeof(struct iphdr))return -EINVAL;free_len len - sizeof(struct iphdr);} else {/* 内核构造IP头整个payload都是数据 */free_len len;}/* 路由查找 */err ipc_parse_header(sk, ipc, msg);if (err 0)return err;/* 根据hdrincl决定是否校验源地址等 */if (!hdrincl) {/* 内核构造IP头时需要获取源地址 */if (!ipc.addr)ipc.addr inet-inet_saddr;}/* 路由查找 */if (ipc.oif)fl4.flowi4_oif ipc.oif;elsefl4.flowi4_oif sk-sk_bound_dev_if;security_sk_classify_flow(sk, flowi4_to_flowi_common(fl4));rt ip_route_output_flow(net, fl4, sk);if (IS_ERR(rt))goto done;}二、 IP_HDRINCL1的精确实现在ip_reply_append当IP_HDRINCL 打开时用户数据的前20字节被视为IP头。内核需要对这个假IP头进行合法性校验。关键的校验逻辑在 raw_send_hdrincl 函数中static int raw_send_hdrincl(struct sock *sk, struct flowi4 *fl4,struct msghdr *msg, size_t len,struct rtable **rtp, int flags){struct iphdr *iph;int err;int offset sizeof(struct iphdr);/* 从用户态拷贝IP头以便校验 */iph (struct iphdr *)msg-msg_iter.iov-iov_base;/* 校验IP头版本号必须为4 */if (iph-version ! 4)return -EINVAL;/* 校验IP头长度不小于20字节 */if (iph-ihl 5)return -EINVAL;/* 校验总长度是否和用户提供的数据长度一致 */if (ntohs(iph-tot_len) 0) {/* tot_len为0表示由内核计算 */} else if (ntohs(iph-tot_len) len) {return -EINVAL;}/* 拷贝协议字段用于路由查找 */fl4-fl4_icmp_type 0;fl4-fl4_icmp_code 0;/* 从用户提供的IP头中提取目的地址 */fl4-daddr iph-daddr;fl4-saddr iph-saddr;/* 检查用户是否设置了一些危险选项 */if (iph-frag_off htons(IP_MF | IP_OFFSET)) {/* 如果启用了分片需要确保分片偏移合法 */}return offset;}注意这里内核会对IP头中的关键字段做基本验证但不做完整验证——因为RAW套接字本身就是设计来给特权用户完全控制IP头的。三、 skb构建与发送raw_sendmsg 在完成路由查找后通过 ip_append_data 或直接调用 ip_push_pending_frames 来发送数据err ip_append_data(sk, fl4, raw_send_hdrinc, ipc, rt,msg-msg_iov, free_len, ipc.transhdrlen,(hdrincl ? IP_HDRINCL : 0) | msg-msg_flags,first_skb, err);if (err)goto error;if (first_skb) {err ip_local_out(net, sk, first_skb);if (err)goto error;}当IP_HDRINCL为1时ip_append_data通过回调raw_send_hdrinc将用户提供的完整IP头连同数据一起拷贝到skb中。当IP_HDRINCL为0时ip_append_data内部构造IP头。四、 checksum校验与ICMP特殊处理对于RAW套接字IP头的校验和由硬件决定。用户提供的IP头中 checksum 字段的填充规则是if (hdrincl) {if (inet-cork.flags IPCORK_OPT)ip_options_build(skb, inet-cork.opt, inet-cork.addr, rt, 0);elseip_options_build(skb, NULL, fl4-daddr, rt, 0);/* 计算IP头校验和 */iph ip_hdr(skb);iph-check 0;ip_send_check(iph);}ip_send_check 计算IP头checksum并写入 iph-check 字段。该函数定义在 net/ipv4/ip_output.cvoid ip_send_check(struct iphdr *iph){iph-check 0;iph-check ip_fast_csum((unsigned char *)iph, iph-ihl);}无论IP_HDRINCL是否开启IP头校验和总是由内核计算。用户设置的值被内核覆盖。五、 分片处理与IP_MF标志raw_sendmsg 在两种模式下有不同的分片行为。IP_HDRINCL0时由 ip_append_data 的常规分片逻辑处理超过MTU的数据被分片成多个skb。IP_HDRINCL1且用户设置了分片相关字段时情况更复杂if (hdrincl iph-frag_off htons(IP_DF)) {/* 用户设置了DF标志禁止分片 */if (free_len dst_mtu(rt-dst)) {err -EMSGSIZE;goto error;}}当用户设置IP_DF且数据长度超过MTU时内核返回EMSGSIZE错误这是PMTU发现机制的基础。用户也可以自行设置IP_MF和偏移字段来控制分片行为此时内核基本上信任用户的设置。六、 安全校验与capability检查原始套接字的使用受到严格的权限控制。在 raw_sendmsg 的入口处需要检查 CAP_NET_RAW 权限static int raw_create(struct net *net, struct socket *sock, int protocol, int kern){struct sock *sk;if (kern)return raw_sk_create(net, sock, protocol, kern);/* 非内核创建的RAW套接字需要CAP_NET_RAW */if (!ns_capable(net-user_ns, CAP_NET_RAW))return -EPERM;return raw_sk_create(net, sock, protocol, kern);}此外内核还通过 security_sock_rcv_skb 和 iptable_raw 等安全模块对raw套接字的数据包进行过滤。即使拥有CAP_NET_RAW权限用户也可能被iptables规则限制。raw_sendmsg 的整个发送路径可以概括为权限检查 - IP_HDRINCL路径选择 - 路由查找 - ip_append_data(skb构建) - ip_local_out(路由输出) - dst_output - neigh_output - dev_queue_xmit。