主要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 回调函数需要提前退出!。
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
|