抓包程序的一些分享

昨晚同白总和adden提到抓包的问题。因为写过相关的代码,遇到过抓包的各种各样的坑,此文分享一些相关的经验。

 

1. Linux cooked capture

在用tcpdump或wireshark看数据包时,有时会看到数据链路层的的包头称为Linux cooked capture。这不是一个真实的包头,当用tcpdump指定从所有网络介质 -i any 抓包时,所有抓到的包的数据链路层被换成了Linux cooked capture。在wireshark官方网站对Linux cooked capture的解释中: 这是一个在linux上被libpcap抓包库使用的“假协议”(pseudo-protocal),当从“any”设备在抓包,或者本地的数据链路层包头无效时被使用。更多,见http://wiki.wireshark.org/SLL

 

2. IP over IP tunnel

wikipedia的示例图显示IP tunnel上的包的结构是:

但实战中用libpcap库从我们的线上机的IP隧道抓包时,pcap_loop或pcap_dispatch或pcap_next函数返回的数据包只有Inner IP header + IP payload,没有Outer IP headr,也没链路层的头部。多么幸运啊!

 

3. 混乱的RAW_SOCKET,PF_PACKET协议族

这不是工业标准定义的神秘领域,非常之混乱,谨慎进入。如果要写这方面的socket,可供参考。个人经验,用linux原生的socket抓包时,socket()的三的参数如下组合时的神奇效果.

     

        _recv_fd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));

        从此socket中读所有的IP包,返回的IP包没有链路层包头,因为第二个参数是SOCK_DGRAM,链路层包头返回时被去掉。第三个参数指定的是IP包。奇怪的是,如果出站的IP包的目的地址不是本机,则出站的IP包不会被抓到。

       

        _recv_fd = socket(AF_PACKET , SOCK_RAW , htons(ETH_P_IP));

        和上一个socket的区别是,这个会返回链路层包头。和上一个socket一样,同样只能装到部分出站的包。

      

            _recv_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);

        这行代码只可以读入站的包,抓不到出站的包。

 

            _recv_fd = socket(AF_PACKET , SOCK_RAW , htons(ETH_P_ALL));

        这行代码可以抓到所有出站,入站的包,数据链路层包头也被返回。

 

4. 外网进来的IP包有可能很大。

N多年前一般的路由的MTU只有1K多,意味着4K大小的IP会被切成小IP包。现在的网络环境与几年前确实大大提高。从外网入来的IP包会大过4096,当时写程序时放IP包的buffer大小为4096,觉得应该无压力,结果观察到程序崩溃,后定位到是放IP包的缓冲区溢出。后来改成8192,暂时没问题。

copyright ykyi.net

Linux抓包技术简介

Linux抓包技术简介

Author: zausiu@gmail.com

1. 简介

大多数应用常景下,我们在IP层上用AF_INET或AF_INET6作为socket的第一个参数,SOCK_STREAM或SOCK_DGRAM作为socket的第二个参数创建最为常见的IPv4或IPv6的TCP或UDP套接字。这样的套接字满足了绝大多数网络应用常景。

 

本文简单介绍三种Linux平台下抓包,截获包的技术。它们提供一些更酷的功能。每种技术的水都很深,作者个人水平有限,按照自己的理解做一般的介绍。仅仅旨在起到科谱的作用。

 

2. RAW SOCKET

参数SOCK_RAW可以作为socket函数的第二个参数传入,这样创建的套接字称为原始套接字。设置Socket的第一个参数可以让指定原始套接字开在数据链路层上或者网络层。

 

下面的语句创建在数据链路层上的原始socket。

Socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));  

在负载不大的情况下,读这个socket可以拿到本机收到的所有数据包。写入这个socket需要程序员负责填充各种包头的数据。

 

Socket(AF_INET, SOCK_RAW, xxxxx协议号);

在IP层上创建IPv4的RAW socket,可以拿到各种经由IPv4的数据包,比如ICMP包,IGMP包等。Steven的<Unix网络编程>第三版第28.4节,第二段:“系统收到的UDP包和TCP包不会传给原始套接字”。但读tcpcopy的源代码时,确实用了原始套接字在网络层读UDP和TCP包。难道书上写的过时了。见代码,来自tcpcopy:

#if (TCPCOPY_UDP)

        fd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);

#else

        fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);

#endif

另外,在IP层上创建的原始套接字有一个选项:IP_HDRINCL,它被开启时,则内核帮助填充IP包头。Linux默认没有开启这个选项。

 

3. NF_Queue

NF_Queue是用户空间的一个库,需要内核模块NETFILTER_NETLINK_QUEUE的支持。官方网址:http://www.netfilter.org。通过这个库可以在数据包被发往用户空间处理前截获数据包,或者在数据包被发往网卡前截获数据库。截获后,可以修改数据包的内容,并决定是否丢充这个数据库。

NF_Queue可以用来编写防火墙等安全软件。

Tcpcopy使用NF_Queue丢弃测试中的程序回复给真实客户程序的数据包,使测试中程序回复的报文不影响真实客户程序。

另一个提供类似功能的库是IP_Queue,它已经被废弃。NF_Queue可以完全取代IP_Queue。我们用的tlinux,内核中built-in了NF_Queue模块。

 

4. libpcap

大名鼎鼎的Libpcap是一个古老的可以工作在所有Unix-like平台上的抓包库,比linux年纪更大。Tcpdump的广泛使用让libpcap成为事实上的行业标准。它可以很方便的编写抓取数据链路层上的数据包,并可以设置过滤条件在内核级别只复制关心的数据包,而数据链路层上的原始套接字会尽最大努力复制所有的包,所以libpcap的工作效率高。而且,pcap使用方便。下面是一个很简单的使用pcap抓到发往80端口的tcp包并打印源IP,目的IP,和目的端口号的程序。非常短,很容易看,起到介绍libpcap的作用。

 int main(int argc, char *argv[])

 {

pcap_t *handle; // pcap用的句柄,和其它各种句柄一样。

char *dev;         // 网络接口名,如eth0, eth1, tunl0 之类

char errbuf[PCAP_ERRBUF_SIZE]; // 存描述出错原因的缓冲

struct bpf_program fp;         // pcap用来过滤数据包用

char filter_exp[] = "tcp and dst port 80"; // 过滤字符串,和tcpdump的写法一样

struct pcap_pkthdr header;      // pcap自己用的包头

const u_char *packet;         // pcap包头后的数据,即抓到的数据

 

int len_of_iphdr;         // ip头的长度

struct in_addr src_ip;    // 源IP地址

struct in_addr dst_ip;    // 目的IP

const u_char *ip_hdr_addr; // IP头的首地址

const u_char *tcp_hdr_addr;     // tcp头的首地址

struct tcphdr *tcp_hdr;         // tcp头结构体

u_short dst_port;             // 目的端口

 

// 如果命令行指定网卡名

if (argv[1])

dev = argv[1]; //就用指定的网卡

else

dev = "eth0"; //否则用eth0

 

}

        

        // 用pcap打开这个网卡

handle = pcap_open_live(dev, BUFSIZ, 0, 1000, errbuf);

if (handle == NULL) {  // 打开网卡出错

fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);

return(2);

}

      

        // 编译过滤文件

if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {

fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));

return(2);

}

        // 设置过滤条件

if (pcap_setfilter(handle, &fp) == -1) {

fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));

return(2);

}

 

        // 抓包

packet = pcap_next(handle, &header);

 

// 至此packet指向抓到的一个目标端口号是80的数据链路层的包

// 它的包结构是 14字节以太网包头 + 可变IPv4包头 + TCP包头

ip_hdr_addr = (u_char*)(packet + sizeof(struct ethernet_hdr));

src_ip.s_addr = ((struct iphdr*)ip_hdr_addr)->saddr;

dst_ip.s_addr = ((struct iphdr*)ip_hdr_addr)->daddr;

len_of_iphdr = ((struct iphdr*)ip_hdr_addr)->ihl * 4;

tcp_hdr_addr = ip_hdr_addr + len_of_iphdr;

tcp_hdr = (struct tcphdr*)tcp_hdr_addr;

dst_port = tcp_hdr->dest;

 

// 经过上面的各种指针偏移,下面打印源IP和目标IP和目标端口.

printf("src addr %s\n", inet_ntoa(src_ip));

printf("dst addr %s\n", inet_ntoa(dst_ip));

printf("dst_port %d\n", ntohs(dst_port));

 

/* And close the session */

pcap_close(handle);

 

return(0);

 }

 

5. 总结

本文只是一个非常简单的介绍。如需要进阶学习,Stevens的网络编译有更详细地关于Raw Socket的内容,研究NF_Queue技术则需要读源代码和收集散在社区各个角落的资料了,而讲libpcap有两篇很好的文章:

"Programming with Libpcap – Sniffing the network from our own application "用Libpcap编程,在我们的程序中嗅探网络。非常漂亮的pdf文档。

http://recursos.aldabaknocking.com/libpcapHakin9LuisMartinGarcia.pdf

The Sniffer’s Guide to Raw Traffic 嗅探器教程。斯坦福大学的Geek于大概2001年写的。

http://yuba.stanford.edu/~casado/pcap/section1.html

copyright ykyi.net