嵌入式 八月 20, 2021

IP报文基础及其在Lwip的实现

文章字数 10k 阅读约需 9 mins. 阅读次数 1000000

  本文主要介绍IP数据报的格式,以及IP相关功能在Lwip中的实现方式,希望能对同样在学习Lwip的小伙伴们有所帮助。

1. IP的背景

  IP协议是TCP/IP协议中最为核心的协议,所有的TCP、UDP、ICMP及IGMP数据都已IP数据报格式传输。IP协议在 TCP/IP协议族的分层中属于网络层,不难理解,IP的主要作用有两个:其一是对上层协议[1]的数据进行封装(增加IP首部),然后交给链路层协议进行发送;其二是对链路层接收到的数据进行解析,并根据解析结果将数据交给对应的上层协议进行处理。

2. IP基础知识介绍

2.1 IP数据报的格式

  IP数据报的格式主要包含IP首部和数据,通常情况下,IP首部的长度为20字节(含有选项字段的除外),具体如下图所示:

IP数据报格式

2.1.1 版本

  协议版本号,当前普遍应用的IP协议版本号是4,因此也常称为IPv4。

2.1.2 首部长度

  首部长度即IP首部所占用的32bit字的数目。因为首部长度是一个4bit段,其最大值是15,也就意味着IP首部的最大长度是60字节。

2.1.3 服务类型(TOS)

  服务类型字段长8位,最初的TOS字段中最高3位表示优先权,随后的4位表示服务类型,最后一位保留,恒设置为0。

  当前的服务类型字段已经作为区分服务(Diffserv)架构的一部分被重新定义了,重定义的服务类型字段中前6位构成了区分代码点(DiffServ Code Point, DSCP),后2位用于显示拥塞通知(Explicit Congestion Notification, ECN)。这两个概念不是很易懂,在Lwip中,该字段由上层协议设定,经过在windows系统下的随机抓包发现,该字段通常设置位0x00,因此对该字段不再深究。

2.1.4 总长度

  总长度即整个IP数据报的长度,单位是字节。因为总长度是一个16位字段,其最大值是65535,所以IP数据报的最大长度位65535字节。利用总长度和首部长度两个字段就可以知道IP数据报中数据的起始位置和长度。

注意总长度和首部长度的单位不同,总长度的单位是字节,而首部长度的单位是32位字。

2.1.5 标识

  标识字段唯一的标识发送的每一份数据报,通常每发送一份报文它的值就会加1。在Lwip的源文件”ip4.c”中,定义了一个变量static u16_t ip_id用于记录IP数据报的唯一标识,其初始值为0,每次发送IP数据报时都将该变量的值填入IP首部的标识字段中,然后将该变量+1。

2.1.6 标志

  标志字段共有3位,其中第1位没有使用。第2位是不分段(DF)位,置1表示路由器不能对数据进行分段处理,如果数据包由于不能被分段而未能被转发,则路由器会丢弃该数据包并向源点发送错误消息。第三位是还有更多分段(MF)位,即表示数据还有其他的分段,当路由器对数据包进行分段时,除最后一个分段外,其他所有的分段MF位都应置1,以便接收端直到接收到MF位为0的分段为止。

2.1.7 生存时间(TTL)

  生存时间反应了从源IP到目的IP经过的路由器数量即跳数。在最初创建IP数据报时,TTL被设置为一个特定的值,通常是32或者64,数据报每经过一个路由器,该路由器就将TTL减1,因此可以通过TTL的值推算IP数据报经过的跳数。

2.1.8 协议

  协议字段给出了IP协议上层协议的协议号,当前已分配的协议号有100多个,下表列出了一些常见的协议号,更多的协议号可以自百度。

协议号 协议
1 Internet消息控制协议(ICMP)
2 Internet组管理协议(IGMP)
4 被IP协议封装的IP
6 传输控制协议(TCP)
17 用户数据报协议(UDP)

2.1.9 首部检验和

  首部检验和是针对IP首部的纠错字段,检验和不计算被封装的数据。检验和的计算方法和验证方法将在后文的代码分析部分进行介绍。

2.1.10 源地址和目的地址

  顾名思义,指IP数据报的来源和目的IP地址,我们平时常见的IP地址如“192.168.0.1”是IP地址的点分十进制表示方法,是方便人读写和记忆的,IP地址对于电子设备来说是一个32位的二进制数。

2.1.11 可选项

  首先,可选项不是所有IP数据报都有的,可选项可以包含源点产生的信息和路由器产生的信息;其次,可选项的长度必须是32位的整数倍,最长长度是40个字节,这一点从首部长度的定义也可以看得出来,如果长度不是32位的整数倍,可以在结尾补0以满足要求。

  常用的可选项有安全和处理限制(常用于军事领域)、记录路径、时间戳、宽松的源站选路、严格的源站选路。因为使用频率很低而且并非所有的主机和路由器都支持这些可选项,此处不再详细叙述,感兴趣的小伙伴可以自行查阅资料进行了解。

2.2 IP地址基础知识

  前面的已经提到,IP地址的是一个32位的二进制数,为了方便记忆和读写,常用点分十进制的方法来表示IP地址。IP地址中包含了网络号和主机号,可能还包含有子网号,为了能正确的从IP地址中获取网络号、主机号、子网号,还需要有子网掩码的辅助。本小结简要介绍IP的点分十进制表示方法,网络号、主机号、子网号和子网掩码的含义。

2.2.1 IP地址的点分十进制表示方法

  为了方便记忆和表述,将32位的IP地址分为4个字节,将每个字节都以十进制表示,在四个字节转换得到的十进制数中间分别加一个点用以区分,这就是我们常见的IP地址的形式。

  比如面这个IP地址(11000000101010000000000100000001)2,直接用二进制表示显得很长,也很难记忆,直接写成10进制数是(3,232,235,777)10,写成16进制数是(0xC0A8 0101),都比较难以记忆,而用点分十进制表示方法则可以写成“192.160.1.1”,显然是一个我们经常会接触到的一个IP地址。由此可见,点分十进制表示方法,在日常使用中是非常方便的。

2.2.2 网络号、主机号和子网号

  简单的讲,互联网是把一个个小的网络链接起来组成一个庞大的网络,因此要找到一个IP地址,首先要找到这个IP地址处于哪个小的网络中,然后再找到这个IP对应于这个网络中的哪一台主机。因此IP地址就分成了网络号和主机号两个部分。实际上,根据网络号找到一个网络后,这个网络可能会划分成几个更小的网络组成的,也就是说我们还需要再找到该IP属于这个网络的哪一个子网,然后才能找到这台主机。而要找到IP属于哪一个子网,自然也就需要一个子网号。因为不是所有的网络都会划分子网,因此并不是所有的IP地址都包含子网号的。

2.2.3 IP地址的分类

  前文提到了IP地址包含了网络号、主机号,可能还有子网号,那么如果我们已知一个IP地址,如何确定这个IP是否含有子网号,它的网络号、子网号(如果有)、主机号分别是什么呢?

  要解决这个问题,首先要了解IP地址的分类:A类地址、B类地址和C类地址。A类地址的前8位表示网络号,并且其最高位恒位0,因此其高8位转换成十进制的范围是1~126[2],如果用点分十进制的方式表示IP地址,那么A类IP地址的第一段应该在1到126中间;B类地址的前16位表示网络号,其最高l两位恒为(10)2,其高8位转换成10进制的范围是128~191;C类地址的前24位表示网络号,其最高三位恒为(110)2,高8位转换位10进制的范围是192~223。

  根据上述规则,就可以确定一个给定IP地址的网络号。

2.2.4 子网掩码

  确定一个IP地址的网络号后,还需要确定其子网号和主机号。前文叙述中,有时会把子网号放在主机号之后,这是因为子网号并不总是存在,但在IP地址中的顺序实际上是网络号、子网号(如果有)、主机号。确定网络号后,必须先确定该IP是否含有子网号,子网号是多少(如果有),然后才能获取主机号。

  要确定IP中的子网号,就需要子网掩码的辅助了。子网掩码也是一个32位的二进制数,并且通常也以点分十进制的方式表示,这一点与IP地址类似。其作用是告诉主机IP地址中有多少位是用来表示子网号的(实际上是告诉主机IP地址中网络号和子网号加起来有多少位),子网掩码中值位1的比特留给网络号和子网号,值为0的比特留给主机号。由于已知网络号的位数,通过子网掩码可以知道网络号和子网号的总位数,自然也就能区分出IP地址中的网络号、子网号和主机号了。

  以上文提到的IP地址“192.168.1.1”为例,假设其子网掩码为“255.255.255.0”。首先根据第一段的值“192”可以确定该IP地址属于C类IP,有24位表示网络号,其网络号位“192.168.1”。子网掩码的高24位为1,低8位为0,说明网络号和子网号加起来共计24位,也就是说这里没有进行子网划分,那么其主机号就是“1”。

  因为IP地址总是按网络号、子网号、主机号排列的,不难理解子网掩码的一个特征:高位是连续的1,低位是连续的0。在Lwip的实现中,“ip4_aaddr.c”中有一个检验子网掩码合法性的函数ip4_addr_netmask_valid,就是利用这个特征,对于一个待检验的数,从最高位起,找到第一个0,然后再检查第一个0后面还有没有1,如果没有,说明待检验数是一个合法的子网掩码。

3. IP相关功能在 Lwip 中的实现

3.1 ip_hdr结构体简介

  ip_hdr结构体就是IP首部的结构体,其定义如下:

struct ip_hdr {
  PACK_STRUCT_FLD_8(u8_t _v_hl);
  PACK_STRUCT_FLD_8(u8_t _tos);
  PACK_STRUCT_FIELD(u16_t _len);
  PACK_STRUCT_FIELD(u16_t _id);
  PACK_STRUCT_FIELD(u16_t _offset);
  PACK_STRUCT_FLD_8(u8_t _ttl);
  PACK_STRUCT_FLD_8(u8_t _proto);
  PACK_STRUCT_FIELD(u16_t _chksum);
  PACK_STRUCT_FLD_S(ip4_addr_p_t src);
  PACK_STRUCT_FLD_S(ip4_addr_p_t dest);
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

  在源代码中包含了对每个元素的注释,结合注释和IP首部的格式,不难理解各元素所代表的的意义。需要注意的一点是,IP首部并不都是按字节定义数据的含义的,但是在ip_hdr结构体中,对部分字段进行了合并,方便了结构体定义。IP首部的版本和首部长度两个字段,其长度都是4位,并且位置相邻,因此在ip_hdr结构体总将这两个字段合并在一起,定义了一个8位的变量_v_hl,其中的”v”便是version,”hl”即head lenth。IP首部的3位标志字段和13位的片偏移字段则合并成一个16位的变量_offset。剩余变量则都与IP首部的字段一一对应,小伙伴们可自行翻阅源码,并对照IP数据报格式自行理解。

  从结构体定义来看,结构体中并没有可选项相关的元素,但这并不意味着Lwip没有支持可选项的能力。

3.2 Lwip中IP层数据发送流程简介

  Lwip中,IP层对发送数据的处理依次经过了ip4_outputip4_output_ifip4_output_if_optip4_output_if_opt_src四个函数。ip4_output函数中,根据目的IP地址确定了该IP数据报的发送路径(即通过哪个网卡发送该数据报),并将传入的数据和网卡信息传递给函数ip4_output_if。在当前定义中,函数ip4_output_if没有做任何操作,直接将数据传送给函数ip4_output_if_optip4_output_if_optip4_output_if多出的两个形参void *ip_optionsu16_t optlen分别传入了实参空指针和0。函数ip4_output_if_opt中根据选定的网卡确定了源地址,然后再次将所有数据传递给函数ip4_output_if_opt_src。函数ip4_output_if_opt_src根据传入的信息,完成IP报文首部的填充,然后调用网卡的发送函数,将IP数据报交由链路层处理。每个函数的具体实现,请小伙伴们自行阅读源码。

  接下来讨论下Lwip对IP首部可选项的处理。首先可以确定的是,Lwip是有处理可选项的能力的,前面提到ip4_output_if_optip4_output_if多出两个形参,这连个形参实际上就是指可选项的内容和长度,在传入实参时直接传入了空指针和0,也就是说所有以函数ip4_output为处理起点的IP数据报,都是没有可选项的。如果想要发送一个首部含有可选项的IP数据报,则上层协议需要调用函数ip4_output_if_opt,而以该函数位处理起点的IP数据报,就跳过了查找发送路径这一功能。在源码”ip4.h”中,有一句注释“Currently, the function ip_output_if_opt() is only used with IGMP ”,据此推测,只有IGMP协议在发送IP数据报时调用的是函数ip4_output_if_opt,而其他协议则调用ip4_output函数,至于该推测是否准确,需要分析上层协议的源码,本文不再进一步扩展。

  首部检验和的计算方法。关于首部检验和的计算方法,不同的参考资料中描述有所不同。也许是因为协议发展过程中有所改变,也许是有些书籍中出现了谬误,也许是这些不同的描述本质上是一样的(这设计到比较深奥的数学知识,已经超出了我的能力范畴),因此吧检验的介绍放到源码分析的部分来讲解,结合代码中的实际操作,确保给小伙伴们呈现除一个正确的计算方法。首部检验和的计算,属于IP首部填充的一部分,其源码实现包含在ip4_output_if_opt_src中。计算的方法是,定义一个32位的变量chk_sum并初始化为0,然后将IP首部的内容按16位字相加,注意此处IP首部的检验和字段本身无需参与运算,然后将得到的结果进行如下处理:

chk_sum = (chk_sum >> 16) + (chk_sum & 0xFFFF);
chk_sum = (chk_sum >> 16) + chk_sum;
chk_sum = ~chk_sum;

最终得到的结果即为检验和,将其填入检验和字段即可。

3.3 Lwip中接收数据处理简介

  Lwip中IP层处理接收数据的函数是ip4_input。该函数首先对IP数据报的合法性进行检查,包括IP版本、检验和、长度等信息的检查,然后判断IP数据报是否确实是却要本机接收的,如果满足以上条件,则根据IP首部中的协议字段,将IP数据报转发给对应的上层协议进行处理。

4 参考资料

  1. RT-TREAD Lwip组件v2.0.2源码;
  2. 《TCP-IP详解卷一:协议》;
  3. 《TCP/IP路由技术(第一卷)(第二版)》。

[1] ICMP 和 IGMP 与IP同样属于网络层,但是因为其数据也需要以IP数据报的格式传输,所以也可以看做是IP的上层协议。
[2] 网络号不能为全0或全1。

0%