作为一个网络使用者,你也许从来没有思考过源及目的主机之间究竟发生了什么,因为网络对用户来说是一个黑匣子,所有的细节都被屏蔽掉了,你只能知道通还是不通,能不能上网?但是作为一个网络设计者,研究者,你就必须要想想?假如你要ping一台位于美国硅谷的服务器,那么ICMP请求报文从源主机发出之后会发生那些事呢?其实报文的所有的路径都是由各种各样的类似与现实生活中的法律一样的网络协议约束下设计的,报文在离开老巢之后会经过二层接入层交换机,在三层交换机汇聚,从边缘路由器上发送出去,流入了茫茫的internet洪流之中。而在这个过程之中,报文会做的就是在网络设备上从转发处理。提到转发就不得不提到二三层转发的概念。其实关于二三层转发的概念在前几篇中都已经有一点涉及,只是并没有很明确的提出来。
下面就举例来讨论一下二三层转发的详细过程:
port1 port2
首先来说说二层转发的处理流程:
1. 假如交换机的port1和port2处于同一个vlan内,PC1想PING主机PC2,那么PC1首先会根据目的PC2的IP地址查询路由表,查询下一跳地址的(ARP -a)的ARP缓存。如果存在对应的表项则直接转发出去,如果不存在,就会进行步骤2处理。
2. PC1向PC2广播发送ARP REQUEST请求报文
3. 交换机port1接口收到ARP请求报文之后会在port1接口学习PC1的mac地址建立ARP及FIB表(port<---->mac),从广播ARP REQUEST
4. PC2会接收到从交换机port2广播出来的ARP请求报文,学习PC1的MAC,构建自己的ARP表项(IP<---->MAC),并向PC1返回ARP REPLAY单播报文
5. 交换机从port2接受到ARP应答报文后,学习PC2的MAC地址构建ARP及FIB表项,根据目的PC1的MAC地址查询FIB表项,然后从port1接口发送出去。
6. PC1接收到了ARP REPLAY后会学习PC2的MAC地址,生成ARP缓存表。然后将学习到的PC2的MAC地址填充在ICMP请求报文之中单播发送
7. 交换机从port1收到ICMP请求报文后会查报文的目的MAC地址是否是本机的地址,查询结果非本机MAC地址,则可以判断为二层转发,然后根据Dest MAC查询FIB表,从交换机port2端口发送出去。
8. PC2接收到ICMP请求报文,返回ICMP应答报文,报文格式不变,将源及目的IP及MAC地址对调发送
9. 交换机接受到ICMP应答报文之后依然会比较目的MAC是否为本机MAC,比较结果为非本机MAC,判断为二层转发,然后从port1接口直接发送。
10.PC1接收到ICMP 应答报文后判断PC1跟PC2双向互通。
对于三层转发其实流程跟二层转发流程类似:
1. 假如交换机的port1和port2处于不同vlan内,PC1想PING主机PC2,那么PC1首先会查路由表(根据PC2的IP),根据相关路由表项的下一跳IP地址查询(ARP -a)自己的ARP缓存。如果存在对应的表项则直接转发出去,如果不存在,就会进行步骤2处理。
2. PC1向PC2广播发送ARP REQUEST请求报文(目的IP为PC2的地址,目的MAC为下一跳的MAC)
3. 交换机port1接口收到ARP请求报文之后比较目的MAC是自己的入接口的MAC地址,而IP并非port1对应vlan接口的IP,则判断为三层转发。
4. 交换机将报文上送CPU处理,根据目的IP(PC2)查路由表,查询结果为下一跳出接口应该从port2所在的VLAN,所以将arp请求在该vlan内广播发送
5. PC2收到ARP请求报文后会学习mac地址构建ARP表返回ARP应答报文。
6. 交换机接受到ARP应答报文后会学习MAC构建ARP及FIB表项,比较目的MAC是否为本机MAC,结果非本机MAC,则判断为三层转发,将报文上送CPU,软件查询路由表,查询结果显示下一跳出接口为port1所在的vlan接口,查询FIB表,从port1接口发送出去。
7. PC1接受到PC2发送的ARP应答报文后学习mac构建ARP表项。
8. PC1根据新建的arp表向PC2发送ICMP请求报文,交换机接收到后根据已经构建的FIB表及ARP表及路由表转发报文。
9. PC2收到后返回ICMP应答报文,PC1收到后判断主机可达。
小结:
判断二层转发和三层转发的最根本的区别在于PC1查路由表,如果SIP和DIP在同一网段则PC1的下一跳即为PC2的地址,所以发出的报文的DMAC就是PC2的MAC地址,设备计较报文的DMAC为非设备MAC,则判断为二层转发,否则如果DMAC跟设备接收报文的端口MAC一致,这也表明SIP和DIP不在同一网段,需要走三层转发,上送CPU处理。
有以上分析可以看出判断二三层转发表面是比较SIP和DIP是否是同一网段。其实质则是转发设备通过比较转发报文的DMAC是否为本机MAC来作出最终判断的。
在做截包模块的过程中,看到过一些数据包的截取方法,如下:
1,利用pcap软件包。pcap的linux版本是libpcap函数库,而在Windows下对应的函数库为Winpcap。如注明的协议分析软件Etheral软件便是基于此软件包(但不局限于)实现的。
2,利用原始套接字。如本项目中的udp代理服务器,就是利用了原始套节字直接在ip层对数据包进行v4和v6数据包的转换操作。
3,利用nitfilter框架。
4,直接在网络协议栈中注册自己的协议。
。。。
下面分别对这几种方法做简单介绍。
1,利用libpcap函数库:
利用libpcap开发网络嗅探器时,一般包含以下几个基本流程:
1)确定捕获网络数据包的网卡。
函数原形:
char *pcap_lookupdev(dev *errbuf);
2)打开网络设备。
函数原形:
pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf)
3)设置过滤条件。
函数原形:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
4)获取数据包。
函数原形:
const u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
5)关闭网络设备。
函数原形:
void pcap_close(pcap_t *p)
2,原始套接字:
这种方法很简单,可参考《UNIX网络编程》。
3,NETFILTER:
netfilter是linux2.4/2.6自带的防火墙框架。它在网络协议栈中比较重要的位置上定义了五个挂接点。分别是:NF_IP_PRE_ROUTING,NF_IP_FORWARD,NF_IP_POST_ROUTING,NF_IP_LOCAL_IN,NF_IP_LOOCAL_OUT.它们在网络子系统的位置如下:
我们可以在这五个挂接点上设置自己的接收函数。也就是说,如果我们有在这五个点上注册自己的钩子的话,则在数据包经由这些挂接点的时候,就会先转发到我们相应的钩子函数中。可以这样理解,想象成钓鱼的过程。这五个点可以想象成可以垂钓的点,而我们的钩子就是鱼钩,在鱼经过这些点的时候,就可以把鱼钓起来。
下面看看如何定义一个钩子:
挂接点的操作即我们说的钩子由结构体struct nf_hook_ops定义,在<linux/netfilter.h>中;
struct nf_hook_ops
{
struct list_head list; /*链表头,用于将此结构接入操作链表,一般情况下可初始化为NULL*/
nf_hookfn *hook; /*用户定义的钩子函数,即数据包处理函数*/
int pf; /*协议族*/
int hooknum; /*挂接点*/
int priority; /*优先级*/
};
其中比较重要的是hook和hooknum,即钩子函数和挂接点。
下面来看一个例子:定义我们自己的钩子vndev_tx_ops。
static struct nf_hook_ops vndev_tx_ops = {
{ NULL, NULL }, vndev_tx ,
PF_INET, NF_IP_LOCAL_OUT,
NF_IP_PRI_FILTER-1
};
即要在NF_IP_LOCAL_OUT这个挂接点定义了一个钩子,相应的钩子函数为vndev_tx。要想使钩子可用,则必须对其进行注册。
这个钩子的注册代码为:nf_register_hook(&vndev_tx_ops);
这样一来,在数据包经过NF_IP_LOCAL_OUT这个挂接点时,数据包就会被钓出来,然后送到我们的数据包处理函数中。
下面来看一个最简单的为netfilter编写的模块。
struct nf_hook_ops
{
struct list_head list; /*链表头,用于将此结构接入操作链表,一般情况下可初始化为NULL*/
nf_hookfn *hook; /*用户定义的钩子函数,即数据包处理函数*/
int pf; /*协议族*/
int hooknum; /*挂接点*/
int priority; /*优先级*/
};
static struct nf_hook_ops vndev_tx_ops = {
{ NULL, NULL }, vndev_tx ,
PF_INET, NF_IP_LOCAL_OUT,
NF_IP_PRI_FILTER-1
};
static int myhook_init(void)
{
return nf_register_hook(&vndev_tx_ops);
}
static void myhook_exit(void)
{
nf_unregister_hook($vndev_tx_ops);
}
module_init(myhook_init);
module_exit(muhook_exit);
4,直接向网络子系统中注册自己的接收模块。
此方法模拟了ip,arp等协议在网络子系统中的注册过程。难点在于要研读linux内核网络部分的相关源代码,弄清楚数据包的走向和其具体实现。简单之处在于一旦读懂了源代码,则进行的操作非常简单。
如果研读过数据包的处理函数netif_receive_skb,则会发现,这个函数会先看ptype_all中是否有注册的协议,如果有,则调用相应的处理函数,然后再到ptype_base中,找到合适的协议,将skb发送到相关协议的处理函数.比如ip协议(ip_rcv)或者arp(arp_rcv)等等。
知道了这一点之后,我们就可以在ptype_all中注册自己的协议,实现需要的功能。
下面来看看ptype_base和ptype_all在内核中的实现。
可以看到,ptype_base为一个hash表,而ptype_all为一个双向链表.每一个里面注册的协议都用一个struct packet_type表示.
struct packet_type
{
unsigned short type; /*协议类型*/
struct net_device *dev;
int (*func) (struct sk_buff *, struct net_device *,
struct packet_type *);
void *data; /* Private to the packet type */
struct packet_type *next;
};
其中需要注意的是dev参数,此参数表明了协议只处理来自dev指向device的数据,当dev=NULL时,表示该协议处理来自所有device的数据.这样,当注册自己的协议时,就可以指定自己想要监听或者接收的device.
其中注册和注销协议的函数为:
dev_add_pack(...)和dev_remove_pack(...)
这两个函数很简单,分别如下:
void dev_add_pack(struct packet_type *pt)
{
int hash;
br_write_lock_bh(BR_NETPROTO_LOCK);
#ifdef CONFIG_NET_FASTROUTE
/* Hack to detect packet socket */
if ((pt->data) && ((int)(pt->data)!=1)) {
netdev_fastroute_obstacles++;
dev_clear_fastroute(pt->dev);
}
#endif
if (pt->type == htons(ETH_P_ALL)) {
netdev_nit++;
pt->next=ptype_all;
ptype_all=pt;
} else {
hash=ntohs(pt->type)&15;
pt->next = ptype_base[hash];
ptype_base[hash] = pt;
}
br_write_unlock_bh(BR_NETPROTO_LOCK);
}
此函数判断协议类型,然后加到ptype_base或者ptype_all中.
void dev_remove_pack(struct packet_type *pt)
{
struct packet_type **pt1;
br_write_lock_bh(BR_NETPROTO_LOCK);
if (pt->type == htons(ETH_P_ALL)) {
netdev_nit--;
pt1=&ptype_all;
} else {
pt1=&ptype_base[ntohs(pt->type)&15];
}
for (; (*pt1) != NULL; pt1 = &((*pt1)->next)) {
if (pt == (*pt1)) {
*pt1 = pt->next;
#ifdef CONFIG_NET_FASTROUTE
if (pt->data)
netdev_fastroute_obstacles--;
#endif
br_write_unlock_bh(BR_NETPROTO_LOCK);
return;
}
}
br_write_unlock_bh(BR_NETPROTO_LOCK);
printk(KERN_WARNING "dev_remove_pack: %p not found./n", pt);
}
此函数也很简单,只是把协议从相关的链表中移除.
了解了上面的相关知识点后,下面来看一个具体的例子。
static struct packet_type my_type =
{
__constant_htons(ETH_P_ALL),
NULL,
packet_get,
NULL,
NULL,
};
这里定义了我们自己的一个协议my_type。如果把自己的协议通过dev_add_pack()函数注册到ptype_all中,则当接收到一个数据包时,数据包将会首先发送到packet_get函数。你可以在这个函数里进行判断,如果是本机发出的数据包,则将skb发送到相应的数据包发送函数,如果是接收的skb,则将skb发送到相应的接收数据包处理模块。
注意,这之前都是讨论的接收数据包的情况,发送情况跟其类似,也是先访问ptype_all,然后再访问ptype_base。所以在注册了我们自己的协议以后,无论是发送数据包还是接收的数据包,都会被我们的截取函数packet_get函数获得。
此方法的优点是效率较高,不依赖于任何的框架模块。缺点是前期准备比较难,因为要读懂很多linux内核网络部分源代码,不容易理解与掌握。