TCP的三次握手
scapy
最近找到一个看上去很有用的东西, 可以让我们自己来发送TCP数据包. scapy .
安装应该不算难, 直接使用pip就可以了. 文档在这里.
pip install scapy
最近我把conda卸载了改用uv, 如果你也使用uv的话那可以这样.
uv init tcp_test
uv add scapy
这样scapy应该就算配置好了, 至少我是这样做的. 文档里说windows还需要做别的事情, 但是我没做.
理论上的三次握手
在书上或者别的什么文章我们知道, TCP握手中, 发送方A会准备一个随机数作为序列号, 就叫做A_SEQ吧, 将SYN位置1, 发送第一个握手包. 这个握手包是整个TCP里唯一ACK位置0的包.
接收方B接收到这个握手包后, 也会准备一个随机数作为序列号, 叫B_SEQ吧, 将SYN和ACK置1, 然后把ACK设置成A_SEQ+1, 发送第二个握手包, 然后进入半连接状态.
A接收到第二个握手包后, 就可以发送第三个握手包了, 序列号设置成A_SEQ+1, ACK号设置成B_SEQ+1. B接收到这个握手包就会进入连接建立状态. 然后双方就开始正式发信息了.
为什么不是四次?
当时学到这里不知道大家会不会有这么一个问题, 为什么是三次握手而不是四次? 假如这第三个握手包丢了A不是不知道吗?
其实没有影响. 可以手动发一次请求看看这个TCP连接的数据包.
curl --url http://157.148.69.186/ --header "Host:www.baidu.com"
在 wireshark 可以找到一条自己发过去的HTTP GET请求, 这个其实是我们三次握手后发送的第一个数据. 点进去看它的SEQ和ACK可以发现, 相对SEQ是1, 相对ACK也是1. 而我们的第三次握手包的相对SEQ是1, 相对ACK也是1.
书上也明确写出了第三次握手包可以携带数据. 也就是说, 无论第三次握手包有没有丢, 我们都可以放心地发送第一份数据, 对方会把这个数据当作第三次握手包的.
在第三个握手包被接收前对方是不能发送数据的, 因此我们发送的数据包一定跟原本的第三个握手包一样.
那如果第三次握手包延时到达了呢, 其实也没什么所谓. 对方会认为这是一个重传的数据包, 丢弃掉.
为什么不是两次?
这个其实挺简单的. 在第三个握手包到达前, 对方不知道我方到底有没有准备好或者有没有接收到正确的序列号, 因此不能发送数据. 只有接收到这第三个握手包后, 对方才知道我方准备好了, 可以进行数据传输.
使用scapy来建立三次握手
scapy是一个强大且易于使用的工具, 可以简单学一下怎么使用.
构造数据包
from scapy.all import *
ip_packet = IP(dst='157.148.69.186') # 构造一个ip数据包
sport = RandShort() # 这是scapy提供的, 可以生成一个随机的端口
tcp_packet = TCP(dport=80, sport=sport) #构造一个tcp报文段
payload = 'GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n' # 准备放在tcp的payload中
packet = ip_packet / tcp_packet / payload # 这样就构造好一个数据包了!
send(packet) # 发送这个数据包
# 或者这样发送
resp = sr1(packet, verbose=False) # 发送并只接受一个响应包
answered, unanswered = sr(packet, multi=True, timeout=5, verbose=False) # multi允许一次请求响应多个包, sr表示获取所有请求响应对, 不过这里只发送了一个包
这里直接运行代码肯定是不成功的, 因为没有进行TCP握手. 我们需要先完成握手.
Hello, baidu!
from scapy.all import *
data = 'GET / HTTP/1.1\r\n'
data += 'Host: www.baidu.com\r\n'
data += 'User-Agent: Mozilla/5.0\r\n'
data += 'Accept: text/html\r\n'
data += 'Connection: close\r\n'
data += '\r\n'
ipLayer = IP(dst='157.148.69.186')
sport = RandShort() # 我方端口(源端口)
dport = 80 # 目标端口
s_seq = RandInt() # 我方序列号(源序列号)
# 发送第一个握手包
resp1 = sr1(ipLayer/TCP(dport=dport, sport=sport, seq=s_seq, flags='S'), verbose=False)
# 读取返回的第二个握手包, 发送第三个握手包
sport = resp1[TCP].dport # 如果继续使用原来的sport那又会是一个随机的
s_seq = resp1[TCP].ack # 这里也一样
d_seq = resp1[TCP].seq # 目标序列号
send(ipLayer/TCP(dport=dport, sport=sport, seq=s_seq, ack=d_seq+1, flags="A"), verbose=False)
answered, _ = sr(ipLayer/TCP(dport=dport, sport=sport, ack=d_seq+1, seq=s_seq, flags="A")/data, multi=True, timeout=5, verbose=False)
for _, resp in answered:
print(resp[TCP].load) # 打印数据包的数据, 可能有点长不打印也没什么关系, 反正主要看wireshark
执行这份代码, 没有意外的话会打印出三份左右的数据, 这是因为html有点长所以分开来发送了.
在wireshark应该也能看到我们完成了三次握手, 并发送了一次HTTP请求. 正常来讲我们还需要在接收到文件后响应一个ACK, 最后也需要挥手. 不过这里没做.
有了这份代码, 可以尝试之前的东西了, 比如去掉第三次握手, 只需要注释掉第三次握手即可.
没有意外的话去掉第三次握手也会正常打印出数据, wireshark也正常. 下面可能还有一些重传, 不用理会, 这是我们没响应ACK导致的.
还可以试着交换第三次握手和发送http请求, 应该可以看到对方对我们的"第三次握手"返回了一个TCP Dup ACK
, 这个通常在接收方没有收到预期序列号的包, 或者乱序, 冗余数据包时发送的.