一、原始套接字的创建
只有超级用户才能创建原始套接字。
int sockFd; sockFd = socket(AF_INET, SOCK_RAW, protocol); |
原始套接字不存在端口号的概念。在原始套接字上调用bind()比较少见,这么做仅仅是设置本地地址。在原始套接字上调用connect()也比较少见,这么做仅仅是设置外地地址。
二、通过原始套接字发送数据
通过raw socket发送数据时,内核会自动对超出外出接口MTU的原始分组执行分片。
普通输出通过sendto()或sendmsg()并指定目的IP地址完成。
如果我们想通过一个原始套接字自己构造IP首部应该怎么做呢?最早原始套接字是不能实现这个目的的,后来某个大牛给了支持traceroute而给出了一个内核补丁,该补丁要求应用进程在调用socket()创建原始套接字时必须指定protocol参数值为IPPROTO_RAW(值为255),它是一个保留值,从不允许作为IP首部中的协议字段出现。时至今日,我们还可以看到很多这样的代码:
if(-1 == (sockFd = socket(AF_INET, SOCK_RAW, 255))) |
不过,后来又有了新的方法,4.3BSD Reno引入了IP_HDRINCL套接字选项,进程可以使用该选项自行构造IPv4首部:
const int on = 1; if(setsocketopt(sockFd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0){ //error handler; } |
如果IP_HDRINCL选项开启,则进程让内核发送数据的起始地址指的是IP首部的第一个字节,进程调用输出函数写出的数据量必须包括IP首部的大小,整个IP首部由进程构造,不过(a)IPv4标识字段可置为0,表示进程让内核来设置该值(b)IPv4首部校验和字段总是由内核计算并存储(c)IPv4选项字段是可选的。
至于IPv6,不存在IP_HDRINCL这样的选项,大概也不存在可以自行构造首部的内核补丁。IPv6首部的几乎所有字段和所有扩展首部都可以通过套接字选项或辅助数据由应用进程指定或获取。如果一定应用进程一定要读入或写出完整的IPv6数据包,就必须使用链路层访问。
三、通过原始套接字接收数据
如果某个IP datagram以片段形式到达,内核会重组好完整的IP datagram之后再传递给raw socket.
内核向原始套接字交付IP datagram的规则:
1. 内核接收到的UDP分组和TCP分组绝对不会交付给任何原始套接字。如果一个进程想要读取含有TCP/UDP分组的完整IP数据包,就必须在数据链路层读取这些分组。
2. 大多数ICMP分组在内核处理完其中的ICMP消息后传递到原始套接字。源自Berkeley的实现只有回射请求、时间戳请求和地址掩码请求完全由内核处理,此外的所有ICMP分组都会传递给raw socket.</