1. 原始套接字(raw socket)简介
原始套接字可以接收本机网卡上的数据帧或者数据包,利用raw socket可以编写基于IP协议的程序。一般的TCP/UDP类型的套接字只能够访问传输层以及传输层以上的数据,而原始套接字却可以访问传输层一下的数据,所以使用raw socket既可以实现应用层的数据操作,也可以实现链路层的数据操作。
1.1 基本原理
网卡对数据帧进行硬过滤(根据网卡的模式不同采取不同的操作,如果设置了混杂模式,则不做任何过滤直接交给下一层,否则非本机mac或者广播mac的会被直接丢弃)。在进入ip层之前,系统会检查系统中是否有通过socket(AP_PACKET,SOCK_RAW,...)创建的套接字,如果有并且协议相符,系统就给每个这样的socket接收缓冲区发送一个数据帧的拷贝。如果数据的校验和出错的话,内核直接丢弃该数据包,而不会拷贝给sock_raw的套接字。
1.2原始套接字创建方式
发送接收ip数据包
socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)
发送接收以太网数据帧
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))
参数说明:
1)AF_INET和PF_PACKET的区别
使用AF_INET可以接收协议类型为(tcp udp icmp等)发往本机的ip数据包,而使用PF_PACKET可以监听网卡上的所有数据帧。
2)SOCK_RAW、SOCK_DGRAM和SOCK_PACKET的区别
第一个参数使用PF_PACKET的时候,这三种类型都可以使用,区别在于
a)使用SOCK_RAW发送的数据必须包含链路层的协议头,接收到的数据包,包含链路层协议头。而使用SOCK_DGRAM不含链路层协议头;
b)SOCK_PACKET已经废弃,不建议使用;
c)使用这三者时,在sendto和recvfrom中使用的地址类型不同,钱两个使用sockaddr_ll类型的地址,第三个使用sockaddr类型的地址;
d)如果socket的第一个参数使用PF_INET,第二个参数使用SOCK_RAW,则可以得到原始的IP包。
3) 使用PF_PACKET和SOCK_RAW时,第三个参数说明
ETH_P_IP 0x800 只接收发往本机mac的ip类型的数据帧
ETH_P_ARP 0x806 只接受发往本机mac的arp类型的数据帧
ETH_P_ARP 0x8035 只接受发往本机mac的rarp类型的数据帧
ETH_P_ALL 0x3 接收发往本机mac的所有类型ip arp rarp的数据帧,接收从本机发出的所有类型的数据帧。(混杂模式打开的情况下,会接收到非发往本地mac的数据帧)
2. raw socket编程
1)创建套接字
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
使用ETH_P_ALL表示接收所有类型的数据帧(ip,arp,rarp)。
2)设置网卡混杂模式
正常情况下,网卡只响应两种数据帧:一种是与自己mac地址相匹配的数据帧;另一种是发向所有机器的广播数据帧。如果网卡要接收所有通过它的数据,而不管是不是发给它的,就必须把网卡置于混杂模式。
struct ifreq ethreq;
strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);
ethreq.ifr_flags |= IFF_PROMISC;
ioctl(sock, SIOCGIFFLAGS, ?req);
3)设置BPF过滤器
通过前面的设置,可以收到所有的数据帧,但是因为数据帧太多,数据量太大,cpu可能被严重占用,然而很多数据帧,程序处理的时候根本不关心。如果只是接收到数据帧之后用if等判断的话,那将会很麻烦,而且判断太多也降低了反应效率。另一种方法就是通过内核处理,设置过滤器把不需要的数据过滤掉。
在使用libpcap编写网络抓包工具时,就用到了BPF过滤器,因为libpcap已经封装好了,只用将过滤表达式如“port 80”指定就可以由libpcap传到内核进行相应的BPF解码从而过滤掉不需要的数据。而使用raw socket就没有这么方便,但是tcpdump提供了一个选项-dd,可以将一段过滤表达式生成为等效的c代码,如#tcpdump -dd port 80,生成结果如下
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 12, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00000060 },
{ 0x6, 0, 0, 0x00000000 },
这段代码对应的数据结构是struct sock_filter,定义如下
struct sock_filter // Filter block
{
__u16 code; // Actual filter code
__u8 jt; // Jump true
__u8 jf; // Jump false
__u32 k; // Generic multiuse field
};
code对应命令代码;jt是jump if true后面的操作数,注意这里用的是相对行偏移,如2就表示向前跳转2行,而不像伪代码中使用绝对行号;jf为jump if false后面的操作数;k对应伪代码中第3列的操作数。
对应的代码中实现如下
struct sock_filter BPF_code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 12, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 },
};
struct sock_fprog Filter;
Filter.len = 15;
Filter.filter = BPF_code;
setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter));
需要注意的是,因为tcpdump默认只返回96字节的数据,所以数据帧里的数据将有大部分被去掉,这样在分析数据包的时候就会因为数据不全导致分析出错,因此在生成BPF代码时,需要用tcpdump -s指定返回的数据长度。采用“tcpdump -dd -s 0 port 80”,其中-s 0表示返回完整的数据包。
4)接收数据进行处理
while (1) {
//最后两个参数置为NULL,表示不绑定地址,来了的数据包都接收
len = recvfrom(sock, buffer, BUF_SIZE, 0, NULL, NULL);
analyze_packet(buffer, len);
}
处理数据帧的时候,先要解析出以太网头,IP头,tcp头,然后剩下的就是数据,再对数据作处理。
Ethernet head |
| IP head |
| TCP head |
| data |
3. 统计结果偏小的优化调整
1)结果会偏小的原因
采用raw socket,是直接由网卡向socket接收缓冲区发数据副本,如果应用程序从socket缓冲区中取出数据,进行处理的效率不高,将可能会造成socket接收缓冲器来不及接收新到来的数据,从而丢失部分数据。
2)调整socket 接收缓存大小
通过getsockopt可以获得socket的当前接收和发送缓存的大小,我的程序中获取出来的发送和接收缓存都是262144字节(256KB)。采用默认的接收缓存大小的测试结果中可能会出现文件大小偏小。
使用setsockopt设置接收缓存的大小为2*1024*1024字节(2M,已经很大了),然后再多次测试,基本上没有结果偏小的情况,但是有时会出现结果偏大(重传以及选择性重传过滤不完全,内容有交叠导致的)
3)使用内存cache
预先申请一块内存,作为内存池,然后每次到来的数据包先存放到内存池中,创建一个线程,负责从内存池中取包数据,进行数据包分析,仍采用默认的接收缓存大小,测试结果可以看出使用了cache,丢失的数据比没有使用cache的少了,但是仍然存在丢失,效果没有增大socket接收缓存好,说明了该程序中的缓存带来的效果不及系统的socket缓存效果。