协议栈的原始套接字从实现上可以分为“链路层原始套接字”和“网络层原始套接字”两大类。本节主要描述各自的特点及其适用范围。
链路层原始套接字可以直接用于接收和发送链路层的MAC帧,在发送时需要由调用者自行构造和封装MAC首部。而网络层原始套接字可以直接用于接收和发送IP层的报文数据,在发送时需要自行构造IP报文头(取决是否设置IP_HDRINCL选项)。
1.1
链路层原始套接字调用socket()函数创建。第一个参数指定协议族类型为PF_PACKET,第二个参数type可以设置为SOCK_RAW或SOCK_DGRAM,第三个参数是协议类型(该参数只对报文接收有意义)。协议类型protocol不同取值的意义具体见表1所示:
- socket(PF_PACKET, type, htons(protocol))
a)
- socket(PF_PACKET, SOCK_RAW, htons(protocol))
b)
- socket(PF_PACKET, SOCK_DGRAM, htons(protocol))
表1
protocol |
值 |
作用 |
ETH_P_ALL |
|
报收本机收到的所有二层报文 |
ETH_P_IP |
0x0800 |
报收本机收到的所有IP报文 |
ETH_P_ARP |
0x0806 |
报收本机收到的所有ARP报文 |
ETH_P_RARP |
0x8035 |
报收本机收到的所有RARP报文 |
自定义协议 |
比如0x0810 |
报收本机收到的所有类型为0x0810的二层报文 |
不指定 |
0 |
不能用于接收,只用于发送 |
…… |
…… |
…… |
表1中protocol的取值中有两个值是比较特殊的。当protocol为ETH_P_ALL时,表示能够接收本机收到的所有二层报文(包括IP, ARP, 自定义二层报文等),同时这种类型套接字还能够将外发的报文再收回来。当protocol为0时,表示该套接字不能用于接收报文,只能用于发送。具体的实现细节在2.2节中会详细介绍。
创建面向连接的TCP和创建面向无连接的UDP套接字,在接收和发送时只能操作数据部分,而不能对IP首部或TCP和UDP首部进行操作。如果想要操作IP首部或传输层协议首部,就需要调用如下socket()函数创建网络层原始套接字。第一个参数指定协议族的类型为PF_INET,第二个参数为SOCK_RAW,第三个参数protocol为协议类型(不同取值的意义见表2)。产品线有使用OSPF和RSVP等协议,需要使用这种类型的套接字。
- socktet(PF_INET, SOCK_RAW, protocol)
a)
网络层原始套接字接收到的报文数据是从IP首部开始的,即接收到的数据包含了IP首部, TCP/UDP/ICMP等首部, 以及数据部分。
b)
网络层原始套接字发送的报文数据,在默认情况下是从IP首部之后开始的,即需要由调用者自行构造和封装TCP/UDP等协议首部。
这种套接字也提供了发送时从IP首部开始构造数据的功能,通过setsockopt()给套接字设置上IP_HDRINCL选项,就需要在发送时自行构造IP首部。
|
表2
protocol |
值 |
作用 |
IPPROTO_TCP |
6 |
报收TCP类型的报文 |
IPPROTO_UDP |
17 |
报收UDP类型的报文 |
IPPROTO_ICMP |
1 |
报收ICMP类型的报文 |
IPPROTO_IGMP |
2 |
报收IGMP类型的报文 |
IPPROTO_RAW |
255 |
不能用于接收,只用于发送(需要构造IP首部) |
OSPF |
89 |
接收协议号为89的报文 |
…… |
…… |
…… |
表2中protocol取值为IPPROTO_RAW是比较特殊的,表示套接字不能用于接收,只能用于发送(且发送时需要从IP首部开始构造报文)。具体的实现细节在2.3节中会详细介绍。
本节主要首先介绍链路层和网络层原始套接字报文的收发总体流程,再分别对两类套接字的创建、接收、发送等具体实现细节进行介绍。
图1
如上图1所示为链路层和网络层原始套接字的收发总体流程。网卡驱动收到报文后在软中断上下文中由netif_receive_skb()处理,匹配是否有注册的链路层原始套接字,若匹配上就通过skb_clone()来克隆报文,并将报文交给相应的原始套接字。对于IP报文,在协议栈的ip_local_deliver_finish()函数中会匹配是否有注册的网络层原始套接字,若匹配上就通过skb_clone()克隆报文并交给相应的原始套接字来处理。
注意:这里只是将报文克隆一份交给原始套接字,而该报文还是会继续走后续的协议栈处理流程。
调用socket()函数创建套接字的流程如下,链路层原始套接字最终由packet_create()创建。
sys_socket()->sock_create()->__sock_create()->packet_create()
- void dev_add_pack(struct packet_type *pt)
- {
-
…… -
if (pt->type == htons(ETH_P_ALL)) { -
netdev_nit++; -
list_add_rcu(&pt->list, &ptype_all); -
} else { -
hash = ntohs(pt->type) & 15; -
list_add_rcu(&pt->list, &ptype_base[hash]); -
} -
…… - }
图2
当protocol为其它非0值时,会将套接字加入到ptype_base链表中。如图3所示,协议栈本身也需要注册packet_type结构,图中浅色的两个packet_type结构分别是IP协议和ARP协议注册的,其处理函数分别为ip_rcv()和arp_rcv()。图中另外3个深色的packet_type结构则是链路层原始套接字注册的,分别用于接收类型为ETH_P_IP、ETH_P_ARP和0x0810类型的报文。
图3
网卡驱动程序接收到报文后,在软中断上下文由netif_receive_skb()处理。首先会逐个遍历ptype_all链表中的packet_type结构,若满足条件“(!ptype->dev || ptype->dev == skb->dev)”,即套接字未绑定或者套接字绑定网口与skb所在网口匹配,就增加报文引用计数并交给packet_rcv()函数处理(若使用PACKET_MMAP收包方式则由tpacket_rcv()函数处理)。
网卡驱动->netif_receive_skb()->deliver_skb()->packet_rcv()/tpacket_rcv()
- ……
- if (sk->sk_type != SOCK_DGRAM) //即SOCK_RAW类型
-
skb_push(skb, skb->data - skb->mac.raw); - ……
- __skb_queue_tail(&sk->sk_receive_queue, skb);
- sk->sk_data_ready(sk, skb->len); //唤醒进程读取数据
- ……
PACKET_MMAP收包方式的实现有所不同,tpacket_rcv()函数将skb->data拷贝到与用户态mmap映射的共享内存中,最后唤醒用户态进程来读取数据。由于报文的内容已存放在内核空间和用户空间共享的缓冲区中,用户态可以直接读取以减少数据的拷贝,所以这种方式效率比较高。
sys_recvmsg()->sock_recvmsg()->…->packet_recvmsg()->skb_recv_datagram()
用户态调用sendto()或sendmsg()发送报文的内核态处理流程如下,由套接字层最终会调用到packet_sendmsg()。
sys_sendto()->sock_sendmsg()->__sock_sendmsg()->packet_sendmsg()->dev_queue_xmit()
注:如果创建套接字时指定type为SOCK_DGRAM,则使用内核构造的MAC首部,用户态发送的数据中不含MAC头部数据。
|
2.2.4
a)
链路层原始套接字可调用bind()函数进行绑定,让packet_type结构dev字段指向相应的net_device结构,即将套接字绑定到相应的网口上。如2.2.2节报文接收的描述,在接收时如果套接口有绑定就需要进一步确认当前skb->dev是否与绑定网口相匹配,只有匹配的才会将报文上送到相应的套接字。
sys_bind()->packet_bind()->packet_do_bind()
b)
以下是比较常用的套接字选项
PACKET_RX_RING:用于PACKET_MMAP收包方式设置接收环形队列
PACKET_STATISTICS:用于读取收包统计信息
c)
链路层原始套接字的信息可通过/proc/net/packet进行查看。如下为图2和图3中创建的原始套接字的信息,可以查看到创建时指定的协议类型、是否绑定网口、已使用的接收缓存大小等信息。这些信息对于分析和定位问题有帮助。
- cat /proc/net/packet
- sk RefCnt Type Proto Iface R Rmem User Inode
- ffff810007df8400 3 3 0810 0 1 0 0 1310
- ffff810007df8800 3 3 0806 0 1 0 0 1309
- ffff810007df8c00 3 3 0800 0 1 560 0 1308
- ffff810007df8000 3 3 0003 0 1 560 0 1307
- ffff810007df3800 3 3 0003 0 1 560 0 1306
如图4所示,在IPV4协议栈中一个传输层协议(如TCP,UDP,UDP-Lite等)对应一个inet_protosw结构,而inet_protosw结构中又包含了proto_ops结构和proto结构。网络子系统初始化时将所有的inet_protosw结构hash到全局的inetsw[]数组中。proto_ops结构实现的是从与协议无关的套接口层到协议相关的传输层的转接,而proto结构又将传输层映射到网络层。
图4
sys_socket()->sock_create()->__sock_create()->inet_create()
|
图5
网卡驱动收到报文后在软中断上下文由netif_receive_skb()处理,对于IP报文且目的地址为本机的会由ip_rcv()最终调用ip_local_deliver_finish()函数。ip_local_deliver_finish()主要功能的代码片段如下,先根据报文的L4层协议类型hash值在图5中的raw_v4_htable表中查找是否有匹配的sock。如果有匹配的sock结构,就进一步调用raw_v4_input()处理网络层原始套接字。不管是否有原始套接字要处理,该报文都会走后续的协议栈处理流程。即会继续匹配inet_protos[]数组,根据L4层协议类型走TCP、UDP、ICMP等不同处理流程。
|
图6
网卡驱动->netif_receive_skb()->ip_rcv()->ip_rcv_finish()->ip_local_deliver()->ip_local
_deliver_finish()->raw_v4_input()->raw_rcv()->raw_rcv_skb()->sock_queue_rcv_skb()
|
sys_recvmsg()->sock_recvmsg()->…->sock_common_recvmsg()->raw_recvmsg()
用户态调用sendto()或sendmsg()发送报文的内核态处理流程如下,最终由raw_sendmsg()进行发送。
sys_sendto()->sock_sendmsg()->__sock_sendmsg()->inet_sendmsg()->raw_sendmsg()
- ……
- if (inet->hdrincl) { //调用者要构造IP首部
-
err = raw_send_hdrinc(sk, msg->msg_iov, len, -
rt, msg->msg_flags); - } else {
-
…… //由内核构造IP首部 -
err = ip_push_pending_frames(sk); - }
- ……
a)
若原始套接字调用bind()绑定了一个地址,则该套接口只能收到目的IP地址与绑定地址相匹配的报文。内核的具体实现是raw_bind(),将inet->rcv_saddr设置为绑定地址。在原始套接字接收时,__raw_v4_lookup()在设置了inet->rcv_saddr字段的情况下,会判断该字段是否与报文目的IP地址相同。
sys_bind()->inet_bind()->raw_bind()
b)
网络层原始套接字的信息可通过/proc/net/raw进行查看。如下为图5所创建的3个网络层原始套接字的信息,可以查看到创建套接字时指定的协议类型、绑定的地址、发送和接收队列已使用的缓存大小等信息。这些信息对于分析和定位问题有帮助。
- cat /proc/net/raw
- sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
- 1: 00000000:0001 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1323 2 ffff8100070b2380
- 6: 00000000:0006 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1322 2 ffff8100070b2080
- 89: 00000000:0059 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1324 2 ffff8100070b2680
注意事项:
a)
b)
c)
3.2
注意事项:
a)
b)
c)
很多网络诊断工具也是利用原始套接字来实现的,经常会使用到的有tcpdump, ping和traceroute等。
tcpdump
该工具用于截获网口上的报文流量。其实现原理是创建ETH_P_ALL类型的链路层原始套接字,读取和解析报文数据并将信息显示出来。
ping
该工具用于检查网络连接。其实现原理是创建网络层原始套接字,指定协议类型为IPPROTO_ICMP。检测方构造ICMP回射请求报文(类型为ICMP_ECHO),根据ICMP协议实现,被检测方收到该请求报文后会响应一个ICMP回射应答报文(类型为ICMP_ECHOREPLY)。然后检测方通过原始套接字读取并解析应答报文,并显示出序号、TTL等信息。
traceroute
该工具用于跟踪IP报文在网络中的路由过程。其实现原理也是创建网络层原始套接字,指定协议类型为IPPROTO_ICMP。假设从A主机路由到D主机,需要依次经过B主机和C主机。使用traceroute来跟踪A主机到D主机的路由途径,具体步骤如下,在每次探测过程中会显示各节点的IP、时间等信息。
a)
b)
c)