查看原文
其他

手动发包只握手两次,我发现了TCP的秘密···

脚本之家 2022-04-23

The following article is from 编程技术宇宙 Author 轩辕之风O

 关注
“脚本之家
”,与百万开发者在一起

作者:轩辕之风O

出处:转载自公众号编程技术宇宙(ID:xuanyuancoding)

星球提问

TCP三次握手这个话题,没有一万,也有九千篇文章写过了。

今天写这篇文章,是因为有球友在我的知识星球里提了这么一个问题:

总结一下三个小问题:

  • 客户端发送完第三次握手后,是不是不管服务器有没有收到,直接就发送数据?
  • TCP的第三次握手能不能携带数据?
  • 如果因为各种原因,服务端并未收到客户端发来的第三次握手包,那客户端后续发送的数据,服务端如何处理?

我的回答

以下是我的回答:

首先来回答这位球友最开始的问题:客户端发送完第三个握手后,是不是不管服务器有没有收到,直接就发送数据?

你可以从理论上来猜测一下,如果上面这个问题的答案是否定的话,也就是说客户端还得要确认服务器收到自己的第三次握手包以后才能发送数据。那怎么确认呢?是不是服务端还得回复自己一下:我收到了你的第三次握手包了,你可以发送数据了。

但如果这样一来,那是不是就变成了四次握手,而不是三次握手了呢?

所以反过来想,这个问题的答案就是肯定的,即:客户端发送完第三次握手包后,不再需要服务端的确认,立即可以发送数据。

下面是《TCP/IP协议详解》(卷1)中的连接建立示意图,你可以看到客户端这一侧,发送完第三次握手包以后,状态就别变成了ESTABLISH状态了,并未等待服务器确认,就开始在传输数据了。

光理论不够,我们再来抓包看一下,下面是我用抓包软件抓了一个TCP连接建立的握手时序图,同样你可以看到,在第三次握手包发送后,左侧的客户端立即就发出了正式的数据传输:一个HTTP请求包。

所以这个问题的答案就清楚了。

接下来看第二个问题:客户端在发送第三次握手包的时候是不是会携带数据一起传输过去?

其实从上面的2个图中你可以看出,TCP三次握手并未携带有效的应用层数据,数据的传输是在握手完成以后才开始的。但是如果我们非得问一句:客户端在发送第三次握手数据包的时候,到底能不能顺带携带一些数据过去呢?

关于这一问题,最权威的答案还是得看RFC标准文档,关于TCP标准协议的规范,是记录在编号793的RFC793一文中,链接如下:

https://www.rfc-editor.org/rfc/rfc793.html

文档有点长,而且是英文版,看起来可能有些吃力。

在事件处理这一节里面,会找到下面这段文字:

大意是说:如果我们的同步包SYN已经得到了确认,就把连接状态改为ESTABLISHED,然后发送的第三次握手包中可能会包含数据(如果已经有数据在排队等待传输的话)

这就说的很清楚了:TCP标准协议规范中,第三次握手包是允许传输数据的!

最后一个问题:如果因为各种原因,服务端并未收到客户端发来的第三次握手包,那客户端后续发送的数据,服务端如何处理?

这里先卖个关子,接着往下看。

接下来才是这篇文章的精华部分:

实验论证

TCP建立连接的三次握手,是操作系统内核协议栈自动完成的,作为底层服务,这个过程对应用程序是透明的,我们开发应用程序的时候,只需要使用应用层编程接口就行了,比如套接字接口。

所以,大部分人对TCP三次握手的概念还是建立在书本上,博客里,公众号文章里,今天,我们自己来发送TCP数据包来实现三次握手!

自己发包,来验证我们上面的结论!

使用的工具,是之前一篇文章中提到的神器:scapy

为了方便查看数据,我找了一个没有HTTPS的网站,通过ping它的域名,拿到了IP地址,向其进行握手并发送GET请求包。

from scapy.all import *
def tcp_test(ip, port, data):

    # 第一次握手,发送SYN包
    # 请求端口和初始序列号随机生成
    # 使用sr1发送而不用send发送,因为sr1会接收返回的内容
    ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)

    # 假定此刻对方已经发来了第二次握手包:ACK+SYN

    # 对方回复的目标端口,就是我方使用的请求端口(上面随机生成的那个)
    sport = ans[TCP].dport
    s_seq = ans[TCP].ack
    d_seq = ans[TCP].seq + 1

    # 第三次握手,发送ACK确认包
    send(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A'), verbose=False)

    # 发起GET请求
    send(IP(dst=ip)/TCP(dport=port, sport=sport, seq=s_seq, ack=d_seq, flags=24)/data, verbose=False)
  
  
if __name__ == '__main__':
    data = 'GET / HTTP/1.1\n'
    data += 'Host: www.chengtu.com\n'
    data += 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36\n'
    data += 'Accept: text/html'
    data += '\r\n\r\n'

    tcp_test("150.138.151.65"80, data)

执行上面这段代码,来抓包看一下:

可以看到,成功的完成了三次握手动作,服务器还返回了数据,证明手动编程来握手是可行的。

下面论证星球中,球友提出的问题:第三个握手包里面能不能携带数据呢?

我们来试一下就知道了:

from scapy.all import *
def tcp_test_2(ip, port, data):

    # 第一次握手,发送SYN包
    # 请求端口和初始序列号随机生成
    # 使用sr1发送而不用send发送,因为sr1会接收返回的内容
    ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)

    # 假定此刻对方已经发来了第二次握手包:ACK+SYN

    # 对方回复的目标端口,就是我方使用的请求端口(上面随机生成的那个)
    sport = ans[TCP].dport
    s_seq = ans[TCP].ack
    d_seq = ans[TCP].seq + 1

    # 第三次握手,发送ACK确认包,顺带把数据一起带上
    send(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A')/data, verbose=False)


if __name__ == '__main__':

    data = 'GET / HTTP/1.1\n'
    data += 'Host: www.chengtu.com\n'
    data += 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36\n'
    data += 'Accept: text/html'
    data += '\r\n\r\n'

    tcp_test_2("150.138.151.65"80, data)

看到了吧,在第三次握手中,我的GET请求就带过去了,TCP协议仍然能够正常工作!

这是Linux的情况,我又找了我们大学的网站试了一下,因为学校网站没用HTTPS(就很离谱),而且是ASP.NET技术栈做的(别问我怎么知道的),服务器是Windows,依然可以正常工作,说明Windows的协议栈也支持这种操作。

接下来验证另一个问题:如果第三次握手包服务器没有收到,就直接发送数据,会发生什么?

怎么验证,很简单,直接把发送第三次握手的那一行注释掉,不发送第三次握手,直接发送GET请求就行了:

from scapy.all import *
def tcp_test(ip, port, data):

    # 第一次握手,发送SYN包
    # 请求端口和初始序列号随机生成
    # 使用sr1发送而不用send发送,因为sr1会接收返回的内容
    ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)

    # 假定此刻对方已经发来了第二次握手包:ACK+SYN

    # 对方回复的目标端口,就是我方使用的请求端口(上面随机生成的那个)
    sport = ans[TCP].dport
    s_seq = ans[TCP].ack
    d_seq = ans[TCP].seq + 1

    # 第三次握手,发送ACK确认包
    # send(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A'), verbose=False)

    # 发起GET请求
    send(IP(dst=ip)/TCP(dport=port, sport=sport, seq=s_seq, ack=d_seq, flags='A')/data, verbose=False)
  
  
if __name__ == '__main__':
    data = 'GET / HTTP/1.1\n'
    data += 'Host: www.chengtu.com\n'
    data += 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36\n'
    data += 'Accept: text/html'
    data += '\r\n\r\n'

    tcp_test("150.138.151.65"80, data)

结果发现依然能正常工作!分析了一下,发现这种方式其实和上面那种情况是等价的:直接在第三次握手包中带了数据。

这里虽然把第三次握手那一行注释了,但直接发送的那个GET请求包中,ACK标记是置位了的,所以服务端就把这个GET包当成了第三次握手了。

所以结论就是:如果第三次握手包服务器没有收到,就直接发送数据,服务器将这个携带应用数据的包当做第三次握手(前提是这一个包中携带有ACK标记)。

除了我上面的回答外,这位球友又评论补充了一个问题:

其实看到这里,这个问题的答案想必已经心中有数了,但咱们还是来实验模拟一下:先发送带数据的请求包,然后再发送第三次握手包,看看会发生什么?

从图中可以看到,直接发送的那个带数据的请求包,被当做了第三次握手包,而后面再发送的那个名义上的第三次握手包,也就是图中黑色的那一行,被当作了重复发送的无效包,被忽略掉了,对通信没有造成影响。

以上就是我对这位球友问题的全部解答。

关注视频号,参与留言送书活动

  推荐阅读:

到底该选择32位还是64位版本的Office?微软为你解答疑惑

最多能创建多少个 TCP 连接?

淘宝二面,面试官居然把TCP三次握手问的这么详细

好家伙!黑客这样干翻了TCP/IP!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存