rst

https://zhangbinalan.gitbooks.io/protocol/content/tcpde_rst.html
一、RST介绍
RST标示复位、用来异常的关闭连接。
1. 发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓冲区中的包,发送RST。
2. 而接收端收到RST包后,也不必发送ACK包来确认。

二、什么时候发送RST包



      1.  建立连接的SYN到达某端口,但是该端口上没有正在 监听的服务。



      2. TCP收到了一个根本不存在的连接上的分节。



      3. 请求超时。 使用setsockopt的SO_RCVTIMEO选项设置recv的超时时间。接收数据超时时,会发送RST包。



三、尝试手动发送RST包



      1. 使用shutdown、close关闭套接字,发送的是FIN,不是RST。



      2. 套接字关闭前,使用sleep。对运行的程序Ctrl+C,会发送FIN,不是RST。



      3. 套接字关闭前,执行return、exit(0)、exit(1),会发送FIN、不是RST。 



      以上几种方法,都不能发送RST包。 发送RST包,需要自己伪造数据包进行发送。



RST攻击 这种攻击只能针对tcp,对udp无效。RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误。



有三个条件可以产生RST包:



  1. 建立连接的SYN到达某端口,但是该端口上没有正在监听的服务
    如:IP为192.168.1.33的主机上并没有开启WEB服务(端口号为0x50),这时我们通过IE去访问192.168.1.33,通过Wireshark抓包,可以看到,对此SYN包的回复为RST。说明此服务器(即IP192.168.1.33)是存在的,不过其上并没有运行WEB Server(如apache)的程序

  2. TCP想取消一个已有连接
    基于什么样的情况才会取消一个已有的连接?

  3. TCP接收到了一个根本不存在的的连接上的分节
    我们知道,TCP在数据传输前,要通过三路握手(three-way handshake)建立连接,即连接建立起后,服务器和客户端都有一个关于此连接的描述,具体形式表现为套接口对,如果收到的某TCP分节,根据源 IP,源tcp port number,及目的IP,目的tcp port number在本地(指服务器或客户端)找不到相应的套接口对,TCP则认为在一个不存在的连接上收到了分节,说明此连接已错,要求重新建立连接,于是发出了RST的TCP包



在TCP协议中RST表示复位,用来异常的关闭连接,在TCP的设计中它是不可或缺的。发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。



其实在网络编程过程中,各种RST错误其实是比较难排查和找到原因的。下面我列出几种会出现RST的情况。



1 端口未打开
服务器程序端口未打开而客户端来连接。这种情况是最为常见和好理解的一种了。去telnet一个未打开的TCP的端口可能会出现这种错误。这个和操作系统的实现有关。在某些情况下,操作系统也会完全不理会这些发到未打开端口请求。



比如在下面这种情况下,主机241向主机114发送一个SYN请求,表示想要连接主机114的40000端口,但是主机114上根本没有打开40000这个端口,于是就向主机241发送了一个RST。这种情况很常见。特别是服务器程序core dump之后重启之前连续出现RST的情况会经常发生。



2 请求超时
曾经遇到过这样一个情况:一个客户端连接服务器,connect返回-1并且error=EINPROGRESS。 直接telnet发现网络连接没有问题。ping没有出现丢包。用抓包工具查看,客户端是在收到服务器发出的SYN之后就莫名其妙的发送了RST。



有89、27两台主机。主机89向主机27发送了一个SYN,表示希望连接8888端口,主机27回应了主机89一个SYN表示可以连接。但是主机27却很不友好,莫名其妙的发送了一个RST表示我不想连接你了。



后来经过排查发现,在主机89上的程序在建立了socket之后,用setsockopt的SO_RCVTIMEO选项设置了recv的超时时间为100ms。而我们看上面的抓包结果表示,从主机89发出SYN到接收SYN的时间多达110ms。(从15:01:27.799961到15:01:27.961886, 小数点之后的单位是微秒)。因此主机89上的程序认为接收超时,所以发送了RST拒绝进一步发送数据。



3 提前关闭
关于TCP,我想我们在教科书里都读到过一句话,’TCP是一种可靠的连接’。 而这可靠有这样一种含义,那就是操作系统接收到的来自TCP连接中的每一个字节,我都会让应用程序接收到。如果应用程序不接收怎么办?你猜对了,RST。



看两段程序:



//server.c



int main(int argc, char** argv)

{

int listen_fd, real_fd;

struct sockaddr_in listen_addr, client_addr;

socklen_t len = sizeof(struct sockaddr_in);

listen_fd = socket(AF_INET, SOCK_STREAM, 0);

if(listen_fd == -1)

{

perror(“socket failed “);

return -1;

}

bzero(&listen_addr,sizeof(listen_addr));

listen_addr.sin_family = AF_INET;

listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);

listen_addr.sin_port = htons(SERV_PORT);

bind(listen_fd,(struct sockaddr )&listen_addr, len);

listen(listen_fd, WAIT_COUNT);

while(1)

{

real_fd = accept(listen_fd, (struct sockaddr
)&client_addr, &len);

if(real_fd == -1)

{

perror(“accpet fail “);

return -1;

}

if(fork() == 0)

{

close(listen_fd);

char pcContent[4096];
read(real_fd,pcContent,4096);
close(real_fd);

exit(0);

}

close(real_fd);

}

return 0;

}
这一段是server的最简单的代码。逻辑很简单,监听一个TCP端口然后当有客户端来连接的时候fork一个子进程来处理。注意看的是这一段fork里面的处理:



char pcContent[4096];
read(real_fd,pcContent,4096);
close(real_fd);
每次只是读socket的前4096个字节,然后就关闭掉连接。



然后再看一下client的代码:



//client.c
int main(int argc, char** argv)

{

int send_sk;

struct sockaddr_in s_addr;

socklen_t len = sizeof(s_addr);

send_sk = socket(AF_INET, SOCK_STREAM, 0);

if(send_sk == -1)

{

perror(“socket failed “);

return -1;

}

bzero(&s_addr, sizeof(s_addr));

s_addr.sin_family = AF_INET;



inet_pton(AF_INET,SER_IP,&s_addr.sin_addr);  
s_addr.sin_port = htons(SER_PORT);
if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
{
perror("connect fail ");
return -1;
}
char pcContent[5000]={0};
write(send_sk,pcContent,5000);
sleep(1);
close(send_sk); } 这段代码更简单,就是打开一个socket然后连接一个服务器并发送5000个字节。刚才我们看服务器的代码,每次只接收4096个字节,那么就是说客户端发送的剩下的4个字节服务端的应用程序没有接收到,服务器端的socket就被关闭掉,这种情况下会发生什么状况呢,还是抓包看一看。


前三行就是TCP的3次握手,从第四行开始看,客户端的49660端口向服务器的9877端口发送了5000个字节的数据,然后服务器端发送了一个ACK进行了确认,紧接着服务器向客户端发送了一个RST断开了连接。和我们的预期一致。



4 在一个已关闭的socket上收到数据
如果某个socket已经关闭,但依然收到数据也会产生RST。



代码如下:



客户端:



int main(int argc, char** argv)

{

int send_sk;

struct sockaddr_in s_addr;

socklen_t len = sizeof(s_addr);

send_sk = socket(AF_INET, SOCK_STREAM, 0);

if(send_sk == -1)

{

perror(“socket failed “);

return -1;

}

bzero(&s_addr, sizeof(s_addr));

s_addr.sin_family = AF_INET;



inet_pton(AF_INET,SER_IP,&s_addr.sin_addr);  
s_addr.sin_port = htons(SER_PORT);
if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
{
perror("connect fail ");
return -1;
}
char pcContent[4096]={0};
write(send_sk,pcContent,4096);
sleep(1);
write(send_sk,pcContent,4096);
close(send_sk); } 服务端: int main(int argc, char** argv) {
int listen_fd, real_fd;
struct sockaddr_in listen_addr, client_addr;
socklen_t len = sizeof(struct sockaddr_in);
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if(listen_fd == -1)
{
perror("socket failed ");
return -1;
}
bzero(&listen_addr,sizeof(listen_addr));
listen_addr.sin_family = AF_INET;
listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
listen_addr.sin_port = htons(SERV_PORT);
bind(listen_fd,(struct sockaddr *)&listen_addr, len);
listen(listen_fd, WAIT_COUNT);
while(1)
{
real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if(real_fd == -1)
{
perror("accpet fail ");
return -1;
}
if(fork() == 0)
{
close(listen_fd);
char pcContent[4096];
read(real_fd,pcContent,4096);
close(real_fd);
exit(0);
}
close(real_fd);
}
return 0; } 客户端在服务端已经关闭掉socket之后,仍然在发送数据。这时服务端会产生RST。


在谈RST攻击前,必须先了解TCP:如何通过三次握手建立TCP连接、四次握手怎样把全双工的连接关闭掉、滑动窗口是怎么传输数据的、TCP的flag标志位里RST在哪些情况下出现。下面我会画一些尽量简化的图来表达清楚上述几点,之后再了解下RST攻击是怎么回事。
   
1、TCP是什么?
 
TCP是在IP网络层之上的传输层协议,用于提供port到port面向连接的可靠的字节流传输。我来用土语解释下上面的几个关键字:
 
port到port:IP层只管数据包从一个IP到另一个IP的传输,IP层之上的TCP层加上端口后,就是面向进程了,每个port都可以对应到用户进程。
 
可靠:TCP会负责维护实际上子虚乌有的连接概念,包括收包后的确认包、丢包后的重发等来保证可靠性。由于带宽和不同机器处理能力的不同,TCP要能控制流量。
 
字节流:TCP会把应用进程传来的字节流数据切割成许多个数据包,在网络上发送。IP包是会失去顺序或者产生重复的,TCP协议要能还原到字节流本来面目。



FIN标志位也看到了,它用来表示正常关闭连接。图的左边是主动关闭连接方,右边是被动关闭连接方,用netstat命令可以看到标出的连接状态。
 
FIN是正常关闭,它会根据缓冲区的顺序来发的,就是说缓冲区FIN之前的包都发出去后再发FIN包,这与RST不同。
 
 
 
 
5、RST标志位
 
RST表示复位,用来异常的关闭连接,在TCP的设计中它是不可或缺的。就像上面说的一样,发送RST包关闭连接时,不必等缓冲区的包都发出去(不像上面的FIN包),直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。
 
TCP处理程序会在自己认为的异常时刻发送RST包。例如,A向B发起连接,但B之上并未监听相应的端口,这时B操作系统上的TCP处理程序会发RST包。
 
又比如,AB正常建立连接了,正在通讯时,A向B发送了FIN包要求关连接,B发送ACK后,网断了,A通过若干原因放弃了这个连接(例如进程重启)。网通了后,B又开始发数据包,A收到后表示压力很大,不知道这野连接哪来的,就发了个RST包强制把连接关了,B收到后会出现connect reset by peer错误。
 
 
 
 
6、RST攻击
 
A和服务器B之间建立了TCP连接,此时C伪造了一个TCP包发给B,使B异常的断开了与A之间的TCP连接,就是RST攻击了。实际上从上面RST标志位的功能已经可以看出这种攻击如何达到效果了。
 
那么伪造什么样的TCP包可以达成目的呢?我们至顶向下的看。
 
假定C伪装成A发过去的包,这个包如果是RST包的话,毫无疑问,B将会丢弃与A的缓冲区上所有数据,强制关掉连接。
 
如果发过去的包是SYN包,那么,B会表示A已经发疯了(与OS的实现有关),正常连接时又来建新连接,B主动向A发个RST包,并在自己这端强制关掉连接。
 
 
 
 
这两种方式都能够达到复位攻击的效果。似乎挺恐怖,然而关键是,如何能伪造成A发给B的包呢?这里有两个关键因素,源端口和序列号。
 
一个TCP连接都是四元组,由源IP、源端口、目标IP、目标端口唯一确定一个连接。所以,如果C要伪造A发给B的包,要在上面提到的IP头和TCP头,把源IP、源端口、目标IP、目标端口都填对。这里B作为服务器,IP和端口是公开的,A是我们要下手的目标,IP当然知道,但A的源端口就不清楚了,因为这可能是A随机生成的。当然,如果能够对常见的OS如windows和linux找出生成source port规律的话,还是可以搞定的。
 
序列号问题是与滑动窗口对应的,伪造的TCP包里需要填序列号,如果序列号的值不在A之前向B发送时B的滑动窗口内,B是会主动丢弃的。所以我们要找到能落到当时的AB间滑动窗口的序列号。这个可以暴力解决,因为一个sequence长度是32位,取值范围0-4294967296,如果窗口大小像上图中我抓到的windows下的65535的话,只需要相除,就知道最多只需要发65537(4294967296/65535=65537)个包就能有一个序列号落到滑动窗口内。RST包是很小的,IP头+TCP头也才40字节,算算我们的带宽就知道这实在只需要几秒钟就能搞定。
 
 
 
 
那么,序列号不是问题,源端口会麻烦点,如果各个操作系统不能完全随机的生成源端口,或者黑客们能通过其他方式获取到source port,RST攻击易如反掌,后果很严重



tcp是全双工的数据通信,也就是说任意一端的连接都可以主动的向对端发送数据。



情况1 目标端口未监听



连接一个未监听的端口,则被连接方会发送一个rst段。



但并不是所有连接未监听端口的情况下都会返回rst段,感觉与具体的网络实现有关。



情况2 目的主机或者网络路径中防火墙拦截



如果目的主机或者网络路径中显式的设置了对数据包的拦截。



当连接目标主机的60000端口时,会产生rst。



情况3 socket接收缓冲取Recv-Q中的数据未完全被应用程序读取时关闭该socket



python端的socket Recv-Q中有nc发送过来的10个字节未被应用read消费掉。



此时,python调用cli的close方法,则会产生rst段。



情况4 向已关闭的socket发送数据



socket调用close,表示本方既没有发送的需求,也没有接收的需求。不同于shutdown



sock connect到60000端口,然后服务端cli调用close,关闭连接。



当前服务端连接处于FIN_WAIT2状态。



客户端通过sock.send向已关闭的连接发送数据,则会产生rst。



情况5 向已关闭的连接发送FIN



cli.close关闭服务端连接,当前服务端连接处于FIN_WAIT2状态,等待对端的FIN段。



服务端的连接在FIN_WAIT2状态超时,当前服务端的连接实际上已经消逝。



此时,客户端调用sock.close()关闭连接,则服务端产生rst。



情况6 向已经消逝的连接中发送数据
消逝连接指的是,当前这个连接状态操作系统已经不再维护,其数据结构内核已经注销。



比如情况5中socket FIN_WAIT2超时后,其实该连接已经不存在。



再比如半打开(Half Open)连接的对端,由于某种原因已经不存在。



最近在工作中遇到一个由于服务端accept()调用过慢导致的已连接队列满,而客户端是半开打(Half Open)连接的情况下产生rst。



注:为了方便模拟已连接队列满的情况,将listen socket的backlog参数设置为1。



服务端监听6000端口,并不进行accept。



通过ss命令,可以看到端口60000的Send-Q为1,代表当前listen socket的已连接队列为1。



通过nc进行连接60000端口,可以看到两个连接都正常建立起来,其中服务端两个连接状态处于ESTABLISHD,但是服务端的PID为-,表示当前socket并没有和进程绑定。



通过ss命令,可以看到,60000端口的Listen socket的Recv-Q的值为2,该值表示已连接队列中有2个连接没有被应用accept取走。



第3个客户端使用nc进行连接,通过netstat查看网络状态,可以看到当前客户端已经完成了握手,而服务端因为已连接队列满,而处于SYN_RECV状态。



SYN_RECV状态的连接存在于listen socket的半连接队列中。



服务端SYN_RECV状态的连接超时以后消逝,而当前第3个nc客户端的连接依然处于ESTABLISHED状态,实际上是一个半打开(Half Open)连接。



对半打开连接进行send操作,则会产生rst。



rst与 broken pipe
对已关闭的管道进行操作会产生SIGPIPE信号。



网络编程中,对已经收到rst的连接进行io操作会产生SIGPIPE信号。



nc作为服务端监听60000端口,python sock进行连接。



nc服务端进程退出。



向已关闭的连接调用send将产生rst。



向已接收到rst的连接进行send将产生SIGPIPE信号。



详情请参考这方面的巨著《TCP/IP详解》和《UNIX网络编程》。



前面说到出现“Connection reset”的原因是服务器关闭了Connection[调用了Socket.close()方法]。大家可能有疑问了:服务器关闭了Connection为什么会返回“RST”而不是返回“FIN”标志。原因在于Socket.close()方法的语义和TCP的“FIN”标志语义不一样:发送TCP的“FIN”标志表示我不再发送数据了,而Socket.close()表示我不在发送也不接受数据了。问题就出在“我不接受数据” 上,如果此时客户端还往服务器发送数据,服务器内核接收到数据,但是发现此时Socket已经close了,则会返回“RST”标志给客户端。当然,此时客户端就会提示:“Connection reset”。详细说明可以参考oracle的有关文档:http://docs.oracle.com/javase/1.5.0/docs/guide/net/articles/connection_release.html。



另一个可能导致的“Connection reset”的原因是服务器设置了Socket.setLinger (true, 0)。但我检查过线上的tomcat配置,是没有使用该设置的,而且线上的服务器都使用了nginx进行反向代理,所以并不是该原因导致的。关于该原因上面的oracle文档也谈到了并给出了解释。



此外啰嗦一下,另外还有一种比较常见的错误“Connection reset by peer”,该错误和“Connection reset”是有区别的:



服务器返回了“RST”时,如果此时客户端正在从Socket套接字的输出流中读数据则会提示Connection reset”;



服务器返回了“RST”时,如果此时客户端正在往Socket套接字的输入流中写数据则会提示“Connection reset by peer”。



前面谈到了导致“Connection reset”的原因,而具体的解决方案有如下几种:



  出错了重试;



  客户端和服务器统一使用TCP长连接;



  客户端和服务器统一使用TCP短连接。



首先是出错了重试:这种方案可以简单防止“Connection reset”错误,然后如果服务不是“幂等”的则不能使用该方法;比如提交订单操作就不是幂等的,如果使用重试则可能造成重复提单。



然后是客户端和服务器统一使用TCP长连接:客户端使用TCP长连接很容易配置(直接设置HttpClient就好),而服务器配置长连接就比较麻烦了,就拿tomcat来说,需要设置tomcat的maxKeepAliveRequests、connectionTimeout等参数。另外如果使用了nginx进行反向代理或负载均衡,此时也需要配置nginx以支持长连接(nginx默认是对客户端使用长连接,对服务器使用短连接)。



使用长连接可以避免每次建立TCP连接的三次握手而节约一定的时间,但是我这边由于是内网,客户端和服务器的3次握手很快,大约只需1ms。ping一下大约0.93ms(一次往返);三次握手也是一次往返(第三次握手不用返回)。根据80/20原理,1ms可以忽略不计;又考虑到长连接的扩展性不如短连接好、修改nginx和tomcat的配置代价很大(所有后台服务都需要修改);所以这里并没有使用长连接。



正常情况tcp四层握手关闭连接,rst基本都是异常情况,整理如下:



0.使用 ping 可以看到丢包情况





  1. GFW




  2. 对方端口未打开,发生在连接建立





  如果对方sync_backlog满了的话,sync简单被丢弃,表现为超时,而不会rst




  1. close Socket 时recv buffer 不为空



  例如,客户端发了两个请求,服务器只从buffer 读取第一个请求处理完就关闭连接,tcp层认为数据没有正确提交到应用,使用rst关闭连接。





  1. 移动链路



    移动网络下,国内是有5分钟后就回收信令,也就是IM产品,如果心跳>5分钟后服务器再给客户端发消息,就会收到rst。也要查移动网络下IM 保持<5min 心跳。




  2. 负载等设备



    负载设备需要维护连接转发策略,长时间无流量,连接也会被清除,而且很多都不告诉两层机器,新的包过来时才通告rst。





   Apple push 服务也有这个问题,而且是不可预期的偶发性连接被rst;rst 前第一个消息write 是成功的,而第二条写才会告诉你连接被重置,



  曾经被它折腾没辙,因此打开每2秒一次tcp keepalive,固定5分钟tcp连接回收,而且发现连接出错时,重发之前10s内消息。





  1. SO_LINGER 应用强制使用rst 关闭



    该选项会直接丢弃未发送完毕的send buffer,可能造成业务错误,慎用; 当然内网服务间http client 在收到应该时主动关闭,使用改选项,会节省资源。





  好像曾经测试过haproxy 某种配置下,会使用rst关闭连接,少了网络交互而且没有TIME_WAIT 问题





  1. 超过超时重传次数、网络暂时不可达




  2. TIME_WAIT 状态





  tw_recycle = 1 时,sync timestamps 比上次小时,会被rst





  1. 设置 connect_timeout



    应用设置了连接超时,sync 未完成时超时了,会发送rst终止连接。




  2. 非正常包





  连接已经关闭,seq 不正确等





  1. keepalive 超时



    公网服务tcp keepalive 最好别打开;移动网络下会增加网络负担,切容易掉线;非移动网络核心ISP设备也不一定都支持keepalive,曾经也发现过广州那边有个核心节点就不支持。




  2. 数据错误,不是按照既定序列号发送数据





11.在一个已关闭的socket上接收数据



12.服务器关闭或异常终止了连接,由于网络问题,客户端没有收到服务器的关闭请求,这称为TCP半打开连接。就算重启服务器,也没有连接信息。如果客户端向提其写入数据,对方就会回应一个RST报文段。





  1. 发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓冲区中的包,发送RST。




  2. 而接收端收到RST包后,也不必发送ACK包来确认。





TCP连接关闭的正常方法是四次握手。但四次握手不是关闭TCP连接的唯一方法. 有时,如果主机需要尽快关闭连接(或连接超时,端口或主机不可达),RST (Reset)包将被发送. 注意,由于RST包不是TCP连接中的必须部分, 可以只发送RST包(即不带ACK标记). 但在正常的TCP连接中RST包可以带ACK确认标记。



TCP之种种连接异常



  1. connect出错:



(1) 若TCP客户端没有收到syn分节的响应,则返回ETIMEOUT错误;调用connect函数时,内核发送一个syn,若无响应则等待6s后再发送一个,若仍然无响应则等待24s后在发送一个,若总共等待75s后仍未收到响应则返回本错误;



(2) 若对客户的syn响应是rst,则表明该服务器在我们指定的端口上没有进程在等待与之连接,这是一种硬错误,客户一收到rst马上返回ECONNREFUSED错误;



(3) 若客户发送的syn在中间的某个路由器上引发了目的不可达icmp错误,则认为是一种软错误。客户主机内核保存该消息,并按照第一种情况的时间间隔继续发送syn,咋某个规定时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或者ENETUNREACH错误返回给进程;




  1. accept返回前连接中止:



在比较忙的服务器中,在建立三次握手之后,调用accept之前,可能出现客户端断开连接的情况;如,三次握手之后,客户端发送rst,然后服务器调用accept。posix指出这种情况errno设置为CONNABORTED;



注意Berkeley实现中,没有返回这个错误,而是EPROTO,同时完成三次握手的连接会从已完成队列中移除;在这种情况下,如果我们用select监听到有新的连接完成,但之后又被从完成队列中删除,此时如果调用阻塞accept就会产生阻塞;



解决办法:



(1) 使用select监听套接字是否有完成连接的时候,总是把这个监听套接字设置为非阻塞;



(2) 在后续的accept调用中忽略以下错误,EWOULDBLOCK(Berkeley实现,客户中止连接), ECONNABORTED(posix实现,客户中止连接), EPROTO(serv4实现,客户中止连接)和EINTR(如果有信号被捕获);




  1. 服务器进程终止(崩溃):



在客户端和服务器端建立连接之后,使用kill命令杀死服务器进程,进程终止会关闭所有打开的描述符,这导致了其向客户端发送了一个FIN,而客户端则响应了一个ack,这就完成了tcp连接终止的前半部分,只代表服务器不在发送数据了;但是客户端并不知道服务器端已经终止了,当客户端向服务器写数据的时候,由于服务器进程终止,所以响应了rst,如果我们使用select等方式,能够立即知道当前连接状态;如下:



(1) 如果对端tcp发送数据,那么套接字可读,并且read返回一个大于0的值(读入字节数);



(2) 如果对端tcp发送了fin(对端进程终止),那么该套接字变为可读,并且read返回0(EOF);



(3) 如果对端tcp发送rst(对端主机崩溃并重启),那么该套接字变为可读,并且read返回-1,errno中含有确切错误码;




  1. sigpipe信号:



当一个进程向某个收到rst的套接字执行写操作的时候,内核向该进程发送一个SIGPIPE信号,该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿的被终止;



不论进程是捕捉了该信号并从信号处理函数中返回,还是简单忽略该信号,写操作都讲返回EPIPE错误;




  1. 服务器主机崩溃:



建立连接之后,服务器主机崩溃,此时如果客户端发送数据,会发现客户端会在一定时间内持续重传,视图从服务器端收到数据的ack,当重传时间超过指定时间后,服务器仍然没有响应,那么返回的是ETIMEDOUT;




  1. 服务器主机不可达:



建立连接之后,服务器主机未崩溃,但是由于中间路由器故障灯,判定主机或网络不可达,此时如果客户端发送数据,会发现客户端会在一定时间内持续重传,视图从服务器端收到数据的ack,当重传时间超过指定时间后,服务器仍然没有响应,那么返回的是EHOSTUNREACH或ENETUNREACH;




  1. 服务器主机崩溃后重启:



当服务器主机崩溃重启后,之前所有的tcp连接丢失,此时服务器若收到来自客户端的数据,会响应一个rst;客户端调用read将返回一个ECONNRESET错误;




  1. 服务器主机关机:



系统关机时,init进程给所有进程发送SIGTERM信号,等待固定的时间,然后给所有仍在运行的进程发送SIGKILL信号,我们的进程会被SIGTERM或者SIGKILL信号终止,所以与前面服务器进程终止相同,进程关闭所有描述符,并发送fin,完成服务器端的半关闭;



发送方发完Reset做点啥?当然是释放TCP连接所占用内存资源!



接收方接到Reset会做啥?当然也是释放TCP连接所占用内存资源!



这当然是理想中的情况,但是现实与理想之间总是有很大的鸿沟。现实的情况是,互联网、广域网、局域网丢包是家常便饭。如果TCP的Reset报文发出了,但是在到达接收方的路上丢了,会造成双方的TCP状态完全不同步,发送方完全释放,另一方依然健在。



如果健在的一方,稍后发送TCP报文,会触发完全释放的一方再次将Reset报文发出,健在的一方收到Reset消息,同样会将TCP连接占用的内存完全释放,这是一种非常普遍的场景。



但是,如果健在的一方迟迟没有报文发出,会一直勇敢地活下去。有同学会说,不就是占用一点内存资源吗?



这种无法释放的内存资源,对于服务器是一种累赘,越早释放越好。



连续两次发送Reset报文



可以一定程度上克服一个Reset报文丢失造成双方状态的不同步现象,毕竟两个报文都丢的概率要小于一个报文的丢失概率。



从SYN_RECEVIED状态进入FIN_WAIT_1状态此时没有需要发送的东西,队列中也没有未完成的东西需要发送,就生成一个FIN包,发送出去,断开连接有要发送的东西,比如ack,就去建立连接2MSL等待时间是什么?MSL(Maximum Segment Lifetime)是报文段的最大生存时间。生存时间是有限的,由于TCP报文段是以IP数据报在网络内传输,而IP数据报通过TTL的跳数限制,因而报文段被丢弃之前,在网络内生存时间有限当TCP执行主动关闭并发回最后一个ACK,该连接必须在TIME_WAIT状态内等待2倍的MSL时间。原因:1:TCP主动关闭端发送的ACK如果丢失了,被动关闭端再次重发FIN,这时候的时间等待能够使得TCP主动关闭端发送最后的ACK不会丢失;2下次新的连接可能会复用同一个端口,如果由于网络延迟,老的数据才到,会与新数据发生混合,等待2MSL可以使得老数据完全消失在2MSL时间段之内,定义这个连接的插口(客户端IP和端口,服务端IP和端口),不能再被 被动断开方使用如果服务端的连接突然断开再立马重新启动,服务器的这个端口在2MSL时间内客户端无法连接【这里客户端是被动断开方】;同理如果是客户端自己断开,再立马使用相同的端口,在2MSL时间内去连服务器也是无法成功的【这里服务器是被动断开方】。这种场景客户端可以再随便换一个端口即可,但是服务端的一般应用端口都是固定的,容易造成麻烦如果多个请求同时到达服务端,服务端是如何处理的?TCP服务器会专门安排一个进程,它永远处于LISTEN状态,用来接收客户端的请求,当请求被接收时,系统中的TCP模块就会创建一个处于ESTABLISHED状态的进程处于LISTEN状态的进程不能接收数据报文段,处于ESTABLISHED状态进程不能接收SYN报文段伯克利TCP实现多连接处理规则为:正等待连接请求一端有一个固定长度的连接队列,队列中的连接已被TCP接受,但是应用层还没有感知应用层指明改队列的最大长度,它通常称为积压值(backlog),取值范围是0-5新连接到达时,如果连接队列有空间,TCP模块将对SYN进行确认并完成连接建立。但应用层只有在3次握手的第3次报文段接收到后才知道这个新连接新连接到达,但是连接队列没有空间,TCP模块不理会SYN,也不发回RST,如果应用层没有及时接受已被该TCP接受的连接,连接占满,客户端的主动打开最终将超时TCP接收连接是放入连接队列,应用层接收连接是从队列中移除队列的积压数与服务器能处理的最大连接数没有关系



TCP报头的标志位



  TCP报头中一共有六个标志位:URG/ACK/PSH/RST/SYN/FIN。 
SYN
  TCP三次握手中,如果A是发起端,则A就对服务器发一个SYN报文。表示建立连接。
ACK
  收到数据或请求后发送响应时发送ACK报文。
RST
  关闭异常连接
FIN
  TCP四次挥手时,表示关闭连接  
PSH
   发送端需要发送一段数据,这个数据需要接收端一收到就进行向上交付。而接收端在收到PSH标志位有效的数据时,迅速将数据交付给应用层。所以PSH又叫急迫比特。 
   但是现在已经不需要将数据交付给应用层了,因为这些效果在TCP栈已经可以自行处理这些问题了。
URG
  URG成为紧急指针,意为URG位有效的数据包,是一个紧急需要处理的数据包,需要接收端在接收到之后迅速处理。



PSH与URG的区别



  说了这么多,是不是发现PSH与URG的效果相似,但是聪明的计算机网络创建者会将两个相同的标志位放入一起吗?很明显不会,所以PSH和URG的区别是什么? 
  首先,PSH与URG的相似之处在于二者所在的数据包都是急需接收端处理的报文。 
  不同之处在于PSH位有效时,当前的数据还会被发送到接收端的缓冲区,并刷新缓冲区,将当前缓冲区中所有数据都交付给上一层——应用层。 
   PSH位就是用来通告接收方立即将收到的报文连同TCP接收缓存里的数据递交应用进程处理,一般会出现在发送方封装最后一个应用字段的TCP报文中,针对TCP交互式应用,则只要封装有应用字段的TCP报文,均会将PSH位置1。当然,应用程序的开发者,可以根据需要,在某个应用功能模块或某个应用操作中,将所有封装应用字段的TCP报文PSH位置1,以提高交互双方的处理效率,这在理论上应该也是可行的。 
   URG位有效的数据包也是在当前报文需要接收端立即处理,但是当前报文不需要经过接收端的缓冲区,直接越过缓冲区,交付往接收端的应用层。
借鉴:https://blog.csdn.net/sinat_36118270/article/details/73927628 



RST作用?原理?RST攻击
RST标志位
正常关闭连接的时候使用FIN,但是如果是关闭异常连接,则使用RST,发送RST包。与FIN包存在两点不同:
RST不必等缓冲区的包都发出去,直接就丢弃缓存区的包发送RST包。而FIN需要先处理完缓存区的包
接收端收到RST包后,也不必发送ACK包来确认。而FIN需要ACK包确认



TCP处理程序会在自己认为的异常时刻发送RST包。存在如下几种场景:
1)A向B发起连接,但B之上并没有应用监听相应的端口,这时B操作系统上的TCP处理程序会发RST包。
2)请求超时
有89、27两台主机。主机89向主机27发送了一个SYN,表示希望连接8888端口,主机27回应了主机89一个SYN表示可以连接。但是主机89却很不友好,莫名其妙的发送了一个RST表示我不想连接你了。
后来经过排查发现,在主机89上的程序在建立了socket之后,用setsockopt的SO_RCVTIMEO选项设置了recv的超时时间为100ms。而我们看上面的抓包结果表示,从主机89发出SYN到接收SYN的时间多达110ms。(从15:01:27.799961到15:01:27.961886, 小数点之后的单位是微秒)。因此主机89上的程序认为接收超时,所以发送了RST拒绝进一步发送数据。
3)在一个已关闭的socket上收到数据
比如,AB正常建立连接了,正在通讯时,A向B发送了FIN包要求关连接,B发送ACK后,网断了,A通过若干原因放弃了这个连接(例如进程重启)。网通了后,B又开始发数据包,A收到后表示压力很大,不知道这野连接哪来的,就发了个RST包强制把连接关了,B收到后会出现connect reset by peer错误。
4)字节流接收不完全
关于TCP,我想我们在教科书里都读到过一句话,’TCP是一种可靠的连接’。 而这可靠有这样一种含义,那就是操作系统接收到的来自TCP连接中的每一个字节,我都会让应用程序接收到。如果应用程序不接收怎么办?RST。
打开一个socket然后连接一个服务器并发送5000个字节。刚才我们看服务器的代码,每次只接收4096个字节,那么就是说客户端发送的剩下的4个字节服务端的应用程序没有接收到,服务器端的socket就被关闭掉,这种情况下服务器就会向客户端发送了一个RST,断开连接



RST攻击
A和服务器B之间建立了TCP连接,此时C伪造了一个TCP包发给B,使B异常的断开了与A之间的TCP连接,就是RST攻击了。
那么伪造什么样的TCP包可以达成目的呢?
1)假定C伪装成A发过去的包,这个包如果是RST包的话,毫无疑问,B将会丢弃与A的缓冲区上所有数据,强制关掉连接。
2)如果发过去的包是SYN包,那么,B会表示A已经发疯了(与OS的实现有关),正常连接时又来建新连接,B主动向A发个RST包,并在自己这端强制关掉连接。
这两种方式都能够达到复位攻击的效果。似乎挺恐怖,然而关键是,如何能伪造成A发给B的包呢?这里有两个关键因素,源端口和序列号。
一个TCP连接都是四元组,由源IP、源端口、目标IP、目标端口唯一确定一个连接。所以,如果C要伪造A发给B的包,要在上面提到的IP头和TCP头,把源IP、源端口、目标IP、目标端口都填对。这里B作为服务器,IP和端口是公开的,A是我们要下手的目标,IP当然知道,但A的源端口就不清楚了,因为这可能是A随机生成的。当然,如果能够对常见的OS如windows和linux找出生成source port规律的话,还是可以搞定的。



序列号问题是与滑动窗口对应的,伪造的TCP包里需要填序列号,如果序列号的值不在A之前向B发送时B的滑动窗口内,B是会主动丢弃的。所以我们要找到能落到当时的AB间滑动窗口的序列号。这个可以暴力解决,因为一个sequence长度是32位,取值范围0-4294967296,如果窗口大小像上图中我抓到的windows下的65535的话,只需要相除,就知道最多只需要发65537(4294967296/65535=65537)个包就能有一个序列号落到滑动窗口内。RST包是很小的,IP头+TCP头也才40字节,算算我们的带宽就知道这实在只需要几秒钟就能搞定。
那么,序列号不是问题,源端口会麻烦点,如果各个操作系统不能完全随机的生成源端口,或者黑客们能通过其他方式获取到source port,RST攻击易如反掌,后果很严重



五种情况tcp会发送rst



1.端口未打开



2.提前关闭



3.请求超时



4.在一个已关闭的socket上收到数据



5.用于拒绝一个非法连接


Category linux