原始套接字可以用来自行组装IP数据包,然后将数据包发送到其他终端。必须在管理员权限下才能使用原始套接字。(总结自《UNIX网络编程 卷1:套接字联网API》)
(1)原始套接字的创建
int sockfd = socket (AF_INET, SOCK_RAW, IPPROTO_xxx);后面的xxx可以是ICMP,UDP,TCP等。
可以在这个套接字上调用bind函数,但是比较少见。bind函数仅仅设置本地地址,因为原始套接字不存在端口的概念。就输出而言,调用bind设置的是将用于从这个原始套接字发送的所有数据报源IP地址。如果不调用bind,内核就把源IP地址设置为外出接口的主IP地址。
(2)原始套接字的输出
如果IP_HDRINCL套接字选项未开启,那么由进程让内核发送的数据的起始位置指的是IP首部之后的第一个字节,因为内核将构造IP首部并把它置于来自进程的数据之前。内核把所有构造IPv4首部的协议字段设置成来之sock调用的第三个参数。
如果IP_HDRINCL套接字选项已开启,那么由进程让内核发送的数据的起始位置指的是IP首部的第一个字节。进程调用输出函数写出的数据量必须包括IP首部的大小。整个IP首部都是由进程构造,不过IP的标识字段可以设置为0,从而告知内核设置该值,IPv4首部校验和字段总是由内核计算并存储,IPv4的选项字段是可选的。
内核会对超出外出接口MTU的原始分组进行分片。
开启IP_HDRINCL的代码是
const int on =1;
if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0)
错误处理;
(3)原始套接字的输入
首先要考虑内核将哪些接收到的IP数据报传递到原始套接字?这要遵循下面的规则
a:接收到的UDP或者TCP分组绝不传递到任何原始套接字,如果一个进程想要读取含有UDP分组或TCP分组的IP数据报,它就必须在数据链路层读取这些分组。
b:大多数ICMP分组在内核处理完其中的ICMP消息后传递到原始套接字。IGMP亦是如此。
c:内核把不认识其协议字段的所有IP数据报传递给原始套接字。内核把这些分组执行的唯一处理是针对某些IP首部字段的最小验证:IP版本,IPv4校验和,首部长度,以及目的地址。
d:如果某个数据报以片段的形式到达,那么在它的所有片段均到达且重组出该数据报之前,不传递任何片段分组给原始套接字。
当内核有一个需要传递到原始套接字的数据报时,它将检查所有进程上的所有原始套接字,以寻找所有匹配的套接字。每个匹配的套接字将被传递送以该IP数据报的一个副本。内核对每个原始套接字均执行以下3个测试,只有这三个测试均为真,内核才把接收到的数据报发送给这个套接字:
a:如果创建这个原始套接字时指定了非0的协议参数(socket的第三个参数),那么接收到的数据报协议字段必须匹配该值。
b:如果这个套接字已由bind调用绑定了某个IP地址,那么接收到的数据报的目的地址必须匹配这个绑定地址。
c:若该套接字调用了connect,那么接收到的数据报的源地址必须匹配这个已连接地址。
注意,如果一个原始套接字是以0值协议参数传递的,并且没有调用bind,connect,那么该套接字将接收可由内核传递到原始套接字的每个原始数据报的一个副本。
而且无论何时往一个原始IPv4套接字上递送一个IP数据报,传递到该套接字所在进程的都是包括IP首部在内的完整数据报,然后对于IPv6原始套接字,传递套接字的只是扣除了IPv6首部和所有扩展首部的payload。