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

本文最初发布于个人公众号「小白debug」,原文链接:动图图解!收到RST,就一定会断开TCP连接吗?我知道大家都期待我立即回答这个问题,但我喜欢先介绍一些原理性的内容,然后再回答问题。我这样做是为了让大家能够逐步建立知识体系,更好地理解文章的上下文。现在,让我们进入正题。以下是本文的目录。

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

如何知道收到了RST?我们知道,内核和应用层是分开的两个层次,网络通信功能在内核中,而客户端或服务端属于应用层。应用层只能通过send/recv与内核进行交互,才能感知到内核是否收到了RST。当本端收到远端发送的RST后,内核会认为连接已关闭。此时,如果本端应用层尝试执行读取数据的操作,比如recv,应用层将收到”Connection reset by peer”的错误提示,意味着远端已关闭连接。如果本端应用层尝试执行写入数据的操作,比如send,应用层将收到”Broken pipe”的错误提示,意味着发送通道已损坏。这两个错误提示在开发过程中经常遇到,我建议大家将本文收藏起来,以备不时之需。

出现RST的场景有哪些?RST通常出现在异常情况下,可以归类为对端端口不可用和提前关闭socket两种情况。对端端口不可用分为两种情况:一种是端口从未”可用”过,比如根本没有进行监听;另一种是曾经”可用”,但现在”不可用”了,比如服务突然崩溃。端口未监听一定会导致RST吗?并非如此。如前所述,发出RST的前提是在正常情况下。让我们来看看源代码。

握手包使用方法

当内核收到数据后,会从物理层、数据链路层、网络层、传输层、应用层逐层传递。到达传输层时,根据数据包的协议是TCP还是UDP,会走不同的函数方法。简单来说,TCP数据包都会经过tcpv4rcv()方法。该方法会从全局哈希表中获取socket。如果服务端执行过listen,就能从全局哈希表中获取到socket。但如果服务端没有执行过listen,全局哈希表中也就没有对应的socket,结果自然是获取不到。在这种情况下,正常情况下服务端会向客户端发送RST。但端口未监听并不一定会导致发出RST。如上所述,发出RST的前提是在数据包没有问题的情况下,比如校验和正确,才会向对端发送RST包。因此,在数据包异常的情况下,默默丢弃而不发送RST是非常合理的做法。

为什么在数据包异常的情况下不发送RST?一个数据包如果连校验和都无法通过,那么很可能出现了问题。可能是在传输过程中被篡改,也可能只是一个胡乱伪造的数据包。在五层网络中,无论是哪一层,只要遇到这种数据包,推荐的做法都是默默丢弃,而不是回复一个消息告诉对方数据有问题。如果对方使用的是TCP,即可靠传输协议,发现很久没有收到ACK响应,自己会进行重传。如果对方使用的是UDP,说明发送端已经接受了”不可靠丢包”的事实,那就丢了就丢了。因此,在数据包异常的情况下,默默丢弃而不发送RST是非常合理的做法。

还是无法理解?那我再举个例子。正常人对你进行抨击时,他说话条理清晰,主谓宾分明。此时,你回击他,你是一个充满热情、正直、富有判断力的好人。而此时一个憨憨也想对你进行抨击,但他的思维混乱,连话都说不清楚,一直阿巴阿巴的。你虽然听不懂,但却受到了震撼。在这种情况下,你会怎么做?一般来说,最好的选择是选择B,因为如果你回击他,他反而会更有动力。希望通过这个例子,你能更好地理解。

程序启动了但是崩溃了。在端口不可用的场景中,除了端口未监听外,还有可能是之前进行了监听,但服务端应用程序突然崩溃。此时,客户端仍然像往常一样正常发送消息,但服务端的内核协议栈会回复一个RST。在开发过程中,这种情况是最常见的。比如,在服务端应用程序中,出现了空指针或数组越界等问题,程序立即崩溃。

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

握手包使用方法

// net/ipv4/tcp_ipv4.c  
// 代码经过删减
int tcp_v4_rcv(struct sk_buff *skb)
{
    // 根据ip、端口等信息 获取sock。
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
    if (!sk)
        goto no_tcp_socket;
no_tcp_socket:
    // 检查数据包有没有出错
    if (skb->len doff << 2) || tcp_checksum_complete(skb)) {
        // 错误记录
    } else {
        // 发送RST
        tcp_v4_send_reset(NULL, skb);
    }
}

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

握手包使用方法

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

为什么要校验是否在窗口范围内?正常情况下,客户端和服务端可以通过RST来断开连接。假设不进行seq校验,如果此时有恶意的第三方介入,构造了一个RST包,并在TCP和IP等报头中填写了客户端的信息,发送到服务端,那么服务端将断开这个连接。同样的,也可以伪造服务端的包发送给客户端。这就是所谓的RST攻击。为了防止这种情况发生,校验seq是否在窗口范围内非常重要。

当收到RST包时,首先会通过tcp_sequence检查seq是否合法,主要是检查seq是否在合法接收窗口范围内。如果不在范围内,RST包将被丢弃。为什么要校验是否在窗口范围内?这是因为在正常情况下,客户端和服务端可以通过RST来断开连接。但如果不进行seq校验,恶意的第三方可能会构造一个RST包,将其seq设置为不合法的值,从而导致连接被非法断开。通过校验seq是否在窗口范围内,可以阻止许多非法消息的传递。虽然加入窗口校验并不能完全防止RST攻击,但它增加了攻击的成本。

收到RST包一定会断开连接吗?结论是,并不一定会断开。让我们来看看源代码。收到RST包后,首先会通过tcp_sequence检查这个seq是否合法,主要是检查这个seq是否在合法接收窗口范围内。如果不在范围内,RST包将被丢弃。那么,我们如何构造合法的seq呢?

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

握手包使用方法

static bool tcp_validate_incoming()
{
    struct tcp_sock *tp = tcp_sk(sk);
    /* 判断seq是否在合法窗口内 */
    if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
        if (!th->rst) {
            // 收到一个不在合法窗口内的SYN包
            if (th->syn)
                goto syn_challenge;
        }
    }
    /* 
     * RFC 5691 4.2 : 发送 challenge ack
     */
    if (th->syn) {
syn_challenge:
        tcp_send_challenge_ack(sk);
    }
}
// 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;
    }
}

握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法握手包使用方法

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

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

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

握手包使用方法握手包使用方法握手包使用方法

{5923

赞(0)