主要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
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,即没有找到合适的网络接口:
for (netif = netif_list; netif != NULL; netif = netif->next) {
if (netif_is_up(netif)) {
if (ip_addr_netcmp(dest, &(netif->ip_addr), &(netif->netmask))) {
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;
}
if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) {
if (ip4_addr_netcmp(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) {
return netif;
}
if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_cmp(dest, netif_ip4_gw(netif))) {
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)) {
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)))) {
if (ip_addr_cmp(¤t_iphdr_dest, &(netif->ip_addr)) ||
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;
}
}
在应用层,无论是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_addr_t local_ip; \
ip_addr_t remote_ip; \
\
u8_t so_options; \
\
u8_t tos; \
\
u8_t ttl \
\
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 \
\
union { \
ip_addr_t ip4; \
IP_PCB_IP6 \
} local_ip; \
union { \
ip_addr_t ip4; \
IP_PCB_IP6 \
} remote_ip; \
\
u8_t so_options; \
\
u8_t tos; \
\
u8_t ttl \
\
IP_PCB_ADDRHINT
为了使代码更具可读性,也为了尽可能的合并 IPv4 和 IPv6 代码,2011 年 5 月 26 日,宏IP_PCB有了新的变化:
#define IP_PCB \
IP_PCB_ISIPV6_MEMBER \
\
ipX_addr_t local_ip; \
ipX_addr_t remote_ip; \
\
u8_t so_options; \
\
u8_t tos; \
\
u8_t ttl \
\
IP_PCB_ADDRHINT
这次变化很小,只是定义了一个新的IP地址类型ipX_addr_t,用来代替上个版本的联合体:
#if LWIP_IPV6
typedef union {
ip_addr_t ip4;
ip6_addr_t ip6;
} ipX_addr_t;
#else
typedef ip_addr_t ipX_addr_t;
#endif
不知道你们是如何看待新的类型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
typedef struct ip_addr {
union {
ip6_addr_t ip6;
ip4_addr_t ip4;
} u_addr;
u8_t type;
} ip_addr_t;
#else
#if LWIP_IPV4
typedef ip4_addr_t ip_addr_t;
#else
typedef ip6_addr_t ip_addr_t;
#endif
#endif
TCP初始序列号
TCP 报文段首部有一个序列号字段,它是一个32位的计数器,从 0 到 4294967295,它的值为当前报文段中第一个数据的字节序号。TCP 在建立连接的时候需要初始序列号(Initial Sequence Number:ISN),lwIP 为每个新的 TCP 连接生成一个 TCP 初始序列号。tcp.c 中的函数tcp_next_iss用于这个目的。
在1.4.1版本中,这个函数长这样:
u32_t
tcp_next_iss(void)
{
static u32_t iss = 6510;
iss += tcp_ticks;
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函数为:
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
static u32_t iss = 6510;
LWIP_ASSERT("tcp_next_iss: invalid pcb", pcb != NULL);
LWIP_UNUSED_ARG(pcb);
iss += tcp_ticks;
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 则是释放优先级小于自己的控制块内存。
accept 回调函数
在 TCP 服务器模式下,处于监听状态的 TCP_PCB ,如果收到客户端发送的 SYN 同步标志,表示一个客户端在请求建立连接了。 lwIP 会为这个新连接申请一个 TCP_PCB ,这一过程在 tcp_listen_input 函数中完成的。然而 TCP_PCB 的个数是有限的,如果申请失败,对于失败的处理, lwIP 2.1.x 版本与 lwIP 1.4.1不同。 先看 lwIP 1.4.1 的代码(经简化):
static err_t
tcp_listen_input(struct tcp_pcb_listen *pcb)
{
npcb = tcp_alloc(pcb->prio);
if (npcb == NULL) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\n"));
return ERR_MEM;
}
return ERR_OK;
}
可以看到, lwIP 1.4.1 版本 tcp_listen_input 函数具有返回值,如果申请 TCP_PCB 失败,则返回 ERR_MEM 错误码。 再看 lwIP 2.1.x 的代码(经简化):
static void
tcp_listen_input(struct tcp_pcb_listen *pcb)
{
npcb = tcp_alloc(pcb->prio);
if (npcb == NULL) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\n"));
TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err);
return;
}
return;
}
区别很明显,首先 lwIP 2.1.x 版本 tcp_listen_input 函数不具有返回值(返回类型为 void ),其次,lwIP 处理内存错误是通过宏 TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err) 调用 tcp_accept API 注册的回调函数来实现的。宏展开代码(简化后)如下所示,注意第二个参数为 NULL :
if(pcb->accept != NULL)
pcb->accept(pcb->callback_arg, NULL, ERR_MEM);
这个功能最早是由 Simon Goldschmidt 在 2016-03-23 提交的,提交记录为:
tcp: call accept-callback with ERR_MEM when allocating a pcb fails on
passive open to inform the application about this error
ATTENTION: applications have to handle NULL pcb in accept callback!
tcp:在被动打开分配 pcb 失败时,使用 ERR_MEM 参数调用 accept 回调函数,以通知应用程序有关此错误 注意:应用程序必须在 accept 回调中处理 pcb 句柄为 NULL 的情况!
这就告诉我们一个重要的信息:lwIP 2.1.x 版本的 accept 回调函数编写方式与 lwIP 1.4.1 版本不同。lwIP 2.1.x 版本的 accept 回调函数 必须 在 accept 回调中处理 pcb 句柄为 NULL 的情况!!举个例子。 lwIP 1.4.1 版本的 accept 回调函数可以这么写:
static err_t telnet_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
char * p_link_info = "已连接到Telnet!\r\n";
tcp_recv(pcb,telnet_recv);
tcp_err(pcb,NULL);
pcb->so_options |= SOF_KEEPALIVE;
tcp_write(pcb, p_link_info, strlen(p_link_info), TCP_WRITE_FLAG_COPY);
return ERR_OK;
}
而 lwIP 2.1.x 版本的accept 回调函数需要这么写:
static err_t telnet_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
char * p_link_info = "已连接到Telnet!\r\n";
if(pcb == NULL)
{
if(err == ERR_MEM)
return ERR_OK;
}
tcp_recv(pcb,telnet_recv);
tcp_err(pcb,NULL);
pcb->so_options |= SOF_KEEPALIVE;
tcp_write(pcb, p_link_info, strlen(p_link_info), TCP_WRITE_FLAG_COPY);
return ERR_OK;
}
这里对 pcb 句柄是否为 NULL 做了处理,如果检测到 NULL,accpet 回调函数需要提前退出!。
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃) 
|