###1. raw socket
常用的网络编程都是在应用层的报文的收发操作,, 这些数据包都是由系统中的协议栈来处理,用户只需要填充应用层报文即可,由系统完成底层报文头的填充并发送, 在某些情况下需要执行更底层的操作,比如修改报文头、避开系统的协议栈, 这个时候就需要使用其他的方式来实现
在linux上, 支持2种raw socket
- AF_INET 协议族的 SOCK_RAW 类型的socket,实现了IP 层的raw socket
- AF_PACKET 协议族的 SOCK_RAW和 SOCK_DGRAM类型的socket, 实现了数据链路层的raw socket
###2. AF_INET raw socket
AF_INET 支持 SOCK_RAW 类型的原始套接字, 支持IP协议之上的协议类型, 例如tcp, udp, icmp等等, 创建方式类似于
socket(AF_INET, SOCK_RAW, IPPROTO_TCP)
注意最后的 protocol 参数不能使用 IPROTO_IP, 因为如果使用IPPROTO_IP,系统根本就不知道该用什么协议
以一个udp数据包为例
14byte 20byte 8byte 100byte 4byte
+-----------------+-----------+------------+--------+-----+
| Ethernet Header | ip header | udp header | data | fcs |
+-----------------+-----------+------------+--------+-----+
则 “socket(AF_INET, SOCK_RAW, IPPROTO_UDP)” 可以接收到 ip header + udp header + fcs (20 + 8 + 100 byte)的数据
####2.1 AF_INET raw socket的能力
AF_INET raw socket 可以获取发往本机的ip数据包, 但是不能完成如下任务:
- 不能收到非发往本地ip的数据包(ip软过滤会丢弃这些不是发往本机ip的数据包)
- 不能收到从本机发送出去的数据包
关于 AF_INET 的raw socket, 还有几点如下
- 在发送数据时, AF_INET 的raw socket需要自己组织 tcp/udp/icmp这些协议头
- AF_INET 的raw socket 默认无法操作ip header
- 如果用户想要自行填充ip header, 则应该使用 setsockopt来设置IP_HDRINCL选项
- 默认会收到所有该类型的数据包, 但是可以通过bind()来指定本地的ip, connect()来指定对端的ip的方式, 进行过滤
- 若不设置IP_HDRINCL, 如果使用bind函数绑定本地IP, 则用此IP填充源IP地址, 若不调用bind则会将源IP地址设置为外出接口的主IP地址
- 如果使用connect函数设置目标IP,则可以使用send或者write函数发送报文,而不需要使用sendto函数
- 端口对于SOCK_RAW而言没有任何意义
- 不支持分片包, 需手动分片
AF_INET 的 SOCK_PACKET 类型的套接字可能不再支持,请不要再使用, 请使用AF_PACKET协议族来代替
AF_INET 的raw socket处理的只是ip层及其以上的数据,比如实现SYN FLOOD攻击、处理PING报文等, 当需要操作更底层的数据的时候,就需要采用其他的方式
####2.2 AF_INET raw socket获取数据包
数据帧进入网络层之后的处理过程如下:
ip_rcv()
ip_rcv_finish()
dst_input()
ip_local_deliver()
raw_local_deliver()
xxx_rcv() /* 例如 tcp_v4_rcv() udp_rcv() */
- 数据包进入ip层输入例程 ip_rcv() 检查如果是非本机IP的数据包并且也非广播数据包, 则丢弃该数据包, 否则, 进行后续处理
- ip数据包在重组(如果需要)后, 首先在raw_local_deliver()中处理raw socket
- 处理完raw socket后, 数据包被发送到对应的协议的处理例程中 (例如 tcp_v4_rcv) 进行常规的处理
raw_local_deliver() 对raw socket的处理流程如下
raw_local_deliver()
raw_v4_input()
raw_rcv()
raw_rcv_skb()
- raw_local_deliver() 中检查是否有通过
socket(AF_INET, SOCK_RAW, ...)
创建的套接字,若有,且协议符合, 则由raw_v4_input()处理 - raw_v4_input()会遍历所有需要接收该数据包的socket, 依次拷贝一个数据包, 然后调用raw_rcv()来处理
- raw_rcv()会调用raw_rcv_skb()将数据包拷贝socket的缓冲区中
###3. AF_PACKET raw soket
linux中添加了 AF_PACKET 协议族, 用于取代旧的 AF_INET中的SOCK_PACKET 套接字, AF_PACKET raw soket 可以操作链路层数据
AF_PACKET 协议族支持2种类型的套接字
- SOCK_RAW : 用户接收的数据包将包含链路层的协议头(即Ethernet 帧头), 发送数据时, 用户需要填充链路层的协议头(即Ethernet 帧头)
- SOCK_DGRAM : 用户接收/发送的数据包都不包含链路层的协议头(即Ethernet 帧头), 发送数据时, 用户指定的物理层地址只是作为参考, kernel将自动填充合适的物理层地址
AF_PACKET 协议族的socket需要指定protocol, 例如
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_EAPOL))
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
ETH_P_ALL可以匹配所有类型的以太网帧
以一个udp数据包为例
14byte 20byte 8byte 100byte 4byte
+-----------------+-----------+------------+--------+-----+
| Ethernet Header | ip header | udp header | data | fcs |
+-----------------+-----------+------------+--------+-----+
- SOCK_RAW 可以接收到 Ethernet header + ip header + udp header + data 即 (14 + 20 + 8 + 100 byte)
- SOCK_DGRAM 可以接收到 ip header + udp header + data 即 (20 + 8 + 100 byte)
####3.1 AF_PACKET raw soket的能力
AF_PACKET raw soket的能力如下:
- 能接收发往本地mac的数据帧
- 能接收从本机发送出去的数据帧(第3个参数需要设置为ETH_P_ALL才行)
- 接收非发往本地mac的数据帧(网卡需要设置为promisc混杂模式)
AF_PACKET 协议族的socket, 还可以使用bind() 指定网络接口, 只接收该接口上的数据帧
####3.2 获取远端发送给本机的数据
首先, 任何网络协议的代码, 都可以通过 dev_add_pack() 来注册一个 struct packet_type, 来声明其感兴趣的以太网帧,感兴趣的网络设备接口, 以及对应的处理函数, 例如
- ip协议栈注册了一个名为“ip_packet_type” 的结构体,关注类型为 ETH_P_IP的以太网帧, 处理处理函数为 ip_rcv()
- arp协议注册了一个名为“arp_packet_type” 的结构体,关注类型为 ETH_P_ARP的以太网帧, 处理函数为arp_rcv()
关注 ETH_P_ALL 的packet_type会被添加到 ptype_all 链表中, 关注 ETH_P_ALL 的packet_type会被添加到 ptype_all 链表中, 而其它的的packet_type会被添加到 ptype_base 数组中对应的链表中, AF_PACKET 族的socket在创建时, 会注册一个对应的packet_type
netif_rx()
netif_receive_skb()
__netif_receive_skb()
__netif_receive_skb_core()
在 __netif_receive_skb_core() 中:
- 先遍历 ptype_all 链表中的所有packet_type, 若数据帧由其感兴趣的网络接口所接收, 则复制一份数据帧交给其处理函数去处理
- 继续遍历 ptype_base 对应的链表中的所有packet_type, 若数据帧由其感兴趣的网络接口所接收,且数据帧类型符合,则复制一份数据帧交给其处理函数去处理
####3.3 获取本机发出的数据
数据包进入数据链路层的过程如下
dev_queue_xmit()
dev_hard_start_sxmit()
dev_queue_xmit_nit()
在dev_hard_start_sxmit()中, 会遍历 ptype_all 链表中的 packet_type, 若若数据帧由其感兴趣的网络接口所发送, 则则复制一份数据帧交给其处理函数去处理
因为在发送过程中, 只会遍历 ptype_all 链表, 因此, 只有在创建时, 使用 ETH_P_ALL 的raw socket才能接收本机发送出去的数据帧
###4. 总结
总的来说, AF_PACKET raw socket功能更为强大和全面,不但可以获取本机收到的数据帧, 还可以获取本机发出的数据帧, 常被用于本机网络数据的监控, 抓包, 例如, tcpdump就使用了 AF_PACKET raw socket, 而AF_INET raw socket 的功能相对较弱, 只能获取本机接收到的数据包, 不能获取本机发送的数据包, 但是可以直接过滤IP层之上的协议, 因此, 常用于开发 ICMP IGMP相关的工具