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