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

2022-12-28,,,,

目录
前言
9.1 IP协议简述
9.2 IP地址分类
9.2.1 私有地址
9.2.2 受限广播地址
9.2.3 直接广播地址
9.2.4 多播地址
9.2.5 环回地址
9.2.6 本地链路地址
9.2.7 本网络本主机地址
9.2.8 子网
9.2.9 NAT 概念
9.3 IP数据报
9.3.1 版本号字段
9.3.2 首部长度字段
9.3.3 服务类型(TOS:type of service)字段
9.3.4 总长度字段
9.3.5 标识字段
9.3.6 标志字段
9.3.7 分片偏移量字段
9.3.8 生存时间(Time-To-Live,TTL)字段
9.3.9 协议字段
9.3.10 首部校验和字段
9.3.11 二进制反码求和
9.3.12 源IP字段
9.3.13 目标IP字段
9.3.14 选项字段
9.3.15 数据区字段
9.3.16 对应wireshark包分析
9.4 IP首部数据结构
9.5 网卡路由
9.5.1 路由网卡匹配
9.5.2 路由网卡匹配基函数
9.5.3 路由网卡匹配支持源IP和目的IP网卡匹配的接口
9.5.4 路由网卡匹配的钩子函数
9.5.5 收包网卡匹配
9.6 IP层数据流图
9.7 IP层输出
9.7.1 发送数据报
9.7.2 ip层前期处理:ip4_output()
9.7.3 发包前的网卡匹配
9.7.4 组建、发送IP包
9.7.5 IP数据报分片
9.8 IP层输入
9.8.1 接收数据报
9.8.2 IP数据报转发
9.8.3 IP数据报重组
9.8.3.1 相关数据结构
9.8.3.2 相关宏
9.8.3.3 相关函数
9.8.3.4 ip4_reass()
9.8.3.5 ip_reass_enqueue_new_datagram()
9.8.3.6 ip_reass_dequeue_datagram()
9.8.3.7 ip_reass_free_complete_datagram()
9.8.3.8 ip_reass_remove_oldest_datagram()
9.8.3.9 ip_reass_chain_frag_into_datagram_and_validate()
9.8.3.10 重组IP数据报的超时机制

前言

默认主讲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的高地址,因为后续的统计字节顺序是返的。等待全部统计完毕后,再把两个字节顺序调换即可。

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

#if (LWIP_CHKSUM_ALGORITHM == 3) /* Alternative version #3 */
/**
* An optimized checksum routine. Basically, it uses loop-unrolling on
* the checksum loop, treating the head and tail bytes specially, whereas
* the inner loop acts on 8 bytes at a time.
*
* @arg start of buffer to be checksummed. May be an odd byte address.
* @len number of bytes in the buffer to be checksummed.
* @return host order (!) lwip checksum (non-inverted Internet sum)
*
* by Curt McDowell, Broadcom Corp. December 8th, 2005
*/
u16_t
lwip_standard_chksum(const void *dataptr, int len)
{
const u8_t *pb = (const u8_t *)dataptr; /* 取数据的地址 */
const u16_t *ps;
u16_t t = 0;
const u32_t *pl;
u32_t sum = 0, tmp; int odd = ((mem_ptr_t)pb & 1); /* 判断是否为奇地址 */ if (odd && len > 0) { /* 如果不是2直接对齐 */
/* 缓存奇地址上的字节,存于 t 的高位。数据地址偏移为偶,2字节对齐。 */
/* 存到高位是为了和后面字节序保持一致,方便在最后一次性更正。 */
((u8_t *)&t)[1] = *pb++;
len--; /* 字节数-1 */
} /* 2字节对齐的数据起始地址 */
ps = (const u16_t *)(const void *)pb; if (((mem_ptr_t)ps & 3) && len > 1) {/* 如果不是4字节对齐 */
/* 把多出来的两字节保存到sum */
sum += *ps++;
len -= 2;
} /* 4字节对齐的数据起始地址 */
pl = (const u32_t *)(const void *)ps; while (len > 7) {
tmp = sum + *pl++; /* ping */
if (tmp < sum) {
tmp++; /* 溢出,手动+1 */
} sum = tmp + *pl++; /* pong */
if (sum < tmp) {
sum++; /* 溢出,手动+1 */
} len -= 8;
} /* 折叠高、低16bit */
sum = FOLD_U32T(sum); /* 处理剩余的字节 */
ps = (const u16_t *)pl; /* 2字节处理 */
while (len > 1) {
sum += *ps++;
len -= 2;
} /* 剩余单字节 */
if (len > 0) { /* 补到前面t的低位 */
((u8_t *)&t)[0] = *(const u8_t *)ps;
} sum += t; /* 把t也一起16bit校验和 */ /* 两次折叠高、低16bit */
sum = FOLD_U32T(sum);
sum = FOLD_U32T(sum); if (odd) { /* 因为前面是从第二个字节和第三个字节开始进行统计的,字节序反了,这里在结果更正。 */
sum = SWAP_BYTES_IN_WORD(sum);
} return (u16_t)sum; /* 返回校验和 */
}
#endif

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首部报文图。

/* The IPv4 header */
struct ip_hdr {
/* version / header length */
PACK_STRUCT_FLD_8(u8_t _v_hl); /* 版本号字段(4)+首部长度字段(4)单位字 */
/* type of service */
PACK_STRUCT_FLD_8(u8_t _tos); /* 服务类型TOS字段(8) */
/* total length */
PACK_STRUCT_FIELD(u16_t _len); /* 总长度字段(16)单位字节 */
/* identification */
PACK_STRUCT_FIELD(u16_t _id); /* 标识字段字段(16) */
/* fragment offset field */
PACK_STRUCT_FIELD(u16_t _offset); /* 标志字段(3)+分片偏移量字段(13)单位两字 */
#define IP_RF 0x8000U /* 标志字段第一位:字段保留 */
#define IP_DF 0x4000U /* 标志字段第二位:不发片掩码 */
#define IP_MF 0x2000U /* 标志字段第三位:更多分片掩码 */
#define IP_OFFMASK 0x1fffU /* 分片偏移量字段的掩码 */
/* time to live */
PACK_STRUCT_FLD_8(u8_t _ttl); /* TTL字段(8) */
/* protocol*/
PACK_STRUCT_FLD_8(u8_t _proto); /* 上层协议类型字段(8) */
/* checksum */
PACK_STRUCT_FIELD(u16_t _chksum); /* 首部校验和字段(16) */
/* source and destination IP addresses */
PACK_STRUCT_FLD_S(ip4_addr_p_t src); /* 源IP字段(32) */
PACK_STRUCT_FLD_S(ip4_addr_p_t dest); /* 目的IP字段(32) */
} PACK_STRUCT_STRUCT;

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

/* 均为网络字节序 */
/* Macros to get struct ip_hdr fields: */
#define IPH_V(hdr) ((hdr)->_v_hl >> 4) /* 获取版本号 */
#define IPH_HL(hdr) ((hdr)->_v_hl & 0x0f) /* 获取首部长度字段值 */
#define IPH_HL_BYTES(hdr) ((u8_t)(IPH_HL(hdr) * 4)) /* 获取首部长度,单位字节 */
#define IPH_TOS(hdr) ((hdr)->_tos) /* 获取服务类型TOS */
#define IPH_LEN(hdr) ((hdr)->_len) /* 获取IP数据报总长度 */
#define IPH_ID(hdr) ((hdr)->_id) /* 获取标识字段 */
#define IPH_OFFSET(hdr) ((hdr)->_offset) /* 获取标志字段+分片偏移量字段 */
#define IPH_OFFSET_BYTES(hdr) ((u16_t)((lwip_ntohs(IPH_OFFSET(hdr)) & IP_OFFMASK) * 8U)) /* 获取分片偏移量,单位字节 */
#define IPH_TTL(hdr) ((hdr)->_ttl) /* 获取TTL */
#define IPH_PROTO(hdr) ((hdr)->_proto) /* 获取协议类型 */
#define IPH_CHKSUM(hdr) ((hdr)->_chksum) /* 获取首部校验和字段 */ /* Macros to set struct ip_hdr fields: */
#define IPH_VHL_SET(hdr, v, hl) (hdr)->_v_hl = (u8_t)((((v) << 4) | (hl))) /* 设置版本号 */
#define IPH_TOS_SET(hdr, tos) (hdr)->_tos = (tos) /* 设置服务类型TOS */
#define IPH_LEN_SET(hdr, len) (hdr)->_len = (len) /* 设置总长度字段值 */
#define IPH_ID_SET(hdr, id) (hdr)->_id = (id) /* 设置标识 */
#define IPH_OFFSET_SET(hdr, off) (hdr)->_offset = (off) /* 设置标志字段+分片偏移量字段 */
#define IPH_TTL_SET(hdr, ttl) (hdr)->_ttl = (u8_t)(ttl) /* 设置TTL */
#define IPH_PROTO_SET(hdr, proto) (hdr)->_proto = (u8_t)(proto) /* 设置协议类型 */
#define IPH_CHKSUM_SET(hdr, chksum) (hdr)->_chksum = (chksum) /* 设置首部校验和 */

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,前面已经匹配过了,除非没有开启该功能)

/**
* Finds the appropriate network interface for a given IP address. It
* searches the list of network interfaces linearly. A match is found
* if the masked IP address of the network interface equals the masked
* IP address given to the function.
*
* @param dest the destination IP address for which to find the route
* @return the netif on which to send to reach dest
*/
struct netif *
ip4_route(const ip4_addr_t *dest)
{
#if !LWIP_SINGLE_NETIF
struct netif *netif; /* 确保在tcpip内核锁内 */
LWIP_ASSERT_CORE_LOCKED(); #if LWIP_MULTICAST_TX_OPTIONS /* 开启了组播TX功能 */
/* 如果目的IP是一个组播地址,且协议栈内创建了组播专用网卡,则选用组播专用网卡接口 */
if (ip4_addr_ismulticast(dest) && ip4_default_multicast_netif) {
return ip4_default_multicast_netif;
}
#endif /* LWIP_MULTICAST_TX_OPTIONS */ /* bug #54569: in case LWIP_SINGLE_NETIF=1 and LWIP_DEBUGF() disabled, the following loop is optimized away */
LWIP_UNUSED_ARG(dest); /* 遍历网卡链表 */
NETIF_FOREACH(netif) {
/* 网卡协议栈有效,网卡链路层也有效,网卡的IP地址不为全0 */
if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) {
/* 匹配传入的目的IP和网卡是否处于同一个子网 */
if (ip4_addr_net_eq(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) {
/* 匹配成功。返回netif,以转发IP数据包 */
return netif;
}
/* 当前网卡子网匹配不成功 */
/* 那就匹配下网关,匹配规则:网卡没有广播功能,目的IP和网关IP一致。也算匹配成功,因为上层的目的是到网关。当前网卡能把数据传达到网卡。 */
if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_eq(dest, netif_ip4_gw(netif))) {
/* 匹配成功。返回netif,以转发IP数据包 */
return netif;
}
}
} /* 到这,遍历网卡,匹配失败 */ /* 开启了环回功能,但是没有创建环回网卡。(因为如果创建了环回网卡,在遍历网卡链表时就已经处理过了,这里不需要再处理) */
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF
/* 如果目的IP是一个环回IP地址,则优先返回默认网卡,否则返回网卡链表中第一个协议栈使能了的网卡作为当前环回网卡 */
if (ip4_addr_isloopback(dest)) { /* 目的IP是一个环回IP */
if ((netif_default != NULL) && netif_is_up(netif_default)) {
/* 优先考虑默认网卡。如果有默认网卡,且默认网卡协议栈使能了,则以此网卡作为本次环回网卡 */
return netif_default;
}
/* 默认网卡没有匹配成功,则从网卡链表中找一个协议栈已使能的网卡作为本次环回网卡 */
NETIF_FOREACH(netif) {
if (netif_is_up(netif)) {
return netif;
}
}
return NULL; /* 都没找到,那就匹配失败 */
}
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */ #ifdef LWIP_HOOK_IP4_ROUTE_SRC /* 如果开启了匹配网卡的钩子宏函数 */
netif = LWIP_HOOK_IP4_ROUTE_SRC(NULL, dest); /* 使用宏钩子匹配 */
if (netif != NULL) {
return netif;
}
#elif defined(LWIP_HOOK_IP4_ROUTE) /* 网卡匹配钩子 */
netif = LWIP_HOOK_IP4_ROUTE(dest); /* 使用宏钩子匹配 */
if (netif != NULL) {
return netif;
}
#endif
#endif /* !LWIP_SINGLE_NETIF */ /* 上述方案都无法匹配到网卡,就检查网卡网卡是否正常,正常则就返回默认网卡 */ if ((netif_default == NULL) || !netif_is_up(netif_default) || !netif_is_link_up(netif_default) ||
ip4_addr_isany_val(*netif_ip4_addr(netif_default)) || ip4_addr_isloopback(dest)) {
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_route: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
IP_STATS_INC(ip.rterr);
MIB2_STATS_INC(mib2.ipoutnoroutes);
return NULL;
} return netif_default; /* 返回默认网络接口 */
}

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地址匹配。

#ifdef LWIP_HOOK_IP4_ROUTE_SRC
/**
* Source based IPv4 routing must be fully implemented in
* LWIP_HOOK_IP4_ROUTE_SRC(). This function only provides the parameters.
*/
struct netif *
ip4_route_src(const ip4_addr_t *src, const ip4_addr_t *dest)
{
if (src != NULL) {
/* 当src==NULL时,钩子会从ip4_route(dest)调用 */
struct netif *netif = LWIP_HOOK_IP4_ROUTE_SRC(src, dest);
if (netif != NULL) {
return netif;
}
}
return ip4_route(dest);
}
#endif /* LWIP_HOOK_IP4_ROUTE_SRC */

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。匹配成功。

/** Return true if the current input packet should be accepted on this netif */
static int
ip4_input_accept(struct netif *netif)
{
LWIP_DEBUGF(IP_DEBUG, ("ip_input: iphdr->dest 0x%"X32_F" netif->ip_addr 0x%"X32_F" (0x%"X32_F", 0x%"X32_F", 0x%"X32_F")\n",
ip4_addr_get_u32(ip4_current_dest_addr()), ip4_addr_get_u32(netif_ip4_addr(netif)),
ip4_addr_get_u32(ip4_current_dest_addr()) & ip4_addr_get_u32(netif_ip4_netmask(netif)),
ip4_addr_get_u32(netif_ip4_addr(netif)) & ip4_addr_get_u32(netif_ip4_netmask(netif)),
ip4_addr_get_u32(ip4_current_dest_addr()) & ~ip4_addr_get_u32(netif_ip4_netmask(netif)))); /* 网卡是否在协议栈中使能且网卡地址有效? */
if ((netif_is_up(netif)) && (!ip4_addr_isany_val(*netif_ip4_addr(netif)))) {
/* 是否是单播到这个接口地址? */
if (ip4_addr_eq(ip4_current_dest_addr(), netif_ip4_addr(netif)) ||
/* 或者广播这个接口的网络地址? */
ip4_addr_isbroadcast(ip4_current_dest_addr(), netif)
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF /* 如果开启环回功能,但是没有创建环回网卡 */
/* 目的IP是一个环回的IP地址,也就是给当前网卡的,能匹配成功 */
|| (ip4_addr_get_u32(ip4_current_dest_addr()) == PP_HTONL(IPADDR_LOOPBACK))
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */
) {
LWIP_DEBUGF(IP_DEBUG, ("ip4_input: packet accepted on interface %c%c\n",
netif->name[0], netif->name[1]));
/* accept on this netif */
return 1;
}
#if LWIP_AUTOIP
/* 更改netif地址后,链路本地地址的连接必须保持(RFC3927 ch. 1.9) */
/* 即是netif网卡地址更新为可路由地址了,原本本地链路地址的连接必须保持,所以数据包能得到当前网卡。匹配成功 */
if (autoip_accept_packet(netif, ip4_current_dest_addr())) {
LWIP_DEBUGF(IP_DEBUG, ("ip4_input: LLA packet accepted on interface %c%c\n",
netif->name[0], netif->name[1]));
/* accept on this netif */
return 1;
}
#endif /* LWIP_AUTOIP */
}
return 0;
}

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也是一大块,后面有时间再完整分析下。

/**
* @ingroup ip
* Output IP packet, netif is selected by source address
*/
#define ip_output(p, src, dest, ttl, tos, proto) \
(IP_IS_V6(dest) ? \
ip6_output(p, ip_2_ip6(src), ip_2_ip6(dest), ttl, tos, proto) : \
ip4_output(p, ip_2_ip4(src), ip_2_ip4(dest), ttl, tos, proto))

9.7.2 ip层前期处理:ip4_output()

ipv4发包:

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

/**
* Simple interface to ip_output_if. It finds the outgoing network
* interface and calls upon ip_output_if to do the actual work.
*
* @param p the packet to send (p->payload points to the data, e.g. next
protocol header; if dest == LWIP_IP_HDRINCL, p already includes an
IP header and p->payload points to that IP header)
* @param src the source IP address to send from (if src == IP4_ADDR_ANY, the
* IP address of the netif used to send is used as source address)
* @param dest the destination IP address to send the packet to
* @param ttl the TTL value to be set in the IP header
* @param tos the TOS value to be set in the IP header
* @param proto the PROTOCOL to be set in the IP header
*
* @return ERR_RTE if no route is found
* see ip_output_if() for more return values
*/
err_t
ip4_output(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
u8_t ttl, u8_t tos, u8_t proto)
{
struct netif *netif; /* 下传到IP层的pbuf的ref必须为1,即是没有被其它地方引用,因为pbuf下传到IP层后,pbuf的payload指针会被更改。
如果这个pbuf被其它地方引用了,可能会导致数据混乱。 */
LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p); /* 根据目的IP地址为数据报寻找一个合适的网络接口(匹配网卡) */
if ((netif = ip4_route_src(src, dest)) == NULL) { /* 没找到,记录信息,返回错误 */
LWIP_DEBUGF(IP_DEBUG, ("ip4_output: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
IP_STATS_INC(ip.rterr);
return ERR_RTE;
} /* 匹配到网卡,传入IP层处理:组包、发送 */
return ip4_output_if(p, src, dest, ttl, tos, proto, netif);
}

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报文发出。

err_t
ip4_output_if_opt_src(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
u8_t ttl, u8_t tos, u8_t proto, struct netif *netif, void *ip_options,
u16_t optlen)
{
#endif /* IP_OPTIONS_SEND */
struct ip_hdr *iphdr;
ip4_addr_t dest_addr;
#if CHECKSUM_GEN_IP_INLINE
u32_t chk_sum = 0;
#endif /* CHECKSUM_GEN_IP_INLINE */ LWIP_ASSERT_CORE_LOCKED(); /* 确保在tcpip内核锁内 */
LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p); /* pbuf没有被其它地方引用 */ MIB2_STATS_INC(mib2.ipoutrequests); /* 流量统计 */ /* IP首部是否已经组建好 */
if (dest != LWIP_IP_HDRINCL) { /* IP首部未组建好 */
u16_t ip_hlen = IP_HLEN; /* 默认IP首部长度 */
#if IP_OPTIONS_SEND /* 开启了IP首部选项字段操作 */
u16_t optlen_aligned = 0;
if (optlen != 0) {
#if CHECKSUM_GEN_IP_INLINE
int i;
#endif /* CHECKSUM_GEN_IP_INLINE */
if (optlen > (IP_HLEN_MAX - IP_HLEN)) { /* 判断IP首部字段是否超限 */
/* optlen 过长,导致IP首部超出限制 */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output_if_opt: optlen too long\n"));
/* 状态信息统计 */
IP_STATS_INC(ip.err);
MIB2_STATS_INC(mib2.ipoutdiscards);
return ERR_VAL;
}
/* 根据协议要求,IP首部选项字段长度要求是4字节的整数倍 */
optlen_aligned = (u16_t)((optlen + 3) & ~3); /* 4字节往大对齐 */
ip_hlen = (u16_t)(ip_hlen + optlen_aligned); /* 总长度字段 */
/* 先处理选项字段 */
/* pbuf payload偏移到IP首部选项字段位置 */
if (pbuf_add_header(p, optlen_aligned)) {
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output_if_opt: not enough room for IP options in pbuf\n"));
/* payload偏移失败,统计信息返回错误 */
IP_STATS_INC(ip.err);
MIB2_STATS_INC(mib2.ipoutdiscards);
return ERR_BUF;
}
/* 先处理选项字段,拷贝选项字段值到IP首部中 */
MEMCPY(p->payload, ip_options, optlen);
if (optlen < optlen_aligned) {
/* 多余字节补0 */
memset(((char *)p->payload) + optlen, 0, (size_t)(optlen_aligned - optlen));
}
#if CHECKSUM_GEN_IP_INLINE
/* 先统计这部分的首部校验和 */
for (i = 0; i < optlen_aligned / 2; i++) {
chk_sum += ((u16_t *)p->payload)[i];
}
#endif /* CHECKSUM_GEN_IP_INLINE */
}
#endif /* IP_OPTIONS_SEND */
/* 常见IP首部处理 */
/* pbuf payload指针偏移 */
if (pbuf_add_header(p, IP_HLEN)) {
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: not enough room for IP header in pbuf\n"));
/* payload偏移失败,统计信息返回错误 */
IP_STATS_INC(ip.err);
MIB2_STATS_INC(mib2.ipoutdiscards);
return ERR_BUF;
} iphdr = (struct ip_hdr *)p->payload;
LWIP_ASSERT("check that first pbuf can hold struct ip_hdr",
(p->len >= sizeof(struct ip_hdr))); IPH_TTL_SET(iphdr, ttl); /* 填写TTL字段 */
IPH_PROTO_SET(iphdr, proto); /* 填写上层协议字段 */
#if CHECKSUM_GEN_IP_INLINE
chk_sum += PP_NTOHS(proto | (ttl << 8)); /* 首部校验和统计 */
#endif /* CHECKSUM_GEN_IP_INLINE */ ip4_addr_copy(iphdr->dest, *dest); /* 填写目的IP字段 */
#if CHECKSUM_GEN_IP_INLINE
/* 首部校验和统计 */
chk_sum += ip4_addr_get_u32(&iphdr->dest) & 0xFFFF;
chk_sum += ip4_addr_get_u32(&iphdr->dest) >> 16;
#endif /* CHECKSUM_GEN_IP_INLINE */ IPH_VHL_SET(iphdr, 4, ip_hlen / 4); /* 填写IP版本号+IP首部长度 */
IPH_TOS_SET(iphdr, tos); /* 填写TOS服务字段 */
#if CHECKSUM_GEN_IP_INLINE
/* 首部校验和统计 */
chk_sum += PP_NTOHS(tos | (iphdr->_v_hl << 8));
#endif /* CHECKSUM_GEN_IP_INLINE */
IPH_LEN_SET(iphdr, lwip_htons(p->tot_len)); /* 填写总长度字段 */
#if CHECKSUM_GEN_IP_INLINE
/* 首部校验和统计 */
chk_sum += iphdr->_len;
#endif /* CHECKSUM_GEN_IP_INLINE */
IPH_OFFSET_SET(iphdr, 0); /* 填写标志字段+分片偏移量字段 */
IPH_ID_SET(iphdr, lwip_htons(ip_id)); /* 填写标识字段 */
#if CHECKSUM_GEN_IP_INLINE
/* 首部校验和统计 */
chk_sum += iphdr->_id;
#endif /* CHECKSUM_GEN_IP_INLINE */
++ip_id; /* ip首部标识,全局值 */ /* 填写源IP地址字段 */
if (src == NULL) {
ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);
} else {
/* src cannot be NULL here */
ip4_addr_copy(iphdr->src, *src);
} #if CHECKSUM_GEN_IP_INLINE
/* 首部校验和统计 */
chk_sum += ip4_addr_get_u32(&iphdr->src) & 0xFFFF;
chk_sum += ip4_addr_get_u32(&iphdr->src) >> 16;
/* 二次16bit折叠。因为一次可能会溢出。第二次是为了解决溢出。 */
chk_sum = (chk_sum >> 16) + (chk_sum & 0xFFFF);
chk_sum = (chk_sum >> 16) + chk_sum;
chk_sum = ~chk_sum; /* 反码 */
IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) { /* 当前网卡支持IP首部的checksum功能 */
iphdr->_chksum = (u16_t)chk_sum; /* 填写首部校验和字段。网络字节序,因为统计的时候就以网络字节序统计,所以这里不必转换 */
}
#if LWIP_CHECKSUM_CTRL_PER_NETIF
else {/* 如果不支持checksum功能 */
IPH_CHKSUM_SET(iphdr, 0); /* 填写首部校验和字段为0 */
}
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
#else /* CHECKSUM_GEN_IP_INLINE */
IPH_CHKSUM_SET(iphdr, 0); /* 默认填写首部校验和字段为0 */
#if CHECKSUM_GEN_IP
IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {
IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, ip_hlen)); /* IP首部校验和字段更新为实际值 */
}
#endif /* CHECKSUM_GEN_IP */
#endif /* CHECKSUM_GEN_IP_INLINE */
} else { /* IP头已经包含在p。如TCP重传 */
if (p->len < IP_HLEN) { /* 长度校验 */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: LWIP_IP_HDRINCL but pbuf is too short\n"));
IP_STATS_INC(ip.err);
MIB2_STATS_INC(mib2.ipoutdiscards);
return ERR_BUF;
}
iphdr = (struct ip_hdr *)p->payload;
ip4_addr_copy(dest_addr, iphdr->dest); /* 获取目的IP地址 */
dest = &dest_addr;
}
/* 状态记录 */
IP_STATS_INC(ip.xmit); LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: %c%c%"U16_F"\n", netif->name[0], netif->name[1], (u16_t)netif->num));
ip4_debug_print(p); #if ENABLE_LOOPBACK /* 环回功能 */
if (ip4_addr_eq(dest, netif_ip4_addr(netif)) /* 目的IP为源网卡IP,则是环回 */
#if !LWIP_HAVE_LOOPIF
|| ip4_addr_isloopback(dest) /* 目的IP是环回IP字段,也是环回 */
#endif /* !LWIP_HAVE_LOOPIF */
) {
/* 数据包环回,则不用通过数据链路层,直达对应网卡的环回链表即可 */
LWIP_DEBUGF(IP_DEBUG, ("netif_loop_output()"));
return netif_loop_output(netif, p);
}
#if LWIP_MULTICAST_TX_OPTIONS
if ((p->flags & PBUF_FLAG_MCASTLOOP) != 0) { /* 该pbuf是要环回的UDP组播 */
netif_loop_output(netif, p); /* 环回到本网卡 */
}
#endif /* LWIP_MULTICAST_TX_OPTIONS */
#endif /* ENABLE_LOOPBACK */
#if IP_FRAG
/* 如果接口mtu设置为0,不用分片 */
/* 分片检查 */
if (netif->mtu && (p->tot_len > netif->mtu)) { /* IP报文超出网卡MTU,则需要分片处理 */
return ip4_frag(p, netif, dest); /* 需要分片处理 */
}
#endif /* IP_FRAG */ /* 不需要分片处理 */
LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: call netif->output()\n"));
return netif->output(netif, p, dest); /* IP层发送数据包。至此,IP报文处理完毕,下一步交给ARP或者直接到数据链路处理。 */
}

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数据结构:

/** A custom pbuf that holds a reference to another pbuf, which is freed
* when this custom pbuf is freed. This is used to create a custom PBUF_REF
* that points into the original pbuf. */
struct pbuf_custom_ref {
/** 'base class' */
struct pbuf_custom pc; /* 用户的控制区。包含一个pbuf和一个释放该pbuf的api */
/* 指向被引用的原始pbuf的指针 */
struct pbuf *original;
};

pbuf_custom_ref数据结构:

struct pbuf_custom {
/* The actual pbuf */
struct pbuf pbuf;
/**This function is called when pbuf_free deallocates this pbuf(_custom) */
pbuf_free_custom_fn custom_free_function;
};

相关数据结构图:

没开启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报文了。

/**
* Fragment an IP datagram if too large for the netif.
*
* Chop the datagram in MTU sized chunks and send them in order
* by pointing PBUF_REFs into p.
*
* @param p ip packet to send
* @param netif the netif on which to send
* @param dest destination ip address to which to send
*
* @return ERR_OK if sent successfully, err_t otherwise
*/
err_t
ip4_frag(struct pbuf *p, struct netif *netif, const ip4_addr_t *dest)
{
struct pbuf *rambuf; /* 分片的pbuf结构 */
#if !LWIP_NETIF_TX_SINGLE_PBUF
/* 用于处理分片IP报文与原IP报文数据区pbuf内存共享使用 */
struct pbuf *newpbuf;
u16_t newpbuflen = 0;
u16_t left_to_copy;
#endif
struct ip_hdr *original_iphdr;
struct ip_hdr *iphdr;
const u16_t nfb = (u16_t)((netif->mtu - IP_HLEN) / 8); /* 分片中允许的最大数据量 */
u16_t left, fragsize; /* 待发送数据长度和当前发送的分片的数据长度 */
u16_t ofo; /* 当前分片偏移量 */
int last; /* 是否为最后一个分片 */
u16_t poff = IP_HLEN; /* 发送的数据在原始数据报pbuf中的偏移量 */
u16_t tmp;
int mf_set; /* 传入的pbuf是否是分片包,后续是否还有更多分片。 */ original_iphdr = (struct ip_hdr *)p->payload;
iphdr = original_iphdr;
if (IPH_HL_BYTES(iphdr) != IP_HLEN) { /* IP首部长度字段 */
/* ip4_frag() 不支持IP首部带选项字段的IP报文分片 */
return ERR_VAL;
}
LWIP_ERROR("ip4_frag(): pbuf too short", p->len >= IP_HLEN, return ERR_VAL); /* 保存原始的分片偏移量字段 */
tmp = lwip_ntohs(IPH_OFFSET(iphdr));
ofo = tmp & IP_OFFMASK;
/* 判断pbuf包是否已经被分片,被分片后后续是否还有更多分片。如果有,那么本次分片的最后一个分片不能标志后续没有更多分片。 */
mf_set = tmp & IP_MF; /* 获取还剩多少数据还没分片处理 */
left = (u16_t)(p->tot_len - IP_HLEN); while (left) { /* 循环分片处理 */
/* 当前分片需要填充的数据size */
fragsize = LWIP_MIN(left, (u16_t)(nfb * 8)); #if LWIP_NETIF_TX_SINGLE_PBUF /* 分片支持新建整个pbuf */
/* 申请新的pbuf内存资源装载当前分片 */
rambuf = pbuf_alloc(PBUF_IP, fragsize, PBUF_RAM);
if (rambuf == NULL) {
goto memerr;
}
LWIP_ASSERT("this needs a pbuf in one piece!",
(rambuf->len == rambuf->tot_len) && (rambuf->next == NULL));
/* 拷贝IP报文数据区的数据到分片IP包中 */
poff += pbuf_copy_partial(p, rambuf->payload, fragsize, poff);
/* pbuf腾出IP首部空间 */
if (pbuf_add_header(rambuf, IP_HLEN)) {
pbuf_free(rambuf);
goto memerr;
}
/* 填充原IP报文首部到当前分片,后面再处理分片的IP首部 */
SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
iphdr = (struct ip_hdr *)rambuf->payload;
#else /* LWIP_NETIF_TX_SINGLE_PBUF */ /* 分片支持数据区共享。这个宏分支,感兴趣的同学可以看下 */
/* 先申请分片IP首部的pbuf */
rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);
if (rambuf == NULL) {
goto memerr;
}
LWIP_ASSERT("this needs a pbuf in one piece!",
(rambuf->len >= (IP_HLEN)));
/* 拷贝原IP报文首部到分片IP报文首部 */
SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
iphdr = (struct ip_hdr *)rambuf->payload; /* 本次分片IP报文占用的size */
left_to_copy = fragsize;
while (left_to_copy) { /* 分片IP报文组装完毕为止 */
struct pbuf_custom_ref *pcr; /* 引用原IP报文数据区pbuf的伪pbuf */
u16_t plen = (u16_t)(p->len - poff); /* 原IP报文当前pbuf节点还剩多少数据没处理 */
LWIP_ASSERT("p->len >= poff", p->len >= poff);
newpbuflen = LWIP_MIN(left_to_copy, plen); /* 选出能处理的size */ if (!newpbuflen) { /* 原IP报文当前pbuf所有数据已经分片处理完毕,可以处理下一个pbuf */
poff = 0;
p = p->next; /* 跳到下一个分片IP报文处理继续组装当前IP分片 */
continue;
}
pcr = ip_frag_alloc_pbuf_custom_ref(); /* 为伪pbuf pcr申请资源 */
if (pcr == NULL) {
pbuf_free(rambuf);
goto memerr;
}
/* 把原IP报文数据区当前分片的数据pbuf引用到pcr->pc->pbuf这个pbuf中 */
newpbuf = pbuf_alloced_custom(PBUF_RAW, newpbuflen, PBUF_REF, &pcr->pc,
(u8_t *)p->payload + poff, newpbuflen);
if (newpbuf == NULL) {
ip_frag_free_pbuf_custom_ref(pcr);
pbuf_free(rambuf);
goto memerr;
} pbuf_ref(p); /* 当前pbuf引用值+1 */
pcr->original = p; /* 保存当前pbuf地址到pcr */
pcr->pc.custom_free_function = ipfrag_free_pbuf_custom; /* 专门的伪pbuf释放API */ /* 拼接pbuf,把本次分片IP首部、数据区的各个pbuf都链在一起 */
pbuf_cat(rambuf, newpbuf);
left_to_copy = (u16_t)(left_to_copy - newpbuflen); /* 检查本次分片IP报文是否整理完毕 */
if (left_to_copy) { /* 还没填充完毕,需要继续填充 */
poff = 0;
p = p->next;
}
}
poff = (u16_t)(poff + newpbuflen); /* 分片在原IP报文中的偏移值更新 */
#endif /* LWIP_NETIF_TX_SINGLE_PBUF */ /* 更正分片IP首部 */
/* 本次分片是否为最后一次分片 */
last = (left <= netif->mtu - IP_HLEN); /* 设置新的偏移量和MF标志 */
tmp = (IP_OFFMASK & (ofo));
if (!last || mf_set) { /* 本函数分片处理的最后一片 且 传入的原IP报文不是分片报文 */
/* 标志位第三位更多分片标记为0,已经为顶层原IP报文的最后一片 */
tmp = tmp | IP_MF;
}
IPH_OFFSET_SET(iphdr, lwip_htons(tmp)); /* 重写分片IP首部的标志位字段+分片偏移量字段 */
IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN))); /* 重写分片IP报文总长度 */
IPH_CHKSUM_SET(iphdr, 0); /* 设置分片IP报文首部校验字段,默认为0 */
#if CHECKSUM_GEN_IP
IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {
IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN)); /* 更新分片IP报文首部校验字段 */
}
#endif /* CHECKSUM_GEN_IP */ /* 发送分片IP报文 */
netif->output(netif, rambuf, dest);
/* 分片状态信息记录 */
IPFRAG_STATS_INC(ip_frag.xmit); /* Unfortunately we can't reuse rambuf - the hardware may still be
* using the buffer. Instead we free it (and the ensuing chain) and
* recreate it next time round the loop. If we're lucky the hardware
* will have already sent the packet, the free will really free, and
* there will be zero memory penalty.
*/
/* 释放分片空间。因为这rambuf只是分片函数内部处理创建的,所以调用netif->output()发送出去后要在这里释放掉。 */
pbuf_free(rambuf);
/* 需要处理的分片待发送数据减少 */
left = (u16_t)(left - fragsize);
/* 分片偏移量增加 */
ofo = (u16_t)(ofo + nfb);
}
MIB2_STATS_INC(mib2.ipfragoks);
return ERR_OK;
memerr:
MIB2_STATS_INC(mib2.ipfragfails);
return ERR_MEM;
}

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:原报文上报。

/**
* This function is called by the network interface device driver when
* an IP packet is received. The function does the basic checks of the
* IP header such as packet size being at least larger than the header
* size etc. If the packet was not destined for us, the packet is
* forwarded (using ip_forward). The IP checksum is always checked.
*
* Finally, the packet is sent to the upper layer protocol input function.
*
* @param p the received IP packet (p->payload points to IP header)
* @param inp the netif on which this packet was received
* @return ERR_OK if the packet was processed (could return ERR_* if it wasn't
* processed, but currently always returns ERR_OK)
*/
err_t
ip4_input(struct pbuf *p, struct netif *inp)
{
const struct ip_hdr *iphdr;
struct netif *netif;
u16_t iphdr_hlen;
u16_t iphdr_len;
#if IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP
int check_ip_src = 1;
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP */
#if LWIP_RAW
raw_input_state_t raw_status;
#endif /* LWIP_RAW */ LWIP_ASSERT_CORE_LOCKED(); IP_STATS_INC(ip.recv);
MIB2_STATS_INC(mib2.ipinreceives); /* 识别IP头 */
iphdr = (struct ip_hdr *)p->payload;
if (IPH_V(iphdr) != 4) { /* 如果IP版本不是ipv4,丢弃该报文 */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_WARNING, ("IP packet dropped due to bad version number %"U16_F"\n", (u16_t)IPH_V(iphdr)));
ip4_debug_print(p);
pbuf_free(p);
IP_STATS_INC(ip.err);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinhdrerrors);
return ERR_OK;
} #ifdef LWIP_HOOK_IP4_INPUT
if (LWIP_HOOK_IP4_INPUT(p, inp)) { /* 传入IP接收数据包钩子函数处理 */
/* IP报文也就被处理了 */
return ERR_OK;
}
#endif /* 获取IP首部长度(以字节为单位) */
iphdr_hlen = IPH_HL_BYTES(iphdr);
/* 获取IP报文长度(以字节为单位) */
iphdr_len = lwip_ntohs(IPH_LEN(iphdr)); /* 修剪pbuf。对于小于60字节的数据包尤其需要这样做 */
if (iphdr_len < p->tot_len) {
pbuf_realloc(p, iphdr_len);
} /* 报头长度超过第一个pbuf长度,或者IP长度超过总pbuf长度? */
if ((iphdr_hlen > p->len) || (iphdr_len > p->tot_len) || (iphdr_hlen < IP_HLEN)) {
if (iphdr_hlen < IP_HLEN) {
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("ip4_input: short IP header (%"U16_F" bytes) received, IP packet dropped\n", iphdr_hlen));
}
if (iphdr_hlen > p->len) {
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("IP header (len %"U16_F") does not fit in first pbuf (len %"U16_F"), IP packet dropped.\n",
iphdr_hlen, p->len));
}
if (iphdr_len > p->tot_len) {
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("IP (len %"U16_F") is longer than pbuf (len %"U16_F"), IP packet dropped.\n",
iphdr_len, p->tot_len));
}
/* 校验错误,丢弃当前pbuf,并记录相关信息 */
pbuf_free(p);
IP_STATS_INC(ip.lenerr);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipindiscards);
return ERR_OK;
} /* 首部校验和字段校验 */
#if CHECKSUM_CHECK_IP
IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_CHECK_IP) {
if (inet_chksum(iphdr, iphdr_hlen) != 0) { LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("Checksum (0x%"X16_F") failed, IP packet dropped.\n", inet_chksum(iphdr, iphdr_hlen)));
ip4_debug_print(p);
pbuf_free(p);
IP_STATS_INC(ip.chkerr);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinhdrerrors);
return ERR_OK;
}
}
#endif /* 拷贝IP地址到对齐的ip_addr_t。ip_data为全局变量 */
ip_addr_copy_from_ip4(ip_data.current_iphdr_dest, iphdr->dest);
ip_addr_copy_from_ip4(ip_data.current_iphdr_src, iphdr->src); /* 将报文与接口匹配,即这个报文是我们的吗? */
if (ip4_addr_ismulticast(ip4_current_dest_addr())) { /* 多播包 */
#if LWIP_IGMP /* IGMP处理 */
if ((inp->flags & NETIF_FLAG_IGMP) && (igmp_lookfor_group(inp, ip4_current_dest_addr()))) { /* 当前网卡在IP报文目的IP的组播组内 */
/* IGMP snooping交换机需要允许源地址为0.0.0.0 (RFC 4541) */
ip4_addr_t allsystems;
IP4_ADDR(&allsystems, 224, 0, 0, 1);
if (ip4_addr_eq(ip4_current_dest_addr(), &allsystems) &&
ip4_addr_isany(ip4_current_src_addr())) {
check_ip_src = 0; /* 标记后面忽略源IP地址 */
}
netif = inp; /* 组播匹配成功 */
} else {
netif = NULL; /* 组播匹配失败 */
}
#else /* LWIP_IGMP */
if ((netif_is_up(inp)) && (!ip4_addr_isany_val(*netif_ip4_addr(inp)))) {
netif = inp; /* 如果不支持IGMP协议功能,但是当前网卡收到这个组播包,就直接匹配当前网卡即可 */
} else {
netif = NULL;
}
#endif /* LWIP_IGMP */
} else { /* 单播或广播IP包 */
/* 优先匹配当前网卡 */
if (ip4_input_accept(inp)) {
netif = inp; /* 匹配成功 */
} else { /* 当前网卡匹配失败,就遍历网卡链表进行匹配 */
netif = NULL;
#if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF
/* 如果网卡没有环回功能,或者已经创建了环回网卡。如果当前IP报文的目的IP是环回类型的,则不会跑到这里,而是在对应网卡的环回数据队列中。 */
if (!ip4_addr_isloopback(ip4_current_dest_addr())) /* IP报文的目的IP不能是环回类型 */
#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */
{
#if !LWIP_SINGLE_NETIF
NETIF_FOREACH(netif) { /* 遍历网卡链表 */
if (netif == inp) { /* 跳过已经尝试匹配的网卡 */
/* we checked that before already */
continue;
}
if (ip4_input_accept(netif)) { /* 进行匹配 */
break;
}
}
#endif /* !LWIP_SINGLE_NETIF */
}
}
} #if IP_ACCEPT_LINK_LAYER_ADDRESSING
/* 通过DHCP报文时,不考虑目的地址。DHCP流量使用链路层寻址(如以太网MAC),所以我们不能对IP进行过滤。
* 参考RFC 1542章节3.1.1,参考RFC 2131)。
*
* 如果想在netif关闭时接受私有广播通信,定义LWIP_IP_ACCEPT_UDP_PORT(dst_port),例如:
* #define LWIP_IP_ACCEPT_UDP_PORT(dst_port) ((dst_port) == PP_NTOHS(12345))
*/
if (netif == NULL) {
/* 远端端口是DHCP服务器? */
if (IPH_PROTO(iphdr) == IP_PROTO_UDP) { /* UDP协议类型 */
const struct udp_hdr *udphdr = (const struct udp_hdr *)((const u8_t *)iphdr + iphdr_hlen);
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: UDP packet to DHCP client port %"U16_F"\n",
lwip_ntohs(udphdr->dest)));
if (IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr->dest)) { /* 目的端口是DHCP客户端端口。能通过协议栈IP层 */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: DHCP packet accepted.\n"));
netif = inp; /* 匹配成功 */
check_ip_src = 0; /* 标记后面忽略源IP地址 */
}
}
}
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */ /* 广播或多播包的源地址?兼容RFC 1122: 3.2.1.3 */
#if LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING
if (check_ip_src /* 忽略源IP地址的跳过:如IGMP和DHCP */
#if IP_ACCEPT_LINK_LAYER_ADDRESSING
/* 允许DHCP服务器源地址为0.0.0.0 (RFC 1.1.2.2: 3.2.1.3/a) */
&& !ip4_addr_isany_val(*ip4_current_src_addr())
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */
)
#endif /* LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING */
{
if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) || /* 广播包 */
(ip4_addr_ismulticast(ip4_current_src_addr()))) { /* 或者是多播包 */
/* IP报文无效 */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_WARNING, ("ip4_input: packet source is not valid.\n"));
/* 释放资源并记录相关信息 */
pbuf_free(p);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinaddrerrors);
MIB2_STATS_INC(mib2.ipindiscards);
return ERR_OK;
}
} /* 网卡匹配完毕 */ if (netif == NULL) { /* 网卡匹配失败 */
/* 不给我们包,路由或丢弃 */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: packet not for us.\n"));
#if IP_FORWARD /* 支持路由转发 */
if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), inp)) { /* 广播包不能转发 */
/* 尝试在(其他)接口转发IP报文 */
ip4_forward(p, (struct ip_hdr *)p->payload, inp);
} else
#endif /* IP_FORWARD */
{ /* 广播包不支持转发,丢弃 */
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinaddrerrors);
MIB2_STATS_INC(mib2.ipindiscards);
}
pbuf_free(p);
return ERR_OK;
} /* 网卡匹配成功,数据包是给我们的 */ /* 数据包由多个分片组成? */
if ((IPH_OFFSET(iphdr) & PP_HTONS(IP_OFFMASK | IP_MF)) != 0) {
#if IP_REASSEMBLY /* 支持IP分片重组 */
LWIP_DEBUGF(IP_DEBUG, ("IP packet is a fragment (id=0x%04"X16_F" tot_len=%"U16_F" len=%"U16_F" MF=%"U16_F" offset=%"U16_F"), calling ip4_reass()\n",
lwip_ntohs(IPH_ID(iphdr)), p->tot_len, lwip_ntohs(IPH_LEN(iphdr)), (u16_t)!!(IPH_OFFSET(iphdr) & PP_HTONS(IP_MF)), (u16_t)((lwip_ntohs(IPH_OFFSET(iphdr)) & IP_OFFMASK) * 8)));
/* 重新组装包 */
p = ip4_reass(p);
if (p == NULL) { /* IP包还没组装好 */
return ERR_OK;
}
/* IP报文重组好了,更新IP首部指针 */
iphdr = (const struct ip_hdr *)p->payload;
#else /* IP_REASSEMBLY == 0, 不支持分片重组 */
/* 不支持分片重组,遇到分片IP报文,直接丢弃 */
pbuf_free(p);
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("IP packet dropped since it was fragmented (0x%"X16_F") (while IP_REASSEMBLY == 0).\n",
lwip_ntohs(IPH_OFFSET(iphdr))));
IP_STATS_INC(ip.opterr);
IP_STATS_INC(ip.drop);
/* unsupported protocol feature */
MIB2_STATS_INC(mib2.ipinunknownprotos);
return ERR_OK;
#endif /* IP_REASSEMBLY */
} #if IP_OPTIONS_ALLOWED == 0 /* no support for IP options in the IP header? */ #if LWIP_IGMP
/* 在IGMP消息中有一个额外的“路由器警报”选项,我们允许但不监督 */
/* 如果IP报文带选项字段,lwip只支持IGMP */
if ((iphdr_hlen > IP_HLEN) && (IPH_PROTO(iphdr) != IP_PROTO_IGMP)) {
#else
if (iphdr_hlen > IP_HLEN) { /* LWIP不支持接收处理带选项字段的IP报文。(IGMP协议除外) */
#endif /* LWIP_IGMP */
LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("IP packet dropped since there were IP options (while IP_OPTIONS_ALLOWED == 0).\n"));
pbuf_free(p);
IP_STATS_INC(ip.opterr);
IP_STATS_INC(ip.drop);
/* unsupported protocol feature */
MIB2_STATS_INC(mib2.ipinunknownprotos);
return ERR_OK;
}
#endif /* IP_OPTIONS_ALLOWED == 0 */ /* 发送到上层 */
LWIP_DEBUGF(IP_DEBUG, ("ip4_input: \n"));
ip4_debug_print(p);
LWIP_DEBUGF(IP_DEBUG, ("ip4_input: p->len %"U16_F" p->tot_len %"U16_F"\n", p->len, p->tot_len)); /* 更新这个全局IP数据 */
ip_data.current_netif = netif; /* 上层处理该数据包的网卡 */
ip_data.current_input_netif = inp; /* 数据链路接收到该数据包的网卡 */
ip_data.current_ip4_header = iphdr; /* IP报文首部 */
ip_data.current_ip_header_tot_len = IPH_HL_BYTES(iphdr); /* IP首部长度(单位:字节) */ #if LWIP_RAW /* RAW */
/* 传到RAW输入 */
raw_status = raw_input(p, inp);
if (raw_status != RAW_INPUT_EATEN) /* RAW传入失败,那就传给其它上层协议模块 */
#endif /* LWIP_RAW */
{
pbuf_remove_header(p, iphdr_hlen); /* 移到有效载荷,不需要检查 */ switch (IPH_PROTO(iphdr)) { /* 根据IP报文协议字段传入对应协议模块 */
#if LWIP_UDP
case IP_PROTO_UDP: /* UDP协议 */
#if LWIP_UDPLITE
case IP_PROTO_UDPLITE: /* UDP-Lite协议 */
#endif /* LWIP_UDPLITE */
MIB2_STATS_INC(mib2.ipindelivers);
udp_input(p, inp); /* 传入UDP协议模块 */
break;
#endif /* LWIP_UDP */
#if LWIP_TCP
case IP_PROTO_TCP: /* TCP协议 */
MIB2_STATS_INC(mib2.ipindelivers);
tcp_input(p, inp); /* 传入TCP协议模块 */
break;
#endif /* LWIP_TCP */
#if LWIP_ICMP
case IP_PROTO_ICMP: /* ICMP协议 */
MIB2_STATS_INC(mib2.ipindelivers);
icmp_input(p, inp); /* 传入ICMP协议模块 */
break;
#endif /* LWIP_ICMP */
#if LWIP_IGMP
case IP_PROTO_IGMP: /* IGMP协议 */
igmp_input(p, inp, ip4_current_dest_addr()); /* 传入IGMP协议模块 */
break;
#endif /* LWIP_IGMP */
default: /* 其它 */
#if LWIP_RAW
if (raw_status == RAW_INPUT_DELIVERED) {
MIB2_STATS_INC(mib2.ipindelivers);
} else
#endif /* LWIP_RAW */
{
#if LWIP_ICMP
/* 不是广播包,也不是多播包,数据链路层给到我们网卡不支持的协议,发送ICMP目的协议不可达 */
if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), netif) &&
!ip4_addr_ismulticast(ip4_current_dest_addr())) {
pbuf_header_force(p, (s16_t)iphdr_hlen); /* 移动到ip头,不需要检查 */
icmp_dest_unreach(p, ICMP_DUR_PROTO); /* 发送ICMP目的协议不可达 */
}
#endif /* LWIP_ICMP */ LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("Unsupported transport protocol %"U16_F"\n", (u16_t)IPH_PROTO(iphdr))); IP_STATS_INC(ip.proterr);
IP_STATS_INC(ip.drop);
MIB2_STATS_INC(mib2.ipinunknownprotos);
}
pbuf_free(p); /* 丢弃,释放pbuf */
break;
}
} /* @todo: this is not really necessary... */
ip_data.current_netif = NULL;
ip_data.current_input_netif = NULL;
ip_data.current_ip4_header = NULL;
ip_data.current_ip_header_tot_len = 0;
ip4_addr_set_any(ip4_current_src_addr());
ip4_addr_set_any(ip4_current_dest_addr()); return ERR_OK;
}

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()

/**
* Determine whether an IP address is in a reserved set of addresses
* that may not be forwarded, or whether datagrams to that destination
* may be forwarded.
* @param p the packet to forward
* @return 1: can forward 0: discard
*/
static int
ip4_canforward(struct pbuf *p)
{
u32_t addr = lwip_htonl(ip4_addr_get_u32(ip4_current_dest_addr())); #ifdef LWIP_HOOK_IP4_CANFORWARD
/* 先钩子函数裁定 */
int ret = LWIP_HOOK_IP4_CANFORWARD(p, addr);
if (ret >= 0) {
return ret;
}
#endif /* LWIP_HOOK_IP4_CANFORWARD */ if (p->flags & PBUF_FLAG_LLBCAST) {
/* don't route link-layer broadcasts */
return 0;
}
if ((p->flags & PBUF_FLAG_LLMCAST) || IP_MULTICAST(addr)) {
/* don't route link-layer multicasts (use LWIP_HOOK_IP4_CANFORWARD instead) */
return 0;
}
if (IP_EXPERIMENTAL(addr)) {
return 0;
}
if (IP_CLASSA(addr)) {
u32_t net = addr & IP_CLASSA_NET;
if ((net == 0) || (net == ((u32_t)IP_LOOPBACKNET << IP_CLASSA_NSHIFT))) {
/* don't route loopback packets */
return 0;
}
}
return 1;
}

ip4_forward()

/**
* Forwards an IP packet. It finds an appropriate route for the
* packet, decrements the TTL value of the packet, adjusts the
* checksum and outputs the packet on the appropriate interface.
*
* @param p the packet to forward (p->payload points to IP header)
* @param iphdr the IP header of the input packet
* @param inp the netif on which this packet was received
*/
static void
ip4_forward(struct pbuf *p, struct ip_hdr *iphdr, struct netif *inp)
{
struct netif *netif; PERF_START;
LWIP_UNUSED_ARG(inp); if (!ip4_canforward(p)) {
goto return_noroute;
} /* RFC3927 2.7:不转发链路本地地址 */
if (ip4_addr_islinklocal(ip4_current_dest_addr())) {
LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: not forwarding LLA %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
ip4_addr1_16(ip4_current_dest_addr()), ip4_addr2_16(ip4_current_dest_addr()),
ip4_addr3_16(ip4_current_dest_addr()), ip4_addr4_16(ip4_current_dest_addr())));
goto return_noroute;
} /* 路由匹配网卡,找到将此IP包转发到的网卡 */
netif = ip4_route_src(ip4_current_src_addr(), ip4_current_dest_addr());
if (netif == NULL) {
LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: no forwarding route for %"U16_F".%"U16_F".%"U16_F".%"U16_F" found\n",
ip4_addr1_16(ip4_current_dest_addr()), ip4_addr2_16(ip4_current_dest_addr()),
ip4_addr3_16(ip4_current_dest_addr()), ip4_addr4_16(ip4_current_dest_addr())));
/* @todo: send ICMP_DUR_NET? */
goto return_noroute;
}
#if !IP_FORWARD_ALLOW_TX_ON_RX_NETIF /* 不允许转发到原链路(无线网卡可以) */
/* 不要将数据包转发到到达的同一个网络接口上 */
if (netif == inp) {
LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: not bouncing packets back on incoming interface.\n"));
goto return_noroute;
}
#endif /* IP_FORWARD_ALLOW_TX_ON_RX_NETIF */ /* 已经路由匹配到能转发当前IP包的网卡了 */ /* TTL减1 */
IPH_TTL_SET(iphdr, IPH_TTL(iphdr) - 1); if (IPH_TTL(iphdr) == 0) { /* TTL值已经到达0了 */
MIB2_STATS_INC(mib2.ipinhdrerrors);
#if LWIP_ICMP
/* 不要发送ICMP报文来响应ICMP报文 */
if (IPH_PROTO(iphdr) != IP_PROTO_ICMP) {
icmp_time_exceeded(p, ICMP_TE_TTL);
}
#endif /* LWIP_ICMP */
return;
} /* 更新IP报文首部校验和。技巧:首部校验和加1即可。注意网络字节序。 */
if (IPH_CHKSUM(iphdr) >= PP_HTONS(0xffffU - 0x100)) {
IPH_CHKSUM_SET(iphdr, (u16_t)(IPH_CHKSUM(iphdr) + PP_HTONS(0x100) + 1));
} else {
IPH_CHKSUM_SET(iphdr, (u16_t)(IPH_CHKSUM(iphdr) + PP_HTONS(0x100)));
} /* 当我们有多个netifs,其中至少有一个具有校验和卸载功能时,IP转发需要将各个校验和字段设置为0,以防止HW硬件算法计算无效的校验和 */
/* https://github.com/lwip-tcpip/lwip/commit/61c67fc2295e522c7a12175581d6928c3951c0bf */
/* http://savannah.nongnu.org/bugs/?func=detailitem&item_id=56288 */
/* 这里有点没看明白:如果网卡没有硬件校验和卸载功能,lwip协议栈网卡开启CHECKSUM_GEN_IP宏使用软件校验和功能,那在这里转发IP包时,岂不是把数据包校验和字段都清空了? */
if (CHECKSUM_GEN_IP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_IP)) { /* 如果使用软件生成了校验和,这里需要清空 */
IPH_CHKSUM_SET(iphdr, 0);
}
switch (IPH_PROTO(iphdr)) { /* 上层协议的校验和也清空 */
#if LWIP_UDP
#if LWIP_UDPLITE
case IP_PROTO_UDPLITE:
#endif
case IP_PROTO_UDP:
if (CHECKSUM_GEN_UDP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_UDP)) {
((struct udp_hdr *)((u8_t *)iphdr + IPH_HL_BYTES(iphdr)))->chksum = 0;
}
break;
#endif
#if LWIP_TCP
case IP_PROTO_TCP:
if (CHECKSUM_GEN_TCP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_TCP)) {
((struct tcp_hdr *)((u8_t *)iphdr + IPH_HL_BYTES(iphdr)))->chksum = 0;
}
break;
#endif
#if LWIP_ICMP
case IP_PROTO_ICMP:
if (CHECKSUM_GEN_ICMP || NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_GEN_ICMP)) {
((struct icmp_hdr *)((u8_t *)iphdr + IPH_HL_BYTES(iphdr)))->chksum = 0;
}
break;
#endif
default:
/* there's really nothing to do here other than satisfying 'switch-default' */
break;
} LWIP_DEBUGF(IP_DEBUG, ("ip4_forward: forwarding packet to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
ip4_addr1_16(ip4_current_dest_addr()), ip4_addr2_16(ip4_current_dest_addr()),
ip4_addr3_16(ip4_current_dest_addr()), ip4_addr4_16(ip4_current_dest_addr()))); IP_STATS_INC(ip.fw);
MIB2_STATS_INC(mib2.ipforwdatagrams);
IP_STATS_INC(ip.xmit); PERF_STOP("ip4_forward");
/* 分片检查&处理 */
/* mtu为0,表示该网卡不支持分片 */
if (netif->mtu && (p->tot_len > netif->mtu)) { /* 需要分片 */
if ((IPH_OFFSET(iphdr) & PP_NTOHS(IP_DF)) == 0) {
#if IP_FRAG
/* 分片发包 */
ip4_frag(p, netif, ip4_current_dest_addr());
#else /* IP_FRAG */
/* @todo: send ICMP Destination Unreachable code 13 "Communication administratively prohibited"? */
#endif /* IP_FRAG */
} else {
#if LWIP_ICMP
/* 发送ICMP目的不可达代码4: "Fragmentation Needed and DF Set" */
icmp_dest_unreach(p, ICMP_DUR_FRAG);
#endif /* LWIP_ICMP */
}
return;
}
/* 不需要分片就直接发出IP层 */
netif->output(netif, p, ip4_current_dest_addr());
return;
return_noroute:
MIB2_STATS_INC(mib2.ipoutnoroutes);
}

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 reassembly helper struct.
* This is exported because memp needs to know the size.
*/
struct ip_reassdata {
struct ip_reassdata *next; /* 单向链表节点 */
struct pbuf *p; /* pbuf链表。是当前IP分片包的pbuf,各个分片的链,是由struct ip_reass_helper数据结构管理。 */
struct ip_hdr iphdr; /* 当前IP报文的IP首部 */
u16_t datagram_len; /* 已经收到的IP报长度或IP报文总长度。如果收到最后一个分片,则当前值为IP报文总长度。 */
u8_t flags; /* 是否收到最后一个分片 */
u8_t timer; /* 设置超时间隔 */
};

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

struct ip_reass_helper {
PACK_STRUCT_FIELD(struct pbuf *next_pbuf); /* 下一个IP分片 */
PACK_STRUCT_FIELD(u16_t start); /* 分片中数据在IP报文的起始位置 */
PACK_STRUCT_FIELD(u16_t end); /* 分片中数据在IP报文的结束位置 */
} PACK_STRUCT_STRUCT;

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重组完成。

/**
* Reassembles incoming IP fragments into an IP datagram.
*
* @param p points to a pbuf chain of the fragment
* @return NULL if reassembly is incomplete, ? otherwise
*/
struct pbuf *
ip4_reass(struct pbuf *p)
{
struct pbuf *r; /* pbuf指针 */
struct ip_hdr *fraghdr; /* IP首部指针 */
struct ip_reassdata *ipr; /* 重组IP报文指针 */
struct ip_reass_helper *iprh; /* IP分片节点管理数据结构指针 */
u16_t offset, len, clen; /* 分片偏移量,分片长度,分片pbuf节点数 */
u8_t hlen; /* IP分片首部长度 */
int valid; /* 插入IP分片后的结果 */
int is_last; /* 最后一个分片标志 */ IPFRAG_STATS_INC(ip_frag.recv);
MIB2_STATS_INC(mib2.ipreasmreqds); fraghdr = (struct ip_hdr *)p->payload; /* 获取IP分片首部 */ if (IPH_HL_BYTES(fraghdr) != IP_HLEN) { /* 不支持带选项字段的IP报文重组 */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: IP options currently not supported!\n"));
IPFRAG_STATS_INC(ip_frag.err);
goto nullreturn;
} offset = IPH_OFFSET_BYTES(fraghdr); /* 获取分片偏移量 */
len = lwip_ntohs(IPH_LEN(fraghdr)); /* 获取分片包长度 */
hlen = IPH_HL_BYTES(fraghdr); /* 获取分片首部长度 */
if (hlen > len) { /* 校验 */
/* invalid datagram */
goto nullreturn;
}
len = (u16_t)(len - hlen); /* 获取IP分片数据字段长度 */ /* 检查是否允许我们排队更多的数据报 */
clen = pbuf_clen(p); /* 获取当前IP分片包含多少个pbuf节点 */
/* 预测当前分片插入重组IP报文后,整个系统的重组IP报文的所有pbuf节点是否超过系统上限值IP_REASS_MAX_PBUFS。
若超过,需要释放老的重组IP报文来腾出空间 */
if ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS) { /* 预测的结果超过系统上限值 */
#if IP_REASS_FREE_OLDEST /* 允许释放老的重组IP报文 */
if (!ip_reass_remove_oldest_datagram(fraghdr, clen) || /* 释放失败 */
((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS)) /* 或者尽可能释放了,但是还是不能满足当前IP分片的插入 */
#endif /* IP_REASS_FREE_OLDEST */
{ /* 需要丢弃当前IP分片 */
/* 没有数据报被释放,仍然有太多的pbuf进入队列 */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: Overflow condition: pbufct=%d, clen=%d, MAX=%d\n",
ip_reass_pbufcount, clen, IP_REASS_MAX_PBUFS));
IPFRAG_STATS_INC(ip_frag.memerr);
/* @todo: 发送ICMP时间超过这里? */
/* 丢弃当前IP分片 */
goto nullreturn;
}
} /* 在当前数据报队列中查找分片所属的重组IP报文,记住队列中的前一个,以便以后退出队列 */
for (ipr = reassdatagrams; ipr != NULL; ipr = ipr->next) {
/* 检查传入的片段是否与当前出现在重组缓冲区中的片段相匹配。如果是这样,我们继续将这个片段复制到缓冲区中 */
if (IP_ADDRESSES_AND_ID_MATCH(&ipr->iphdr, fraghdr)) { /* 匹配成功。根据源IP、目的IP和IP标识这三个字段进行匹配。 */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: matching previous fragment ID=%"X16_F"\n",
lwip_ntohs(IPH_ID(fraghdr))));
IPFRAG_STATS_INC(ip_frag.cachehit);
break; /* 跳出 */
}
} if (ipr == NULL) { /* 还没有当前IP分片的重组IP报文呢,需要新建 */
/* 创建新的重组IP报文,并插入到全局链表reassdatagrams */
ipr = ip_reass_enqueue_new_datagram(fraghdr, clen);
if (ipr == NULL) { /* 创建或插入失败,丢弃当前IP分片 */
goto nullreturn;
}
} else { /* 已经存在 */
if (((lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) == 0) && /* IP报文的第一个IP分片 */
((lwip_ntohs(IPH_OFFSET(&ipr->iphdr)) & IP_OFFMASK) != 0)) { /* 还没有记录过第一个分片的IP首部 */
/* 最先收到的IP分片不一定是IP报文的第一个IP分片。这里需要第一个IP分片的首部是因为对于超过ICMP时间和之后,为了复制所有选项 */
SMEMCPY(&ipr->iphdr, fraghdr, IP_HLEN); /* IP报文的第一个IP分片的IP首部作为整个IP报文首部的基础 */
}
} /* 此时,我们已经创建了一个新重组IP报文,或者指向一个现有重组IP报文 */ /* 检查整个IP报文的长度字段是否合法 */
/* 可以通过最后一个IP分片的偏移量和分片数据区长度可以计算出完整的IP报文的数据区总长度 */
is_last = (IPH_OFFSET(fraghdr) & PP_NTOHS(IP_MF)) == 0;
if (is_last) {
u16_t datagram_len = (u16_t)(offset + len);
if ((datagram_len < offset) || (datagram_len > (0xFFFF - IP_HLEN))) {
/* u16_t 溢出,无法处理此操作 */
goto nullreturn_ipr;
}
}
/* 找到插入这个pbuf的正确位置 */
/* @todo: 如果片段重叠,则修剪pbufs */
valid = ip_reass_chain_frag_into_datagram_and_validate(ipr, p, is_last);
if (valid == IP_REASS_VALIDATE_PBUF_DROPPED) { /* 插入失败,丢弃当前IP分片 */
goto nullreturn_ipr;
}
/* 如果我们来到这里,pbuf已经被加入队列 */ /* 更新重组IP报文数据结构相关字段 */ ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount + clen); /* 更新全局当前所有重组IP报文的pbuf节点数,用于判断是否踩上限线 */
if (is_last) { /* 如果是最后一个分片,则可以知道整个IP报文的长度 */
u16_t datagram_len = (u16_t)(offset + len); /* 偏移量+当前分片IP包数据区长度就是IP报文数据区总长度 */
ipr->datagram_len = datagram_len; /* 转为记录IP报文数据区总长度 */
ipr->flags |= IP_REASS_FLAG_LASTFRAG; /* 标记已经收到最后一个IP分片,且上面长度值改为记录IP报文数据区总长度 */
LWIP_DEBUGF(IP_REASS_DEBUG,
("ip4_reass: last fragment seen, total len %"S16_F"\n",
ipr->datagram_len));
} if (valid == IP_REASS_VALIDATE_TELEGRAM_FINISHED) { /* 全部IP分片接收完毕,可以进行重组 */
struct ip_reassdata *ipr_prev;
u16_t datagram_len = (u16_t)(ipr->datagram_len + IP_HLEN); /* IP报文总长度 */ r = ((struct ip_reass_helper *)ipr->p->payload)->next_pbuf; /* r指向第二个分片 */ /* 将原始IP头复制回第一个pbuf,作为完整IP报文的IP首部 */
fraghdr = (struct ip_hdr *)(ipr->p->payload);
SMEMCPY(fraghdr, &ipr->iphdr, IP_HLEN);
IPH_LEN_SET(fraghdr, lwip_htons(datagram_len));
IPH_OFFSET_SET(fraghdr, 0); /* 清空偏移量和标志字段 */
IPH_CHKSUM_SET(fraghdr, 0); /* 清空首部校验和 */
/* @todo: do we need to set/calculate the correct checksum? */
#if CHECKSUM_GEN_IP
IF__NETIF_CHECKSUM_ENABLED(ip_current_input_netif(), NETIF_CHECKSUM_GEN_IP) {
IPH_CHKSUM_SET(fraghdr, inet_chksum(fraghdr, IP_HLEN)); /* 重置IP首部校验和 */
}
#endif /* CHECKSUM_GEN_IP */ p = ipr->p; /* p指向第一个分片 */ /* 遍历所有分片将其合并到第一个分片的pbuf中 */
while (r != NULL) {
iprh = (struct ip_reass_helper *)r->payload; /* 获取IP分片管理区 */ pbuf_remove_header(r, IP_HLEN); /* 隐藏每个后续片段的IP头 */
pbuf_cat(p, r); /* 合并分片 */
r = iprh->next_pbuf; /* 指向下一个分片。遍历 */
} /* 重组完毕,需要移除重组IP报文相关资源 */ if (ipr == reassdatagrams) { /* reassdatagrams链表是一个没有哨兵的单向非循环链表。如果移除的是首节点,则不需要记录前一个节点 */
ipr_prev = NULL;
} else { /* 如果不是首个节点,则需要知道这个节点的前一个节点,才能把当前节点从单向链表中正常删除 */
for (ipr_prev = reassdatagrams; ipr_prev != NULL; ipr_prev = ipr_prev->next) {
if (ipr_prev->next == ipr) {
break; /* 找到了 */
}
}
} /* 释放重组IP报文的节点 */
ip_reass_dequeue_datagram(ipr, ipr_prev); /* 并调整当前排队等待重组的pbuf的数量 */
clen = pbuf_clen(p);
LWIP_ASSERT("ip_reass_pbufcount >= clen", ip_reass_pbufcount >= clen);
ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount - clen); MIB2_STATS_INC(mib2.ipreasmoks); /* 返回重组后的IP报文pbuf */
return p;
}
/* 还没收到所有IP分片 */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_pbufcount: %d out\n", ip_reass_pbufcount));
return NULL; nullreturn_ipr: /* 丢弃当前IP分片 */
LWIP_ASSERT("ipr != NULL", ipr != NULL);
if (ipr->p == NULL) {
/* 在创建新的数据报条目后删除pbuf:也删除该条目 */
LWIP_ASSERT("not firstalthough just enqueued", ipr == reassdatagrams);
ip_reass_dequeue_datagram(ipr, NULL);
} nullreturn:
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: nullreturn\n"));
IPFRAG_STATS_INC(ip_frag.drop);
pbuf_free(p); /* 丢弃当前IP分片,释放pbuf资源 */
return NULL;
}

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分片包首部。

/**
* Enqueues a new fragment into the fragment queue
* @param fraghdr points to the new fragments IP hdr
* @param clen number of pbufs needed to enqueue (used for freeing other datagrams if not enough space)
* @return A pointer to the queue location into which the fragment was enqueued
*/
static struct ip_reassdata *
ip_reass_enqueue_new_datagram(struct ip_hdr *fraghdr, int clen)
{
struct ip_reassdata *ipr;
#if ! IP_REASS_FREE_OLDEST
LWIP_UNUSED_ARG(clen);
#endif /* 申请一个新的reassdata结构体 */
ipr = (struct ip_reassdata *)memp_malloc(MEMP_REASSDATA);
if (ipr == NULL) {
#if IP_REASS_FREE_OLDEST
/* 空间不足就释放老的重组IP报文条目 */
if (ip_reass_remove_oldest_datagram(fraghdr, clen) >= clen) {
ipr = (struct ip_reassdata *)memp_malloc(MEMP_REASSDATA);
}
if (ipr == NULL)
#endif /* IP_REASS_FREE_OLDEST */
{
IPFRAG_STATS_INC(ip_frag.memerr);
LWIP_DEBUGF(IP_REASS_DEBUG, ("Failed to alloc reassdata struct\n"));
return NULL; /* 申请资源失败 */
}
}
memset(ipr, 0, sizeof(struct ip_reassdata));
ipr->timer = IP_REASS_MAXAGE;/* 有效期赋值 */ /* 插入单向链表头 */
ipr->next = reassdatagrams;
reassdatagrams = ipr;
/* 复制IP头,以便稍后进行测试和输入 */
/* @todo: no ip options supported? */
SMEMCPY(&(ipr->iphdr), fraghdr, IP_HLEN);
return ipr;
}

9.8.3.6 ip_reass_dequeue_datagram()

ip_reass_dequeue_datagram()

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

/**
* Dequeues a datagram from the datagram queue. Doesn't deallocate the pbufs.
* @param ipr points to the queue entry to dequeue
*/
static void
ip_reass_dequeue_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev)
{
if (reassdatagrams == ipr) { /* 如果删除的是链表头,需要更新链表头 */
/* 更新链表头 */
reassdatagrams = ipr->next;
} else {
/* 它不是第一个,所以它必须有一个有效的'prev' */
LWIP_ASSERT("sanity check linked list", prev != NULL);
prev->next = ipr->next; /* 从链表中移除当前重组IP报文条目 */
} /* 释放资源 */
memp_free(MEMP_REASSDATA, ipr);
}

9.8.3.7 ip_reass_free_complete_datagram()

ip_reass_free_complete_datagram()

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

/**
* Free a datagram (struct ip_reassdata) and all its pbufs.
* Updates the total count of enqueued pbufs (ip_reass_pbufcount),
* SNMP counters and sends an ICMP time exceeded packet.
*
* @param ipr datagram to free
* @param prev the previous datagram in the linked list
* @return the number of pbufs freed
*/
static int
ip_reass_free_complete_datagram(struct ip_reassdata *ipr, struct ip_reassdata *prev)
{
u16_t pbufs_freed = 0;
u16_t clen;
struct pbuf *p;
struct ip_reass_helper *iprh; LWIP_ASSERT("prev != ipr", prev != ipr);
if (prev != NULL) {
LWIP_ASSERT("prev->next == ipr", prev->next == ipr);
} MIB2_STATS_INC(mib2.ipreasmfails);
#if LWIP_ICMP
iprh = (struct ip_reass_helper *)ipr->p->payload;
if (iprh->start == 0) {
/* 收到了第一个分片报文,发送ICMP超时 */
/* 首先,从r->p中取出第一个pbuf. */
p = ipr->p;
ipr->p = iprh->next_pbuf;
/* 然后,将原始头部复制过去 */
SMEMCPY(p->payload, &ipr->iphdr, IP_HLEN);
icmp_time_exceeded(p, ICMP_TE_FRAG);
clen = pbuf_clen(p);
LWIP_ASSERT("pbufs_freed + clen <= 0xffff", pbufs_freed + clen <= 0xffff);
pbufs_freed = (u16_t)(pbufs_freed + clen);
pbuf_free(p); /* 释放pbuf */
}
#endif /* LWIP_ICMP */ /* 首先,free接收的所有pbufs。需要分别释放各个pbuf,因为它们还没有被链接 */
p = ipr->p;
while (p != NULL) {
struct pbuf *pcur;
iprh = (struct ip_reass_helper *)p->payload;
pcur = p;
/* 在释放之前获取下一个指针 */
p = iprh->next_pbuf;
clen = pbuf_clen(pcur);
LWIP_ASSERT("pbufs_freed + clen <= 0xffff", pbufs_freed + clen <= 0xffff);
pbufs_freed = (u16_t)(pbufs_freed + clen);
pbuf_free(pcur);
}
/* 然后,从列表中解耦结构ip_reassdata并释放它 */
ip_reass_dequeue_datagram(ipr, prev);
LWIP_ASSERT("ip_reass_pbufcount >= pbufs_freed", ip_reass_pbufcount >= pbufs_freed);
ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount - pbufs_freed); return pbufs_freed;
}

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报文节点为止。

#if IP_REASS_FREE_OLDEST
/**
* Free the oldest datagram to make room for enqueueing new fragments.
* The datagram 'fraghdr' belongs to is not freed!
*
* @param fraghdr IP header of the current fragment
* @param pbufs_needed number of pbufs needed to enqueue
* (used for freeing other datagrams if not enough space)
* @return the number of pbufs freed
*/
static int
ip_reass_remove_oldest_datagram(struct ip_hdr *fraghdr, int pbufs_needed)
{
/* @todo Can't we simply remove the last datagram in the linked list behind reassdatagrams? */
struct ip_reassdata *r, *oldest, *prev, *oldest_prev;
int pbufs_freed = 0, pbufs_freed_current;
int other_datagrams; /* 释放数据报,直到允许进入'pbufs_needed' pbufs队列,但不要释放'fraghdr'所属的数据报! */
do { /* 外循环:每次循环最多释放一个重组IP报文,但是不一定释放够pbuf节点数。 */
oldest = NULL;
prev = NULL;
oldest_prev = NULL; /* 被删除的节点的前一个节点。单向链表删除节点的逻辑需要。 */
other_datagrams = 0;
r = reassdatagrams;
while (r != NULL) { /* 内循环:遍历reassdatagrams,找出其它最老的重组IP报文 */
if (!IP_ADDRESSES_AND_ID_MATCH(&r->iphdr, fraghdr)) { /* 和fraghdr不是同一个数据报,都需要被检索 */
/* 记录和fraghdr不是同一个数据报个数 */
other_datagrams++;
if (oldest == NULL) {
oldest = r;
oldest_prev = prev;
} else if (r->timer <= oldest->timer) {
/* 比之前最老的还要老 */
oldest = r;
oldest_prev = prev;
}
}
if (r->next != NULL) {
prev = r;
}
r = r->next;
}
if (oldest != NULL) {
/* 释放资源 */
pbufs_freed_current = ip_reass_free_complete_datagram(oldest, oldest_prev);
pbufs_freed += pbufs_freed_current; /* 记录已经释放了多少个pbuf节点 */
}
} while ((pbufs_freed < pbufs_needed) && (other_datagrams > 1)); /* 如果释放的pbuf节点满足要求,或者没有其它pbuf节点可以释放了,就退出循环 */
return pbufs_freed; /* 返回释放pbuf节点的个数 */
}
#endif /* IP_REASS_FREE_OLDEST */

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分片收到了,就检查下所有分片是否都收到。

/**
* Chain a new pbuf into the pbuf list that composes the datagram. The pbuf list
* will grow over time as new pbufs are rx.
* Also checks that the datagram passes basic continuity checks (if the last
* fragment was received at least once).
* @param ipr points to the reassembly state
* @param new_p points to the pbuf for the current fragment
* @param is_last is 1 if this pbuf has MF==0 (ipr->flags not updated yet)
* @return see IP_REASS_VALIDATE_* defines
*/
static int
ip_reass_chain_frag_into_datagram_and_validate(struct ip_reassdata *ipr, struct pbuf *new_p, int is_last)
{
struct ip_reass_helper *iprh, *iprh_tmp, *iprh_prev = NULL;
struct pbuf *q;
u16_t offset, len;
u8_t hlen;
struct ip_hdr *fraghdr;
int valid = 1; /* 从当前分片中提取IP长度和IP分片偏移量 */
fraghdr = (struct ip_hdr *)new_p->payload;
len = lwip_ntohs(IPH_LEN(fraghdr));
hlen = IPH_HL_BYTES(fraghdr);
if (hlen > len) {
/* 无效的数据报 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
len = (u16_t)(len - hlen); /* 获取IP分片数据区长度 */
offset = IPH_OFFSET_BYTES(fraghdr); /* 获取偏移量 */ /* IP分片的首部有用的信息已经统计到重组IP报文节点首部了,
即是IP分片的首部中的数据可以丢弃,该空间可以利用起来,
当前用于分片连接节点,指向下一个分片。
即是把IP分片的首部部分空间重置为struct ip_reass_helper数据结构。 */
LWIP_ASSERT("sizeof(struct ip_reass_helper) <= IP_HLEN",
sizeof(struct ip_reass_helper) <= IP_HLEN);
iprh = (struct ip_reass_helper *)new_p->payload; /* 提前分片IP首部重置为struct ip_reass_helper */
iprh->next_pbuf = NULL;
iprh->start = offset; /* 偏移量起始 */
iprh->end = (u16_t)(offset + len); /* 偏移量尾部 */
if (iprh->end < offset) {
/* u16_t 溢出,无法处理此操作 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
} /* 插入分片链表。按分片偏移量升序插入 */
for (q = ipr->p; q != NULL;) { /* 遍历分片链表 */
iprh_tmp = (struct ip_reass_helper *)q->payload; /* 分片节点 */
if (iprh->start < iprh_tmp->start) { /* 找到位置 */
iprh->next_pbuf = q; /* 插入操作(链表) */
if (iprh_prev != NULL) { /* 插入位置前面也有IP分片 */
/* 而不是具有最低偏移量的分片 */
#if IP_REASS_CHECK_OVERLAP
if ((iprh->start < iprh_prev->end) || (iprh->end > iprh_tmp->start)) {
/* 插入分片与前一分片或后一分片重叠,丢弃 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
#endif /* IP_REASS_CHECK_OVERLAP */
iprh_prev->next_pbuf = new_p; /* 插入操作(链表) */
if (iprh_prev->end != iprh->start) {
/* 前一个分片与当前分片不连续,中间还缺数据,所以可以标记还没有接收到所有IP分片 */
valid = 0;
}
} else { /* 当前分片插入的位置在接收到的所有分片之前 */
#if IP_REASS_CHECK_OVERLAP
if (iprh->end > iprh_tmp->start) {
/* 分片与下一个分片重叠,丢弃 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
#endif /* IP_REASS_CHECK_OVERLAP */
/* 更新链表头 */
ipr->p = new_p;
}
break;
} else if (iprh->start == iprh_tmp->start) { /* 收到重叠IP分片,丢弃 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
#if IP_REASS_CHECK_OVERLAP
} else if (iprh->start < iprh_tmp->end) { /* 重叠:直接丢弃,不需要保留新数据报 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
#endif /* IP_REASS_CHECK_OVERLAP */
} else { /* 插入分片偏移量比所有分片都要大,即是要插到链表最后 */
/* 检查到目前为止收到的碎片是否没有洞 */
if (iprh_prev != NULL) {
if (iprh_prev->end != iprh_tmp->start) {
/* 数据不连续,当前分片和前一个分片之间缺少分片,标记下未接收完所有IP分片 */
valid = 0;
}
}
}
q = iprh_tmp->next_pbuf; /* 遍历下一个分片 */
iprh_prev = iprh_tmp; /* 保存前一个分片 */
} /* 如果q为空,即是到达链表尾了 */
if (q == NULL) {
if (iprh_prev != NULL) { /* 如果 */
/* 这是(目前),偏移量最大的分片:插入链表尾 */
#if IP_REASS_CHECK_OVERLAP
LWIP_ASSERT("check fragments don't overlap", iprh_prev->end <= iprh->start);
#endif /* IP_REASS_CHECK_OVERLAP */
iprh_prev->next_pbuf = new_p; /* 插入 */
if (iprh_prev->end != iprh->start) { /* 还缺少IP分片 */
valid = 0; /* 标记还没接收到所有IP分片 */
}
} else { /* 当前重组IP报文中还没有IP分片 */
#if IP_REASS_CHECK_OVERLAP
LWIP_ASSERT("no previous fragment, this must be the first fragment!",
ipr->p == NULL);
#endif /* IP_REASS_CHECK_OVERLAP */
/* 这是我们收到的这个IP数据报的第一个分片 */
ipr->p = new_p;
}
} /* 至此,插入完毕,检查是否已经收到所有IP分片 */ /* 如果我们已经收到了最后一个碎片 */
if (is_last || ((ipr->flags & IP_REASS_FLAG_LASTFRAG) != 0)) { /* 最后一个分片收到了 */
if (valid) { /* 前面遍历插入分片前面的所有分片时也没有发现有数据不连续 */
/* 继续检查后面没有被遍历过的IP分片 */
if ((ipr->p == NULL) || (((struct ip_reass_helper *)ipr->p->payload)->start != 0)) { /* 还没收到第一个分片 */
valid = 0; /* 标记还没接收到所有IP分片 */
} else { /* 首尾IP分片都收到了,需要遍历剩余的 */
/* 并且检查这个数据报之后是否没有漏洞 */
iprh_prev = iprh;
q = iprh->next_pbuf;
while (q != NULL) { /* 遍历 */
iprh = (struct ip_reass_helper *)q->payload;
if (iprh_prev->end != iprh->start) { /* 发现漏洞 */
valid = 0; /* 标记还没有接收到所有IP分片 */
break; /* 退出遍历 */
}
iprh_prev = iprh;
q = iprh->next_pbuf;
}
if (valid) { /* 至此,说明当前IP报文的所有IP分片已到达,打个log庆祝下 */
LWIP_ASSERT("sanity check", ipr->p != NULL);
LWIP_ASSERT("sanity check",
((struct ip_reass_helper *)ipr->p->payload) != iprh);
LWIP_ASSERT("validate_datagram:next_pbuf!=NULL",
iprh->next_pbuf == NULL);
}
}
}
/* 按需返回 */
return valid ? IP_REASS_VALIDATE_TELEGRAM_FINISHED : IP_REASS_VALIDATE_PBUF_QUEUED;
}
/* 至此,IP分片插入正常,但是还没有收到所有IP分片 */
return IP_REASS_VALIDATE_PBUF_QUEUED;
}

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报文有效期,过期的删除,未过期的减少有效期。

/**
* Reassembly timer base function
* for both NO_SYS == 0 and 1 (!).
*
* Should be called every 1000 msec (defined by IP_TMR_INTERVAL).
*/
void
ip_reass_tmr(void)
{
struct ip_reassdata *r, *prev = NULL; r = reassdatagrams;
while (r != NULL) {
/* 减量计时器,一旦它达到0,清理不完整的分片 */
if (r->timer > 0) {
r->timer--; /* 有效期减少 */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_tmr: timer dec %"U16_F"\n", (u16_t)r->timer));
prev = r;
r = r->next; /* 遍历下一个 */
} else { /* 当前重组IP条目已经过期了 */
struct ip_reassdata *tmp; /* 过渡变量 */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_tmr: timer timed out\n"));
tmp = r;
/* 在释放之前获取下一个重组IP报文指针 */
r = r->next;
/* 删除过期的重组IP报文:删除重组IP报文节点资源及其所有的IP分片pbuf */
ip_reass_free_complete_datagram(tmp, prev);
}
}
}

【lwip】09-IPv4协议&超全源码实现分析的相关教程结束。

《【lwip】09-IPv4协议&超全源码实现分析.doc》

下载本文的Word格式文档,以方便收藏与打印。