• 【lwip】09-IPv4协议&超全源码实现分析


    前言

    默认主讲ipv4。

    概念性的内容简单过一遍即可,主要还是在源码实现方面。

    原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16859723.html

    9.1 IP协议简述

    IP 协议(Internet Protocol),又称之为网际协议,IP 协议处于 IP 层工作,它是整个 TCP/IP 协议栈的核心协议,上层协议都要依赖 IP 协议提供的服务,IP 协议负责将数据报从源主机发送到目标主机,通过 IP 地址作为唯一识别码。

    IP 协议是一种无连接的不可靠数据报交付协议,协议本身不提供任何的错误检查与恢复机制。

    IP地址是协议地址,MAC地址是硬件地址,所处的层级不一样。

    常见的广域网路由器就工作在IP层。

    在本次笔记lwip源码实现分析内容概述:

    • IP地址及其分类、特殊IP地址;
    • 子网划分、子网掩码、NAT等概念;
    • IP层数据包结构一级数据报输入处理;
    • IP层数据包的发送及分片操作;
    • IP层分片数据重装过程。

    9.2 IP地址分类

    A类地址:1.0.0.1—126.155.255.254

    B类地址:128.0.0.1—191.255.255.254

    C类地址:192.0.0.1—223.255.255.254

    D类地址:224.0.0.1—239.255.255.254

    E类地址:240.0.0.1—255.255.255.254

    具体更加细致的地址作用,自行百度。

    9.2.1 私有地址

    A类的:10.x.x.x 是私有地址。

    B类的:172.16.0.0—172.31.255.255 是私有地址。

    C类的:全都是。192.0.0.1—223.255.255.254 是私有地址。

    9.2.2 受限广播地址

    受限广播地址是网络号与主机号都为 1 的地址:255.255.255.255

    为了避免这个广播地址往整个互联网里发送广播包,在任何情况下,路由器都会禁止转发目的地址为 255.255.255.255 的广播数据包,要不然会给整个互联网带来网络性灾难。

    9.2.3 直接广播地址

    直接广播地址是主机号全为 1 而得到的地址,广播地址代表本网络内的所有主机,使用该地址可以向网络内的所有主机发送数据。

    A 类地址的广播地址为:XXX.255.255.255

    B 类地址的广播地址为:XXX.XXX.255.255

    C 类地址的广播地址为:XXX.XXX.XXX.255

    9.2.4 多播地址

    多播地址属于分类编址中的 D 类地址,D 类地址只能用作目的地址,而不能作为主机中的源地址。

    多播地址用在一对多的通信。

    9.2.5 环回地址

    127.x.x.x 是保留地址,用作循环测试用(127.0.0.1 为保留地址,一般用于环回地址)

    9.2.6 本地链路地址

    169.254.x.x 是本地链路地址。

    AUTOIP协议使用。

    即是如果 IP 地址是自动获取 IP 地址,而在网络上又没有找到可用的 DHCP 服务器,就会得到其中一个 IP。

    9.2.7 本网络本主机地址

    IP 地址 32bit 全为 0 的地址(0.0.0.0)表示的是本网络本主机,只能做源IP地址。

    在当设备启动时但又不知道自己的 IP 地址情况下常见。

    9.2.8 子网

    子网掩码 & 判断是否在同一子网
    IP 与 子网掩码 做 按位与 ,就可以得出该 IP 的子网网段。
    如:

    • 子网掩码:255.255.255.0
    • IP-1: 192.168.1.2 & 255.255.255.0 = 192.168.1.0
    • IP-2: 192.168.1.123 & 255.255.255.0 = 192.168.1.0
    • IP-3: 192.168.2.123 & 255.255.255.0 = 192.168.2.0
    • 因为 192.168.1.0 = 192.168.1.0,所以IP-1与IP-2处于同一子网。
    • 因为 192.168.1.0 != 192.168.2.0,所以IP-1与IP-3不在同一子网。

    在发数据包时,子网的作用

    • 若源IP和目标IP在同一子网:直接获取目标IP主机的MAC,然后把数据包丢出去。
    • 若源IP和目标IP不在同一子网:获取默认网关的 MAC ,然后把数据包丢给默认网关那边。

    9.2.9 NAT 概念

    在计算机网络中,网络地址转换(Network Address Translation,缩写为 NAT),也叫做网络掩蔽或者 IP 掩蔽(IP masquerading)。

    当前只需要知道NAT就是网络地址转换的作用即可,详细技术细节自行百度。

    9.3 IP数据报

    9.3.1 版本号字段

    占用4 bit。

    这个字段规定了数据报的 IP 协议版本。

    如:

    • IPv4:值为4。
    • IPv6:值为6。

    9.3.2 首部长度字段

    占用4 bit。

    用于记录 IP 首部的数据的长度。

    单位,字。最大可以表示15*4=60字节。

    IP首部的长度默认是20 byte,但是如果有选项字段,就不止20 byte了。

    9.3.3 服务类型(TOS:type of service)字段

    占用8 bit。

    该字段用于描述当前IP数据报急需的服务类型,如:

    • 最小延时;
    • 最大吞吐量;
    • 最高可靠性;
    • 最小费用等。

    路由器在转发数据报时,可以根据该字段的值来为数据报选择最合理的路由路径。

    9.3.4 总长度字段

    占用16 bit。

    是 IP 数据报的总长度(IP首部+数据区)。

    单位,字节。最大能表示65535字节。

    数据报很少有超过1500字节的,因为以太网数据帧的数据最大长度为1500字节。

    如果一个IP数据报过大时,需要进行分片处理。

    9.3.5 标识字段

    标识字段、标志字段和13位偏移字段常在IP数据报分片时使用。

    占用16 bit。

    用于标识IP层发出去的每一份IP数据报,每发送一份,该值+1。

    如果IP数据报被分片,该字段在每个分片的IP数据报上是一致的,表示属于同一个IP数据报。

    在接收端会根据该字段识别同一个IP数据报进行重装。

    9.3.6 标志字段

    占用3 bit。

    第一个bit保留未用。

    第二个bit是不分片标志位

    • 0:则表示 IP 层在必要的时候可以对其进行分片处理。
    • 1:则表示 IP 数据报在发送的过程中不允许进行分片,如果这个 IP 数据报的大小超过链路层能承载的大小,这个 IP 数据报将被丢弃。

    第三个bit是更多分片标志位

    • 0:后续没有更多分片。即是当前分片的IP数据报是最后一个分片。
    • 1:表示后续还有分片。即是当前分片的IP数据报不是最后一个分片。

    9.3.7 分片偏移量字段

    占用13 bit。

    表示当前分片所携带的数据在整个 IP 数据报中的相对偏移位置。

    单位,8字节(2个字)。

    目标主机要接收到从0分片偏移量到最高分片偏移量的所有分片才能进行组装出完整的IP数据报。

    9.3.8 生存时间(Time-To-Live,TTL)字段

    占用8 bit。

    该字段用来确保数据报不会永远在网络中循环。

    IP数据报没经过一台路由器处理,该值-1。

    如果TTL字段下降到0,则路由器会丢弃该数据报,且会返回一个 ICMP 差错报文给源主机。

    9.3.9 协议字段

    占用8 bit。

    表示上层协议类型。即是表示数据区的数据是哪个协议的数据报。如:

    • 6:表示TCP协议。
    • 17:表示UDP协议。
    • 其它值可以自行度娘。

    9.3.10 首部校验和字段

    占用16 bit。

    只针对IP首部做校验,并不关系数据区在传输过程中是否出错,所以对于数据区的校验需要由上层协议负责。

    路由器要对每个收到的 IP 数据报计算其首部检验和,如果数据报首部中携带的检验和与计算得到的检验和不一致,则表示出现错误,路由器一般会丢弃检测出错误的 IP 数据报。

    需要注意的是,由于IP数据报首部的TTL字段每结果应该路由器都会-1,所以IP数据报首部检验和字段每经过一个路由器都要重新计算赋值。

    参考:关于wireshark的Header checksum出问题解决方案:https://www.it610.com/article/1290714377560858624.htm

    检验和计算可能由网络网络驱动,协议驱动,甚至是硬件完成。

    高层校验通常是由协议执行,并将完成后的包转交给硬件。

    比较新的网络硬件可以执行一些高级功能,如IP检验和计算,这被成为checksum offloading。网络驱动不会计算校验和,只是简单将校验和字段留空或填入无效信息,交给硬件计算。

    发送数据时首部校验和计算:二进制反码求和。

    • 把IP数据包的校验和字段置为全0。
    • 将首部中的每 2 个字节当作一个数,依次求和。
    • 把结果取反码。
    • 把得到的结果存入校验和字段中。

    接收数据时,首部校验和验证过程:

    • 首部中的每 2 个字节当作一个数,依次进行求和,包括校验和字段。
    • 检查计算出的校验和的结果是否全为1(反码应为16个0)。
    • 如果等于零,说明被整除,校验和正确。否则,校验和就是错误的,协议栈要抛弃这个数据包。

    为什么计算出的校验和结果全为1?

    因为如果校验依时次求和,不包含校验和字段的话,得出的值就是校验和字段的反码。

    校验和的反码和校验和求和,当然是全1啦。

    9.3.11 二进制反码求和

    IP/ICMP/IGMP/TCP/UDP等协议的校验和算法都是相同的。

    二进制反码求和:(求和再反码,结果一致)

    • 对两个二进制数进行加法运算。
    • 加法规则:0+0=0,0+1=1,1+1=10(进位1加到下一bit)。
    • 若最高两位相加仍然有进位,则在最后的结果中+1即可。
    • 对最终结果取反码。

    相关源码参考LwIP\core\inet_chksum.c中的lwip_standard_chksum()

    这里提取版本3分析:

    • 前期先确保4字节对齐,如果不是4字节对齐,就补到4字节对齐。

    • 后面采用32 bit累加。溢出后,在低位+1。

      • 为什么?:这里读者可能会有个疑问,IP数据包的校验和不是要求16 bit求和的吗?这里为什么能用32 bit求和?
      • 答:起始要求是16 bit,但是实际计算时只要大于16 bit即可,因为到最后,可以把高位折叠加到低位。
      • 例子:按32bit累加,溢出就在低位+1。其实就是两组两个(高、低)16 bit对应累加,低16 bit累加的进位给高16 bit里加回1了。而高16 bit累加的进位在底16 bit里加回1了(手动)。这样,累加到最后剩下32bit。把高16bit和低16bit进行累加,进位再加1即可快速得到16bit的校验和。
    • 数据后部分可能不是8字节对齐,所以剩余的字节也需要16bit校验和处理。

    思路图:

    由于目的是16 bit的校验和。其实可以看成两组2个8bit对应相加,低8bit组进位给高8bit组,高8bit组进位给低8bit组。所以相加值是对应高、低8bit相互独立的。

    而下面函数就是利用这个特性,如果首字节为奇地址,先单独取出来放到t的高地址,因为后续的统计字节顺序是返的。等待全部统计完毕后,再把两个字节顺序调换即可。

    如果是偶地址开始,那符合校验和规则,最后不需要调换字节顺序。

    9.3.12 源IP字段

    占用32 bit。

    为源主机的IP地址。

    9.3.13 目标IP字段

    占用32 bit。

    为目标主机的IP地址。

    9.3.14 选项字段

    0到40字节。

    对于IP数据报首部来说,其大小必须为4字节的整数倍。

    如果选项字段长度不为4的倍数,则需要用0进行填充。

    在 LwIP 中只识别选项字段,不会处理选项字段的内容。

    该字段在IPv6报文中已经被移除了。

    9.3.15 数据区字段

    IP 数据报的最后的一个字段,装载着当前IP数据报的数据,是上层协议的数据报。

    9.3.16 对应wireshark包分析

    9.4 IP首部数据结构

    注意:网络字节序是大端的。

    ipv4的IP首部数据结构:对应IP首部报文图。

    由于IP首部部分字段的操作涉及到bit,所以lwip也封装出对应的宏操作。

    9.5 网卡路由

    9.5.1 路由网卡匹配

    从官方源码看,匹配网卡的流程:

    ip4_route_src()

    • 先函数LWIP_HOOK_IP4_ROUTE_SRC()匹配。
    • 然后到ip4_route()基函数匹配。

    9.5.2 路由网卡匹配基函数

    ip4_route()

    • 多播IP优先匹配多播专用网卡ip4_default_multicast_netif

    • 遍历网卡:

      • 有效网卡先匹配子网;
      • 子网匹配失败就匹配网关,网卡不能有广播能力。
    • 环回IP:如果开启了各个网卡的环回功能,且没有创建环回网卡:

      • 说明:因为创建了环回网卡,在遍历链表时,就会把环回IP 127.x.x.x都会匹配到环回网卡。
      • 对于环回IP,优先匹配默认网卡netif_default
      • 再遍历网卡,第一个协议栈有效的网卡即可。
    • 钩子匹配:(由用户实现)

      • LWIP_HOOK_IP4_ROUTE_SRC(src, dest);
      • LWIP_HOOK_IP4_ROUTE(dest);
    • 以上都没有匹配成功,则使用netif_default,必须条件:

      • 默认网卡netif_default存在;
      • 默认网卡协议栈有效;
      • 默认网卡数据链路有效;
      • 默认网卡IP有效。
      • 匹配的目的IP不能为环回IP(因为如果是环回IP,前面已经匹配过了,除非没有开启该功能)

    9.5.3 路由网卡匹配支持源IP和目的IP网卡匹配的接口

    匹配网卡,一般是按照目的IP来匹配,但是可以通过LWIP_HOOK_IP4_ROUTE_SRC()钩子宏函数来实现源IP地址和目的IP地址匹配。

    ip4_route_src()

    • 如果源IP地址不为空,则会先传入LWIP_HOOK_IP4_ROUTE_SRC()钩子函数来匹配网卡。
    • 钩子函数匹配失败或者源IP地址为空,则由ip4_route()只根据目的IP地址匹配。

    9.5.4 路由网卡匹配的钩子函数

    通过分析前面的基函数和接口函数,可发现其实现是支持宏钩子函数,即是支持用户自己实现网卡匹配的逻辑的。

    有两个宏钩子:

    • LWIP_HOOK_IP4_ROUTE_SRC(src, dest):钩子入口参数有源IP和目的IP。
    • LWIP_HOOK_IP4_ROUTE(dest):钩子入口参数只有目的IP。

    9.5.5 收包网卡匹配

    当IP层收到一个IP报文时,也要收包网卡匹配。

    而且IP包的输入和输出的网卡匹配是不一样的,比如普通的IP单播包,输出时,只需要找到目的IP和网卡处于同一个子网或者是该网卡的网关即可匹配。而输入时,需要明确目的IP就是该网卡IP。

    收包的网卡匹配除了ip4_input_accept()这个主要函数外,还有很多独立的匹配条件,具体看IP层输入章节。

    这里只分析ip4_input_accept()

    • 在调用该API前,应该先配置全局IP数据结构成员值:struct ip_globals ip_data;

    • 需要被匹配的网卡必须在协议栈方向使能了,且IP地址为有效地址。

      • 单播包,目的地址和网卡地址一致,网卡匹配成功。
      • 广播包,IP地址bit全1,必定是广播地址。如果网卡就被广播能力,且IP地址的主机号bit全1,也是子网广播地址。都匹配成功。
      • 环回,没有环回网卡,且目的IP地址为环回IP IPADDR_LOOPBACK。匹配成功。

    9.6 IP层数据流图

    9.7 IP层输出

    ipv4。

    当上层需要发送数据时,会先将自己的数据包组装在一个pbuf中。并将payload指针指向对应协议首部。

    然后调用ip_output()发送数据,需要给ip_output()函数提供源IP、目的IP、协议类型、TTL等重要信息让其组IP包。

    该函数直接或者间接调用ip4_route_src()根据目的IP选出一个匹配的网卡作为本次IP数据包传输网卡。

    选出后调用ip4_output_if()进行IP数据包组包,并调用netif->output()发送出去。或者调用netif_loop_output()环回到本网卡。

    9.7.1 发送数据报

    上层调用ip_output()把数据转交给IP层处理,lwip支持ipv4和ipv6,这里默认分析ipv4,因为ipv6也是一大块,后面有时间再完整分析下。

    9.7.2 ip层前期处理:ip4_output()

    ipv4发包:

    • 检查pbuf的引用ref是否为1。为1才能说明当前pbuf没有被其它地方引用,因为IP层处理可能会改变这个pbuf的部分指针值,如payload。
    • 调用ip4_route_src()匹配网卡。
    • 调用ip4_output_if()把数据包传入IP层处理。

    9.7.3 发包前的网卡匹配

    IP层收到上层的数据包后,需要匹配到网络接口,才能组IP包发出去。

    这里调用ip4_route_src()进行网卡匹配。具体分析参考前面。

    9.7.4 组建、发送IP包

    注意几个函数的区别:

    • ip4_output_if():这个函数封装了底层IP层组包、发送的实现函数。
    • ip4_output_if_src():这个函数就是IP层组包、发送的实现函数。不支持IP首部的选项字段。
    • ip4_output_if_opt():这也是IP层组包、发送的实现函数,会用选中的网卡IP地址覆盖传入的源IP地址。支持IP首部的选项字段。
    • ip4_output_if_opt_src():这也是IP层组包、发送的实现函数,不会用选中的网卡IP地址覆盖传入的源IP地址。支持IP首部的选项字段。

    相关宏:

    IP_OPTIONS_SEND

    • IP首报文首部选项字段宏开关。
    • 如果开启了该宏,则会调用上述带_opt字样的函数,操作IP首部报文的选项字段。

    LWIP_IP_HDRINCL:缺省为NULL

    • 如果把这个宏当目的IP地址传入IP层组包、发送的相关函数ip4_output_if()或其底层函数时,表示当前这个pbuf已经组好IP首部了。
    • 一般用于TCP重传。

    LWIP_CHECKSUM_CTRL_PER_NETIF

    • 允许每个网卡配置checksum功能。

    相关变量:

    • ip_id:IP首部标识,全局值。

    我们就分析ip4_output_if_opt_src()函数,比较全。

    ip4_output_if_opt_src()

    • 先处理选项字段,在处理IP首部其它字段。
    • struct pbuf *p:传输层协议需要发送的数据包pbuf,payload指针已指向传输层协议首部。
    • const ip4_addr_t *src:源IP地址。
    • const ip4_addr_t *dest:目的IP地址。
    • u8_t ttl:IP首部TTL字段。
    • u8_t tos:IP首部TOS字段。
    • u8_t proto:IP首部上层协议字段。
    • struct netif *netif:发送IP数据报的网卡。
    • void *ip_options:IP首部选项字段值。
    • u16_t optlen:IP首部选项字段的长度。

    概要内容:

    • 通过目的IP判断当前pbuf是否已经组好IP报文首部。如果组好了,就不需要继续重组了。如tcp重传。

    • 如果传入的pbuf报文还没组好IP报文首部,则根据传入的相关数据和IP报文内容进行组包。

    • 组好包后检查目的IP是否是环回IP(如环回IP、当前网卡的IP),如果是就调用netif_loop_output()进行环回处理。

    • 如果不是环回数据包,就需要发到数据链路。

      • IP分片:如果IP报文总长大于网卡MTU,则需要调用ip4_frag()进行IP分片。
      • 如果不需要IP分片,直接调用netif->output()将IP报文发出。

    9.7.5 IP数据报分片

    注意:lwip分片偏移不支持IP首部带选项字段的。

    从IP报文首部就可知,有分片概念。

    不是每个底层网卡都能承载每个 IP 数据报长度的报文。如:

    • 以太网帧最大能承载 1500 个字节的数据。
    • 某些广域网链路的帧可承载不超过 576 字节的数据。

    一个链路层帧能承载的最大数据量叫做最大传送单元(Maximum TransmissionUnit,MTU)。

    IP 数据报的分片偏移量是用 8 的整数倍记录的,所以每个数据报中的分片数据大小也必须是 8 的整数倍。

    IP数据报分片主要关注IP首部的标识字段、标志字段和分片偏移量字段。具体往前看。

    参考图:

    相关源码实现在ip4_frag.c

    相关宏:

    • LWIP_NETIF_TX_SINGLE_PBUF:分片是否支持新建一整个pbuf处理。

      • 1:分片时,直接申请各个IP分片包的pbuf即可(含IP首部+数据区)。
      • 0:分片时,申请各个分片的管理区,MEMP_FRAG_PBUF类型。其数据结构为pbuf_custom_ref。该数据结构包含本次IP分片包的原IP报文pbuf地址,释放引用的api,指向分片IP报文数据区的pbuf。然后将这个分片IP首部的pbuf和这个IP报文数据区的pbuf拼接起来即可。组成新的分片IP报文。

    相关数据结构:

    pbuf_custom数据结构:

    pbuf_custom_ref数据结构:

    相关数据结构图:

    • 没开启LWIP_NETIF_TX_SINGLE_PBUF宏的IP分片报文数据结构:

    • 开启LWIP_NETIF_TX_SINGLE_PBUF宏的分片IP报文数据结构(按简单的画):

    ip4_frag()

    • 与分片重组ip4_reass()这个API对应。

    • 需要注意的是:需要检查本次分片处理传入的原IP报文struct pbuf *p是否也是一个分片包。如果是,那么它可能不是顶层原IP报文分片的最后一片,这样的话,在本次分片处理最后一片的分片IP报文首部标志字段的还有更多分片标志位不能置位0。因为在顶层未分片IP报文角度看来,这还不是真正意义上的最后一片。

    • 下面函数分析时,按LWIP_NETIF_TX_SINGLE_PBUF宏分支分析更加助于理解。

    • 处于非LWIP_NETIF_TX_SINGLE_PBUF宏分支:如果读者需要分析,然后看不懂这个分支,可以看下我的笔记:

      • 先申请一个保存分片IP首部的pbuf:rambuf
      • 然后再申请一个pbuf_custom_ref数据结构的伪pbuf:pcr
      • 然后把未分片的IP报文的pbuf对应分片的数据区地址给到pcr->pc->pbuf->payload,共享数据区内存嘛。
      • 然后把分片IP首部的pbuf和分片IP的数据pbuf拼接起来:pbuf_cat(rambuf, pcr->pc->pbuf->payload);,这样就组成了分片的IP报文了。

    9.8 IP层输入

    9.8.1 接收数据报

    翻看前面网络接口层章节应该知道底层接收数据流,在网卡驱动收到数据,调用netif->input()把对应API和pbuf转发到lwip内核线程执行。

    以以太网为例,网卡接收数据线程通过把数据给到netif->input(),该函数内部外包以太网链路层ethernet_input()到lwip内核线程去跑,如果是ARP协议的以太网帧,则传到ARP模块etharp_input()处理。如果是IPv4的以太网帧,则传到ip4_input()处理。

    相关宏:

    LWIP_HOOK_IP4_INPUT(p, inp):IPV4接收数据包钩子函数。

    • 接收数据时检查了IP报文的版本为IPV4,就会把这个IP报文传给这个钩子函数。
    • 如果钩子放回true,则表示由钩子处理,外部丢弃。

    LWIP_IP_ACCEPT_UDP_PORT(dst_port):在netif关闭时接受私人广播通信用。

    ip4_input()

    • IP报文校验。

    • 传入钩子LWIP_HOOK_IP4_INPUT(p, inp)

    • 匹配目的网卡。就是判断当前IP报文是不是给我的。

      • 多播包:

        • 开启了IGMP:当前网卡在当前IP报文目的IP组播内,匹配成功。
        • 没有开启IGMP:当前网卡有效即可匹配成功。
      • 广播包和单播包:都调用ip4_input_accept()API匹配。前面有分析。

        • 先匹配收到该IP报文的网卡;
        • 再遍历网卡链表。注意:如果没有环回功能或者有环回网卡,且IP报文目的IP地址是环回字段的IP地址,不能遍历网卡链表。因为环回,需要用环回接口,在前面匹配网卡就应该配上了,不会跑到这里。
      • 开启了DHCP,且面前没有匹配上网卡:

        • 通过IP报文可以判断当前IP报文是否是UDP协议。
        • 通过UDP协议解析目的端口是否是DHCP客户端端口LWIP_IANA_PORT_DHCP_CLIENT(68)。
        • 是DHCP报文,直接收到当前网卡。不需要做目的IP校验。
    • 没有匹配到网卡,即是IP报文不是给我们的,但是开启了IP_FORWARD转发功能。如果目的IP不是广播地址,可以调用ip4_forward()进行转发。

    • 匹配到网卡,处理IP报文,如果IP报文被分片了,需要调用ip4_reass()重组。

    • 收到完整的IP报文后,根据IP报文的上层协议类型字段,给到对应的协议模块。lwip支持:

      • IP_PROTO_UDP:UDP协议。udp_input(p, inp);
      • IP_PROTO_TCP:TCP协议。tcp_input(p, inp);
      • IP_PROTO_ICMP:ICMP协议。icmp_input(p, inp);
      • IP_PROTO_IGMP:IGMP协议。igmp_input(p, inp, ip4_current_dest_addr());
      • LWIP_RAW:原报文上报。

    9.8.2 IP数据报转发

    如果数据包给到我们网卡数据链路层了,但是目的IP不是给我们网卡IP层的,那可能是想通过我们网卡转发该包。

    相关宏:

    • IP_FORWARD_ALLOW_TX_ON_RX_NETIF

      • 允许ip_forward()在接收到数据包的netif上发送数据包。这应该只用于无线网络。
      • 为1时,请确保netif驱动程序正确标记传入的链路层广播/组播数据包等使用相应的pbuf标志!

    调用ip4_forward()将数据包转发:

    • 先判断IP包是否能转发。主要调用ip4_canforward()判断。

      • 钩子函数:LWIP_HOOK_IP4_CANFORWARD(src, dest)。现有用户实现钩子裁定能否转发当前IP包。
      • 链路层的广播包。不能转发。
      • 链路层的多播包。不能转发。
      • 目的IP为255.x.x.x的IP包。不能转发。
      • 目的IP为127.x.x.x(环回)的IP包。不能转发。
      • 目的IP为169.254.x.x(本地链路IP)的IP包,不能转发。
    • 匹配新网卡。调用ip4_route_src(src, dest)路由匹配网卡,进行转发该IP包。

    • 检查路由匹配成功的网卡。

      • 一般情况下,是不能转发回原链路的。
      • 如果需要转发回原链路,一般只能用于无线网卡。
    • IP报文TTL字段值减1。如果TTL值为0,则,丢弃该IP包,并通过ICMP icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)告知源端路由不可达。(如果当前报文也是ICMP报文,则不用回复ICMP路由不可达)

    • TTL字段值更新后,IP报文的首部校验和字段也要更新。

      • 技巧:由于IP报文首部只是TTL字段减1,所以首部校验和字段只需要加1即可。
    • 使用新路由匹配的网卡把当前IP包转发出去。

      • 需要分片:调用ip4_frag()转发出去。
      • 不需要分片:调用网卡IP接口netif->output()转发出去。

    ip4_canforward()

    ip4_forward()

    9.8.3 IP数据报重组

    如果IP层收到一个IP报文,目的IP是给我们网卡IP层的,且检查当前IP包的首部是是一个分片包,则需要IP报文重组。

    重组IP报文的源码实现比较复杂,所以需要耐心分析。

    重装IP报文比分片IP报文要难,就是因为,重装IP报文的每个IP包到达的时间不是按顺序的,会出现后发的IP分片包比先发的IP分片包早到达(网络路由问题),这需要我们按序重组好。

    注意:lwip当前不支持IP首部带选项字段的IP报文进行分片和重组。

    9.8.3.1 相关数据结构

    代码的数据结构是非常重要的,通过对数据结构的逻辑处理能封装出各种功能的API。

    一个完整IP报文由多个IP分片包组成。

    这个IP报文用struct ip_reassdata数据结构管理;

    而每个IP分片包用struct ip_reass_helper数据结构管理。

    • 为了节省空间,这个数据结构和IP包pbuf的空间共用。把收到的分片pbuf的IP包首部重置为这个数据结构。因为最终的IP报文只需要一个IP首部即可,每个分片的IP首部空间可以利用起来。

    lwip协议栈用全局变量struct ip_reassdata *reassdatagrams;管理整个协议栈的各个在重组中的IP报文,为单向链表。

    重组中的IP报文数据结构struct ip_reassdata

    各个IP分片包的数据结构struct ip_reass_helper

    9.8.3.2 相关宏

    IP_REASS_MAXAGE:默认15。为每个重组IP报文有效期。超时还没重组好就需要删除重组IP报文。

    IP_REASS_MAX_PBUFS:系统重组IP报文所有pbuf节点上限值。

    IP_REASS_FREE_OLDEST:若开启,遇到系统重组IP报文所有pbuf节点达到系统上限值IP_REASS_MAX_PBUFS后,允许释放旧的重组IP报文所有IP分片。

    9.8.3.3 相关函数

    ip4_reass(struct pbuf *p)

    • IPv4分片报文重组函数。
    • 供给lwip内核接收IP包时使用的API。
    • 不支持待选项字段的IP报文进行重组。

    ip_reass_enqueue_new_datagram(struct ip_hdr *fraghdr, int clen):新建一个重组IP报文条目并插入reassdatagrams链表中。

    ip_reass_dequeue_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev):释放一个重组IP报文条目。

    ip_reass_free_complete_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev):释放一个重组IP报文条目及其所有pbuf。

    ip_reass_remove_oldest_datagram(struct ip_hdr *fraghdr, int pbufs_needed):删除老的重组IP报文。

    ip_reass_chain_frag_into_datagram_and_validate(struct ip_reassdata *ipr, struct pbuf *new_p, int is_last):插入IP分片到对应重组IP报文。

    9.8.3.4 ip4_reass()

    源码参考:ip4_frag.c

    IP报文重组调用ip4_reass()先分析该函数的总体框架,后面再分析各个函数的细节:

    • 检查IP分片包。

      • 收到一个IP分片后,检查IP分片的合法性。IP首部长度是否符合要求。目前LWIP不支持带选项的IP包分片和重组。
      • IP首部长度不能大于IP包长度。
      • 确保重组中的IP报文所有pbuf节点不能超过系统上限值IP_REASS_MAX_PBUFS。如果预判超出了,需要调用ip_reass_remove_oldest_datagram()删除最老的重组中的IP报文,直至够空间记录当前IP分片包的pbuf节点数为止。
    • 记录IP分片包到reassdatagrams链表中对应IP报文。

      • 找到当前IP分片的重组IP报文:检索reassdatagrams链表,是否有当前IP分片的IP报文。如果没有,需要调用ip_reass_enqueue_new_datagram()创建一个新的重组IP报文数据结构,并插入到reassdatagrams链表中。如果有,还需要检查当前IP分片是否是IP报文中的第一个IP分片包,如果是,需要把这个IP分片包的IP首部更新到重组IP报文数据结构的IP报文首部字段ip_reassdata->iphdr

        • 匹配重组IP报文条件:源IP、目的IP、IP标识这三个字段一致即可。
      • 预判IP报文是否溢出:如果当前IP分片包是最后一个,可以通过IP分片偏移量加上当前IP分片包的长度可以计算出完整的IP报文长度,如果这个长度溢出IP报文的长度字段,则丢弃,并删除重组中的IP报文。

      • 当前IP分片插入重组IP报文的IP分片链表中。调用ip_reass_chain_frag_into_datagram_and_validate()将其插入。返回值由以下三个:

        • IP_REASS_VALIDATE_TELEGRAM_FINISHED:所有IP分片已经接收完毕,可以完成IP报文重组工作。
        • IP_REASS_VALIDATE_PBUF_QUEUED:当前IP分片正常插入,但是当前IP报文还没有接收完所有IP分片。
        • IP_REASS_VALIDATE_PBUF_DROPPED:当前IP分片插入失败,即是丢弃当前IP分片。
      • 更新reassdatagrams链表中所有pbuf节点的数量值ip_reass_pbufcount(全局)。因为系统设置有上限IP_REASS_MAX_PBUFS,所以要动态记录。

    • 当前IP分片是否为最后一个分片。

      • 如果是最后一个分片,则更新重组IP报文数据结构中的长度值为总长度值ip_reassdata->datagram_len,标记重组IP报文收到最后一个IP分片ip_reassdata->flags |= IP_REASS_FLAG_LASTFRAG
    • IP报文的所有IP分片包是否已经接收完毕。

      • 如果所有IP分片都收到了,则可进行重组。
    • 重组完整IP报文。遍历所有IP分片,并合并。节省空间。

      • 统计IP报文的总长度。首部+数据=IP_HLEN+ip_reassdata->datagram_len
      • 把重组IP报文数据结构中的IP首部字段拷贝到第一个分片的IP首部作为完整IP报文的首部。
      • 遍历重组IP报文中所有IP分片,将其合并,除了首个IP分片的IP首部作为完整IP报文的首部外,其它IP分片的IP首部都移除。
      • 至此,完整的IP报文pbuf重组完成。

    9.8.3.5 ip_reass_enqueue_new_datagram()

    ip_reass_enqueue_new_datagram()

    • 申请重组IP报文节点ip_reassdata数据结构的空间。
    • 如果MEMP_REASSDATA内存池空间不足,则可以释放老的重组IP报文节点。
    • 初始化该结构体。
    • 有效期配置为IP_REASS_MAXAGE
    • 插入reassdatagrams链表。
    • 保存IP分片包首部。

    9.8.3.6 ip_reass_dequeue_datagram()

    ip_reass_dequeue_datagram()

    • 释放重组IP报文条目资源。
    • 在过期还没重组好,或者已经重组好,或者需要为新的重组IP报文腾空间,都需要把不需要的重组IP报文删除,并从链表中移除。
    • ipr:需要删除的重组IP报文节点。
    • prev:被删除节点的前一个节点。由调用者提供。“奇怪的设计”

    9.8.3.7 ip_reass_free_complete_datagram()

    ip_reass_free_complete_datagram()

    • 释放重组IP报文节点及其所有pbuf。
    • ICMP:如果收到了第一个IP分片,在重组删除时,需要返回一个ICMP超时。
    • 释放重组IP报文的所有pbuf。
    • 释放重组IP报文。

    9.8.3.8 ip_reass_remove_oldest_datagram()

    ip_reass_remove_oldest_datagram()

    • 删除老的重组IP报文。
    • IP_REASS_FREE_OLDEST宏控制。
    • fraghdr:当前IP分片。传入该参数是因为防止在遍历链表删除老的IP重组IP报文时,跳过这个重组IP报文,因为我们的目的就是为这个重组IP报文新来的IP分片腾空间。
    • pbufs_needed:需要腾出的pbuf数。
    • 逻辑简单:遍历、找到最老的非当前重组IP报文的重组IP报文节点、删除。一直遍历删除到够空闲pbuf数或者没有其它重组IP报文节点为止。

    9.8.3.9 ip_reass_chain_frag_into_datagram_and_validate()

    ip_reass_chain_frag_into_datagram_and_validate()

    • 检查和插入一个分片到重组数据报中。

    • ipr:重组IP数据报。

    • new_p:新的分片。

    • is_last:是否是最后一个分片。

    • 返回:

      • IP_REASS_VALIDATE_TELEGRAM_FINISHED:当前重组IP数据报已经收到所有分片。
      • IP_REASS_VALIDATE_PBUF_QUEUED:分片成功插入重组IP数据报中,但是该重组IP数据报还没有接收到所有IP分片。
      • IP_REASS_VALIDATE_PBUF_DROPPED:插入分片失败。
    • 获取分片IP首部信息统计到重组IP数据报管理中。

    • 插入分片到重组IP数据报的分片链表中。

    • 如果最后一个IP分片收到了,就检查下所有分片是否都收到。

    9.8.3.10 重组IP数据报的超时机制

    每个重组的IP数据报都有生命周期,超时都还没接收完所有IP分片包,则需要放弃等待剩余分片,并释放该重组IP数据报所有资源。

    决定删除重组的IP数据报时,需要返回ICMP超时到网络中告知对端放弃本次IP报文接收。

    节拍由IP_TMR_INTERVAL决定,默认1000,即是1秒跑一次。

    每个重组IP报文最大时间为IP_REASS_MAXAGE,默认15。即是系统收到第一个得到的分片IP开始计时,在15秒内没有接收完所有IP报文,便要放弃本次重组。

    ip_reass_tmr()

    • 概述的目的就是遍历重组IP报文链表,检查每个正在重组的IP报文有效期,过期的删除,未过期的减少有效期。

    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16859723.html
  • 关于博主: 嵌入式从业者。RTOS、Linux lwip mbedtls...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    html5期末大作业:基于html+css+javascript+jquery+bootstarp响应式图书电商HTML模板网上书店(25页)
    基于Scrapyd与Gerapy部署scrapy爬虫方案【可用于分布式爬虫部署】
    开放式运动耳机排行榜哪个好用,排行靠前的五款运动耳机分享
    【webrtc】rtp包组帧
    从OC角度思考OKR的底层逻辑
    ImageMol
    如何使用rclone将腾讯云COS桶中的数据同步到华为云OBS
    Gitlab之间数据迁移的5种方式
    Halcon 多相机统一坐标系
    音视频会议需要哪些设备配置
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16859723.html