握手包 动图图解!收到RST,就一定会断开TCP连接吗?

在线wifi跑包 金刚包跑包 cap跑包 hccapx ewsa在线 就来 握手包跑包

各位好 又见面了 我是曹操 今天给大家带来一篇新的教程

希望各位细心学习 低调用网

握手包

大家都知道,我喜欢玩文字游戏,就是不直接回答问题。我喜欢先介绍一些原理性的内容,然后再回答问题。为什么要这样做呢?不!我就是不这样做!但是大家可以根据目录直接找到自己感兴趣的部分。我之所以先介绍一些原理,是希望大家能先了解一些基础知识,然后逐步深入,这样有助于建立知识体系,减少知识断层。好了,让我们进入正题。下面是本文的目录。

握手包

当收到RST时是否一定会断开连接?什么是RST?我们都知道,在正常情况下,断开TCP连接需要进行四次挥手,这是一种优雅的方式。但在异常情况下,双方可能都不正常,甚至无法进行挥手,因此需要一种机制来强制关闭连接。RST就是用于这种情况,通常用于异常关闭连接。它是TCP包头中的一个标志位。在正常情况下,无论是发送还是接收到设置了这个标志位的数据包,相应的内存、端口和其他连接资源都会被释放,从效果上看就是TCP连接被关闭了。而接收到RST的一方通常会看到一个”connection reset”或”connection refused”的错误。

握手包

TCP报头中的RST位是什么?如何知道收到了RST?我们知道,内核和应用层是分开的两个层次,网络通信功能在内核中,而客户端或服务端属于应用层。应用层只能通过send/recv与内核进行交互,才能感知到内核是否收到了RST。当本端收到远端发送的RST时,内核已经认为连接已关闭。此时,如果本端应用层尝试执行读取数据的操作,如recv,应用层将收到”Connection reset by peer”的错误,表示远端已关闭连接。

握手包握手包

ResetByPeer是什么意思?如果本端应用层尝试执行写入数据的操作,如send,应用层将收到”Broken pipe”的错误,表示发送通道已损坏。这两个错误在开发过程中经常遇到,建议将本文收藏起来,以备不时之需,说不定会对您有所帮助。

RST出现的场景有哪些?RST通常出现在异常情况下,可以归类为对端端口不可用和提前关闭socket两种情况。

端口不可用分为两种情况。一种是端口从未”可用”过,例如从未监听过;另一种是曾经”可用”,但现在”不可用”了,例如服务突然崩溃。

握手包

TCP连接未监听的端口是指服务端的listen方法会创建一个socket并放入全局哈希表中。此时,客户端向服务端发起一个connect请求。服务端在收到数据包后,会根据IP和端口从哈希表中获取socket。如果服务端执行过listen,就能从全局哈希表中获取到socket。但如果服务端没有执行过listen,哈希表中就不会有对应的socket,结果当然是获取不到。此时,正常情况下服务端会向客户端发送RST。端口未监听一定会发送RST吗?并不一定。如上所述,发送RST的前提是在正常情况下。让我们看看源代码。

握手包

当内核收到数据后,会从物理层、数据链路层、网络层、传输层、应用层逐层传递。到达传输层时,根据当前数据包的协议是TCP还是UDP,会走不同的函数方法。可以简单地认为,TCP数据包都会经过tcpv4rcv()方法。该方法会从全局哈希表中获取socket。如果此时服务端没有执行过listen,那么肯定无法获取到socket,会跳转到notcpsocket的逻辑。请注意,会先执行tcpchecksumcomplete()方法,目的是检查数据包的校验和是否合法。校验和可以验证数据在端到端传输过程中是否出现异常。校验和由发送端计算,然后由接收端验证。计算范围包括数据包的TCP首部和TCP数据。如果在发送端到接收端传输过程中,数据发生任何改动,例如被第三方篡改,接收方就能检测到校验和错误,此时TCP段将被丢弃。只有在数据包校验和正确的情况下,才会发送RST包给对端。因此,在数据包异常的情况下,默默丢弃而不发送RST是非常合理的。

握手包

还是无法理解?那我再举个例子。正常人对你进行抨击时,他说话条理清晰,主谓宾分明。此时,你回击他,你是一个充满热情、正直、有判断力的好人。而此时,一个思维混乱、说话不清楚的人也想对你进行抨击,你虽然听不懂他在说什么,但却受到了震撼。这时,你会怎么做?一般来说,最好的选择是不理他,因为你越理他,他越来劲。希望通过这个例子,你能理解了。程序启动但崩溃了,在端口不可用的场景中,除了端口未监听外,还有可能是之前监听了,但服务端应用程序突然崩溃了。此时,客户端仍然像往常一样发送消息,而服务器的内核协议栈会回复一个RST。在开发过程中,这种情况是最常见的。例如,在服务端应用程序中出现了空指针或数组越界等错误,程序立即崩溃。

握手包

TCP监听了但崩溃了。这种情况与端口未监听本质上是相似的。在服务端应用程序崩溃后,之前监听的端口资源被释放,从效果上看,类似于处于CLOSED状态。此时,服务端收到客户端发送的消息,内核协议栈会根据IP和端口从全局哈希表中查找socket,结果当然是找不到对应的socket数据,于是执行与”端口未监听”时相同的逻辑,发送一个RST。客户端在收到RST后也释放了socket资源,从效果上看,连接断开了。

RST和502错误的关系。在上述情况中,当服务端应用程序崩溃后,如果客户端继续发送数据,会收到一个RST。但如果在客户端和服务端之间加入了一个nginx,就像下图所示。

握手包

RST和502错误。nginx充当客户端和服务端之间的”中间人”角色,负责转发请求和响应结果。但当服务端应用程序崩溃,例如出现了野指针或OOM问题时,转发到服务器的请求必然无法得到响应,后端服务端会返回一个RST给nginx。nginx在收到这个RST后会断开与服务端的连接,并向客户端返回一个502错误码。因此,出现502错误通常是因为后端程序崩溃。基于这一点,建议查看监控是否发生了OOM,或者日志中是否有空指针等错误信息。

socket提前关闭。这种情况分为本端提前关闭和远端提前关闭。

本端提前关闭。如果本端的socket接收缓冲区中仍有未读取的数据,此时提前关闭socket,本端会先清空接收缓冲区的数据,然后向远端发送一个RST。

握手包握手包

远端提前关闭。如果远端已经关闭了socket,此时本端仍然尝试向远端发送数据,远端会回复一个RST。

close()触发TCP四次挥手。大家都知道,TCP是全双工通信,即可以发送数据,也可以接收数据。close()的含义是同时关闭发送和接收消息的功能。当客户端执行close()时,在正常情况下,会发送第一次挥手FIN,然后服务端回复第二次挥手ACK。如果在第二次和第三次挥手之间,服务端尝试向客户端发送数据,客户端不仅不会接收这个消息,还会向服务端发送一个RST消息,直接结束这次连接。

对方未收到RST会怎么样?我们知道,TCP是可靠传输,意味着本端发送一个数据后,远端会回复一个ACK,表示”我收到了这个包”。而RST不需要ACK确认。因为RST本来就是用于处理异常情况的,既然已经处于异常情况下,还指望对方能正常回复一个ACK吗?这是不现实的。但问题又来了,网络环境非常复杂,丢包是常有的事情,既然RST包不需要ACK确认,那么如果对方没有收到RST会怎么样呢?

握手包

RST丢失。如果RST丢失,问题也不大。例如,在上图中,服务端发送了RST后,服务端认为连接不可用了。如果客户端之前发送了数据,但很久没有收到ACK响应,客户端会重新发送,重新发送时会触发一个新的RST包。如果客户端之前没有发送数据,但服务端的RST丢失,TCP有一个keepalive机制,会定期发送探测包,这种数据包到达服务端时,也会重新触发一个RST。

// net/ipv4/tcp_input.c
static bool tcp_validate_incoming()
{
    // 获取sock
    struct tcp_sock *tp = tcp_sk(sk);
    // step 1:先判断seq是否合法(是否在合法接收窗口范围内)
    if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
        goto discard;
    }
    // step 2:执行收到 RST 后该干的事情
    if (th->rst) {
        if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt)
            tcp_reset(sk);
        else
            tcp_send_challenge_ack(sk);
        goto discard;
    }
}

握手包

RST丢失后的keepalive。收到RST一定会断开连接吗?先说结论,不一定会断开。让我们看看源代码。收到RST包后,首先通过tcp_sequence检查该seq是否合法,主要是检查该seq是否在合法接收窗口范围内。如果不在范围内,RST包将被丢弃。为什么要检查是否在窗口范围内?正常情况下,客户端和服务端可以通过RST来断开连接。假设不进行seq检查,如果此时有恶意的第三方介入,构造了一个RST包,并在TCP和IP等报头中填写了客户端的信息,发送到服务端,那么服务端将断开这个连接。同样的,也可以伪造服务端的包发送给客户端。这就是所谓的RST攻击。

struct tcp_skb_cb {
    __u32       seq;        /* Starting sequence number */
}

RST攻击。当受到RST攻击时,从现象上看,客户端会认为服务端崩溃了,这对用户体验非常不利。如果想要进行RST攻击,攻击程序需要猜测合法的seq值。窗口数值seq本质上只是一个uint32类型的值。如果在合法窗口范围内疯狂猜测seq的值,并构造相应的数据包发送到目标机器,虽然概率较低,但总是能够试出来,从而实现RST攻击。这种盲目猜测seq的方式被称为合法窗口盲打(blind in-window attacks)。

觉得这种方式太笨了?还有更聪明的方式,但在此之前,需要先了解下面这个问题。

在已连接状态下收到第一次握手包会怎么样?我们需要了解一个问题,例如,当服务端处于已连接(ESTABLISHED)状态时,如果收到客户端发送的第一次握手包(SYN),会发生什么?

以前我以为服务端会认为客户端是个憨憨,直接RST连接。但实际情况并非如此。

握手包

当客户端发送一个不在合法窗口范围内的SYN包时,服务端会发送一个带有正确seq值的ACK包,这个ACK包被称为challenge ack。

challenge ack抓包。上图是抓包的结果,使用scapy伪造一个seq=5的包发送到服务端(端口9090),服务端会回复一个带有正确seq值的challenge ack包给客户端(端口8888)。

利用challenge ack获取seq。上述提到的challenge ack仿佛为那些盲目猜测seq的人打开了一扇新的大门。获得challenge ack后,攻击程序可以以ack值为基础,在一定范围内设置seq,从而大大增加RST攻击的成功率。

赞(0)