lwIP2.1.x 更新详情
Posted zhzht19861011
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lwIP2.1.x 更新详情相关的知识,希望对你有一定的参考价值。
主要BUG修复和性能改善
- 新增对 IPv6 协议的支持
- 重写
ppp
模块(与apache pppd
保持同步) - 重写
SNMP
模块(包括MIB
解析器) - 修复
DHCP
模块的一个 BUG,该 BUG 可能导致损失DHCP
租约时间 - 增强RX处理路径的健壮性
- 支持
TCP
窗口缩放 - 支持
FreeRTOS-MPU
- 增强
DNS
健壮性 RX
数据包支持使用PBUF_REF
- 增加配置宏
LWIP_NETCONN_FULLDUPLEX
,允许netconn/sockets
用于从每个单独的线程读取/写入 - 移动和重新排序统计信息(主要用于
memp/mib2
)
网络接口标志
修改网络接口 netif 的up
标志为管理标志。up
标志不再具有以前的“IP4 地址有效”含义,现在,如果一个网络接口 netif 没有设置up
标志,则用户不能在这个网络接口上收发数据!即便是使用 DHCP,在启动 DHCP 客户端之前,也要将 netif 设置为up
状态。
这与lwIP 1.4.1版本不同。可以从两个版本的函数注释上看出来:
lwIP 1.4.1版本 | lwIP 2.1.2版本 |
---|---|
设置网络接口netif为up 状态,可用于处理流量。注意:在为 down 状态的网络接口上启用DHCP,DHCP完成配置后会将接口设置为up 状态。 | 设置网络接口netif为up 状态,可用于处理流量。 |
通过调用函数netif_set_up(netif)
,将指定的netif设置为up
状态。代码如下:
void netif_set_up(struct netif *netif)
if (!(netif->flags & NETIF_FLAG_UP))
netif->flags |= NETIF_FLAG_UP;
#if LWIP_SNMP
snmp_get_sysuptime(&netif->ts);
#endif /* LWIP_SNMP */
NETIF_STATUS_CALLBACK(netif);
if (netif->flags & NETIF_FLAG_LINK_UP)
netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV4|NETIF_REPORT_TYPE_IPV6);
可以看到将网络接口设置为up
状态是通过为标志
字段增加NETIF_FLAG_UP
标志实现的。
netif_set_up(netif)
做什么?
lwIP协议栈使用一个名为
netif
的网络接口结构来描述物理网卡。负责控制和维护物理网卡状态的函数有数个,其中就有netif_set_up
和netif_down_up
。
netif_set_up
设置网络接口为up
,这是一个软件标志,用于控制网络接口使能。
具体怎么控制?
在IP层,函数
ip_route(ip_addr_t *dest)
根据指定的目的IP地址找到正确的网络接口 netif。这个函数会判断网络接口是否为up
状态,如果网络接口没有up
,则返回NULL
,即没有找到合适的网络接口:// 节选自 lwip1.4.1 /* iterate through netifs */ for (netif = netif_list; netif != NULL; netif = netif->next) /* network mask matches? */ if (netif_is_up(netif)) //<--这里 if (ip_addr_netcmp(dest, &(netif->ip_addr), &(netif->netmask))) /* return netif on which to forward IP packet */ return netif; if ((netif_default == NULL) || (!netif_is_up(netif_default))) //<--这里 LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip_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); snmp_inc_ipoutnoroutes(); return NULL; // 注意: lwip 2.1.x 略有不同, 不但检查了netif 是否处于 up 状态, 还检查了物理层连接是否处于 up 状态: // 节选自lwip 2.1.3 /* is the netif up, does it have a link and a valid address? */ if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) /* network mask matches? */ if (ip4_addr_netcmp(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) /* return netif on which to forward IP packet */ return netif; /* gateway matches on a non broadcast interface? (i.e. peer in a point to point interface) */ if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_cmp(dest, netif_ip4_gw(netif))) /* return netif on which to forward IP packet */ return 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)) /* No matching netif found and default netif is not usable. If this is not good enough for you, use LWIP_HOOK_IP4_ROUTE() */ 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;
我们再看看应用层的发送函数:
- UDP的发送API函数
udp_send
和udp_sendto
都会调用ip_route
;- TCP的发送API函数
tcp_output
也会调用ip_route
所以换句话说,如果一个netif没有设置成
up
状态,应用层发送的数据将被忽略。还是在IP层,函数
ip_input
用于处理IP数据包。函数会根据收的的IP数据包的目的IP找到正确的网络接口netif。在找的过程中,会判断网络接口是否是up
状态,如果网络接口没有up
,则表示找不到网络接口。如果网络接口没有找到,并且数据包不是来自DHCP服务器则数据被丢掉。
/* 寻找网络接口 */ if ((netif_is_up(netif)) && (!ip_addr_isany(&(netif->ip_addr)))) //<--这里 /* unicast to this interface address? */ if (ip_addr_cmp(¤t_iphdr_dest, &(netif->ip_addr)) || /* or broadcast on this interface network address? */ ip_addr_isbroadcast(¤t_iphdr_dest, netif)) LWIP_DEBUGF(IP_DEBUG, ("ip_input: packet accepted on interface %c%c\\n", netif->name[0], netif->name[1])); /* break out of for loop */ break;
在应用层,无论是TCP还是UDP接收API函数,都是从
ip_input
获取的数据,所以如果一个netif没有设置成up
状态,应用层将接收不到数据。
协议栈通过一个软件标志,就可以把应用层发送和接收拿捏的死死的,这样可以方便的处理数量流量。但这只是针对应用层而言的,因为 lwip1.4.1 协议栈给DHCP开了后门。
DHCP发送数据时,直接调用udp_sendto_if
,绕过了ip_route
函数,即使netif没有设置up
也可以发送数据。
接收方面,接收处理函数专门对DHCP开了绿灯,即便找不到合适的网络接口,也能将数据递交给HDCP处理函数,而且师出有名,有RFC 1542
背书。
这就产生了不一致:如果使用静态IP,用户必须调用函数netif_set_up
,将网络接口设置为up
状态,但是如果使用DHCP动态获取IP,在分配到IP后,由DHCP客户端将网络接口设置为up
状态。
DHCP客户端的这种操作,会让很多人误用up
标志。网络接口设置为up
状态的时候,就意味着设备已经分配到IP地址,所以很多人会检查网络接口是否为up
状态来判断IP地址是否有效。这让这个标志产生了二义性:本来是一个方便处理数据流量的软件标志,又担当了**“IP地址有效”**的含义。
所以早在2012年8月,lwIP的开发人员Simon Goldschmidt
就提出了这个问题:
网络接口netif为
down
状态时,DHCP 和 AutoIP 仍可以在这个网络接口上正确的发送和接收数据。我认为这样不够清晰,应该只有一种方式,即在发送或接收时,netif 必须始终处于
up
状态。DHCP 和AutoIP 的初始化阶段应该只是将 netif 的 IP 地址设置为 ‘0.0.0.0’,并且路由器应该忽略使用 ‘0.0.0.0’ IP 的 netif。
然后在2015年3月份,Simon Goldschmidt
修复了netif up/down
处理不清晰的问题,也就是本节一开提到的更新内容:
修改网络接口 netif 的
up
标志为管理标志。up
标志不再具有以前的“IP4 地址有效”含义,现在,如果一个网络接口 netif 没有设置up
标志,则用户不能在这个网络接口上收发数据!即便是使用 DHCP,在启动 DHCP 客户端之前,也要将 netif 设置为up
状态。
lwip2.1.x 是如何堵住DHCP的呢?
-
在 dhcp.c 中,去除所有
netif_set_up
和netif_set_down
函数。即 DHCP 不能更新 netif 标志。 -
在 dhcp.c 的
dhcp_start
函数,增加代码:LWIP_ERROR("netif is not up, old style port?", netif_is_up(netif), return ERR_ARG;);
如果 netif 不是
up
状态,则函数返回参数错误
(ERR_ARG)代码。即如果 netif 不是up
状态,DHCP不能启动。
小插曲:
2015年3月Simon Goldschmidt
修复了netif up/down
处理不清晰的问题后,函数netif_set_up
的注释并没有修改,注释仍显示在为down
状态的网络接口上启用DHCP,DHCP完成配置后会将接口设置为up
状态。直到2015年的10月份,才由Erik Ekman
修改了这个函数的注释,变成了2.1.2版本现在的样子。
IPv6
增加了 IPv6 功能(双栈或 IPv4/IPv6 二选一)
1.4.1 版本也有 IPv6,但那是实验性质的(见…\\lwip-1.4.1\\src\\core\\ipv6目录下的README
文件),并不能在实际项目中使用的。
ipv6目录下的
README
文件内容:IPv6 support in lwIP is very experimental.
在 2011 年 5 月,开发者Ivan Delamer
编写了可以并行运行 IPv4 和 IPv6 的双栈版本,这也是 lwIP 的第一个可以工作的 IPv6 版本。现在我们在…\\lwip-2.1.2\\src\\core\\ipv6 目录下看到的文件,也是在这个时候成型的。但这个版本仍很初级,并需要大量测试。
所以 2012 年 12 月发布的 lwIP 1.4.1 正式版中并不包括Ivan Delamer
编写的IPv6模块。
时间来到了 2016 年 8 月 3 日。这天,开发者goldsimon
删除了 ipv6 目录下的README
文件,并在提交日志中写下了 IPv6 再也不是实验性质的了 😃:
IPv6 is NOT experimental any more 😃
此时距离第一个可以工作的 IPv6 版本,已经过去了 5 年多,有关 IPv6 的修改提交多达 350+ 次。
改善 IPv4/v6 的地址处理
2011 年 5 月,lwIP1.4.1 版本,IP 地址使用类型ip_addr_t
表示。
#define IP_PCB \\
/* ip addresses in network byte order */ \\
ip_addr_t local_ip; \\
ip_addr_t remote_ip; \\
/* Socket options */ \\
u8_t so_options; \\
/* Type Of Service */ \\
u8_t tos; \\
/* Time To Live */ \\
u8_t ttl \\
/* link layer address resolution hint */ \\
IP_PCB_ADDRHINT
2011 年 5 月 18,lwIP 终于有了第一个可以并行运行 IPv4 和 IPv6 的双栈版本。随之而来的是,ip.h 文件中宏IP_PCB
的定义也发生了变化,变量local_ip
和remote_ip
需要兼容ipv6,所以IP地址使用了联合体表示:
#define IP_PCB \\
IP_PCB_ISIPV6 \\
/* ip addresses in network byte order */ \\
union \\ /*<--这里*/
ip_addr_t ip4; \\
IP_PCB_IP6 \\
local_ip; \\
union \\ /*<--这里*/
ip_addr_t ip4; \\
IP_PCB_IP6 \\
remote_ip; \\
/* Socket options */ \\
u8_t so_options; \\
/* Type Of Service */ \\
u8_t tos; \\
/* Time To Live */ \\
u8_t ttl \\
/* link layer address resolution hint */ \\
IP_PCB_ADDRHINT
为了使代码更具可读性,也为了尽可能的合并 IPv4 和 IPv6 代码,2011 年 5 月 26 日,宏IP_PCB
有了新的变化:
#define IP_PCB \\
IP_PCB_ISIPV6_MEMBER \\
/* ip addresses in network byte order */ \\
ipX_addr_t local_ip; \\ /*<--这里*/
ipX_addr_t remote_ip; \\ /*<--这里*/
/* Socket options */ \\
u8_t so_options; \\
/* Type Of Service */ \\
u8_t tos; \\
/* Time To Live */ \\
u8_t ttl \\
/* link layer address resolution hint */ \\
IP_PCB_ADDRHINT
这次变化很小,只是定义了一个新的IP地址类型ipX_addr_t
,用来代替上个版本的联合体:
#if LWIP_IPV6
/* A union struct for both IP version's addresses. */
typedef union
ip_addr_t ip4;
ip6_addr_t ip6;
ipX_addr_t;
#else /* LWIP_IPV6 */
typedef ip_addr_t ipX_addr_t;
#endif /* LWIP_IPV6 */
不知道你们是如何看待新的类型ipX_addr_t
,我第一次看到它,虽然我还不怎么明白这个类型的含义,就觉得它不应该出现在源码中。
新的类型ipX_addr_t
与 lwIP 代码风格很不搭,也容易让人疑惑,大写的X
到底代表什么意思。
两年后,2013 年 6 月。Simon Goldschmidt
表示对目前的 IPv6,有一件事情仍然不满意,那就是ipX_addr_t
的实现。有几点原因:
-
不够不清晰。虽然类型
ipX_addr_t
同时涵盖 IPv4 和 IPv6 地址,但是一旦独立于 PCB(ip_pcb
)使用,仍无法分辨类型ipX_addr_t
定义的变量是 IPv4 还是 IPv6 地址。 -
不易使用。引入
ipX_addr_t
(或者是说联合体)是为了节省空间,但是Simon Goldschmidt
深思之后,认为这不值得。能用 IPv6 的应用,不会计较以字节为单位的内存得失。应该把关注点放到如何更容易使用 IPv6 模块上。 -
编译出错。使能 IPv6 之后,下面的 smtp 示例代码无法编译:
char* ipa = ip_ntoa(&pcb->local_ip);
最终,在 2015 年 3 月,为了代码整洁清晰,也为了应用程序的兼容性,Simon Goldschmidt
决定删除所有ipX_addr_t
:
ip_addr_t
应为通用地址类型,对 API 可见。将ipX_addr_t
重命名为ip_addr_t
;- IPv4地址类型从
ip_addr_t
重命名为ip4_addr_t
; - IPv6地址类型保持不变,仍为
ip6_addr_t
; ip4_addr_t
和ip6_addr_t
仅用于协议栈内部,或者调用与版本相关的函数时使用。
这是一项大工程,涉及的文件达到了 74 个,其中新类型ip_addr_t
定义为:
#if LWIP_IPV4 && LWIP_IPV6
/**
* @ingroup ipaddr
* A union struct for both IP version's addresses.
* ATTENTION: watch out for its size when adding IPv6 address scope!
*/
typedef struct ip_addr
union
ip6_addr_t ip6;
ip4_addr_t ip4;
u_addr;
/** @ref lwip_ip_addr_type */
u8_t type;
ip_addr_t;
#else /* LWIP_IPV4 && LWIP_IPV6 */
#if LWIP_IPV4
typedef ip4_addr_t ip_addr_t;
#else /* LWIP_IPV4 */
typedef ip6_addr_t ip_addr_t;
#endif /* LWIP_IPV4 */
#endif /* LWIP_IPV4 && LWIP_IPV6 */
TCP初始序列号
TCP 报文段首部有一个序列号字段,它是一个32位的计数器,从 0 到 4294967295,它的值为当前报文段中第一个数据的字节序号。TCP 在建立连接的时候需要初始序列号(Initial Sequence Number:ISN),lwIP 为每个新的 TCP 连接生成一个 TCP 初始序列号。tcp.c 中的函数tcp_next_iss
用于这个目的。
在1.4.1版本中,这个函数长这样:
/**
* Calculates a new initial sequence number for new connections.
*
* @return u32_t pseudo random sequence number
*/
u32_t
tcp_next_iss(void)
static u32_t iss = 6510;
iss += tcp_ticks; /* XXX */
return iss;
这个算法很简单,算法产生的值是可以预测的。以至于 lwIP 的 TCP 连接可能成为 TCP 欺骗攻击的目标。
对 ISN 的准确预测是 IP 欺骗、数据注入和会话劫持能成功的先决条件。举一个TCP Reset攻击例子:
如果能准确预测ISN,就可以假冒客户端向服务器发送RST包,要求复位连接。由于RST包并不需要向应用层提交,服务器端的 TCP 协议栈只要接到该包就立即终止此次 TCP 连接,这将让真正的客户端连不上服务器,导致了拒绝服务攻击。
根据研究结果表明,Windows 2000、Windows XP SP1 的 ISN 都是可以预测的,从 Windows XP SP2 开始,ISN才能难以预测。
RFC 6528 中标准化了 ISN 生成算法。但是,该算法需要高分辨率计时器和加密散列函数。这远远超出了 lwIP 本身的范围。
所以 lwIP 增加了LWIP_HOOK_TCP_ISN
,这是一个钩子函数,可以让开发者根据自己的硬件平台实现自己的 ISN。该钩子函数提供了很大的灵活性,既可以用简单的随机数生成 ISN,也可以实现完整的 RFC 6528 规定的算法。
新的tcp_next_iss
函数为:
/**
* Calculates a new initial sequence number for new connections.
*
* @return u32_t pseudo random sequence number
*/
u32_t
tcp_next_iss(struct tcp_pcb *pcb)
#ifdef LWIP_HOOK_TCP_ISN
LWIP_ASSERT("tcp_next_iss: invalid pcb", pcb != NULL);
return LWIP_HOOK_TCP_ISN(&pcb->local_ip, pcb->local_port, &pcb->remote_ip, pcb->remote_port);
#else /* LWIP_HOOK_TCP_ISN */
static u32_t iss = 6510;
LWIP_ASSERT("tcp_next_iss: invalid pcb", pcb != NULL);
LWIP_UNUSED_ARG(pcb);
iss += tcp_ticks; /* XXX */
return iss;
核心应用文件移动
将一些核心应用从contrib
仓库移动到lwIP
仓库的src/apps
文件夹。
对比版本lwIP 1.4.1
和lwIP 2.1.2
的src
文件夹内容,可以发现lwIP 2.1.2
版本多了一个apps
文件夹。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rt2IjFWi-1654936610999)(lwIP-2_1_2版本更新.assets/image-20210827060505099.png)]
最开始,也就是 2015 年 10 月 8 日,apps
文件夹下只有一个应用sntp
客户端,这是一个简单网络时间协议,可以实现与授时服务器对时的功能。
在lwIP 1.4.1
版本时,sntp
客户端还位于contrib-1.4.1\\apps
文件夹下。
新版本的 sntp 客户端程序有了少许升级:新的版本通过配置可以使用到 2104 年,而 lwIP 1.4.1
版本只能使用到 2036 年 2 月 7 日。
一天之后,也就是 10 月 9 日,Simon Goldschmidt
为 APP 头文件指定了规则:必须有一个 xxx.h
和一个可选的 xxx_opts.h
。比如 sntp
应用就有一个 sntp.h
和一个 sntp_opts.h
头文件。
还是在这一天,apps
文件夹下添加了 lwiperf
应用。这是一个简单的性能测试客户端/服务器,配合上位机软件 iPerf2
可用于测试运行 lwIP
设备的最大传输速度和带宽。目前仅能测试 TCP
。
还是在这一天,apps
文件夹下添加了 netbiosns
应用。这是NetBIOS 名称服务器的示例实现。
2015年11月13日,apps
文件夹下添加了 SNMP
应用。在这之前, SNMP
都是内核的一部分(路径 lwip-1.4.1\\src\\core\\snmp
),现在 Dirk Ziegelmeier
将它彻底地从内核剥离出来,成为一个应用。
2015 年 11 月 16 日,apps
文件夹下添加了 http
应用。之前位于contrib-1.4.1\\apps
文件夹下。
2016 年 8 月 14 日,apps
文件夹下添加了 mNDS
应用。
2016 年 10 月 3 日,apps
文件夹下添加了 tftp
服务器应用。
2016 年 12 月 20 日, apps
文件夹下添加了 MQTT
客户端应用。
2017 年 3 月 29 日, apps
文件夹下添加了 smtp
客户端应用。之前位于contrib-1.4.1\\apps
文件夹下。
tcp_alloc
新建 tcp_pcb
控制块时,若内存不足,tcp_alloc
函数会尝试释放正在使用的控制块内存:
- 释放处于
TIME_WAIT
状态且存在时间最久的控制块内存;若本步骤没有释放任何内存,则: - 释放优先级更低且存在时间最久的有效(active)控制块内存。
lwIP-1.4.1 和 lwIP-2.1.2 在处理步骤1时是相同的,处理步骤2则不同。
在步骤2中,lwIP-1.4.1 会释放优先级小于等于自己的控制块内存;lwIP-2.1.2 则是释放优先级小于自己的控制块内存。
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
以上是关于lwIP2.1.x 更新详情的主要内容,如果未能解决你的问题,请参考以下文章