一:原始套接字
1:原始套接字作用
原始套接字提供普通的TCP和UDP套接字所不能提供的以下3种能力:
有了原始套接字,进程可以读写ICMP, IGMP等分组。比如ping程序就是使用原始套接字发送ICMP回射请求并接收ICMP回射应答。
有了原始套接字,进程可以读写内核不处理其协议字段的IPv4数据报。大多数内核仅仅处理IPv4协议字段为1(ICMP)、2(IGMP)、6(TCP)和17(UDP)的数据报。然而为协议字段定义的值还有不少,比如ODPF路由协议既不是用TCP,也不是用UDP,而是通过收发协议字段为89的IP数据报而直接使用IP。因此,实现OSPF协议的gated守护进程必须使用原始套接字读与写这些IP数据报,因为内核不知道如何处理协议字段值为89的IPv4数据报。
有了原始套接字,进程还可以使用IP_HDRINCL套接字选项自行构造IPv4首部,这个能力可以用来构造TCP和UDP分组。
2:创建原始套接字
使用SOCK_RAW可以创建原始套接字,比如要创建一个IPv4的原始套接字:
socket(AF_INET,SOCK_RAW, protocol);
其中的protocol是形如IPPROTO_XXX的某个常值,比如IPPROTO_IGMP。
只有超级用户才能创建原始套接字,这么做可以防止普通用户网网络写自行构造的IP数据报。
可以在原始套接字上调用bind函数,不过这比较少见。原始套接字没有端口的概念,因此bind函数仅仅设置本地地址。bind设置的是,将用于从这个原始套接字发送的所有数据报的源IP地址(只在IP_HDRINCL套接字选项未开启的前提下)。如果不调用bind,内核就把源IP地址设置为外出接口的主IP地址。
可以在原始套接字上调用connect函数,不过也比较少见。调用connect之后,sendto调用可以改为write或send调用,因为目的IP地址已经指定了。
3:原始套接字的输出
普通原始套接字通过调用sendto或sendmsg函数,并指定目的IP地址进行输出。
如果IP_HDRINCL套接字选项未开启,那么由进程让内核发送的数据的起始地址是IP首部之后的第一个字节,因为内核将构造IP首部并把他置于来自进程的数据之前。内核把所构造的IPv4首部的协议字段设置成来自socket的第三个参数。
如果IP_HDRINCL套接字选项已经开启,那么由进程让内核发送的数据的起始地址指的是IP首部的第一个字节。进程调用输出函数写出的数据量必须包含IP首部的大小。整个IP首部由进程构造,不过:IPv4标识字段可置为0,从而告知内核设置该值;IPv4首部校验和字段总是由内核计算并存储;IPv4选项字段是可选的。
内核会对超出外出接口MTU的原始分组执行分片。
对于IPv4,计算并设置IPv4首部之后所含的任何首部校验和是用户进程的责任。
4:原始套接字的输入
内核把哪些接收到的IP数据报传递给原始套接字?遵循以下规则:
接收到的UDP和TCP分组绝不传递到任何原始套接字。如果一个进程想要读取含有UDP分组或TCP分组的IP数据报,他就必须在数据链路层读取这些分组。
大多数ICMP分组在内核处理完其中的ICMP消息后传递到原始套接字。源自Berkeley的实现,把不是回射请求、时间戳请求或地址掩码请求(这三类ICMP消息全有内核处理)的所有接收到的ICMP分组传递给原始套接字。
所有IGMP分组在内核完成处理其中的IGMP消息后传递到原始套接字。
内核不认识其协议字段的所有IP数据报传递到原始套接字。内核对这些分组执行的唯一处理是针对某些IP首部字段的最小验证:IP版本,IPv4首部校验和、首部长度以及目的IP地址。
如果某个数据报以分片形式到达,那么在他的所有分片均到达并且重组出该数据报之前,不传递任何片段分组到原始套接字。
当内核有一个需要传递到原始套接字的IP数据报时,它将检查所有进程上的所有原始套接字,以寻找所有匹配的套接字。每个匹配的套接字将被传送以该IP数据报的一个副本。内核对每个原始套接字均执行如下3个测试,只有这三个测试结果都为真,内核才把接收到的数据报传送给这个原始套接字。
如果创建这个原始套接字时指定了非0的协议参数,那么接收到的数据报协议字段必须匹配该值。
如果这个套接字已由bind调用绑定了某个IP地址,那么接收到的数据报的目的地址必须匹配这个绑定地址。
若该套接字调用了connect,那么接收到的数据报的源地址必须匹配这个已连接地址。
注意,如果一个原始套接字是以0值协议参数创建的,并且没有调用bind,connect,那么该套接字将接收可由内核传递到原始套接字的每个原始数据报的一个副本。
另外,无论何时往一个原始IPv4套接字上递送一个IP数据报,传递到该套接字所在进程的都是包括IP首部在内的完整数据报。
二:数据链路访问
目前大多数操作系统都为应用程序提供访问数据链路层的强大功能,这种功能可以提供如下能力:
能够监视有数据链路层接收的分组,使得诸如tcpdump之类的程序能够在普通计算机系统上运行,而不需使用专门的硬件设备来监视分组。如果结合使用网络接口进入混杂模式的能力,那么应用程序甚至能够监视本地电缆上流通的所有分组,而不仅仅是以程序运行所在主机为目的地的分组。
能够作为普通应用进程而不是内核的一部分运行某些程序,比如RARP服务器的大多数Unix版本就是普通的应用进程。它们从数据链路读入RARP请求,然后往数据链路写出RARP应答。
Unix上访问数据链路层的3个常用方法是BSD的分组过滤器BPF、SVR4的数据链路提供者接口DLPI和Linux的SOCK_PACKET接口。
libpcap是访问操作系统所提供的分组捕获机制的分组捕获函数库,它是与实现无关的。目前它只支持分组的读入。libpcap目前支持源自Berkeley内核中的BPF、Solaris和HP-UX中的DLPI、SunOS中的NIT、LInux的SOCK_PACKET套接字和PF_PACKET套接字,以及其他若干操作系统。tcpdump就是用该函数库。
libnet函数库提供构造任意协议的分组并将其输出到网络中的接口。它以与实现无关的方式提供原始套接字访问方式和数据链路访问方式。
摘自《Unix网络编程卷一:套接字联网API》第28章和第29章