使用 libnet 与 libpcap 构造 TCPIP 协议软件
转载
级别: 初级 褚蓬飞 (white_cpf@21cn.com), 中国科学院软件技术研究所 2003 年 6 月 01 日
本文在 RED HAT Linux8.0 + 以太网环境下,利用 libnet 和 libpcap 库实现了一个以太网上用户态的单进程的 TCP/IP 协议软件包:minitcpip, 该软件实现了 TCP 协议的基本通讯功能,并提供了一个调试接口和一个与标准 SOCKET 接口类似的接口函数库 minisocket,方便用户的调试与应用 软件的调用。这个用户态的协议软件包的实现,为学习综合使用 libnet 和 libpcap 提供了良好的范例;通过对这个软件包的学习,还可以加深对 TCP/IP 协议(尤其是在以太网上)的运行原理的理解;另外,由于这个软件包运行在单进程、用户态环境下,也为调试和学习带来了极大的方便。 |
概述
目前有许多不同的成熟的 TCP/IP 协议的实现版本,其中大部分都在操作系统的核心实现,这种方案固然是提高 TCP/IP 协议软件的效率的必然所选,但却给 TCP/IP 协议的学习、研究和调试带来了很大的困难。于是,如果不考虑 TCP/IP 协议软件实现的效率问题,在应用进程中实现一个 TCP/IP 协议软 件,是具有一定的意义和价值的。
本文作者构造了一个单进程的 TCP/IP 协议软件:minitcpip,并提供了一个 SOCKET 接口函数 库:minisocket。在实现这个协议软件函数库时,作者选择采用了 libnet+libpcap 的方式在用户态下实现这个软件,不仅是因为这样可以 避开一些操作系统对底层网络开发的种种限制带来的不便,将精力集中在对协议软件本身的理解上;另外一个原因,则是为大家学习和综合使用 libnet 和 libpcap 提供一个范例。
下文首先介绍了 libnet 和 libpcap 函数库及其使用,并给出了一个利用其实现 ARP 协议的例程 — 该协议的实现也 包括在 minitcpip 软件之中,然后给出了本文的协议软件和 SOCKET 函数库实现的方案,并围绕本文主题,对涉及到的一些关键技术问题进行了分析, 最后,对这种实现方法做了一个简单的总结,指出了这种实现方法的一些局限。
何谓 libnet、libpcap
目前众多的网络安全程序、工具和软件都是基于 socket 设计和开发的。由于在安全程序中通常需要对网络通讯的细节 (如连接双方地址 / 端口、服务类型、传输 控制等) 进行检查、处理或控制,象数据包截获、数据包头分析、数据包重写、甚至截断连接等,都几乎在每个网络安全程序中必须实现。为了简化网络安全程序的编写过程,提高网络安全程序的性能和健壮性,同时使代码更易重用与移植,最好的方法就是将最常用和最繁复的过程函数,如监听套接口的打开 / 关闭、数据包截获、数据包构造/发送/接收等,封装起来,以 API library 的方式提供给开发人员使用。
在众多的 API library 中,对于类 Unix 系统平台上的网络安全工具开发而言,目前最为流行的 C API library 有 libnet、libpcap、libnids 和 libicmp 等。它们分别从不同层次和角度提供了不同的功能函数。使网络开发人员能够 忽略网络底层细节的实现,从而专注于程序本身具体功能的设计与开发。其中,
libnet 提供的接口函数主要实现和封装了数据包的构造和发送过程。
libpcap 提供的接口函数主要实现和封装了与数据包截获有关的过程。
利用这些 C 函数库的接口,网络安全工具开发人员可以很方便地编写出具有结构化强、健壮性好、可移植性高等特点的程序。因此, 这些函数库在网络安全工具的开发中具有很大的价值,在 scanner、sniffer、firewall、IDS 等领域都获得了极其广泛的应用,著名的 tcpdump 软件、ethereal 软件等就是在 libpcap 的基础上开发的。
另外也应该指出:由于其功能强大,这些函数库也被黑客用来构造 TCP/IP 网络程序对目标主机进行攻击。然而,TCP /IP 网络的安全不可能也不应该建立在禁止大家使用工具的基础上,一个理想的网络,首先必须是一个开放的网络,这个网络应该在使用任何工具的情况下都是安 全的和健壮的。从这点考虑,这些工具的使用,对促进现有网络系统的不断完善是大有裨益的。
libnet 函数库框架和使用
libnet 是一个小型的接口函数库,主要用 C 语言写成,提供了低层网络数据报的构造、处理和发送功能。libnet 的开发目的是:建立一个简单统一的网络编程接口以屏蔽不同操作系统低层网络编程的差别,使得程序员将精力集中在解决关键问题上。他的主要特点是:
高层接口:libnet 主要用 C 语言写成
可移植性:libnet 目前可以在 Linux、FreeBSD、Solaris、WindowsNT 等操作系统上运行,并且提供了统一的接口
数据报构造:libnet 提供了一系列的 TCP/IP 数据报文的构造函数以方便用户使用
数据报的处理:libnet 提供了一系列的辅助函数,利用这些辅助函数,帮助用户简化那些烦琐的事务性的编程工作
数据报发送:libnet 允许用户在两种不同的数据报发送方法中选择。
另外 libnet 允许程序获得对数据报的绝对的控制,其中一些是传统的网络程序接口所不提供的。这也是 libnet 的魅力之一。
libnet 支持 TCP/IP 协议族中的多种协议,比如其上一个版本 libnet1.0 支持了 10 种协议,一些新的协议,比如对 IPV6 的支持还在开发之中。
libnet 目前最新的版本是 1.1 版本,在该版本中,作者将这些函数做了进一步的封装,用户的使用步骤也得到了进一步的简 化。内存的初始化、管理、释放等以及校验和的计算等函数,在默认情况下,都无须用户直接干预,使得 libnet 的使用更为方便。作者还提供了基于老版本的 应用程序移植到新版本上的方法指导。利用 libnet1.1 函数库开发应用程序的基本步骤以及几个关键的函数使用方法简介如下:
初始化
libnet_t *libnet_init (int injection_type, char *device, char *err_buf);
该函数初始化 libnet 函数库,返回一个 libnet_t 类型的描述符,以备随后的构造数据报和发送数据报的函数中使用。injection_type 指明了发送数据报使用的接口类型,如数据链 路层或者原始套接字等。Device 是一个网络设备名称的字符串,在 Linux 下是 "eth0" 等。如果函数错误,返回 NULL,而 err_buf 字符串 中将携带有错误的原因。
数据报的构造
libnet 提供了丰富的数据报的构造函数,可以构造 TCP/IP 协议族中大多数协议的报文,还提供了一些对某些参数取默认数值的更简练的构造函数供用户选择。比如 libnet_autobuild_ipv4 () 等。
数据报的发送
int libnet_write (libnet_t *l);
该函数将 l 中描述的数据报发送的网络上。成功将返回发送的字节数,如果失败,返回 - 1。你可以调用 libnet_geterror () 得到错误的原因
退出
void libnet_destroy (libnet_t *l);
libpcap 函数库框架和使用
libpcap 的英文意思是 Packet Capture library,即数据包捕获函数库。该库提供的 C 函数接口可用于需要捕获经过网络接口(通过将网卡设置为混杂模式,可以捕获所有经过该接口的数据报,目 标地址不一定为本机)数据包的系统开发上。著名的 TCPDUMP 就是在 libpcap 的基础上开发而成的。libpcap 提供的接口函数主要实现和封装了 与数据包截获有关的过程。这个库为不同的平台提供了一致的编程接口,在安装了 libpcap 的平台上,以 libpcap 为接口写的程序,能够自由的跨平台 使用。在 Linux 系统下,libpcap 可以使用 BPF (Berkeley Packet Filter) 分组捕获机制来获得很高的性能。
利用 libpcap 函数库开发应用程序的基本步骤以及几个关键的函数使用方法简介如下:
char *pcap_lookupdev (char *errbuf)
该函数用于返回可被 pcap_open_live () 或 pcap_lookupnet () 函数调用的网络设备名(一个字符串指针)。如果函数出错,则返回 NULL,同时 errbuf 中存放相关的错误消息。
int pcap_lookupnet (char *device, bpf_u_int32 *netp,bpf_u_int32 *maskp, char *errbuf)
获得指定网络设备的网络号和掩码。netp 参数和 maskp 参数都是 bpf_u_int32 指针。如果函数出错,则返回 - 1,同时 errbuf 中存放相关的错误消息。
打开设备
pcap_t *pcap_open_live (char *device, int snaplen,int promisc, int to_ms,char *ebuf)
获得用于捕获网络数据包的数据包捕获描述字。 device 参数为指定打开的网络设备名。snaplen 参数定义捕获数据的最大字节数。promisc 指定是否将网络接口置于混杂模式。to_ms 参数 指定超时时间(毫秒)。ebuf 参数则仅在 pcap_open_live () 函数出错返回 NULL 时用于传递错误消息。
编译和设置过滤器
int pcap_compile (pcap_t *p, struct bpf_program *fp,char *str, int optimize, bpf_u_int32 netmask)
将 str 参数指定的字符串编译到过滤程序中。fp 是一个 bpf_program 结构的指针,在 pcap_compile () 函数中被赋值。optimize 参数控制结果代码的优化。netmask 参数指定本地网络的网络掩码。
int pcap_setfilter (pcap_t *p, struct bpf_program *fp)
指定一个过滤程序。fp 参数是 bpf_program 结构指针,通常取自 pcap_compile () 函数调用。出错时返回 - 1;成功时返回 0。抓取下一个数据包
抓取数据包
int pcap_dispatch (pcap_t *p, int cnt,pcap_handler callback, u_char *user)
捕获并处理数据包。cnt 参数指定函数返回前 所处理数据包的最大值。cnt=-1 表示在一个缓冲区中处理所有的数据包。cnt=0 表示处理所有数据包,直到产生以下错误之一:读取到 EOF;超时读 取。callback 参数指定一个带有三个参数的回调函数,这三个参数为:一个从 pcap_dispatch () 函数传递过来的 u_char 指针,一个 pcap_pkthdr 结构的指针,和一个数据包大小的 u_char 指针。如果成功则返回读取到的字节数。读取到 EOF 时则返回零值。出错时则返回 - 1, 此时可调用 pcap_perror () 或 pcap_geterr () 函数获取错误消息。
int pcap_loop (pcap_t *p, int cnt, pcap_handler callback, u_char *user)
功能基本与 pcap_dispatch () 函 数相同,只不过此函数在 cnt 个数据包被处理或出现错误时才返回,但读取超时不会返回。而如果为 pcap_open_live () 函数指定了一个非零值的 超时设置,然后调用 pcap_dispatch () 函数,则当超时发生时 pcap_dispatch () 函数会返回。cnt 参数为负值时 pcap_loop () 函数将始终循环运行,除非出现错误。
u_char *pcap_next (pcap_t *p, struct pcap_pkthdr *h)
返回指向下一个数据包的 u_char 指针。
void pcap_close (pcap_t *p)
关闭 p 参数相应的文件,并释放资源。
其他的辅助函数
FILE *pcap_file (pcap_t *p)
返回被打开文件的文件名。
int pcap_fileno (pcap_t *p)
返回被打开文件的文件描述字号码。
综合使用 libnet 和 libpcap:ARP 例程
综合使用 libnet 和 libpcap 可以构造强有力的网络分析、诊断、和应用程序。一个具有普遍意义的综合使用 libnet 和 libpcap 的程序的原理框架如图 1 所示:
本节给出一个综合应用 libnet 和 libpcap 的简单例程,其功能是在接收到一个来自特定主机的 ARP 请求报文之后,发出 ARP 回应报文,通知该主机请 求的 IP 地址对应的 MAC 地址。这个程序实现了标准的 ARP 协议,但是却不同于操作系统内核中标准的实现方法:该程序利用了 libpcap 在数据链路层抓 包,利用了 libnet 向数据链路层发包,是使用 libnet 和 libpcap 构造 TCP/IP 协议软件的一个例程。该程序很简单,但已经可以说明 libnet 和 libpcap 的综合使用方法:
/* tell destination host with ip 'dstip' that the host with request ip'srcip' is with mac address srcmac
* author: white cpf 2003.5.15.
* compile: gcc arp.c -lnet -lpcap -o arp
*/
#include <libnet.h>
#include <pcap.h>
void usage(char *exename) {
printf("tell dstip with dstmac that srcip is at srcmac. \n");
printf("usage: % s -d dstip -s srcip -D dstmac -S srcmac \n", exename);
return;
}
// 程序输入:来自命令行参数
u_char ip_src[4], ip_dst[4];
u_char enet_src[6], enet_dst[6];
extern int mac_strtochar6(u_char *enet, char *macstr); // 将字符串格式的 MAC 地址转换为 6 字节类型 r
int get_cmdline(int argc, char *argv[]); // 命令行参数处理函数
int main(int argc, char *argv[]) {
libnet_t *l;
libnet_ptag_t t;
u_char *packet;
u_long packet_s;
char device[5] = "eth0";
char errbuf[LIBNET_ERRBUF_SIZE];
char filter_str[100] = "";
struct bpf_program fp; /* hold compiled program */
char *dev;
pcap_t *descr;
struct pcap_pkthdr hdr; /* pcap.h */
u_char *packet;
bpf_u_int32 maskp; /* subnet mask */
bpf_u_int32 netp; /* ip */
int promisc = 0; /* set to promisc mode? */
int pcap_time_out = 5;
int c, ret;
u_long i;
if(get_cmdline(argc, argv) <= 0) {
usage(argv[0]);
exit(0);
}
dev = pcap_lookupdev(errbuf);
if(dev == NULL) {
fprintf(stderr, "% s\n", errbuf);
return -1;
}
ret = pcap_lookupnet(dev, &netp, &maskp, errbuf);
if(ret == -1) {
fprintf(stderr, "% s\n", errbuf);
return -1;
}
descr = pcap_open_live(dev, BUFSIZ, promisc, pcap_time_out, errbuf);
if(descr == NULL) {
printf("pcap_open_live (): % s\n", errbuf);
return -1;
}
sprintf(filter_str, "arp and (src net % d.% d.% d.% d)", ip_dst[0], ip_dst[1], ip_dst[2], ip_dst[3]);
if(pcap_compile(descr, &fp, filter_str, 0, netp) == -1) {
printf("Error calling pcap_compile\n");
return -1;
}
if(pcap_setfilter(descr, &fp) == -1) {
printf("Error setting filter\n");
return -1;
}
while(1) {
printf("wait packet:filter:% s\n", filter_str);
packet = pcap_next(descr, &hdr);
if(packet == NULL) {
continue;
}
l = libnet_init(LIBNET_LINK_ADV, device, errbuf);
if(l == NULL) {
fprintf(stderr, "libnet_init () failed: % s", errbuf);
exit(EXIT_FAILURE);
}
t = libnet_build_arp(ARPHRD_ETHER, /* hardware addr */
ETHERTYPE_IP, /* protocol addr */
6, /* hardware addr size */
4, /* protocol addr size */
ARPOP_REPLY, /* operation type */
enet_src, /* sender hardware addr */
ip_src, /* sender protocol addr */
enet_dst, /* target hardware addr */
ip_dst, /* target protocol addr */
NULL, /* payload */
0, /* payload size */
l, /* libnet handle */
0); /* libnet id */
if(t == -1) {
fprintf(stderr, "Can't build ARP header: % s\n", libnet_geterror(l));
goto bad;
}
t = libnet_autobuild_ethernet(enet_dst, /* ethernet destination */
ETHERTYPE_ARP, /* protocol type */
l); /* libnet handle */
if(t == -1) {
fprintf(stderr, "Can't build ethernet header: % s\n", libnet_geterror(l));
goto bad;
}
c = libnet_adv_cull_packet(l, &packet, &packet_s);
if(c == -1) {
fprintf(stderr, "libnet_adv_cull_packet: % s\n", libnet_geterror(l));
goto bad;
}
c = libnet_write(l);
if(c == -1) {
fprintf(stderr, "Write error: % s\n", libnet_geterror(l));
goto bad;
}
continue;
bad:
libnet_destroy(l);
return (EXIT_FAILURE);
}
libnet_destroy(l);
return (EXIT_FAILURE);
}
int get_cmdline(int argc, char *argv[]) {
char c;
char string[] = "d:s:D:S:h";
while((c = getopt(argc, argv, string)) != EOF) {
if(c == 'd') *((unsigned int *)ip_dst) = (unsigned int)inet_addr(optarg);
else if(c == 's')
*((unsigned int *)ip_src) = (unsigned int)inet_addr(optarg);
else if(c == 'D')
mac_strtochar6(enet_dst, optarg);
else if(c == 'S')
mac_strtochar6(enet_dst, optarg);
else if(c == 'h')
return 0;
else
return -1;
}
return 1;
}
minitcpip 协议软件系统框架
图 3 与图 4 是 minitcpip 协议软件系统的框架图。其中,minitcpip 协议软件在一个单独的进程中实现。这个进程作为 TCP/IP 协议软件服务器 建立 C/S 模型向应用程序提供服务。其通讯采用了命名管道建立 C/S 模型的方式,任何用户的应用进程对 minitcpip 的使用必须作为客户端,通过 minisocket 函数库进行。其通讯模型见图 2。
图 2 利用管道建立 C/S 模型
协议软件进程一旦运行,则初始化 libnet、libpcap,初始化 TCP/IP 连接管理表(TCB)以及接收与发送缓冲区,打开众所周知的 FIFO 等操 作,然后等待客户机发来的命令(通过众所周知的 FIFO)。在收到合法的命令,包括建立连接、发送数据、接收数据、关闭连接和设置连接属性等等之后,就作 出相应的分析与处理。比如根据命令中指定的源 IP、目的 IP、源端口、目的端口开始监控和接收过滤网络上的数据(如下文,实际是监控三个文件描述符)等 等。
为了方便监控和调试协议软件内部状态,协议软件同时等待标准输入设备发来的命令,协议软件将根据标准输入的合法命令形成到标准输出设备的输出。
除了在周知口等待客户机的命令、在标准输入设备等待监控命令之外,协议软件还必须同时等待来自网络设备的数据。为了在同一个 进程中可以高效率的处理这些不同来源的数据,软件通过使用 libpcap 提供的函数接口 int pcap_fileno (pcap_t *p),得到被打开的网络设备文件的文件描述符。在得到这个描述符之后,就可以和管道文件描述符同等的使用 select () 函数进行并行处理了。
在网络设备文件的文件描述字可读时,软件将调用 u_char *pcap_next (pcap_t *p, struct pcap_pkthdr *h)
函数来获得下一个抓到的数据包。
该协议软件的原理性的实现代码如下:
int main (){
初始化 libnet、libpcap;分配接收、发送缓冲区;初始化定时器;等等
while (1){
if (发送缓冲区有数据){
发送数据
}
调用 select () 函数等待 3 个文件描述符是否准备好,这三个文件描述符分别是:PCAP 文件描述符、周知口管道读文件描述符、标准输入文件描述符
if (PCAP 文件描述符准备好){
调用 pcap_next () 函数来获得下一个抓到的数据包
}
if (周知口读文件描述符准备好){
读取数据
根据进程内部的 TCB 中的信息,按照 TCP 协议规范进行分析处理
}
if (标准输入文件描述符准备好){
读取数据
分析处理,比如将内部信息回馈到标准输出文件描述符
}
if (超时){
超时处理
}
}
}
如上面的流程所示,在收到网络上的数据包时,即根据 TCP/IP 协议 IP、TCP 等报文格式进行分析处理,并将接收到的数据回传给客户端应用程序。在收到周知管道的数据包时,则根据数据包的命令类型进行相 应的操作,比如对其中的一条命令 --SEND 命令,在接收到这条命令后,就将其后附的数据写入发送缓冲区,在随后的循环中,这些数据将被依次发送到网络上 去。在周知管道与回送管道上进行的通讯采用了一个自定义的协议,后文对此作了简单介绍。
最终系统在运行时的基本框架如下两图所示:其中,图 3 是系统在运行时的整体结构,图 4 是协议软件进程内部的结构。
图 3 系统整体结构
图 4 协议软件进程内部结构
TCP/IP 协议数据处理模块是一组函数,与关键数据结构 TCP 表(TCB)等配合,负责实现 TCP/IP 协议的功能。
对 minitcpip 的协议实现的两点讨论
经过多年的发展,目前广泛应用的标准 TCP/IP 软件已经能够支持以太网、串行链路等多种物理设备,本文所讨论的实现主要是集中在以太网之上。
下面的讨论主要集中在定时器的设置和与操作系统的互斥两个问题上。
实际应用的 TCP/IP 协议软件是一个非常复杂的系统,具有流量控制和拥塞控制机制,一般都是由 TCP/IP 输出进程、输入 进程、定时器进程等多个系统进程配合完成。而实现这些机制的一个重要的基础是定时器的设置与处理。本文中 minitcpip 由于是在单进程中实现,难以实 现复杂的精确定时功能,所以在定时器的处理上进行了相当的简化。其中,TCP 协议状态机中的 TIME_WAIT 状态定时间隔是一个基础参数,一旦 minitcpip 运行之后,这个参数就不会改变,或者只能通过标准输入口进行调试目的的人工修改;每一个 TCP/IP 连接的重发定时器的策略则是根据该 连接所有的数据报的接收效果而建立的,具体实现是设置一个最小时间间隔参数和一个最大时间间隔参数,系统初始化后,重发定时间隔取最小值,此后每发生一次 超时(没有收到该连接的任何数据报或者没有收到具有 ACK 标志且确认序号正确的数据报),就将该重发定时间隔翻倍,直到达到最大值为止;如果在正确的收到 若干数据报后都没有发生超时,就将重发定时时间间隔减半,直到达到最小数值。系统通过重发定时间隔与系统当前时钟减去保存的上次发送的时钟值的比较结果得 出是否应该重新发送分组。而 select () 函数所使用的溢出时间则取所有连接的定时间隔的最小数值,以保证及时的发送分组。这样的策略当然效率不高,但 是已经可以保证协议软件的正确运行。
另外,minitcpip 协议软件是在本机同时存在一个标准 TCP/IP 协议栈的情况下实现的,BPF 的原理是复制而不是 截获本机收到的数据报文。因此,操作系统也会对收到的报文进行处理,这个问题如果处理不好,就会存在一些与操作系统内部的 TCP/IP 软件相互干扰的问 题。比如在源主机上,如果指定的源端口已经打开,则势必要影响本来正常运行的程序,最后导致二者都不能正常工作。同时,在使用 minitcpip 与远端目 标主机上的标准 TCP/IP 协议软件建立连接的时候,本机的操作系统也会收到目标主机的报文,并且发现这个报文指定的 IP 地址和端口并不在打开的端口表项 中,于是操作系统认为收到了一个不合法的报文。许多操作系统对这一事件的反应是发送一个带有 RST 标志的报文出去,从而导致目标主机的连接复位。
这个问题有几种可能的解决办法:
因为实现平台使用了开放源代码的 Linux 系统,所以可以考虑修改内核中 TCP 软件实现部分(TCP 软件向应用进程提交数据的过滤部分)对不合法报文的缺省行为。
如果目标主机上也使用 minitcpip 实现 TCP/IP 通讯,则我们可以修改标准 TCP/IP 数据报中标志位作用的定义,比如可以忽略所有收到的带有 RST 标志的数据报,而在需要发送复位数据报的时候,改用别的保留标志位来代替。
客户机在建立 TCP 连接的时候,使用一个空闲的本网段的 IP 地址作为源 IP 地址。这样操作系统就会认为所有发往该 IP 地址的数据报都不是本机应该接收的,从而不做反应;然而我们的 minitcpip 却可以正常的收到这个数据报。当然,为了远端目标主机可以与这个 "莫 须有" 的 IP 地址通讯,需要有一个 ARP 协议的运行进程,向所有发出请求的机器发送本机 IP 对应的 MAC 地址。前文中已经提供了一个类似的样例程序,稍做 修改就可实现这个功能。这个程序也可以集成在 minitcpip 中实现。
minitcpip 使用了其中第三种解决办法,网卡必须设置在混杂模式。这样在网络负载大的场合,CPU 占用率会比较高,丢包的概率会增加,效率会下降。因此 minitcpip 只适用于小负载的场合。
私有协议简介
minitcpip 向客户端提供的服务采用了 C/S 模型,这个服务的模型图如前图 2 所示,关于其细节请参见《UNIX 环境高级编程》。任何用户的应用进程对 minitcpip 的使用必须通过 minisocket 函数库进行,而 minisocket 与 minitcpip 之间则通过一个自定义的私有协议进行。这 个私有协议实现在用户进程与协议软件进程之间利用命名管道建立的 C/S 通讯中。
命名管道已经是可靠的本地进程间通讯机制,然而 minisocket 与 minitcpip 协议软件之间传递的不仅仅是发送 与接收的字节流,还包括对 SOCKET 的控制,比如申请 SOCKET 号,建立 TCP 连接、关闭 TCP 连接等控制命令,所以必须设计一个简单的通讯协议来满 足具体的通讯要求。详细的通讯协议的规格说明这里不再列出。
管道通讯一般不会发生数据包的丢失和错序问题,所以该协议实质上只需要负责包的捡出(利用转义和数据长度和校验和)和分辨 不同种类的数据包(信息包、控制包)。若管道操作本身出现错误,则程序异常退出。下图 5 是这个私有协议使用的数据报格式:具体的数据包的检出与字符填充方 案等由于与本文主题关系不大,这里不再详述。
图 5 私有协议数据报格式
其 中,数据域的第一个字节总是命令类型,顾名思义,命令类型定义了应用程序与协议软件之间所有的通讯数据的类型,主要包括两类命令,即控制命令和信息传输命 令,控制命令用来控制 TCP 连接的状态等,信息命令则是客户机真正要传输的数据或服务器收到的回发给客户机的数据。具体的命令格式列表不再详述。
minisocket 用户编程接口与例程
客 户机通过调用 minisocket 接口与 minitcpip 通讯,这个接口封装了私有协议繁琐的细节,提供了类似与标准 SOCKET 接口的简单统一的函数 库给用户使用。目前实现的 minisocket 接口函数库还相当简单,功能也很有限,只包括 TCP 协议客户端的实现。在编写客户端应用程序时,用户只需要 知道少数几个数据结构和函数原型就可以了:
struct mini_sock {
u8 type; //=TCP or UDP
u32 dip; // destination ip
u32 dport; // destination port
u16 sip; // source ip
u16 sport; // source port
// ……
}
该数据结构定义在 mini_socket.h 中,包含了用户需要建立的 TCP 连接的所有参数
用户函数接口定义:
int mini_socket (struct mini_sock*)
该函数创建一个 SOCKET,struct socket * 中必须正确填写欲创建的 SOCKET 有关的参数。
成功的返回值实际上是协议软件写入数据的客户机专用 FIFO 文件描述符。这样用户可以使用标准的 select () 函数实现单进程的多个输入输出的处理。但是在这个文件描述符上的读写操作必须使用 minisocket 提供的函数才能正确进行。
函数 | 作用 |
int mini_connect (int socket) | 该函数用来通知协议软件通过与目标主机三次握手后,建立 TCP 连接 |
int mini_recv (int socket, char* buf,int* buflen) | 该函数从指定的 SOCKET 接收数据 |
int mini_send (int socket,char* buf,int buflen) | 该函数将指定的数据发送到 SOCKET |
int mini_close (int socket) | 该函数用来通知协议软件关闭指定的 socket 连接 |
使用本协议软件库函数实现 ECHO 服务的一个简单例程如下:
/***************************************************
* Simple tcp echo client using libnet and libpcap and mini_socket.c
* file: miniecho.c
* Date: 2003.5.
* Author: white cpf
* compile: gcc -Wall -lpcap -lnet miniecho.c mini_socket.c -o miniecho
* Run: readhat 8.0 you must be root and run ifconfig to see eth0 OK
*****************************************************/
#include <libnet.h>
#include <pcap.h>
#include "mini_socket.h"
#define uchar unsigned char
#define MAXBUFLEN 2048
char buf[MAXBUFLEN];
int buflen;
char recvbuf[MAXBUFLEN];
int recvedlen;
int main(int argc, char *argv[]) {
int ret;
int i;
char sip[40] = "169.254.159.112";
char sport[10] = "7777";
char dip[40] = "169.254.159.111";
char dport[10] = "5000";
struct socket ti;
int s;
ti.sip = inet_addr(sip);
ti.dip = inet_addr(dip);
ti.sport = (unsigned short)atoi(sport);
ti.dport = (unsigned short)atoi(dport);
s = mini_socket(&ti);
if(s < 0) {
printf("mini_socket () error\n");
return 0;
}
ret = mini_connect(s); // connect to tcpip using TCP three time handshaking
if(ret < 0) {
printf("mini_connect () error\n");
return 0;
}
while(1) {
// get input from stdin, quit when EOF or "myquit!" input
if(fgets(buf, sizeof(buf), stdin) == 0) break;
if(strcmp(buf, "myquit!") == 0) break;
ret = mini_send(s, buf, strlen(buf));
if(ret <= 0) {
printf("mini_send () return % d\n", ret);
break;
}
ret = mini_recv(s, recvbuf, &recvedlen);
if(ret <= 0) {
printf("mini_recv () return % d\n", ret);
break;
}
recvbuf[recvedlen] = 0;
printf("recved [% d bytes]:% s\n", recvedlen, recvbuf);
}
mini_close(s); // close tcpip using TCP three time handshaking
}
结论
本文通过使用目前广泛流行的 libnet 和 libpcap 函数库,在 Linux 环境下实现了一个单进程的 TCP/IP 协议软件:minitcpip,并且提供了一个调用接口:minisocket,通过这个接口,可以创建基于 TCP/IP 协议的应用程序。
本文实现的 minitcpip 协议软件只具有最小的 TCP/IP 通讯功能,效率也非常一般。但是,作为一个学习 libnet、libpcap,学习 TCP/IP 软件在以太网上的实现原理的目的,这个项目却具有一定的价值。另外,采用这种方案实现的协议与 libnet、libpcap、操作系统之间的层次关系清晰,可以想象,minisocket 中与环境无关的代码可以很容易的移植到目前越来越广泛使用的 嵌入式设备的网络应用上,这是作者写作本文的又一个考虑。
由于时间紧张,minitcpip 协议软件的实现方法许多都有继续斟酌之处,而且作者本身也在学习过程中,错误之处在所难免,本文若能为大家学习 libnet、libpcap、tcpip 有所帮助,作者欣慰之至。同时希望大家能给出批评和指正,以利共同提高。
参考资料
www.tcpdump.org
www.packetfactory.net/libnet
《UNIX 环境高级编程》 机械工业出版社 2000 年 2 月 W.Richard Stevens 著
《用 TCP/IP 进行网际互连 第二卷:设计、实现和内部构成》第 2 版 电子工业出版社 1998 年 DOUGLES E.COMER DAVID L.STEVENS 著
《一个 TCP 调试系统的设计与实现》 章淼 熊勇强 吴建平 清华大学计算机系网络研究所
关于作者
褚蓬飞,男,1976 年生。1998 年清华大学毕业后入青岛海信集团技术中心工作。期间参加了 863 项目 - 306 主题中的项目 — 数字化家庭信息系统家庭网 关的研发工作。2002 年入中国科学院软件技术研究所多媒体通讯与网络教研室学习,研究方向:多媒体通讯与网络技术。你可以通过 white_cpf@21cn.com 与其联系。 评论