ITPub博客

首页 > > > 深入浅出连接建立(2)

深入浅出连接建立(2)

作者:得一技术 时间:2019-09-06 11:45:42 0 删除 编辑

TCP是一种面向连接的、可靠的、基于字节流的通信协议,具有流量和网络拥塞控制等功能。这些功能需要通信双方协商好相关信息,包括传递窗口大小、各自的初始序列号等,而这些工作是通过三次握手完成的。

从互联网角度来考虑,设计三次握手有如下目的:

1、 通信双方之间建立初步信任

2、 初始化传输参数和状态

3、 降低互联网错误连接的概率 

从最终效果来看,三次握手的设计是相当成功的,是整个互联网成功运行的基础。

 1、三次握手

1.1 序列号设计

TCP对每一个TCP stream传输方向都采用了序列号(sequence number),用来确保数据的可靠传输。通过ack确认序列号,可以确认哪些数据已经成功传递给对方,哪些数据还需要重传。相关数据字段见下图。

除了可靠传输,sequence number的初始选择对互联网安全非常关键。如果选择的初始值是可以预测的,那就很容易通过序列值去伪造连接,这对于整个互联网会是个灾难。 

Sequence number初始化值的实现算法很多,但都具备这样的特性:随机,不可预测。

在wireshark中,采用绝对值查看sequence值,方法如下:

下图中每一个sequence值都是不一样的。

wireshark为了方便用户查看,默认采用sequence序列相对值。因此,下图中wireshark里抓包文件中的不同连接,sequence序列值显示都为0。

连接是由<客户端IP,客户端端口,服务器IP,服务器端口>组成。如果连接一样,sequence初始值也一样,那么能不能通过syn数据包建立连接呢?答案是不确定的,需要具体问题具体分析。当服务器端已经有相应的TCP状态,那么TCP很容易判断这个syn数据包是不是旧的连接请求;如果没有相应的TCP状态,那么TCP就会回复第二次握手数据包,以验证客户端是不是真的在建立连接,一旦服务器端接收到第三次握手数据包,一般情况下,服务器端成功与客户端建立连接。三次握手的重要作用之一是可以减少错误连接请求。

 1.2三次握手详解

下面讲述三次握手是如何完成的。


当客户端发起connect()请求时,委托给客户端TCP去完成连接建立过程,客户端TCP会发送第一次握手数据包(带有SYN seq=x,其中seq是序列号)过去,同时标记这个连接处于SYN_SENT状态。当第一次握手数据包到达服务器TCP的时候,如果被判断合法,服务器TCP把这个连接状态改为SYN_RECV状态,发送第二次握手数据包给客户端以确认自己身份(通过ack=x+1),同时要求客户端确认信息(SYN seq=y),并把连接信息放入SYN queue。客户端收到了第二次握手数据包,检查服务器端ack确认的序列号是否是预期的序列号(第一次握手数据包的序列号x+1),这里ack序列号为x+1,客户端TCP确认合法,客户端端连接建立成功,进入ESTABLISHED状态,并发送对服务器端的确认(ack=y+1)。服务器端TCP收到第三次握手数据包,检查客户端发过来的ack序列号是否是y+1,如果是,从SYN

queue取出连接信息,置为ESTABLISHED状态,并放入accept

queue,通知应用去取连接。

这里最关键的信息是序列号的确认,一旦序列号不合法,连接就建立不起来,TCP通过这种机制构建起互联网第一道安全屏障。

正因为三次握手机制,攻击者也不容易随意构建假连接。我们从上图可以看出,客户端建立连接需要第一次握手数据包和第三次握手数据包。伪造第一次握手数据包没有难度,因为没有ack确认信息;伪造第三次握手数据包,需要的最关键信息是ack=y+1,y是第二次握手数据包的服务器端序列号。当攻击者一旦能够捕获第二次握手数据包(如在第二次握手数据包必经的路由器),那么攻击者就可以伪造连接请求,服务器端无法判断这是真正的客户端连接请求,还是伪造的连接请求。因此,抓包权限要谨慎开通,避免被人当成连接攻击的肉鸡。 

1.3 初始化参数

三次握手信息,还包括下面的初始化信息:

1、 窗口大小

2、 最大分段大小

3、 selective选择性确认算法

4、 Window scale参数

5、 Timestamp信息

具体见下图

为了更加有效传输数据,在三次握手的前两个阶段,客户端和服务器端互相发送了窗口大小,告诉对方本方的接收能力,以防对端发送数据过多。客户端和服务器端互相发送最大分段大小,是为了告诉对方数据包的最大大小(MSS推断出MTU),防止数据包无法传递和减少拆分。双方协商是否采用选择性确认算法,提升ack确认数据的效率,防止大量低效率重传。window scale用来扩大窗口大小,解决TCP字段窗口65535大小的限制。这些协商过程在三次握手阶段完成。

Timestamp信息在连接建立阶段会试探性协商,一旦协商成功,后续过程会一直传递timestamp信息。

 下图展示了客户端尝试发送timestamp信息给服务器端。

 第二次握手数据包不仅发送了服务器的timestamp信息,也回送了客户端的timestamp信息,具体见下图。

因此,三次握手过程不仅是建立信任的过程,也是参数协商的过程。 

1.4 为什么不选择二次握手?

从协议设计角度来看,两次握手简单高效;但从安全角度来看,二次握手设计是有缺陷的。上图展示了两次握手过程,对于客户端来说,服务器是可信任的,因为服务器进行了ack确认,而对于服务器来说,客户端没有进行任何确认。这样的设计在安全方面有着非常致命的缺陷,因为攻击者可以随意模拟客户端来攻击服务器。

互联网存在着大量带有邪恶目的第一次握手数据包(此类数据包远超游荡的第一次握手重传数据包),如果采用二次握手过程,很容易导致服务器连接爆满,而利用三次握手,可以很好地屏蔽这类问题,这是三次握手最大的价值所在。

2、连接建立与应用的关系

2.1 对应关系

连接建立与应用之间的关系,大概可以通过下图来表示:

我们可以通过syn backlog和somaxconn系统参数来影响建立连接的过程。syn backlog用来设置syn队列的长度(假设没有设置syncookies),而somaxconn用来设置accept queue(已完成连接的队列)的长度。用户态程序也可以在调用listen函数的时候设置backlog来影响accept queue。

2.2 实验syncookies关闭状态下的syn backlog

首先要确认syncookies关闭,如下图:

然后我们通过下图命令修改syn backlog为2,也即syn queue只能容纳两个连接的syn连接请求(第一次握手数据包对应syn backlog)。

我们利用tcpburn(具体用法参考tcpburn章节)来发起1024个连接syn请求(第一次握手数据包)。

下图中只有3个syn 请求被服务器TCP接收,其它都因为queue overflow被服务器TCP丢弃。

从这里可以看出,在数量比较小的情况下,能够接纳的syn数量=syn backlog +1

把syn backlog增大为32个,会怎么样呢?

下图发送100个并发syn过去

结果只有25个成功放入syn队列,为什么呢?

 查看Linux源代码

上述箭头所指公式:syn backlog – queue len < syn

backlog / 4 

我们设置了syn_backlog=32, syn_backlog >> 2为8,算式如下:

32 -  queue len < 8

即queue len > 24会去执行tcp_peer_is_proven,意味着queue len为25的时候,执行tcp_peer_is_proven,由于后续syn数据包没有通过检测都被TCP丢弃了,所以我们看到下图只有25个TCP状态。

下图展示了客户端tcpburn发送了syn数据包,一旦被服务器TCP接收,就会有第二次握手数据包(SYN,ACK) 。

下图syn数据包没有被接收,tcpburn客户端只能重传。

我们继续扩大syn backlog的大小,如下图,配置了128大小。

我们发送1024个syn连接请求。

下图成功了97个。

根据算法:syn backlog – queue len < syn

backlog / 4 满足这个条件就会去执行tcp_peer_is_proven(一旦执行,tcpburn syn连接数据包都会被drop掉)。

128 – 97 = 31  < 128/4=32,这里97正好是没有执行tcp_peer_is_proven的最大queue len的长度。 

2.3 实验syncookies开启状态下的syn backlog

首先,开启syncookies(防止syn

flood攻击的一种手段,默认开启)。

设置syn backlog为2。

利用tcpburn发起syn请求。

结果显示syn queue长度为255,如下图。

我们继续增大syn backlog为128

继续使用tcpburn来发起syn请求。

显示还是255。

继续增大到1024大小

显示还是255,说明跟syn

backlog设置没有关系。

我们查看netstat -st结果,下图是测试之前的值(4105 syn cookies sent)。


上图是测试以后的值(5131 syn cookies sent)。

 

这说明如果syncookies启用了,那么syn backlog相关设置都会被忽略,这与如下的官方说明是一致的:

“When syncookies are enabled,there is no logical maximum length and this

setting is ignored.“

2.4 实验somaxconn

官方对listen backlog参数说明如下:

If thebacklogargumentis greater than the value in

/proc/sys/net/core/somaxconn,then it is silently truncated to that value; the default value in thisfile is 128.

我们实验一下是否真是这样呢?

查看系统默认somaxconn的值,如下图:

同时查看syn backlog的值,默认也是128。

我们把somaxconn修改为2,如下图:

在Linux系统,Nginx默认的listen backlog值为511,如下图:

修改Nginx代码,让Nginx不去接收新连接,如果accept queue足够大,这些连接会被阻塞在accept queue。

启动Nginx,监听8080端口。

 启动抓包,如下图:

利用telnet来进行对Nginx的访问,下面三个telnet实例都成功了。

第四个telnet实例没有成功。

最终报连接超时,如下图:

分析抓包情况,最后一个会话6次重传第一次握手数据包,最终仍没有被TCP接收。

 我们设置了somaxconn为2,按道理只能接收2个telnet连接请求,为什么是3呢?

我们从Linux源代码找线索:

上图中,如果backlog设置大于系统设置somaxconn,则会被截断为系统参数somaxconn的值。

对Nginx的监听socket,如上图,设置sk_max_ack_backlog为backlog(被截断过的值)。

继续分析代码,如上图,如果判断accept queue为满,则会走overflow过程。

判断queue是否full,是由sk_acceptq_is_full函数来做的,这个函数体如下:

只有超过了sk_max_ack_backlog的值才会报overflow,也即sk_max_ack_backlog+1才能走drop过程。

综合上述内容,我们可以看出accept queue能够存放的连接数量为:

min(应用层backlog,系统层somaxconn) +1 

我们根据上述公式继续做实验。

修改Nginx listen backlog为2

修改系统参数somaxconn=4

公式计算出min(4,2)+ 1 = 3

我们进行测试,发现确实只有3个连接建立成功,下图ESTABLISED状态的个数为3。

我们恢复Nginx listen backlog为511。为什么Nginx选择511而不是512?原因可以用上述公式解释,Nginx选择511,即accept queue能够最多存放512个连接。

设置系统参数somaxconn为128

利用tcpburn发送129个用户连接

建立连接后,利用telnet来访问Nginx服务,如下图,最终显示连接超时。


利用netstat查看成功建立连接的数量,如下图,是129,其它状态的连接没有。

如果我们模拟132个连接呢?

我们分析抓包文件,累计有129个连接成功,如下图。

tcp.stream eq 129(从0开始计算),即第130个TCP会话比较特殊,看似完成三次握手过程,其实连接根本就没有建立,因为后续过程在重传第二次握手数据包和第三次握手数据包。

从131个TCP连接请求开始,均是如下图。

我们查看TCP状态,显示有130个TCP状态,其中129个ESTABLISHED状态和1个

SYN_RECV状态。tcpburn发起的连接请求与telnet的主要区别是IP地址不一样,tcpburn用了192.168.100.130地址,而telnet用了172.17.0.3地址。

我们继续扩大试验范围:

增大系统参数somaxconn=1024

按照公式,最大支持min(511,

1024) + 1 =512

我们发起512个连接请求

下图显示512个连接成功建立。

利用telnet去建立连接,最终显示连接超时。

查看TCP状态,telnet的TCP状态为空,如下图。

从中可以看出telnet发起的连接,没有TCP状态。

我们利用tcpburn进一步试验:

尝试1024个连接


除了成功建立512个连接,还有两个处于SYN_RECV状态。

从上面可以看出,不同IP地址访问,Linux会有细节上的不同。这也说明了TCP的复杂性。

我们查看SYN cookies值有没有发生变化。

上图是实验之前的值,下图实验之后的值,我们发现SYN cookies值没有变化。

 这说明,在我们实验环境下,内核对正常连接并没有去发送SYN cookies。

 

3、案例分析

3.1 Nginx乌龙事件

实验Nginx 0.9.4版本,Nginx

listen backlog在Linux环境下默认为511,系统somaxconn默认为128,利用tcpburn测试(具体见下图)。

得到的结果如下:

能够成功建立129个连接。

 

接着我们实验Nginx 0.9.5版本,此版本修改了listen backlog,从511改为了-1(系统默认的backlog)。

我们利用tcpubrn发起连接请求,如下:

测试结果也是129连接。

看似Nginx对listen

backlog的修改没有问题。

我们查看Nginx发布的版本日志:

从上面可以看出Nginx作者感谢了用户的建议。

然而到了Nginx 1.0.1版本,Nginx

-1改回到了511,到底发生了什么呢?

利用tcpcopy对我们的某个系统进行了测试,测试结果如下:

141机器是线上机器,而148机器是测试机器。

在该测试中,Nginx 0.9.5~1.0.0版本吞吐量下降厉害,跟Nginx变动正好吻合

下图是Nginx作者对用户的答疑。

采用511是比较安全的限制,在Linux系统改回-1,会导致某些系统吞吐量下降。 

这个案例说明了系统的复杂性。不同环境下,情况是不一样的。需要实际环境中实际测试,才能少犯错误。

3.2 localhost连接超时

我们进行如下实验,启动Nginx 8080端口服务,但不去accept用户连接。

利用tcpburn来发起连接请求

这里显示成功建立了129个连接

我们利用telnet进行本地localhost访问,结果如下图:

访问127.0.0.1居然连接超时了,为什么呢?

抓包文件如下:

由于Nginx没有及时接收连接,导致accept

queue满了。

Localhost访问8080服务,由于syn queue大小受制于accept queue大小,且受到tcpburn syn冲击,导致localhost的syn请求不能放入syn queue队列,syn数据包也重传了6次,最终报连接超时。

从这个案例可以看出,只要服务器不接收连接或者接收连接速度慢,就可能导致服务不可用(localhost都无法建立连接),由此可见连接攻击的可怕性。 

4、总结

连接建立的过程看似简单,但其实作用巨大。三次握手是互联网TCP/IP协议中至关重要的设计。

连接建立过程与应用之间的关系很紧密,需要具体问题具体分析,不能犯经验主义或者片面主义错误。

 

 

 

 

 

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69935868/viewspace-3124752/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-07-25

  • 博文量
    16
  • 访问量
    11255