搭建一个和linux开发者知识共享和学习的平台
分类: 嵌入式
2023-08-28 10:48:40
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提前关闭。
端口不可用分为两种情况。要么是这个端口从来就没有"可用"过,比如根本就没监听(listen)过;要么就是曾经"可用",但现在"不可用"了,比如服务突然崩了。
端口未监听服务端listen 方法会创建一个sock放入到全局的哈希表中。
此时客户端发起一个connect请求到服务端。服务端在收到数据包之后,第一时间会根据ip和端口从哈希表里去获取sock。
如果服务端执行过listen,就能从全局哈希表里拿到sock。
但如果服务端没有执行过listen,那哈希表里也就不会有对应的sock,结果当然是拿不到。此时,正常情况下服务端会发rst给客户端。
端口未监听就一定会发rst吗?不一定。上面提到,发rst的前提是正常情况下,我们看下源码。
// net/ipv4/tcp_ipv4.c内核在收到数据后会从物理层、数据链路层、网络层、传输层、应用层,一层一层往上传递。到传输层的时候,根据当前数据包的协议是tcp还是udp走不一样的函数方法。可以简单认为,tcp数据包都会走到 tcp_v4_rcv()。这个方法会从全局哈希表里获取 sock,如果此时服务端没有listen()过 , 那肯定获取不了sock,会跳转到no_tcp_socket的逻辑。
注意这里会先走一个 tcp_checksum_complete(),目的是看看数据包的校验和(checksum)是否合法。
如果在发送端到接收端传输过程中,数据发生任何改动,比如被第三方篡改,那么接收方能检测到校验和有差错,此时tcp段会被直接丢弃。如果校验和没问题,那才会发rst。
所以,只有在数据包没问题的情况下,比如校验和没问题,才会发rst包给对端。
端口不可用的场景里,除了端口未监听以外,还有可能是从前监听了,但服务端机器上做监听操作的应用程序突然崩了,此时客户端还像往常一样正常发送消息,服务器内核协议栈收到消息后,则会回一个rst。在开发过程中,这种情况是最常见的。
比如你的服务端应用程序里,弄了个空指针,或者数组越界啥的,程序立马就崩了。
这种情况跟端口未监听本质上类似,在服务端的应用程序崩溃后,原来监听的端口资源就被释放了,从效果上来看,类似于处于closed状态。
此时服务端又收到了客户端发来的消息,内核协议栈会根据ip端口,从全局哈希表里查找sock,结果当然是拿不到对应的sock数据,于是走了跟上面"端口未监听"时一样的逻辑,回了个rst。客户端在收到rst后也释放了sock资源,从效果上来看,就是连接断了。
这种情况分为本端提前关闭,和远端提前关闭。
本端提前关闭如果本端socket接收缓冲区还有数据未读,此时提前close() socket。那么本端会先把接收缓冲区的数据清空,然后给远端发一个rst。
远端提前关闭远端已经close()了socket,此时本端还尝试发数据给远端。那么远端就会回一个rst。
tcp是全双工通信,意思是发送数据的同时,还可以接收数据。
close()的含义是,此时要同时关闭发送和接收消息的功能。
客户端执行close(), 正常情况下,会发出第一次挥手fin,然后服务端回第二次挥手ack。如果在第二次和第三次挥手之间,如果服务方还尝试传数据给客户端,那么客户端不仅不收这个消息,还会发一个rst消息到服务端。直接结束掉这次连接。
tcp是可靠传输,意味着本端发一个数据,远端在收到这个数据后就会回一个ack,意思是"我收到这个包了"。
而rst,不需要ack确认包。
因为rst本来就是设计来处理异常情况的,既然都已经在异常情况下了,还指望对方能正常回你一个ack吗?可以幻想,不要妄想。
rst丢了,问题不大。比方说服务端,发了rst之后,服务端就认为连接不可用了。
如果客户端之前发送了数据,一直没等到这个数据的确认ack,就会重发,重发的时候,自然就会触发一个新的rst包。
而如果客户端之前没有发数据,但服务端的rst丢了,tcp有个keepalive机制,会定期发送探活包,这种数据包到了服务端,也会重新触发一个rst。
收到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包,第一步会通过tcp_sequence先看下这个seq是否合法,其实主要是看下这个seq是否在合法接收窗口范围内。如果不在范围内,这个rst包就会被丢弃。
黄色的部分,就是指接收窗口,只要rst包的seq不在这个窗口范围内,那就会被丢弃。
正常情况下客户端服务端双方可以通过rst来断开连接。假设不做seq校验,如果这时候有不怀好意的第三方介入,构造了一个rst包,且在tcp和ip等报头都填上客户端的信息,发到服务端,那么服务端就会断开这个连接。同理也可以伪造服务端的包发给客户端。这就叫rst攻击。
已连接状态下收到第一次握手包会怎么样?我们需要了解一个问题,比如服务端在已连接(established)状态下,如果收到客户端发来的第一次握手包(syn),会怎么样?
以前我以为服务单会认为客户端憨憨了,直接rst连接。
但实际,并不是。
static bool tcp_validate_incoming()上面提到的这个challenge ack ,仿佛为盲猜seq的老哥们打开了一个新世界。
在获得这个challenge ack后,攻击程序就可以以ack值为基础,在一定范围内设置seq,这样造成rst攻击的几率就大大增加了。
总结