Linux内核IPv4 RAW套接字深度解析:从数据包构造到可靠传输的挑战
引言
在网络编程的世界中,RAW套接字提供了最底层的网络访问能力,允许开发者直接与网络层交互。Linux内核中的net/ipv4/raw.c
文件实现了IPv4 RAW套接字的核心功能,为应用程序提供了自定义IP数据包和直接操作IP头部的能力。本文将深入分析Linux 4.19内核中RAW套接字的实现机制,探讨如何利用这一强大功能,并阐明为何基于RAW套接字实现可靠传输是一项极具挑战性的任务。
一、RAW套接字的核心实现机制
1.1 哈希表管理与套接字查找
Linux内核使用哈希表来高效管理RAW套接字。在net/ipv4/raw.c
中,核心数据结构是raw_hashinfo
:
c
struct raw_hashinfo raw_v4_hashinfo = {.lock = __RW_LOCK_UNLOCKED(raw_v4_hashinfo.lock), };
这个全局哈希表通过协议号将套接字分布到不同的桶中,每个桶使用读写锁保护,确保并发访问的安全性。当网络栈需要将接收到的IP数据包传递给RAW套接字时,会调用raw_v4_input
函数:
c
static int raw_v4_input(struct sk_buff *skb, const struct iphdr *iph, int hash) {// 根据协议号和地址查找匹配的RAW套接字// ... }
该函数根据IP头部的协议号、源地址和目标地址等信息,在哈希表中查找匹配的套接字,并将数据包传递给所有符合条件的套接字。
1.2 数据包接收与传递
当RAW套接字接收到数据包时,raw_rcv
函数负责处理:
c
int raw_rcv(struct sock *sk, struct sk_buff *skb) {if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb)) {atomic_inc(&sk->sk_drops);kfree_skb(skb);return NET_RX_DROP;}nf_reset(skb);skb_push(skb, skb->data - skb_network_header(skb));raw_rcv_skb(sk, skb);return 0; }
这个函数首先进行安全策略检查,然后调整SKB指针以确保网络头正确,最后将数据包放入套接字的接收队列。
二、构造和发送自定义IP数据包
2.1 开启RAW套接字与IP_HDRINCL选项
要自定义IP数据包,首先需要创建RAW套接字并设置IP_HDRINCL选项:
c
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); int hincl = 1; setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &hincl, sizeof(hincl));
设置IP_HDRINCL选项后,内核将不会自动构造IP头部,而是完全由应用程序负责构造完整的IP数据包。
2.2 构造IP头部
应用程序需要自行填充IP头部结构:
c
struct ipheader {unsigned char iph_ihl:4, iph_ver:4;unsigned char iph_tos;unsigned short int iph_len;unsigned short int iph_ident;unsigned short int iph_flags:3, iph_offset:13;unsigned char iph_ttl;unsigned char iph_protocol;unsigned short int iph_chksum;struct in_addr iph_sourceip;struct in_addr iph_destip; };
填充完毕后,需要计算校验和以确保数据完整性:
c
unsigned short calculate_checksum(unsigned short *ptr, int nbytes) {register long sum;unsigned short oddbyte;register short answer;sum = 0;while (nbytes > 1) {sum += *ptr++;nbytes -= 2;}// ... 校验和计算逻辑return answer; }
2.3 发送自定义数据包
通过sendto
系统调用发送构造好的数据包:
c
struct sockaddr_in dest_addr; memset(&dest_addr, 0, sizeof(dest_addr)); dest_addr.sin_family = AF_INET; inet_pton(AF_INET, "192.168.1.1", &(dest_addr.sin_addr));sendto(sockfd, packet, packet_len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
在内核中,这一操作最终由raw_sendmsg
函数处理,该函数根据是否设置了IP_HDRINCL选项决定是调用raw_send_hdrinc
直接发送应用程序构造的数据包,还是由内核协助构造IP头部。
三、可靠传输的挑战与实现难度
虽然RAW套接字提供了强大的底层网络访问能力,但基于它实现可靠传输面临着巨大挑战。
3.1 TCP可靠传输机制的复杂性
TCP通过多种机制提供可靠传输服务,包括:
序列号与确认机制:确保数据有序到达和确认
超时重传机制:处理丢失的数据包
流量控制:通过滑动窗口防止接收方过载
拥塞控制:动态调整发送速率避免网络拥塞
连接管理:三次握手建立连接,四次挥手终止连接
这些机制在内核中经过数十年的优化和调试,形成了极为复杂的实现。
3.2 在应用层重新实现传输层的挑战
当使用RAW套接字时,开发者需要在应用层重新实现所有这些机制:
c
// 需要在应用层定义类似TCP的协议头 struct my_protocol_header {uint32_t sequence_number; // 手动管理序列号uint32_t ack_number; // 手动管理确认号uint8_t flags; // 自定义标志位uint16_t window_size; // 手动窗口通告uint16_t checksum; // 应用层校验和 };
这要求开发者:
实现精确的超时重传计时器,包括动态RTT估算
处理复杂的拥塞控制算法(如BIC、CUBIC或BBR)
管理连接状态和序列号空间
处理各种边界情况和错误条件
3.3 性能与效率问题
即使成功在应用层实现了可靠传输机制,其性能也往往远低于内核实现:
上下文切换开销:每次数据包处理都需要在用户空间和内核空间之间切换
内存拷贝开销:数据需要在用户空间缓冲区和内核SKB之间来回拷贝
计时器精度:用户空间的计时器精度通常低于内核空间
并发处理:难以高效处理大量并发连接
四、RAW套接字的适用场景
虽然实现可靠传输极为困难,但RAW套接字在特定场景下非常有用:
网络诊断工具:实现自定义的ping、traceroute等工具
协议研究:实验新的网络协议或扩展现有协议
网络安全:开发入侵检测系统或进行安全测试
特殊网络设备:实现路由器、防火墙等网络设备的功能
实时音视频传输:对延迟极其敏感的应用,可以牺牲一定可靠性换取低延迟
五、内核与用户空间的协作
Linux内核的RAW套接字实现体现了内核与用户空间的高效协作模式:
内存管理:内核使用SKB缓冲区管理网络数据包,用户空间通过系统调用访问这些数据
协议处理:内核处理底层的IP协议,用户空间可以专注于应用层逻辑
并发控制:内核通过锁机制保护共享资源,用户空间无需关心并发问题
错误处理:内核提供统一的错误处理机制,用户空间可以通过返回值获取错误信息
结论
Linux内核中的RAW套接字实现为应用程序提供了直接访问网络层的强大能力,使得自定义IP数据包和协议成为可能。然而,这种能力伴随着巨大的责任和挑战。
虽然技术上可以在应用层基于RAW套接字重新实现TCP的所有可靠传输机制,但这实际上是一项极其复杂且效率低下的任务。内核中的TCP实现经过数十年的优化和调试,处理了各种网络条件和边界情况,提供了高性能的可靠传输服务。
对于绝大多数应用场景,直接使用内核提供的TCP套接字是更加明智和高效的选择。RAW套接字更适合那些需要底层网络访问的特殊应用,如网络诊断、协议研究和网络安全工具等。
理解RAW套接字的工作原理和限制,有助于开发者做出更加合理的技术选型,并在确实需要底层网络访问时,能够更加高效和安全地利用这一强大功能。
net\ipv4\raw.c
/** INET An implementation of the TCP/IP protocol suite for the LINUX* operating system. INET is implemented using the BSD Socket* interface as the means of communication with the user level.** RAW - implementation of IP "raw" sockets.** Authors: Ross Biro* Fred N. van Kempen, <waltje@uWalt.NL.Mugnet.ORG>** Fixes:* Alan Cox : verify_area() fixed up* Alan Cox : ICMP error handling* Alan Cox : EMSGSIZE if you send too big a packet* Alan Cox : Now uses generic datagrams and shared* skbuff library. No more peek crashes,* no more backlogs* Alan Cox : Checks sk->broadcast.* Alan Cox : Uses skb_free_datagram/skb_copy_datagram* Alan Cox : Raw passes ip options too* Alan Cox : Setsocketopt added* Alan Cox : Fixed error return for broadcasts* Alan Cox : Removed wake_up calls* Alan Cox : Use ttl/tos* Alan Cox : Cleaned up old debugging* Alan Cox : Use new kernel side addresses* Arnt Gulbrandsen : Fixed MSG_DONTROUTE in raw sockets.* Alan Cox : BSD style RAW socket demultiplexing.* Alan Cox : Beginnings of mrouted support.* Alan Cox : Added IP_HDRINCL option.* Alan Cox : Skip broadcast check if BSDism set.* David S. Miller : New socket lookup architecture.** This program is free software; you can redistribute it and/or* modify it under the terms of the GNU General Public License* as published by the Free Software Foundation; either version* 2 of the License, or (at your option) any later version.*/#include <linux/types.h>
#include <linux/atomic.h>
#include <asm/byteorder.h>
#include <asm/current.h>
#include <linux/uaccess.h>
#include <asm/ioctls.h>
#include <linux/stddef.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/export.h>
#include <linux/spinlock.h>
#include <linux/sockios.h>
#include <linux/socket.h>
#include <linux/in.h>
#include <linux/mroute.h>
#include <linux/netdevice.h>
#include <linux/in_route.h>
#include <linux/route.h>
#include <linux/skbuff.h>
#include <linux/igmp.h>
#include <net/net_namespace.h>
#include <net/dst.h>
#include <net/sock.h>
#include <linux/ip.h>
#include <linux/net.h>
#include <net/ip.h>
#include <net/icmp.h>
#include <net/udp.h>
#include <net/raw.h>
#include <net/snmp.h>
#include <net/tcp_states.h>
#include <net/inet_common.h>
#include <net/checksum.h>
#include <net/xfrm.h>
#include <linux/rtnetlink.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/compat.h>
#include <linux/uio.h>struct raw_frag_vec {struct msghdr *msg;union {struct icmphdr icmph;char c[1];} hdr;int hlen;
};struct raw_hashinfo raw_v4_hashinfo = {.lock = __RW_LOCK_UNLOCKED(raw_v4_hashinfo.lock),
};
EXPORT_SYMBOL_GPL(raw_v4_hashinfo);int raw_hash_sk(struct sock *sk)
{struct raw_hashinfo *h = sk->sk_prot->h.raw_hash;struct hlist_head *head;head = &h->ht[inet_sk(sk)->inet_num & (RAW_HTABLE_SIZE - 1)];write_lock_bh(&h->lock);sk_add_node(sk, head);sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);write_unlock_bh(&h->lock);return 0;
}
EXPORT_SYMBOL_GPL(raw_hash_sk);void raw_unhash_sk(struct sock *sk)
{struct raw_hashinfo *h = sk->sk_prot->h.raw_hash;write_lock_bh(&h->lock);if (sk_del_node_init(sk))sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);write_unlock_bh(&h->lock);
}
EXPORT_SYMBOL_GPL(raw_unhash_sk);struct sock *__raw_v4_lookup(struct net *net, struct sock *sk,unsigned short num, __be32 raddr, __be32 laddr,int dif, int sdif)
{sk_for_each_from(sk) {struct inet_sock *inet = inet_sk(sk);if (net_eq(sock_net(sk), net) && inet->inet_num == num &&!(inet->inet_daddr && inet->inet_daddr != raddr) &&!(inet->inet_rcv_saddr && inet->inet_rcv_saddr != laddr) &&!(sk->sk_bound_dev_if && sk->sk_bound_dev_if != dif &&sk->sk_bound_dev_if != sdif))goto found; /* gotcha */}sk = NULL;
found:return sk;
}
EXPORT_SYMBOL_GPL(__raw_v4_lookup);/** 0 - deliver* 1 - block*/
static int icmp_filter(const struct sock *sk, const struct sk_buff *skb)
{struct icmphdr _hdr;const struct icmphdr *hdr;hdr = skb_header_pointer(skb, skb_transport_offset(skb),sizeof(_hdr), &_hdr);if (!hdr)return 1;if (hdr->type < 32) {__u32 data = raw_sk(sk)->filter.data;return ((1U << hdr->type) & data) != 0;}/* Do not block unknown ICMP types */return 0;
}/* IP input processing comes here for RAW socket delivery.* Caller owns SKB, so we must make clones.** RFC 1122: SHOULD pass TOS value up to the transport layer.* -> It does. And not only TOS, but all IP header.*/
static int raw_v4_input(struct sk_buff *skb, const struct iphdr *iph, int hash)
{int sdif = inet_sdif(skb);struct sock *sk;struct hlist_head *head;int delivered = 0;struct net *net;read_lock(&raw_v4_hashinfo.lock);head = &raw_v4_hashinfo.ht[hash];if (hlist_empty(head))goto out;net = dev_net(skb->dev);sk = __raw_v4_lookup(net, __sk_head(head), iph->protocol,iph->saddr, iph->daddr,skb->dev->ifindex, sdif);while (sk) {delivered = 1;if ((iph->protocol != IPPROTO_ICMP || !icmp_filter(sk, skb)) &&ip_mc_sf_allow(sk, iph->daddr, iph->saddr,skb->dev->ifindex, sdif)) {struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC);/* Not releasing hash table! */if (clone)raw_rcv(sk, clone);}sk = __raw_v4_lookup(net, sk_next(sk), iph->protocol,iph->saddr, iph->daddr,skb->dev->ifindex, sdif);}
out:read_unlock(&raw_v4_hashinfo.lock);return delivered;
}int raw_local_deliver(struct sk_buff *skb, int protocol)
{int hash;struct sock *raw_sk;hash = protocol & (RAW_HTABLE_SIZE - 1);raw_sk = sk_head(&raw_v4_hashinfo.ht[hash]);/* If there maybe a raw socket we must check - if not we* don't care less*/if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash))raw_sk = NULL;return raw_sk != NULL;}static void raw_err(struct sock *sk, struct sk_buff *skb, u32 info)
{struct inet_sock *inet = inet_sk(sk);const int type = icmp_hdr(skb)->type;const int code = icmp_hdr(skb)->code;int err = 0;int harderr = 0;if (type == ICMP_DEST_UNREACH && code == ICMP_FRAG_NEEDED)ipv4_sk_update_pmtu(skb, sk, info);else if (type == ICMP_REDIRECT) {ipv4_sk_redirect(skb, sk);return;}/* Report error on raw socket, if:1. User requested ip_recverr.2. Socket is connected (otherwise the error indicationis useless without ip_recverr and error is hard.*/if (!inet->recverr && sk->sk_state != TCP_ESTABLISHED)return;switch (type) {default:case ICMP_TIME_EXCEEDED:err = EHOSTUNREACH;break;case ICMP_SOURCE_QUENCH:return;case ICMP_PARAMETERPROB:err = EPROTO;harderr = 1;break;case ICMP_DEST_UNREACH:err = EHOSTUNREACH;if (code > NR_ICMP_UNREACH)break;err = icmp_err_convert[code].errno;harderr = icmp_err_convert[code].fatal;if (code == ICMP_FRAG_NEEDED) {harderr = inet->pmtudisc != IP_PMTUDISC_DONT;err = EMSGSIZE;}}if (inet->recverr) {const struct iphdr *iph = (const struct iphdr *)skb->data;u8 *payload = skb->data + (iph->ihl << 2);if (inet->hdrincl)payload = skb->data;ip_icmp_error(sk, skb, err, 0, info, payload);}if (inet->recverr || harderr) {sk->sk_err = err;sk->sk_error_report(sk);}
}void raw_icmp_error(struct sk_buff *skb, int protocol, u32 info)
{int hash;struct sock *raw_sk;const struct iphdr *iph;struct net *net;hash = protocol & (RAW_HTABLE_SIZE - 1);read_lock(&raw_v4_hashinfo.lock);raw_sk = sk_head(&raw_v4_hashinfo.ht[hash]);if (raw_sk) {int dif = skb->dev->ifindex;int sdif = inet_sdif(skb);iph = (const struct iphdr *)skb->data;net = dev_net(skb->dev);while ((raw_sk = __raw_v4_lookup(net, raw_sk, protocol,iph->daddr, iph->saddr,dif, sdif)) != NULL) {raw_err(raw_sk, skb, info);raw_sk = sk_next(raw_sk);iph = (const struct iphdr *)skb->data;}}read_unlock(&raw_v4_hashinfo.lock);
}static int raw_rcv_skb(struct sock *sk, struct sk_buff *skb)
{/* Charge it to the socket. */ipv4_pktinfo_prepare(sk, skb);if (sock_queue_rcv_skb(sk, skb) < 0) {kfree_skb(skb);return NET_RX_DROP;}return NET_RX_SUCCESS;
}int raw_rcv(struct sock *sk, struct sk_buff *skb)
{if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb)) {atomic_inc(&sk->sk_drops);kfree_skb(skb);return NET_RX_DROP;}nf_reset(skb);skb_push(skb, skb->data - skb_network_header(skb));raw_rcv_skb(sk, skb);return 0;
}static int raw_send_hdrinc(struct sock *sk, struct flowi4 *fl4,struct msghdr *msg, size_t length,struct rtable **rtp, unsigned int flags,const struct sockcm_cookie *sockc)
{struct inet_sock *inet = inet_sk(sk);struct net *net = sock_net(sk);struct iphdr *iph;struct sk_buff *skb;unsigned int iphlen;int err;struct rtable *rt = *rtp;int hlen, tlen;if (length > rt->dst.dev->mtu) {ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport,rt->dst.dev->mtu);return -EMSGSIZE;}if (length < sizeof(struct iphdr))return -EINVAL;if (flags&MSG_PROBE)goto out;hlen = LL_RESERVED_SPACE(rt->dst.dev);tlen = rt->dst.dev->needed_tailroom;skb = sock_alloc_send_skb(sk,length + hlen + tlen + 15,flags & MSG_DONTWAIT, &err);if (!skb)goto error;skb_reserve(skb, hlen);skb->priority = sk->sk_priority;skb->mark = sk->sk_mark;skb->tstamp = sockc->transmit_time;skb_dst_set(skb, &rt->dst);*rtp = NULL;skb_reset_network_header(skb);iph = ip_hdr(skb);skb_put(skb, length);skb->ip_summed = CHECKSUM_NONE;sock_tx_timestamp(sk, sockc->tsflags, &skb_shinfo(skb)->tx_flags);if (flags & MSG_CONFIRM)skb_set_dst_pending_confirm(skb, 1);skb->transport_header = skb->network_header;err = -EFAULT;if (memcpy_from_msg(iph, msg, length))goto error_free;iphlen = iph->ihl * 4;/** We don't want to modify the ip header, but we do need to* be sure that it won't cause problems later along the network* stack. Specifically we want to make sure that iph->ihl is a* sane value. If ihl points beyond the length of the buffer passed* in, reject the frame as invalid*/err = -EINVAL;if (iphlen > length)goto error_free;if (iphlen >= sizeof(*iph)) {if (!iph->saddr)iph->saddr = fl4->saddr;iph->check = 0;iph->tot_len = htons(length);if (!iph->id)ip_select_ident(net, skb, NULL);iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);skb->transport_header += iphlen;if (iph->protocol == IPPROTO_ICMP &&length >= iphlen + sizeof(struct icmphdr))icmp_out_count(net, ((struct icmphdr *)skb_transport_header(skb))->type);}err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT,net, sk, skb, NULL, rt->dst.dev,dst_output);if (err > 0)err = net_xmit_errno(err);if (err)goto error;
out:return 0;error_free:kfree_skb(skb);
error:IP_INC_STATS(net, IPSTATS_MIB_OUTDISCARDS);if (err == -ENOBUFS && !inet->recverr)err = 0;return err;
}static int raw_probe_proto_opt(struct raw_frag_vec *rfv, struct flowi4 *fl4)
{int err;if (fl4->flowi4_proto != IPPROTO_ICMP)return 0;/* We only need the first two bytes. */rfv->hlen = 2;err = memcpy_from_msg(rfv->hdr.c, rfv->msg, rfv->hlen);if (err)return err;fl4->fl4_icmp_type = rfv->hdr.icmph.type;fl4->fl4_icmp_code = rfv->hdr.icmph.code;return 0;
}static int raw_getfrag(void *from, char *to, int offset, int len, int odd,struct sk_buff *skb)
{struct raw_frag_vec *rfv = from;if (offset < rfv->hlen) {int copy = min(rfv->hlen - offset, len);if (skb->ip_summed == CHECKSUM_PARTIAL)memcpy(to, rfv->hdr.c + offset, copy);elseskb->csum = csum_block_add(skb->csum,csum_partial_copy_nocheck(rfv->hdr.c + offset,to, copy, 0),odd);odd = 0;offset += copy;to += copy;len -= copy;if (!len)return 0;}offset -= rfv->hlen;return ip_generic_getfrag(rfv->msg, to, offset, len, odd, skb);
}static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{struct inet_sock *inet = inet_sk(sk);struct net *net = sock_net(sk);struct ipcm_cookie ipc;struct rtable *rt = NULL;struct flowi4 fl4;int free = 0;__be32 daddr;__be32 saddr;u8 tos;int err;struct ip_options_data opt_copy;struct raw_frag_vec rfv;int hdrincl;err = -EMSGSIZE;if (len > 0xFFFF)goto out;/* hdrincl should be READ_ONCE(inet->hdrincl)* but READ_ONCE() doesn't work with bit fields.* Doing this indirectly yields the same result.*/hdrincl = inet->hdrincl;hdrincl = READ_ONCE(hdrincl);/** Check the flags.*/err = -EOPNOTSUPP;if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message */goto out; /* compatibility *//** Get and verify the address.*/if (msg->msg_namelen) {DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name);err = -EINVAL;if (msg->msg_namelen < sizeof(*usin))goto out;if (usin->sin_family != AF_INET) {pr_info_once("%s: %s forgot to set AF_INET. Fix it!\n",__func__, current->comm);err = -EAFNOSUPPORT;if (usin->sin_family)goto out;}daddr = usin->sin_addr.s_addr;/* ANK: I did not forget to get protocol from port field.* I just do not know, who uses this weirdness.* IP_HDRINCL is much more convenient.*/} else {err = -EDESTADDRREQ;if (sk->sk_state != TCP_ESTABLISHED)goto out;daddr = inet->inet_daddr;}ipcm_init_sk(&ipc, inet);if (msg->msg_controllen) {err = ip_cmsg_send(sk, msg, &ipc, false);if (unlikely(err)) {kfree(ipc.opt);goto out;}if (ipc.opt)free = 1;}saddr = ipc.addr;ipc.addr = daddr;if (!ipc.opt) {struct ip_options_rcu *inet_opt;rcu_read_lock();inet_opt = rcu_dereference(inet->inet_opt);if (inet_opt) {memcpy(&opt_copy, inet_opt,sizeof(*inet_opt) + inet_opt->opt.optlen);ipc.opt = &opt_copy.opt;}rcu_read_unlock();}if (ipc.opt) {err = -EINVAL;/* Linux does not mangle headers on raw sockets,* so that IP options + IP_HDRINCL is non-sense.*/if (hdrincl)goto done;if (ipc.opt->opt.srr) {if (!daddr)goto done;daddr = ipc.opt->opt.faddr;}}tos = get_rtconn_flags(&ipc, sk);if (msg->msg_flags & MSG_DONTROUTE)tos |= RTO_ONLINK;if (ipv4_is_multicast(daddr)) {if (!ipc.oif)ipc.oif = inet->mc_index;if (!saddr)saddr = inet->mc_addr;} else if (!ipc.oif) {ipc.oif = inet->uc_index;} else if (ipv4_is_lbcast(daddr) && inet->uc_index) {/* oif is set, packet is to local broadcast and* and uc_index is set. oif is most likely set* by sk_bound_dev_if. If uc_index != oif check if the* oif is an L3 master and uc_index is an L3 slave.* If so, we want to allow the send using the uc_index.*/if (ipc.oif != inet->uc_index &&ipc.oif == l3mdev_master_ifindex_by_index(sock_net(sk),inet->uc_index)) {ipc.oif = inet->uc_index;}}flowi4_init_output(&fl4, ipc.oif, sk->sk_mark, tos,RT_SCOPE_UNIVERSE,hdrincl ? IPPROTO_RAW : sk->sk_protocol,inet_sk_flowi_flags(sk) |(hdrincl ? FLOWI_FLAG_KNOWN_NH : 0),daddr, saddr, 0, 0, sk->sk_uid);if (!hdrincl) {rfv.msg = msg;rfv.hlen = 0;err = raw_probe_proto_opt(&rfv, &fl4);if (err)goto done;}security_sk_classify_flow(sk, flowi4_to_flowi(&fl4));rt = ip_route_output_flow(net, &fl4, sk);if (IS_ERR(rt)) {err = PTR_ERR(rt);rt = NULL;goto done;}err = -EACCES;if (rt->rt_flags & RTCF_BROADCAST && !sock_flag(sk, SOCK_BROADCAST))goto done;if (msg->msg_flags & MSG_CONFIRM)goto do_confirm;
back_from_confirm:if (hdrincl)err = raw_send_hdrinc(sk, &fl4, msg, len,&rt, msg->msg_flags, &ipc.sockc);else {if (!ipc.addr)ipc.addr = fl4.daddr;lock_sock(sk);err = ip_append_data(sk, &fl4, raw_getfrag,&rfv, len, 0,&ipc, &rt, msg->msg_flags);if (err)ip_flush_pending_frames(sk);else if (!(msg->msg_flags & MSG_MORE)) {err = ip_push_pending_frames(sk, &fl4);if (err == -ENOBUFS && !inet->recverr)err = 0;}release_sock(sk);}
done:if (free)kfree(ipc.opt);ip_rt_put(rt);out:if (err < 0)return err;return len;do_confirm:if (msg->msg_flags & MSG_PROBE)dst_confirm_neigh(&rt->dst, &fl4.daddr);if (!(msg->msg_flags & MSG_PROBE) || len)goto back_from_confirm;err = 0;goto done;
}static void raw_close(struct sock *sk, long timeout)
{/** Raw sockets may have direct kernel references. Kill them.*/ip_ra_control(sk, 0, NULL);sk_common_release(sk);
}static void raw_destroy(struct sock *sk)
{lock_sock(sk);ip_flush_pending_frames(sk);release_sock(sk);
}/* This gets rid of all the nasties in af_inet. -DaveM */
static int raw_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{struct inet_sock *inet = inet_sk(sk);struct sockaddr_in *addr = (struct sockaddr_in *) uaddr;u32 tb_id = RT_TABLE_LOCAL;int ret = -EINVAL;int chk_addr_ret;if (sk->sk_state != TCP_CLOSE || addr_len < sizeof(struct sockaddr_in))goto out;if (sk->sk_bound_dev_if)tb_id = l3mdev_fib_table_by_index(sock_net(sk),sk->sk_bound_dev_if) ? : tb_id;chk_addr_ret = inet_addr_type_table(sock_net(sk), addr->sin_addr.s_addr,tb_id);ret = -EADDRNOTAVAIL;if (addr->sin_addr.s_addr && chk_addr_ret != RTN_LOCAL &&chk_addr_ret != RTN_MULTICAST && chk_addr_ret != RTN_BROADCAST)goto out;inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)inet->inet_saddr = 0; /* Use device */sk_dst_reset(sk);ret = 0;
out: return ret;
}/** This should be easy, if there is something there* we return it, otherwise we block.*/static int raw_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,int noblock, int flags, int *addr_len)
{struct inet_sock *inet = inet_sk(sk);size_t copied = 0;int err = -EOPNOTSUPP;DECLARE_SOCKADDR(struct sockaddr_in *, sin, msg->msg_name);struct sk_buff *skb;if (flags & MSG_OOB)goto out;if (flags & MSG_ERRQUEUE) {err = ip_recv_error(sk, msg, len, addr_len);goto out;}skb = skb_recv_datagram(sk, flags, noblock, &err);if (!skb)goto out;copied = skb->len;if (len < copied) {msg->msg_flags |= MSG_TRUNC;copied = len;}err = skb_copy_datagram_msg(skb, 0, msg, copied);if (err)goto done;sock_recv_ts_and_drops(msg, sk, skb);/* Copy the address. */if (sin) {sin->sin_family = AF_INET;sin->sin_addr.s_addr = ip_hdr(skb)->saddr;sin->sin_port = 0;memset(&sin->sin_zero, 0, sizeof(sin->sin_zero));*addr_len = sizeof(*sin);}if (inet->cmsg_flags)ip_cmsg_recv(msg, skb);if (flags & MSG_TRUNC)copied = skb->len;
done:skb_free_datagram(sk, skb);
out:if (err)return err;return copied;
}static int raw_init(struct sock *sk)
{struct raw_sock *rp = raw_sk(sk);if (inet_sk(sk)->inet_num == IPPROTO_ICMP)memset(&rp->filter, 0, sizeof(rp->filter));return 0;
}static int raw_seticmpfilter(struct sock *sk, char __user *optval, int optlen)
{if (optlen > sizeof(struct icmp_filter))optlen = sizeof(struct icmp_filter);if (copy_from_user(&raw_sk(sk)->filter, optval, optlen))return -EFAULT;return 0;
}static int raw_geticmpfilter(struct sock *sk, char __user *optval, int __user *optlen)
{int len, ret = -EFAULT;if (get_user(len, optlen))goto out;ret = -EINVAL;if (len < 0)goto out;if (len > sizeof(struct icmp_filter))len = sizeof(struct icmp_filter);ret = -EFAULT;if (put_user(len, optlen) ||copy_to_user(optval, &raw_sk(sk)->filter, len))goto out;ret = 0;
out: return ret;
}static int do_raw_setsockopt(struct sock *sk, int level, int optname,char __user *optval, unsigned int optlen)
{if (optname == ICMP_FILTER) {if (inet_sk(sk)->inet_num != IPPROTO_ICMP)return -EOPNOTSUPP;elsereturn raw_seticmpfilter(sk, optval, optlen);}return -ENOPROTOOPT;
}static int raw_setsockopt(struct sock *sk, int level, int optname,char __user *optval, unsigned int optlen)
{if (level != SOL_RAW)return ip_setsockopt(sk, level, optname, optval, optlen);return do_raw_setsockopt(sk, level, optname, optval, optlen);
}#ifdef CONFIG_COMPAT
static int compat_raw_setsockopt(struct sock *sk, int level, int optname,char __user *optval, unsigned int optlen)
{if (level != SOL_RAW)return compat_ip_setsockopt(sk, level, optname, optval, optlen);return do_raw_setsockopt(sk, level, optname, optval, optlen);
}
#endifstatic int do_raw_getsockopt(struct sock *sk, int level, int optname,char __user *optval, int __user *optlen)
{if (optname == ICMP_FILTER) {if (inet_sk(sk)->inet_num != IPPROTO_ICMP)return -EOPNOTSUPP;elsereturn raw_geticmpfilter(sk, optval, optlen);}return -ENOPROTOOPT;
}static int raw_getsockopt(struct sock *sk, int level, int optname,char __user *optval, int __user *optlen)
{if (level != SOL_RAW)return ip_getsockopt(sk, level, optname, optval, optlen);return do_raw_getsockopt(sk, level, optname, optval, optlen);
}#ifdef CONFIG_COMPAT
static int compat_raw_getsockopt(struct sock *sk, int level, int optname,char __user *optval, int __user *optlen)
{if (level != SOL_RAW)return compat_ip_getsockopt(sk, level, optname, optval, optlen);return do_raw_getsockopt(sk, level, optname, optval, optlen);
}
#endifstatic int raw_ioctl(struct sock *sk, int cmd, unsigned long arg)
{switch (cmd) {case SIOCOUTQ: {int amount = sk_wmem_alloc_get(sk);return put_user(amount, (int __user *)arg);}case SIOCINQ: {struct sk_buff *skb;int amount = 0;spin_lock_bh(&sk->sk_receive_queue.lock);skb = skb_peek(&sk->sk_receive_queue);if (skb)amount = skb->len;spin_unlock_bh(&sk->sk_receive_queue.lock);return put_user(amount, (int __user *)arg);}default:
#ifdef CONFIG_IP_MROUTEreturn ipmr_ioctl(sk, cmd, (void __user *)arg);
#elsereturn -ENOIOCTLCMD;
#endif}
}#ifdef CONFIG_COMPAT
static int compat_raw_ioctl(struct sock *sk, unsigned int cmd, unsigned long arg)
{switch (cmd) {case SIOCOUTQ:case SIOCINQ:return -ENOIOCTLCMD;default:
#ifdef CONFIG_IP_MROUTEreturn ipmr_compat_ioctl(sk, cmd, compat_ptr(arg));
#elsereturn -ENOIOCTLCMD;
#endif}
}
#endifint raw_abort(struct sock *sk, int err)
{lock_sock(sk);sk->sk_err = err;sk->sk_error_report(sk);__udp_disconnect(sk, 0);release_sock(sk);return 0;
}
EXPORT_SYMBOL_GPL(raw_abort);struct proto raw_prot = {.name = "RAW",.owner = THIS_MODULE,.close = raw_close,.destroy = raw_destroy,.connect = ip4_datagram_connect,.disconnect = __udp_disconnect,.ioctl = raw_ioctl,.init = raw_init,.setsockopt = raw_setsockopt,.getsockopt = raw_getsockopt,.sendmsg = raw_sendmsg,.recvmsg = raw_recvmsg,.bind = raw_bind,.backlog_rcv = raw_rcv_skb,.release_cb = ip4_datagram_release_cb,.hash = raw_hash_sk,.unhash = raw_unhash_sk,.obj_size = sizeof(struct raw_sock),.useroffset = offsetof(struct raw_sock, filter),.usersize = sizeof_field(struct raw_sock, filter),.h.raw_hash = &raw_v4_hashinfo,
#ifdef CONFIG_COMPAT.compat_setsockopt = compat_raw_setsockopt,.compat_getsockopt = compat_raw_getsockopt,.compat_ioctl = compat_raw_ioctl,
#endif.diag_destroy = raw_abort,
};#ifdef CONFIG_PROC_FS
static struct sock *raw_get_first(struct seq_file *seq)
{struct sock *sk;struct raw_hashinfo *h = PDE_DATA(file_inode(seq->file));struct raw_iter_state *state = raw_seq_private(seq);for (state->bucket = 0; state->bucket < RAW_HTABLE_SIZE;++state->bucket) {sk_for_each(sk, &h->ht[state->bucket])if (sock_net(sk) == seq_file_net(seq))goto found;}sk = NULL;
found:return sk;
}static struct sock *raw_get_next(struct seq_file *seq, struct sock *sk)
{struct raw_hashinfo *h = PDE_DATA(file_inode(seq->file));struct raw_iter_state *state = raw_seq_private(seq);do {sk = sk_next(sk);
try_again:;} while (sk && sock_net(sk) != seq_file_net(seq));if (!sk && ++state->bucket < RAW_HTABLE_SIZE) {sk = sk_head(&h->ht[state->bucket]);goto try_again;}return sk;
}static struct sock *raw_get_idx(struct seq_file *seq, loff_t pos)
{struct sock *sk = raw_get_first(seq);if (sk)while (pos && (sk = raw_get_next(seq, sk)) != NULL)--pos;return pos ? NULL : sk;
}void *raw_seq_start(struct seq_file *seq, loff_t *pos)
{struct raw_hashinfo *h = PDE_DATA(file_inode(seq->file));read_lock(&h->lock);return *pos ? raw_get_idx(seq, *pos - 1) : SEQ_START_TOKEN;
}
EXPORT_SYMBOL_GPL(raw_seq_start);void *raw_seq_next(struct seq_file *seq, void *v, loff_t *pos)
{struct sock *sk;if (v == SEQ_START_TOKEN)sk = raw_get_first(seq);elsesk = raw_get_next(seq, v);++*pos;return sk;
}
EXPORT_SYMBOL_GPL(raw_seq_next);void raw_seq_stop(struct seq_file *seq, void *v)
{struct raw_hashinfo *h = PDE_DATA(file_inode(seq->file));read_unlock(&h->lock);
}
EXPORT_SYMBOL_GPL(raw_seq_stop);static void raw_sock_seq_show(struct seq_file *seq, struct sock *sp, int i)
{struct inet_sock *inet = inet_sk(sp);__be32 dest = inet->inet_daddr,src = inet->inet_rcv_saddr;__u16 destp = 0,srcp = inet->inet_num;seq_printf(seq, "%4d: %08X:%04X %08X:%04X"" %02X %08X:%08X %02X:%08lX %08X %5u %8d %lu %d %pK %d\n",i, src, srcp, dest, destp, sp->sk_state,sk_wmem_alloc_get(sp),sk_rmem_alloc_get(sp),0, 0L, 0,from_kuid_munged(seq_user_ns(seq), sock_i_uid(sp)),0, sock_i_ino(sp),refcount_read(&sp->sk_refcnt), sp, atomic_read(&sp->sk_drops));
}static int raw_seq_show(struct seq_file *seq, void *v)
{if (v == SEQ_START_TOKEN)seq_printf(seq, " sl local_address rem_address st tx_queue ""rx_queue tr tm->when retrnsmt uid timeout ""inode ref pointer drops\n");elseraw_sock_seq_show(seq, v, raw_seq_private(seq)->bucket);return 0;
}static const struct seq_operations raw_seq_ops = {.start = raw_seq_start,.next = raw_seq_next,.stop = raw_seq_stop,.show = raw_seq_show,
};static __net_init int raw_init_net(struct net *net)
{if (!proc_create_net_data("raw", 0444, net->proc_net, &raw_seq_ops,sizeof(struct raw_iter_state), &raw_v4_hashinfo))return -ENOMEM;return 0;
}static __net_exit void raw_exit_net(struct net *net)
{remove_proc_entry("raw", net->proc_net);
}static __net_initdata struct pernet_operations raw_net_ops = {.init = raw_init_net,.exit = raw_exit_net,
};int __init raw_proc_init(void)
{return register_pernet_subsys(&raw_net_ops);
}void __init raw_proc_exit(void)
{unregister_pernet_subsys(&raw_net_ops);
}
#endif /* CONFIG_PROC_FS */
这段代码是 Linux 4.9 内核中 IPv4 RAW 套接字的实现,位于 net/ipv4/raw.c
。RAW 套接字允许应用程序直接访问网络层(IP层),可以用于处理自定义的 IP 数据包,甚至自行构造 IP 头部。它常用于实现自定义协议、网络诊断工具(如 ping
、traceroute
)等。
以下是对该文件的详细分析:
1. 主要功能与特性
直接 IP 层访问:允许读写原始 IP 数据包。
协议处理:可以处理特定的 IP 协议(如 ICMP、IGMP 等)。
IP_HDRINCL 选项:允许应用程序自行构造 IP 头部。
ICMP 过滤:支持对接收的 ICMP 消息进行过滤。
多播支持:可以加入多播组并接收多播流量。
内核内部使用:也被用于内核内部的某些功能(如 ICMP 错误处理)。
2. 关键数据结构
raw_hashinfo
c
struct raw_hashinfo raw_v4_hashinfo = {.lock = __RW_LOCK_UNLOCKED(raw_v4_hashinfo.lock), };
这是一个全局的哈希表,用于存储所有 IPv4 RAW 套接字。每个套接字根据其绑定的协议号(inet_num
)哈希到不同的桶中。读写锁用于保护哈希表的并发访问。
raw_frag_vec
c
struct raw_frag_vec {struct msghdr *msg;union {struct icmphdr icmph;char c[1];} hdr;int hlen; };
用于在发送数据时处理分片和协议头信息,特别是在处理 ICMP 协议时。
3. 核心函数分析
raw_v4_input
c
static int raw_v4_input(struct sk_buff *skb, const struct iphdr *iph, int hash)
功能:处理输入的 IP 数据包,并将其传递给匹配的 RAW 套接字。
流程:
根据 IP 协议号计算哈希值,找到对应的哈希桶。
遍历该桶中的套接字,查找匹配的套接字(协议号、源/目标地址、设备等)。
如果找到匹配的套接字,且满足过滤条件(如 ICMP 过滤、多播过滤),则克隆 SKB 并调用
raw_rcv
将数据包传递给套接字。
raw_rcv
c
int raw_rcv(struct sock *sk, struct sk_buff *skb)
功能:将数据包放入套接字的接收队列。
流程:
检查安全策略(XFRM)。
重置网络过滤器状态。
调整 SKB 指针(确保网络头正确)。
调用
raw_rcv_skb
将数据包放入接收队列。
raw_sendmsg
c
static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
功能:发送原始 IP 数据包。
流程:
检查消息标志和长度。
解析目标地址(从
msg_name
或套接字已连接地址)。处理控制消息(如 IP 选项)。
初始化流信息(
flowi4
),用于路由查找。根据是否设置
IP_HDRINCL
选项:如果设置,调用
raw_send_hdrinc
由应用程序构造完整 IP 头部。否则,由内核构造 IP 头部,调用
ip_append_data
和ip_push_pending_frames
发送数据。
raw_recvmsg
c
static int raw_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int noblock, int flags, int *addr_len)
功能:从套接字接收数据。
流程:
处理错误队列(
MSG_ERRQUEUE
)。从接收队列中获取 SKB。
将数据复制到用户空间。
填充源地址信息(如果请求)。
处理控制消息(如 IP 选项)。
4. 套接字选项处理
raw_setsockopt
和 raw_getsockopt
处理 RAW 套接字特有的选项,如
ICMP_FILTER
。其他选项传递给 IP 层的通用处理函数。
5. ICMP 错误处理
raw_err
和 raw_icmp_error
处理 ICMP 错误消息(如目的不可达、重定向等)。
根据错误类型更新套接字错误状态,并通知应用程序。
6. 套接字生命周期管理
raw_close
和 raw_destroy
释放套接字资源,刷新未发送的数据包。
raw_bind
绑定套接字到特定地址和协议。
7. /proc 文件系统支持
raw_init_net
和 raw_exit_net
在每个网络命名空间中创建
/proc/net/raw
文件,用于显示当前 RAW 套接字状态。
raw_seq_show
格式化显示 RAW 套接字的信息(本地地址、远程地址、状态等)。
8. 并发与锁
使用读写锁(
raw_v4_hashinfo.lock
)保护全局哈希表的并发访问。使用套接字锁保护单个套接字的发送和接收队列。
9. 其他功能
多播支持:通过
ip_mc_sf_allow
检查多播过滤。IP 选项处理:支持发送和接收 IP 选项。
内核内部使用:RAW 套接字也被用于内核内部的 ICMP 处理。
10. 导出符号
一些函数被导出(
EXPORT_SYMBOL_GPL
),供其他内核模块使用(如 IPsec、多播路由等)。
总结
net/ipv4/raw.c
实现了 Linux 内核中的 IPv4 RAW 套接字功能,提供了直接访问 IP 层的能力。它处理套接字的创建、绑定、发送、接收、错误处理以及各种控制操作。代码中充分考虑了并发安全、内存管理和性能优化,同时支持丰富的功能如 ICMP 过滤、多播和 IP 选项处理。
自定义 IP 数据包与自行构造 IP 头部
在 Linux 中,使用 RAW 套接字可以自定义 IP 数据包并自行构造 IP 头部。这允许你完全控制 IP 数据包的内容,包括协议类型、源/目的地址、标识符、分段信息等。
基本步骤
1. 创建 RAW 套接字
c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netinet/ip.h>int main() {int sockfd;// 创建 RAW 套接字,指定 IPPROTO_RAW 表示我们将自己构造 IP 头部if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {perror("socket() failed");exit(EXIT_FAILURE);}// 设置 IP_HDRINCL 选项,告诉内核我们不希望它构造 IP 头部int hincl = 1;if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &hincl, sizeof(hincl)) < 0) {perror("setsockopt() failed");close(sockfd);exit(EXIT_FAILURE);}// 其余代码...close(sockfd);return 0; }
2. 构造 IP 头部
c
// IP 头部结构体 (与内核中的定义匹配) struct ipheader {unsigned char iph_ihl:4, iph_ver:4;unsigned char iph_tos;unsigned short int iph_len;unsigned short int iph_ident;unsigned short int iph_flags:3, iph_offset:13;unsigned char iph_ttl;unsigned char iph_protocol;unsigned short int iph_chksum;struct in_addr iph_sourceip;struct in_addr iph_destip; };// 计算校验和的辅助函数 unsigned short calculate_checksum(unsigned short *ptr, int nbytes) {register long sum;unsigned short oddbyte;register short answer;sum = 0;while (nbytes > 1) {sum += *ptr++;nbytes -= 2;}if (nbytes == 1) {oddbyte = 0;*((u_char*)&oddbyte) = *(u_char*)ptr;sum += oddbyte;}sum = (sum >> 16) + (sum & 0xffff);sum += (sum >> 16);answer = (short)~sum;return answer; }// 构造 IP 数据包 void build_ip_packet(struct ipheader *iph, const char *src_ip, const char *dst_ip, u_char protocol, const char *data, int data_len) {// 填充 IP 头部iph->iph_ver = 4; // IPv4iph->iph_ihl = 5; // 头部长度 (5 * 4 = 20 字节)iph->iph_tos = 0; // 服务类型iph->iph_len = htons(sizeof(struct ipheader) + data_len); // 总长度iph->iph_ident = htons(54321); // 标识符iph->iph_flags = 0; // 分段标志iph->iph_offset = 0; // 分段偏移iph->iph_ttl = 255; // 生存时间iph->iph_protocol = protocol; // 协议类型iph->iph_chksum = 0; // 先置0,后面计算校验和// 源和目的IP地址inet_pton(AF_INET, src_ip, &(iph->iph_sourceip));inet_pton(AF_INET, dst_ip, &(iph->iph_destip));// 计算IP头部校验和iph->iph_chksum = calculate_checksum((unsigned short *)iph, sizeof(struct ipheader));// 复制数据char *packet = (char *)iph + sizeof(struct ipheader);memcpy(packet, data, data_len); }
3. 发送自定义 IP 数据包
c
void send_custom_packet(int sockfd, const char *dst_ip, u_char protocol, const char *data, int data_len) {char packet[1024];struct ipheader *iph = (struct ipheader *)packet;struct sockaddr_in dest_addr;// 构建目标地址结构memset(&dest_addr, 0, sizeof(dest_addr));dest_addr.sin_family = AF_INET;inet_pton(AF_INET, dst_ip, &(dest_addr.sin_addr));// 构造 IP 数据包 (使用虚假源IP)build_ip_packet(iph, "192.168.1.100", dst_ip, protocol, data, data_len);// 发送数据包if (sendto(sockfd, packet, ntohs(iph->iph_len), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {perror("sendto() failed");} else {printf("Packet sent to %s\n", dst_ip);} }
4. 完整示例
c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netinet/ip.h>// 上面定义的函数和结构体...int main() {int sockfd;// 创建 RAW 套接字if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {perror("socket() failed");exit(EXIT_FAILURE);}// 设置 IP_HDRINCL 选项int hincl = 1;if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &hincl, sizeof(hincl)) < 0) {perror("setsockopt() failed");close(sockfd);exit(EXIT_FAILURE);}// 发送自定义 ICMP 数据包 (ping)char icmp_data[] = "Hello, Raw Socket!";send_custom_packet(sockfd, "8.8.8.8", IPPROTO_ICMP, icmp_data, strlen(icmp_data));// 发送自定义 TCP 数据包 (需要构造完整的 TCP 头部)// 注意:这里只是示例,实际需要构造完整的 TCP 头部char tcp_data[] = "TCP Data";send_custom_packet(sockfd, "192.168.1.1", IPPROTO_TCP, tcp_data, strlen(tcp_data));close(sockfd);return 0; }
注意事项
需要 root 权限:创建 RAW 套接字需要超级用户权限。
协议选择:你可以指定任何 IP 协议号(如 IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP 等)。
完整协议栈:如果你选择高层协议(如 TCP/UDP),你需要自己构造完整的协议头部(TCP/UDP 头部),包括计算校验和。
接收响应:要接收响应,你需要绑定到特定的协议并使用 recvfrom() 读取数据。
网络地址转换:注意使用 htons()/htonl() 等函数确保正确的字节序。
内核处理:设置了 IP_HDRINCL 后,内核不会修改你的 IP 头部(除了源地址在某些情况下),也不会计算校验和。
高级用法
对于更高级的用法,你可以:
构造完整的 TCP/UDP/ICMP 数据包
实现自定义的网络协议
进行网络测试和诊断
开发安全工具(如扫描器、防火墙测试工具)
这种能力非常强大,但也需要你对网络协议有深入的理解,并且要谨慎使用,因为不当的使用可能会影响网络正常运行。
这是一个非常深刻且重要的问题。答案是:不可以,绝对不会像 TCP 那样自动获得可靠传输。
自定义 IP 数据包(使用 RAW Socket 和 IP_HDRINCL
)只是为你提供了在网络层(IP层) 手动构造和发送数据块的能力。IP 协议本身是无连接、不可靠的。它不提供任何保证。
要将这种底层能力变得可靠,你需要在应用层自己完全重新实现 TCP 的所有可靠传输机制,这是一个极其复杂和艰巨的任务。
下面我将详细解释为什么不行,以及如果你执意要做的巨大挑战。
TCP 提供的可靠传输机制 vs. 你的 RAW Socket
当你使用自定义 IP 数据包时,你得到的只是一个“哑管道”,下图对比了 TCP 提供的服务和你需要自己实现的一切:
flowchart TD
A[目标: 可靠的数据传输] --> B[使用标准 TCP Socket]
A --> C[使用自定义IP数据包<br>(RAW Socket)]B --> B1[自动获得所有可靠传输机制]
B1 --> B2[全功能的传输层服务]C --> C1[仅获得不可靠的IP层数据报发送能力]
C1 --> C2[需在应用层重新实现整个传输层]subgraph B2 [TCP 提供的核心功能]Direction1[顺序保证]Direction2[丢包检测与重传]Direction3[流量控制]Direction4[拥塞控制]Direction5[连接管理]
endsubgraph C2 [你需要自己实现的功能]Direction1_1[手动序列号与确认(ACK)]Direction2_1[手动超时重传计时器(RTO)]Direction3_1[手动滑动窗口与窗口通告]Direction4_1[手动实现拥塞避免、慢启动等算法]Direction5_1[手动三次握手、四次挥手]
endB2 -.->|自动处理| B
C2 -.->|巨大挑战| C
正如上图所示,通过 RAW Socket 发送自定义 IP 数据包,意味着你放弃了操作系统内核经过数十年千锤百炼的 TCP 实现,选择在应用层徒手重建整个传输层。
如何基于 RAW Socket 实现可靠传输?(巨大挑战)
如果你因为特殊需求(如研究、定制极度特殊的网络协议)必须这么做,你需要实现以下所有功能:
序列号与确认 (Sequencing & Acknowledgments)
你必须在你的自定义协议头中添加序列号 (Sequence Number) 字段。
接收方收到数据后,必须向你返回一个确认 (ACK) 数据包,其中包含它期望收到的下一个序列号。
你需要处理重复ACK和累计ACK。
超时重传 (Retransmission Timeout - RTO)
每发送一个数据包,你都需要启动一个计时器。
如果在一个设定的超时时间 (RTO) 内没有收到对应的 ACK,你必须重新发送该数据包。
挑战:TCP 的 RTO 是动态计算的,基于持续测量的往返时间 (RTT)。你需要实现一套类似的算法(如 Jacobson/Karels 算法)来自适应网络变化,否则固定的超时时间会效率极低。
连接管理 (Connection Management)
三次握手:你需要自己定义 SYN 和 ACK 标志,并实现建立连接的同步过程。
四次挥手:同样,你需要自己定义 FIN 标志,实现连接终止过程,并处理可能出现的半关闭状态。
流量控制 (Flow Control)
你需要实现滑动窗口协议。
接收方需要在其 ACK 中通告一个接收窗口 (Window Size),告诉发送方自己还有多少缓冲区空间。
发送方必须遵守这个窗口,不能发送超过窗口大小的数据。
拥塞控制 (Congestion Control)
这是最复杂的部分,目的是保护网络本身不过载。
你需要实现如慢启动 (Slow Start)、拥塞避免 (Congestion Avoidance)、快速重传 (Fast Retransmit)、快速恢复 (Fast Recovery) 等算法。
TCP 拥有多种成熟的拥塞控制算法(如 Cubic、BBR),你需要选择并实现其中之一。
数据完整性
虽然 IP 和 TCP 都有校验和,但你的自定义协议也必须有。你需要为你的数据计算校验和,以防止传输中的比特错误。
一个简单的(不可靠的)自定义协议头示例
这只是一个概念性示例,离真正的可靠传输相差十万八千里:
c
// 你自定义的伪协议头结构 struct my_protocol_header {uint16_t source_port;uint16_t dest_port;uint32_t sequence_number; // 你手动管理的序列号uint32_t ack_number; // 你手动管理的确认号uint8_t flags; // 你自定义的标志位 (如 SYN, ACK, FIN)uint16_t window_size; // 你手动通告的窗口大小uint16_t checksum; // 你必须计算的校验和// ... 可能还有其他字段 };// 紧接着是你的应用数据
每次发送和接收,你都需要处理这些字段。
结论与建议
绝对不要为了“可靠传输”而使用 RAW Socket:这相当于为了造一辆车,决定先从炼钢开始。直接使用 TCP Socket 是正确、高效且可靠的选择。
RAW Socket 的适用场景:
实现一种全新的、不存在于内核中的传输层或网络层协议(例如,用于学术研究)。
开发网络诊断工具,如自定义的
ping
、traceroute
。发送特定类型的 ICMP 消息。
进行网络安全研究,如构造数据包进行测试或渗透。
在某些极端嵌入式环境中,没有完整的 TCP/IP 协议栈。
总而言之,自定义 IP 数据包给了你强大的网络底层控制能力,但随之而来的是巨大的复杂性和责任。可靠传输是 TCP 的核心价值,直接使用它是唯一明智的选择。自己实现一个可靠的协议更像是一个庞大的研究项目,而不是一个产品开发功能。