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
函数:没有找到匹配的本地控制块 pcbtcp_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_RCVD
(LISTEN
状态的服务器,接收到 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_SEND
( CLOSED
状态的客户端,发送 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
控制块!
具体就是:
- 先调用
tcp_kill_timewait
函数,试图找到TIME_WAIT
状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用tcp_abort(pcb)
函数 “杀” 掉这个连接,这会发送RST
标志,以便通知远端释放连接; - 如果第 1 步失败了,则调用
tcp_kill_state
函数,试图找到LAST_ACK
和CLOSING
状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用tcp_abandon(pcb, 0)
函数 “杀” 掉这个连接,注意这个函数并不会发送RST
标志,处于这两种状态的连接都是等到对方发送的ACK
就会结束连接,不会有数据丢失; - 如果第 2 步也失败了,则调用
tcp_kill_prio(prio)
函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接!如果找到符合条件的控制块 pcb ,则调用tcp_abort(pcb)
函数 “杀” 掉这个连接,这会发送RST
标志。
TCP 连接具有优先级。
优先级由一个u8_t
整数指定,数值越小,优先级越低。
优先级可以在TCP_PRIO_MIN
和TCP_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)连接。
一个是“小于”,一个是“小于等于”。
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
以上是关于lwIP 细节之二:协议栈什么情况下发送 RST 标志的主要内容,如果未能解决你的问题,请参考以下文章