CHAPTER1:从网络接口层说起
一、简介:
网络接口层位于物理层之上,提供一组接口供协议栈和物理设备交互。这一层的实现费了我很大心思。起初,我认为linux下调用网卡驱动和ARM上一样,可以直接调用网卡驱动的函数,于是跟着《Linux设备驱动程序》辛辛苦苦的写了一个网卡驱动,写完才发现,不知道怎么在程序中调用。查了无数资料,又看了看linux下网络部分的源码,才知道有个dev.c文件,里面定义了一组函数,可以和网卡驱动交互,于是我决定在一个模块里实现网络接口层。但内核编程对于我这个菜鸟来说实在难了点,好不容易写出了收包部分,在发包部分出了问题,一运行模块就挂起,还不能用rsmod命令移出,只能重启。正当我一筹莫展的时候,《使用libnet和libpcap构造TCP/IP协议软件》一文给了我很大启发,将协议栈做在用户层!这样既避免了纷繁的内核错误,又便于调试。我移植的目的不是替代linux本身的协议栈,仅仅是出于学习的目的,这样的方式大大简化了工作。最终,我采取了这种方式,用libpcap和libnet实现了网络接口层。
先简单的介绍一下libpcap和libnet。
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和libpcap构造TCP/IP协议软件》
在我实现的网络接口层中,主要使用了两个libnet函数,一个是libnet_t * libnet_init(int injection_type, char *device, char *err_buf),用于初始化libnet,另一个int libnet_write_link(libnet_t *l, u_int8_t *packet, u_int32_t size),用于在链路层发送数据包。这和《使用libnet和libpcap构造TCP/IP协议软件》中的方式不一样。该文的作者为了简化协议栈的编写,其封包工作使用了libnet提供的自动封包函数,而我们要移植的是一个标准协议栈,构造包的工作应该交给协议栈完成,网络接口层要做的仅仅是把协议栈构造好的帧(以太网包称为帧)发送出去,其它都不用管。所以这里选用了libnet中最底层的libnet_write_link函数,直接将帧交付给网卡发送出去。也就是说,用libnet为协议栈提供数据包的发送功能,其流程可以简单的表示为下图:
这里顺便提一下,我所使用的libnet是1.1.1版本的,比起1.0.x版本的改动比较大,很多函数名和参数都发生了变化,新的函数在其libnet-functions.h中都有说明,如果你查阅的资料中提到的函数不能使用,可以在这个文件中找到其新函数的声明。Libnet的下载地址:http://www.packetfactory.net/projects/libnet/(安装方法很简单,网上很多文章有介绍,可以在baidu上搜索一下)。
Libpcap简介:
libpcap 是 unix/linux 平台下的网络数据包捕获函数包,大多数网络监控软件都以它为基础。Libpcap 可以在绝大多数类 unix 平台下工作,本文分析了 libpcap 在 linux 下的源代码实现,其中重点是 linux 的底层包捕获机制和过滤器设置方式,同时也简要的讨论了 libpcap 使用的包过滤机制 BPF。
网络监控
绝大多数的现代操作系统都提供了对底层网络数据包捕获的机制,在捕获机制之上可以建立网络监控(Network Monitoring)应用软件。网络监控也常简称为sniffer,其最初的目的在于对网络通信情况进行监控,以对网络的一些异常情况进行调试处理。但随着互连网的快速普及和网络攻击行为的频繁出现,保护网络的运行安全也成为监控软件的另一个重要目的。例如,网络监控在路由器,防火墙、入侵检查等方面使用也很广泛。除此而外,它也是一种比较有效的黑客手段,例如,美国政府安全部门的"肉食动物"计划。
包捕获机制
从广义的角度上看,一个包捕获机制包含三个主要部分:最底层是针对特定操作系统的包捕获机制,最高层是针对用户程序的接口,第三部分是包过滤机制。
不同的操作系统实现的底层包捕获机制可能是不一样的,但从形式上看大同小异。数据包常规的传输路径依次为网卡、设备驱动层、数据链路层、IP 层、传输层、最后到达应用程序。而包捕获机制是在数据链路层增加一个旁路处理,对发送和接收到的数据包做过滤/缓冲等相关处理,最后直接传递到应用程序。值得注意的是,包捕获机制并不影响操作系统对数据包的网络栈处理。对用户程序而言,包捕获机制提供了一个统一的接口,使用户程序只需要简单的调用若干函数就能获得所期望的数据包。这样一来,针对特定操作系统的捕获机制对用户透明,使用户程序有比较好的可移植性。包过滤机制是对所捕获到的数据包根据用户的要求进行筛选,最终只把满足过滤条件的数据包传递给用户程序。
Libpcap 应用程序框架
Libpcap 提供了系统独立的用户级别网络数据包捕获接口,并充分考虑到应用程序的可移植性。Libpcap 可以在绝大多数类 unix 平台下工作,参考资料 A 中是对基于 libpcap 的网络应用程序的一个详细列表。在 windows 平台下,一个与libpcap 很类似的函数包 winpcap 提供捕获功能,其官方网站是http://winpcap.polito.it/。
Libpcap 软件包可从 http://www.tcpdump.org/ 下载,然后依此执行下列三条命令即可安装,但如果希望 libpcap 能在 linux 上正常工作,则必须使内核支持"packet"协议,也即在编译内核时打开配置选项 CONFIG_PACKET(选项缺省为打开)。
./configure
./make
./make install
libpcap 源代码由 20 多个 C 文件构成,但在 Linux 系统下并不是所有文件都用到。可以通过查看命令 make 的输出了解实际所用的文件。本文所针对的libpcap 版本号为 0.8.3,网络类型为常规以太网。
――摘自《基于 linux 平台的 libpcap 源代码分析》
Libpcap完成了网络接口层中收包的工作,主要使用了下面几个函数:
1、 pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms,char *ebuf)
用于初始化pcap。
2、 int pcap_compile(pcap_t *p, struct bpf_program *program,char *buf, int optimize, bpf_u_int32 mask)
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
这两个函数主要用于安装过滤器,只接收发给本机的数据包。
3、 int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
用于捕获网卡收到的数据包,交由callback参数指定的函数处理
以上几个函数完成了协议栈的收包的功能,其中pcap_loop函数参数callback指定的处理函数(packet_rx函数)是网络接口层向协议栈提交数据包的接口,在其中会调用协议栈的ni_in函数,进行多路分解,这在后面讲解源码时会提到。整个收包流程非常简单,可用下图表示:
以上简单的介绍了一下网络接口层的设计,其中主要使用了libnet和libpcap两个库,关于这两个库的使用网上有很多文章介绍,感兴趣的朋友可以参阅相关资料。至于我们在网络接口层中使用到的功能,在后面的源码部分会专门提到。
二、源码:
这里我先给出整个网络接口层的源码,因为我自身的coding水平很菜,经常在学习别人的源码时深感注释太少,所以自己写的程序中注释都尽可能的详细。代码很简单,高手们可以很容易的看懂其功能,如果你的水平和我一样属于菜鸟,可以看看后面对每个函数的专门讲解。为了简化,下面在提到comer的TCP/IP协议栈时一律简称comer。
/* zinterface.c -提供数据链路层函数(2006.4.10)*/
/*用libnet 和libpcap 构造数据链路层。
*使用其他函数前,首先应该调用init_interface 初始化
*libnet 和libpcap,以及网络接口。
*/
#include
#include
#include
#include
#include
#include
char errbuf[LIBNET_ERRBUF_SIZE]; //libnet 错误信息
libnet_t *l_dsc; //libnet 描述符
char ebuf[PCAP_ERRBUF_SIZE];//用于接收pcap产生的错误信息
pcap_t *pd;//pcap设备描述符
Bool is_init_interface = FALSE;//接口初始化成功标志位
struct netif nif[1];//定义在netif中声明的全局接口
libnet_t *init_libnet()
{
l_dsc = libnet_init(LIBNET_LINK_ADV, NULL,errbuf);
return l_dsc;
}
//打开pcap,获得描述符
/*各参数依次为:设备名(linux下为eth0~ethN),捕获数据的最大字节,
*混杂模式(1表示混杂),超时时间,接收错误的字符数组
*/
pcap_t *init_libpcap()
{
pd = pcap_open_live("eth0",DEFAULT_SNAPLEN,1,1000,ebuf);
return pd;
}
void xinu_dev_init(struct netif *pni, IPaddr ip,IPaddr net_ip , IPaddr mask_ip,
u_char *hwaddr,IPaddr subnet,IPaddr nbrc)
{
strcpy(pni->ni_name, "eth0");
pni->ni_state = NIS_UP;//初始化为打开状态
pni->ni_ip = ip;//初始化接口ip
pni->ni_net =net_ip;//网络号
pni->ni_subnet = subnet;//子网广播地址
pni->ni_mask = mask_ip;//子网掩码
pni->ni_brc = 0xFFFFFFFF;//广播ip地址
pni->ni_nbrc = nbrc;//组播地址
pni->ni_mtu = 1500;//最大传输单元
pni->ni_hwtype = AR_HARDWARE;//指定设备类型为以太网(仿照linux中,为1)
memcpy(pni->ni_hwa.ha_addr,hwaddr,EP_ALEN);//设定物理地址
pni->ni_hwa.ha_len = EP_ALEN;//物理地址长度6
memset(pni->ni_hwb.ha_addr,0xFF,EP_ALEN);//设定广播物理地址
pni->ni_hwb.ha_len = EP_ALEN;//广播//物理地址长度6
pni->ni_ivalid = 0;
pni->ni_nvalid = 0;
pni->ni_svalid = 0;
pni->ni_dev =0;
pni->ni_ipinq = newq(NI_INQSZ, QF_NOWAIT); //为接口分配一个队列
pni->ni_outq = 0;
pni->ni_descr = NULL;
pni->ni_mtype = 0;
pni->ni_speed = 0;
pni->ni_admstate = 0;
pni->ni_lastchange = 0;
pni->ni_ioctets = 0;
pni->ni_iucast = 0;
pni->ni_inucast = 0;
pni->ni_idiscard = 0;
pni->ni_ierrors = 0;
pni->ni_iunkproto = 0;
pni->ni_ooctets = 0;
pni->ni_oucast = 0;
pni->ni_onucast = 0;
pni->ni_odiscard = 0;
pni->ni_oerrors = 0;
pni->ni_oqlen = 0;
pni->ni_maxreasm = 0;
}
//取得子网广播地址
//仅仅支持c 类ip地址(192.0.1.0 ~ 223.225.225.0)
IPaddr get_subnet(IPaddr netp,IPaddr mask)
{
u_char netp_last = ((u_char *)&netp)[3];
u_char mask_last =((u_char *)&mask)[3];
IPaddr subnet = netp;
int i;
int all_bit_1 = 255;
for (i = 0;i<=7;i++)
{
if ((u_char)(mask_last << i) == 0)
break;
}
all_bit_1 >>= i;
((u_char*)&subnet)[3] |= all_bit_1;
return subnet;
}
//取得组播地址
//只支持A、B、C 类地址
IPaddr get_nbrc(IPaddr ip)
{
u_long all_bit24_1 = 0xffffff00;
u_long all_bit16_1 = 0xffff0000;
u_long all_bit8_1 = 0xff000000;
IPaddr nbrc;
if (IP_CLASSA(ip))
nbrc = ip | all_bit24_1;
else if (IP_CLASSB(ip))
nbrc = ip | all_bit16_1;
else if (IP_CLASSC(ip))
nbrc = ip | all_bit8_1;
else
nbrc = 0;
return nbrc;
}
//初始化libnet 和libpcap,以及网络接口
int init_interface()
{
IPaddr interface_ip; //接口ip 地址
IPaddr interface_mask; //接口子网掩码
IPaddr interface_net_ip; //接口网络号
u_char *interface_hwaddr; //接口硬件地址
IPaddr interface_subnet;//接口子网广播地址
IPaddr interface_nbrc;//组播地址
//初始化libpcap
if ((init_libpcap()) == NULL)
{
printf("Init libpcap error!/n");
return SYSERR;
}
//初始化libnet
if ((init_libnet()) == NULL)
{
printf("Init libnet error!/n");
return SYSERR;
}
//取得硬件地址
if ((interface_hwaddr = (u_char *)libnet_get_hwaddr(l_dsc)) == NULL)
{
printf("Get interface MAC fault!/n");
return SYSERR;
}
//取得网络号和子网掩码
if((pcap_lookupnet("eth0",(bpf_u_int32*)&interface_net_ip,(bpf_u_int32
*)&interface_mask,ebuf)) == SYSERR)
{
printf("Get interface NET_IP/MASK fault!/n");
return SYSERR;
}
//取得ip 地址
if ((interface_ip =libnet_get_ipaddr4(l_dsc) )== -1)
{
printf("Get interface IP error!/n");
return SYSERR;
}
//取得子网广播地址
interface_subnet = get_subnet(interface_net_ip,interface_mask);
//取得组播地址
interface_nbrc = get_nbrc(interface_ip);
//初始化网络接口
xinu_dev_init(&nif[0], interface_ip, interface_net_ip, interface_mask,
interface_hwaddr,interface_subnet,interface_nbrc);
xinu_dev_init(&nif[1], interface_ip, interface_net_ip, interface_mask,
interface_hwaddr,interface_subnet,interface_nbrc);
//初始化成功,设置标志位
is_init_interface = TRUE;
return OK;
}
//利用libnet 的libnet_write_link 函数从链路层将包发送出去
//返回值是已发送的字节数
int link_write(u_char *buf,unsigned len)
{
struct ep *pep = (struct ep *) buf;
int send_bytes_num;
if (is_init_interface != TRUE)
return SYSERR;
if (l_dsc != NULL)
send_bytes_num = libnet_write_link(l_dsc,(u_int8_t *)&pep->ep_eh,len);
else
return SYSERR;
return send_bytes_num;
}
extern void arptbl_add(struct arpentry * tbl_prt);
extern struct arpentry *paetbl;
//收到包后交给协议栈的ni_in 函数处理
void packet_rx(u_char * user, const struct pcap_pkthdr * h, const u_char * p)
{
struct ep *pep = (struct ep *) malloc(sizeof(struct ep));
struct eh *peh;
peh = (struct eh *) p;
pep->ep_len = h->len;
memcpy(&(pep->ep_eh),peh,sizeof(struct eh));
//这里复制的长度减去了帧头的长度
memcpy(pep->ep_data,p+sizeof(struct eh),pep->ep_len - EP_HETHLEN);
ni_in(&nif[1],pep,pep->ep_len);
arptbl_add(paetbl);
}
int pcap_receive()
{
char filter_str[20];
u_char *ip ;
struct bpf_program fp;
if (is_init_interface != TRUE)
return SYSERR;
//将ni_ip 转化为4 元素的的u_char 数组,ip 指向数组首地址
ip = (u_char *) &nif[1].ni_ip;
//安装过滤器,只接收发给本机的数据包
sprintf(filter_str, "dst net %d.%d.%d.%d",ip[0],ip[1],
ip[2],ip[3]);
if (pcap_compile(pd,&fp,filter_str,0,nif[1].ni_net) == -1)
{
printf("Pcap complier init error!/n");
return SYSERR;
}
if (pcap_setfilter(pd,&fp) == -1)
{
printf("pcap setfilter init error!/n");
return SYSERR;
}
/*无限循环获取数据包
*参数依次是:设备描述符,要抓取的包个数(-1表示无限循环抓取),
*处理数据包的回调函数,user 参数不详
*/
if (pcap_loop(pd,-1,packet_rx,NULL) < 0 )
{
fprintf(stderr,"pcap_loop: %s/n",pcap_geterr(pd));
return SYSERR;
}
}
我们首先从头文件说起。Kernel.h 和 q.h 两个头文件都是comer的TCP/IP协议栈中定义的,前者定义了整个协议栈中使用的常量和一些宏,后者是xinu系统队列管理函数所包含的头文件,主要定义了一些队列管理所用到的常量。如果看过comer《用TCP/IP进行网际互联第二卷:设计、实现与内核》的朋友可能已经注意到了,znetwork.h这个文件和协议栈中network.h文件从命名上看很相识,实际上,znetwork.h就是network.h的一个简化版本,里面包含了一些zinterface.c文件中函数所要用到的头文件和常量。这是一个非常无奈的做法,直接包含network.h文件会使整个程序变得更加标准、整洁,但实际上是不可行的。关键在libnet.h文件。Libnet.h文件是libnet库用到的头文件,其中包含了很多linux下的头文件,而comer的xinu操作系统,从名字上就可以看出和unix有很大的关系(你反着读看看:)),所以其中很多常量的命名会和linux下的头文件冲突,为了避免这种情况,我定义了znetwork.h这个简化版本,包含了必要的又不会产生冲突的头文件和常量。Zinterface.h仅仅声明了zinterface.c中的函数而已。最后是pcap.h,很明显,它是libpcap库用到的头文件。
接着看看函数:
1、init_interface函数
在使用其他函数前,必须先调用Init_interface函数,它完成了libnet、libpcap以及comer中网络接口的初始化。Init_interface首先调用了init_libpcap和init_libnet函数,从上面的源码可以看出,这两个函数仅仅调用了pcap_open_live和libnet_init初始化libpcap和libnet,并返回pcap和libnet的描述符供其他pcap、libnet函数使用。这里将描述符pcap_t *pd(pcap描述符)、libnet_t *l_dsc(libnet描述符)定义成了公有变量。紧接着,调用了libnet_get_hwaddr、pcap_lookupnet、libnet_get_ipaddr4用于获取网卡硬件地址、网络号、子网掩码以及ip地址。这里需要特别提到的是int pcap_lookupnet(device, netp, maskp, errbuf)函数,我们注意到其中第二个参数netp。网上广为流传的pcap资料中都声称netp代表了ip地址,部分资料(好像是在什么叶子的blog中)还信誓旦旦的提醒大家:千万不要被它的名字迷惑了,它就是ip地址。实际上,正如它名字提示的一样,它确实是网络号。大家可以把它和用libnet_get_ipaddr4得到的ip地址比较一下,或者用它和子网掩码做位与就可以看出。接下来,这里调用了get_subnet和get_nbrc用于获得子网广播地址和组播地址。所有的准备工作完成后,调用了xinu_dev_init函数初始化comer中的网络接口,对struct netif结构中的各个字段赋值(struct netif结构定义见comer中netif.h文件)。这里初始化了两个接口,nif[0]和nif[1]。Comer的协议栈考虑了路由器的情况,即一台主机有多个网络接口,但我们大多数人的机器上只有一个网卡,所以只定义了两个接口,nif[0]代表本地结构,也就是linux下的lo回环(本地接口),nif[1]代表网卡。大家可能注意到了,两个接口被一样的初始化,其实这是不对的。Lo回环接口的初始化操作不应该是这样,但因为写这篇文章的时候我还没有做完ip层,所以暂时将它初始化为和代表网卡的接口一样,在用到它时再更改它的初始化代码。最后,我们将全局变量is_init_interface设为了TRUE,它标志了init_interface函数初始化成功,这样其他函数才能使用pcap和libnet库。
2、get_subnet函数
get_subnet函数是用于获得子网广播地址。由于没有找到现成的函数可用,我自己实现了这个功能。如果你还不明白子网的划分方法,建议你先在网上查一下相关文章了解一下。get_subnet函数只支持c类ip地址,也就是192.0.1.0 ~ 223.225.225.0的ip段。如果你的ip是a类和b类,可以参照该函数的实现改写。这里首先根据子网掩码,用一个循环判断出子网号占了几位主机号(c类ip的最后8位代表主机号),用I表示所占位数。然后将一个全1的字节右移I位再和网络号的最后八位做位或操作,这样就将主机位全部置1,得到了子网广播地址。
4、 get_nbrc函数
get_nbrc函数用于获得组播地址。它的实现更简单,首先使用了comer中的宏判断出是哪类ip地址(这些宏在ip.h中定义),然后将相应类型ip地址的主机号全部置1,就得到了组播地址。注意,get_nbrc只支持a、b、c类ip地址。
5、 xinu_dev_init函数
xinu_dev_init函数功能很简单,初始化了comer中netif.h文件定义的网络接口。其字段定义在netif.h文件中都有很详细的说明,在此不再累述。这里传入了 ip,net_ip,mask,hwaddr,subnet,nbrc 6个参数,分别代表ip地址、网络号、子网掩码、硬件地址、子网广播地址、组播地址,用于初始化接口的相应字段。需要提一下的是ni_iping字段,它表示ip层的输入队列序号,这里使用了newq函数在初始化时为它分配了一个新队列。Newq函数在comer中定义于sys/gpq.c文件中,由于移植的需要,我进行了改写,命名为zgpq.c,在后面介绍ARP的实现时会提到。因为我是边移植边写文章,所以netif接口中的很多字段暂时没有用到,故简单的初始化为0,再用到的时候会更改初始化的值。
6、pcap_receive函数
pcap_receive函数使用pcap的pcap_loop函数无限循环获取数据包,并将数据包提交给packet_rx函数处理。函数首先根据is_init_interface变量判断是否初始化了libpcap,然后使用pcap_compile和pcap_setfilter安装了一个过滤字符串(pcap_compile和pcap_setfilter函数的用法见网上相关文章),只接收发往本机的数据包。接着调用pcap_loop捕获数据包。注意,我们在调用pcap_open_live初始化pcap时,通过设置其第三个参数为1将网卡设置成了混杂模式,用于捕获网络上的所有数据包。这是因为在实现到ip层以上时(如udp、tcp),请求连接的操作会造成两个协议栈的同时响应(我们的协议栈和linux本身的协议栈),为了不产生影响,以后我们会更改xinu_dev_init函数,虚拟一个ip地址。这样,linux本身的协议栈就会丢弃发向该ip的数据包,而我们通过混杂模式捕获了网上所有的数据包,并通过设置过滤器,就可以得到发向我们协议栈的数据包。
7、 packet_rx函数
packet_rx函数是pcap_loop中callback参数所指定的数据包处理函数。Pcap_loop捕获到数据包后交由packet_rx函数处理。注意,packet_rx函数的参数必须像程序中那样定义,这是由pcap规定的。u_char* user代表pcap_loop函数中设定的字符串指针、struct pcap_pkthdr *h指向收集数据包相关信息的结构体的指针,const u_char *p是指向数据包报文的指针。在这里,我们用malloc分配了一个以太网帧数据结构pep(其结构定义见comer的ether.h文件),将p指针指向数据区中的报文复制一份给pep。这里用了两个memcpy函数分别复制以太网帧头和帧数据区。H->len表示了收到帧数据的长度。数据复制完成后,调用了ni_in函数,这是comer中多路分解的实现函数,在介绍ARP实现时会提到。现在仅仅需要知道网络接口层是通过ni_in将收到的数据包提交给协议栈即可。细心的你可能已经注意到了arptbl_add这个函数,以及packet_rx函数上的extern struct arpentry *paetbl声明。它们和我们的网络接口层没有任何关系,放在这里仅仅是因为我们后面的ARP实例中使用了内存映射,必须在这里调用映射函数。在读程序的时候可以将它们忽略掉。
8、link_write函数
link_write函数提供了以太网帧的发送功能。它主要用于替代comer中的write函数。这里有两个参数,buf代表要发送的数据,len表示数据长度。在确定了libnet已经被初始化后,调用了libnet_write_link函数发送数据。注意,我们这里发送的是&pep->ep_eh而不是pep(pep是指向以太网帧结构的指针),是因为comer中的以太网帧结构是一个扩展帧,在帧头之前还有10字节的附加信息,我不确定发送这样的帧是否会得到对方响应,故从pep结构中的ep_eh字段开始发送(ep_eh代表以太网帧头),发送一个标准以太网帧。(libnet_write_link函数有一个奇怪的bug,在后面介绍ARP实例时会提到)
到此为止,网络接口层的就已经全部介绍完了,它提供一组函数供协议栈发送和接收数据包,在后面的ARP介绍中,我们将会通过一个ARP实例看到它是怎么工作的。
――未完待续