lwIP 细节之二:协议栈什么情况下发送 RST 标志

Posted 研究是为了理解

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lwIP 细节之二:协议栈什么情况下发送 RST 标志相关的知识,希望对你有一定的参考价值。

一次网络故障,抓包发现使用 lwIP 协议栈的设备不停的发送 RST 标志,为了探索 lwIP 协议栈什么情况下发送 RST 标志,就有了这篇笔记。

注:除非特别说明,以下内容针对 lwIP 2.0.0 及以上版本。

RST 标志是通过 tcp_rst 函数发送的。这个函数声明为:

/**
 * Send a TCP RESET packet (empty segment with RST flag set) either to
 * abort a connection or to show that there is no matching local connection
 * for a received segment.
 *
 * Called by tcp_abort() (to abort a local connection), tcp_input() (if no
 * matching local pcb was found), tcp_listen_input() (if incoming segment
 * has ACK flag set) and tcp_process() (received segment in the wrong state)
 *
 * Since a RST segment is in most cases not sent for an active connection,
 * tcp_rst() has a number of arguments that are taken from a tcp_pcb for
 * most other segment output functions.
 *
 * @param pcb TCP pcb (may be NULL if no pcb is available)
 * @param seqno the sequence number to use for the outgoing segment
 * @param ackno the acknowledge number to use for the outgoing segment
 * @param local_ip the local IP address to send the segment from
 * @param remote_ip the remote IP address to send the segment to
 * @param local_port the local TCP port to send the segment from
 * @param remote_port the remote TCP port to send the segment to
 */
void
tcp_rst(const struct tcp_pcb *pcb, 
					u32_t seqno, 
					u32_t ackno,
        			const ip_addr_t *local_ip, 
        			const ip_addr_t *remote_ip,
        			u16_t local_port, 
        			u16_t remote_port)

从注释得知,tcp_rst 函数发送 TCP RESET 数据包(带有 RST 标志的空帧),用于中止连接或者向对方表明你指定的数据接收者查无此人(接收到的数据帧没有匹配的本地控制块 pcb)。

tcp_rst 函数主要由以下函数调用:

  • tcp_abort 函数:中止一个本地连接
  • tcp_input 函数:没有找到匹配的本地控制块 pcb
  • tcp_listen_input 函数:接收到的帧设置了 ACK 标志
  • tcp_process 函数:接收到包含错误状态的帧

tcp_input 函数中

1.本地连接接收端已经关闭,但仍收到数据,调用 tcp_abort 函数,发送 RST 标志,通知远程主机并非所有数据都被处理。简化后的代码如下所示:

void
tcp_input(struct pbuf *p, struct netif *inp)

  // 经过一系列检测,没有错误
  
  /* 在本地找到有效的控制块 pcb */
  if (pcb != NULL) 
    err = tcp_process(pcb);
	/* 报文中包含有效数据 */
	if (recv_data != NULL) 
	  if (pcb->flags & TF_RXCLOSED) 
		/* received data although already closed -> abort (send RST) to
		   notify the remote host that not all data has been processed */
		pbuf_free(recv_data);
		tcp_abort(pcb);
		goto aborted;
	  
	  
	  /* Notify application that data has been received. */
	  TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);	  
	
  
  return;  

这个代码也能看出,如果报文中包含有效数据,指向数据的 pbuf 缓存由应用程序释放,其它情况都是由内核释放。

2.根据接收到的数据包内容(IP、端口号)查找本地控制块 pcb ,发现匹配的 pcb 处于 TIME_WAIT 状态,则调用 tcp_timewait_input 函数,在这个函数中若满足:报文段包含握手 SYN 标志报文段编号合法,则调用 tcp_rst 函数发送 RST 标志。

void
tcp_input(struct pbuf *p, struct netif *inp)

  // 经过一系列检测,没有错误
  // 在 tcp_active_pcbs 链表中没有找到匹配的控制块 pcb
	
  if (pcb == NULL) 
	/*在 tcp_tw_pcbs 链表中查找匹配的控制块 pcb*/
    for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) 
      if (pcb->remote_port == tcphdr->src &&
          pcb->local_port == tcphdr->dest &&
          ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr()) &&
          ip_addr_cmp(&pcb->local_ip, ip_current_dest_addr())) 
        /* 找到匹配的控制块 pcb */
        LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packed for TIME_WAITing connection.\\n"));
        tcp_timewait_input(pcb);
        pbuf_free(p);
        return;
      
    
  

3.根据接收到的数据包内容(IP、端口号)查找本地控制块 pcb ,若发现“查无此人”,则向数据发送者发送 RST 标志,以复位连接。

void
tcp_input(struct pbuf *p, struct netif *inp)

  // 经过一系列检测,没有错误
  // 在 tcp_active_pcbs 链表中【没有】找到匹配的控制块 pcb
  // 在 tcp_tw_pcbs 链表中【没有】找到匹配的控制块 pcb
  // 在 tcp_listen_pcbs.listen_pcbs 链表中【没有】找到匹配的控制块 pcb
	
  /* If no matching PCB was found, send a TCP RST (reset) to the
       sender. */
  LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_input: no PCB match found, resetting.\\n"));
  if (!(TCPH_FLAGS(tcphdr) & TCP_RST)) 
    tcp_rst(NULL, ackno, seqno + tcplen, ip_current_dest_addr(),
              ip_current_src_addr(), tcphdr->dest, tcphdr->src);
  
  pbuf_free(p);
  return;

这在客户端程序中很有用。

假如你的设备使用 lwIP 协议栈(裸机),作客户端,使用出错重连机制连接上位机。如果上位机程序关闭,就会出现以下过程:

设备发送 SYN 建立连接 -> 上位机操作系统找不到匹配的控制块 pcb (因为上位机程序关闭了)-> 上位机发送 RST 标志复位连接 -> 设备触发出错重连 -> 设备发送 SYN 建立连接 -> …

出现死循环连接!!
这一过程可能会非常快,当设备很多时,会在局域网中形成 SYN 风暴。

这个知识还可以用于判断连接失败原因。
比如你已经开启了上位机程序,但发现连不上,抓包发现每次连接,上位机都回复 RST 标志,最可能的原因是你连接的端口号错了

tcp_process 函数中

1.处于 SYN_RCVDLISTEN 状态的服务器,接收到 SYN 握手标志后,进入 SYN_RCVD )状态的连接。

  • 收到正确的 ACK 标志后,回调 accept 函数,表示有新的连接建立。若 accept 回调函数返回值不是 ERR_OK ,则调用 tcp_abort()函数,发送 RST 标志。
  • 如果收到的 ACK 序号不合法,则调用 tcp_rst 函数,发送 RST 标志。

简化后的代码为:

/**
 * Implements the TCP state machine. Called by tcp_input. In some
 * states tcp_receive() is called to receive data. The tcp_seg
 * argument will be freed by the caller (tcp_input()) unless the
 * recv_data pointer in the pcb is set.
 *
 * @param pcb the tcp_pcb for which a segment arrived
 *
 * @note the segment which arrived is saved in global variables, therefore only the pcb
 *       involved is passed as a parameter to this function
 */
static err_t
tcp_process(struct tcp_pcb *pcb)

  /* Do different things depending on the TCP state. */
  switch (pcb->state) 
    case SYN_RCVD:
      if (flags & TCP_ACK) 
        /* expected ACK number? */
        if (TCP_SEQ_BETWEEN(ackno, pcb->lastack + 1, pcb->snd_nxt)) 
          pcb->state = ESTABLISHED;
            tcp_backlog_accepted(pcb);
          /* Call the accept function. */
          TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);
          if (err != ERR_OK) 
            /* If the accept function returns with an error, we abort
             * the connection. */
            /* Already aborted? */
            if (err != ERR_ABRT) 
              tcp_abort(pcb);											// <--这里
            
            return ERR_ABRT;
          
         else 
          /* incorrect ACK number, send RST */
          tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(),	// <--这里
                  ip_current_src_addr(), tcphdr->dest, tcphdr->src);
        
       
      break;
  
  return ERR_OK;

2.处于 SYN_SENDCLOSED 状态的客户端,发送 SYN 握手标志后,进入 SYN_SEND )状态的连接,期待收到 SYN + ACK 标志,如果收到的报文中只有 ACK 标志,则调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:

static err_t
tcp_process(struct tcp_pcb *pcb)

  /* Do different things depending on the TCP state. */
  switch (pcb->state) 
    case SYN_SENT:
      /* received SYN ACK with expected sequence number? */
      if ((flags & TCP_ACK) && (flags & TCP_SYN)
          && (ackno == pcb->lastack + 1)) 
        //处理正确的报文
      
      /* received ACK? possibly a half-open connection */
      else if (flags & TCP_ACK) 
        /* send a RST to bring the other side in a non-synchronized state. */
        tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(),		// <-- 这里
                ip_current_src_addr(), tcphdr->dest, tcphdr->src);
        /* Resend SYN immediately (don't wait for rto timeout) to establish
          connection faster, but do not send more SYNs than we otherwise would
          have, or we might get caught in a loop on loopback interfaces. */
        if (pcb->nrtx < TCP_SYNMAXRTX) 
          pcb->rtime = 0;
          tcp_rexmit_rto(pcb);
        
      
      break;
  
  return ERR_OK;

tcp_rst 函数下面的代码也显示了另外一个小知识,在这种情况下,协议栈会立即重发一个 SYN 握手标志。这样做可以更快的建立连接。

tcp_close 函数中

发现还有数据没被应用层处理,或者接收窗口值不正确,则调用调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:

if ((pcb->state == ESTABLISHED) || (pcb->state == CLOSE_WAIT)) 
  if ((pcb->refused_data != NULL) || (pcb->rcv_wnd != TCP_WND_MAX(pcb))) 
    /* don't call tcp_abort here: we must not deallocate the pcb since
       that might not be expected when calling tcp_close */
    tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,	// <-- 这里
            pcb->local_port, pcb->remote_port);

    tcp_pcb_purge(pcb);
    TCP_RMV_ACTIVE(pcb);
    /* Deallocate the pcb since we already sent a RST for it */
    if (tcp_input_pcb == pcb) 
      /* prevent using a deallocated pcb: free it from tcp_input later */
      tcp_trigger_input_pcb_close();
     else 
      tcp_free(pcb);
    
    return ERR_OK;
  

tcp_slowtmr 函数中

如果使能保活定时器,并且保活超时(默认超时时间:2 小时 + 9*75 秒),则调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:

/**
 * Called every 500 ms and implements the retransmission timer and the timer that
 * removes PCBs that have been in TIME-WAIT for enough time. It also increments
 * various timers such as the inactivity timer in each PCB.
 *
 * Automatically called from tcp_tmr().
 */
void
tcp_slowtmr(void)

    /* Check if KEEPALIVE should be sent */
    if (ip_get_option(pcb, SOF_KEEPALIVE) &&
        ((pcb->state == ESTABLISHED) ||
         (pcb->state == CLOSE_WAIT))) 
      if ((u32_t)(tcp_ticks - pcb->tmr) >
          (pcb->keep_idle + TCP_KEEPCNT_DEFAULT * TCP_KEEPINTVL_DEFAULT) / TCP_SLOW_INTERVAL) 
        LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: KEEPALIVE timeout. Aborting connection to "));

        ++pcb_remove;
        ++pcb_reset;
       else if ((u32_t)(tcp_ticks - pcb->tmr) >
                 (pcb->keep_idle + pcb->keep_cnt_sent * TCP_KEEPINTVL_DEFAULT )
                 / TCP_SLOW_INTERVAL) 
        err = tcp_keepalive(pcb);
        if (err == ERR_OK) 
          pcb->keep_cnt_sent++;
        
      
    
    /* If the PCB should be removed, do it. */
    if (pcb_remove) 
      if (pcb_reset) 
        tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,
                pcb->local_port, pcb->remote_port);
      
    
  

与保活相关的时间定义在 tcp_priv.h 中:

#ifndef  TCP_KEEPIDLE_DEFAULT
#define  TCP_KEEPIDLE_DEFAULT     7200000UL /* Default KEEPALIVE timer in milliseconds */
#endif

#ifndef  TCP_KEEPINTVL_DEFAULT
#define  TCP_KEEPINTVL_DEFAULT    75000UL   /* Default Time between KEEPALIVE probes in milliseconds */
#endif

#ifndef  TCP_KEEPCNT_DEFAULT
#define  TCP_KEEPCNT_DEFAULT      9U        /* Default Counter for KEEPALIVE probes */

tcp_listen_input 函数中

1.处于监听状态的连接只接收 SYN 标志,如果收到了 ACK 标志,则调用 tcp_rst 函数,发送 RST 标志。简化后的代码为:

static void
tcp_listen_input(struct tcp_pcb_listen *pcb)

  if (flags & TCP_ACK) 
    /* For incoming segments with the ACK flag set, respond with a RST. */
    LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_listen_input: ACK in LISTEN, sending reset\\n"));
    tcp_rst((const struct tcp_pcb *)pcb, ackno, seqno + tcplen, ip_current_dest_addr(),
            ip_current_src_addr(), tcphdr->dest, tcphdr->src);
  
  return;

2.在 tcp_listen_input 函数中,接收到 SYN 标志后,就会调用 tcp_alloc 函数申请 TCP_PCB 控制块。
tcp_alloc 函数设计原则是尽一切可能返回一个有效的 TCP_PCB 控制块,因此,当 TCP_PCB 不足时,函数可能 “杀死”(kill)正在使用的连接,以释放 TCP_PCB 控制块!
具体就是:

  1. 先调用 tcp_kill_timewait 函数,试图找到 TIME_WAIT 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志,以便通知远端释放连接;
  2. 如果第 1 步失败了,则调用 tcp_kill_state 函数,试图找到 LAST_ACKCLOSING 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abandon(pcb, 0) 函数 “杀” 掉这个连接,注意这个函数并不会发送 RST 标志,处于这两种状态的连接都是等到对方发送的 ACK 就会结束连接,不会有数据丢失;
  3. 如果第 2 步也失败了,则调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长有效(active)连接!如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志。

TCP 连接具有优先级。
优先级由一个 u8_t 整数指定,数值越小,优先级越低。
优先级可以在 TCP_PRIO_MINTCP_PRIO_MAX 之间选择,默认是 TCP_PRIO_NORMAL

#define TCP_PRIO_MIN    1
#define TCP_PRIO_NORMAL 64
#define TCP_PRIO_MAX    127

这个信息也很重要,它告诉我们低优先级的 TCP 连接可以被高优先级连接给抢占掉!!
如果一个 TCP 连接很重要,那么你应该手动提高它的优先级。方法是在 accept 回调函数中,使用 tcp_setprio(pcb, new_prio) 函数更改 TCP 连接的优先级:

static err_t xxxx_protocol_accept(void *arg, struct tcp_pcb *pcb, err_t err)

    if(pcb == NULL)
        return ERR_OK;
    
	tcp_setprio (pcb, TCP_PRIO_MAX);						//<--这里
	tcp_recv(pcb, xxxx_protocol_recv);
    tcp_err(pcb, xxxx_protocol_err);
	
	pcb->so_options |= SOF_KEEPALIVE;                       //增加保活机制
    
    tcp_link_accept(pcb->remote_ip.addr, pcb->remote_port); //这是我的私有函数, 用于跟踪链接上线
    
	return(ERR_OK);

另外一个需要注意的地方,上面我们说“调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接”,这是针对 lwIP 2.0.0 及以上版本说的,lwIP 1.4.1 和这个不同,应表述为:
对于lwIP 1.4.1 版本,调用 tcp_kill_prio(prio) 函数,试图找到小于等于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接。
一个是“小于”,一个是“小于等于”。






读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)

开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系

以上是关于lwIP 细节之二:协议栈什么情况下发送 RST 标志的主要内容,如果未能解决你的问题,请参考以下文章

lwIP 细节之二:协议栈什么情况下发送 RST 标志

lwIP 细节之三:TCP 回调函数是何时调用的(编辑中)

lwIP 细节之三:TCP 回调函数是何时调用的(编辑中)

linux 协议栈tcp的rst报文中,seq的选取问题

lwIP 细节之三:TCP 回调函数是何时调用的

如何通过lwip协议栈发送数据给上位机