热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

tcp/ip二三层转发

作为一个网络使用者,你也许从来没有思考过源及目的主机之间究竟发生了什么,因为网络对用户来说是一个黑匣子,所有的细节都被屏蔽掉了,你只能知道通还是不通,能不能上网?但是作为一个网络设计者,研究者,你就

 作为一个网络使用者,你也许从来没有思考过源及目的主机之间究竟发生了什么,因为网络对用户来说是一个黑匣子,所有的细节都被屏蔽掉了,你只能知道通还是不通,能不能上网?但是作为一个网络设计者,研究者,你就必须要想想?假如你要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定义,在中;
 
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内核网络部分源代码,不容易理解与掌握。



推荐阅读
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • 本文整理了一份基础的嵌入式Linux工程师笔试题,涵盖填空题、编程题和简答题,旨在帮助考生更好地准备考试。 ... [详细]
  • 如何配置VisualSVN以确保提交时必须填写日志信息
    在软件开发团队中,成员们有时会忘记在提交代码时添加必要的备注信息。为了规范这一流程,可以通过配置VisualSVN来强制要求团队成员在提交文件时填写日志信息。本文将详细介绍如何设置这一功能。 ... [详细]
  • 小程序的授权和登陆
    小程序的授权和登陆 ... [详细]
  • malloc 是 C 语言中的一个标准库函数,全称为 memory allocation,即动态内存分配。它用于在程序运行时申请一块指定大小的连续内存区域,并返回该区域的起始地址。当无法预先确定内存的具体位置时,可以通过 malloc 动态分配内存。 ... [详细]
  • HTTP(HyperTextTransferProtocol)是超文本传输协议的缩写,它用于传送www方式的数据。HTTP协议采用了请求响应模型。客服端向服务器发送一 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • CentOS 7 中 iptables 过滤表实例与 NAT 表应用详解
    在 CentOS 7 系统中,iptables 的过滤表和 NAT 表具有重要的应用价值。本文通过具体实例详细介绍了如何配置 iptables 的过滤表,包括编写脚本文件 `/usr/local/sbin/iptables.sh`,并使用 `iptables -F` 清空现有规则。此外,还深入探讨了 NAT 表的配置方法,帮助读者更好地理解和应用这些网络防火墙技术。 ... [详细]
  • 在CentOS 7环境中安装配置Redis及使用Redis Desktop Manager连接时的注意事项与技巧
    在 CentOS 7 环境中安装和配置 Redis 时,需要注意一些关键步骤和最佳实践。本文详细介绍了从安装 Redis 到配置其基本参数的全过程,并提供了使用 Redis Desktop Manager 连接 Redis 服务器的技巧和注意事项。此外,还探讨了如何优化性能和确保数据安全,帮助用户在生产环境中高效地管理和使用 Redis。 ... [详细]
  • Vue 开发技巧:实现数据过滤与排序功能详解
    Vue 开发技巧:实现数据过滤与排序功能详解 ... [详细]
  • 阿里巴巴终面技术挑战:如何利用 UDP 实现 TCP 功能?
    在阿里巴巴的技术面试中,技术总监曾提出一道关于如何利用 UDP 实现 TCP 功能的问题。当时回答得不够理想,因此事后进行了详细总结。通过与总监的进一步交流,了解到这是一道常见的阿里面试题。面试官的主要目的是考察应聘者对 UDP 和 TCP 在原理上的差异的理解,以及如何通过 UDP 实现类似 TCP 的可靠传输机制。 ... [详细]
  • Android 构建基础流程详解
    Android 构建基础流程详解 ... [详细]
  • QT框架中事件循环机制及事件分发类详解
    在QT框架中,QCoreApplication类作为事件循环的核心组件,为应用程序提供了基础的事件处理机制。该类继承自QObject,负责管理和调度各种事件,确保程序能够响应用户操作和其他系统事件。通过事件循环,QCoreApplication实现了高效的事件分发和处理,使得应用程序能够保持流畅的运行状态。此外,QCoreApplication还提供了多种方法和信号槽机制,方便开发者进行事件的定制和扩展。 ... [详细]
author-avatar
dgsfdg3t4543
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有