Python网络:TCP和UDP
#编程 #python #networking

在最后一期中,我们查看了IP标头。您可能看到的一件事是端口号。这对于建立互联网连接非常重要。好吧,事实证明,IP倾向于封装其他协议(这就是为什么将协议作为标头的一部分)。在本文中,我们将研究有关Internet流量的两个流行协议:TCP和UDP。在开始安装scapy之前,我们将使用它来使事情变得更容易,并且将在UDP部分中使用的dnslib

$ pip install scapy dnslib

请注意,如果您还没有阅读前两期,并且是这些概念的新事物,我强烈建议您阅读它们。它们包含将在本文中使用的基本知识。

TCP流

TCP或传输控制协议可以在网络通信中鲁棒性。考虑客户端发送服务器数据的考虑,无法保证服务器已经活着或服务器收到的数据包实际上是来自客户端的。为了可视化这个问题,我将使用Wireshark,这是数据包周围的图形接口。安装,运行和配置Wireshark后,我致电Google.com:

$ curl www.google.com

当我这样做时,wireshark的填充有大量的数据,我将在这里剖析。因此,首先是交流的前四部分:

59468 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=554354431 TSecr=0 WS=128
80 → 59468 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1412 SACK_PERM TSval=2112863826 TSecr=554354431 WS=256
59468 → 80 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=554354458 TSecr=2112863826
HTTP    144 GET / HTTP/1.1 

因此,在进行任何HTTP调用之前,发生3次单独的交易。呼叫转储显示为:

  • Syn
  • syn,ack
  • ack

认为这是一个敲门的人:

  • 敲门(SYN)
  • 是谁? (SYN,ACK)
  • 包装交付(ACK)

假设您期望打开门的包装,并且数据交易(接受包)将开始。现在,送货员不仅站在那里。一旦他们放下所有合格的套餐,他们将继续进行其他交货。因此正在发生另一个交流顺序:

59468 → 80 [FIN, ACK] Seq=79 Ack=20198 Win=72320 Len=0 TSval=554354561 TSecr=2112863930
80 → 59468 [FIN, ACK] Seq=20198 Ack=80 Win=65536 Len=0 TSval=2112863955 TSecr=554354561
59468 → 80 [ACK] Seq=80 Ack=20199 Win=72320 Len=0 TSval=554354587 TSecr=2112863955
  • 包装的签名(s)(fin,ack)
  • 送货人员确认签名,谢谢您(Fin,ack)
  • 您关上门(ACK)

SYN/SYN+ACK/ACK链的目的是确保可以与连接的另一端进行通信。 SYN+ACK告诉客户端服务器可以响应,ACK告诉服务器客户端可以响应。如果在设定的时间后没有对任何调用的响应,则该连接将作为超时删除。该系统还导致攻击向量被称为SYN flood。在此攻击中,恶意客户端将发送SYN数据包,而无需从服务器响应SYN+ACK。服务器正在使用系统资源来保存正在进行的请求。这足以使服务器无法处理合法流量。

鳍通信具有类似的性质,它让服务器知道已收到所有数据。该系统之所以存在,主要是因为对数据的响应非常普遍,以多个块发送。它还可以让服务器知道它不需要重新发送任何数据包,因为TCP的稳健性之一具有重试数据可能因连接不良而丢失的数据。

TCP标头布局

很像IP IP TCP协议由标准RFC9293决定。 Wikipedia具有TCP标头看起来的nice visual layout

Visual view of a TCP header

在尺寸方面,布局离IP标头不远。数据偏移量的工作原理与IP标头的IHL非常相似,并且至少是五个32位值,具有32位选项。当IP标头处理大部分时,交通流管理并不多。

TCP标题解析

现在是时候实际使用Python(尤其是struct.unpack)在TCP标头上工作了。查看我们拥有的标题:

  • H:16位源端口
  • H:16位目标端口
  • L:32位序列号
  • L:32位确认号
  • b:4位数据偏移 +填充
  • b:8位旗
  • H:16位窗口尺寸
  • H:16位校验和
  • h:16位紧急指针

这当然是假设没有可选的32位选项。这次我带了一个包装包,该数据包的IHL为5,因此这意味着标头就像最小IP标头20个字节一样。我将提供我使用的数据包数据,因此您可以更轻松地遵循:

>>> packet = b'E\x02\x01\xd8\x9b\xd9@\x00\x80\x06\x00\x00\xac\x12\x80\x01\xac\x12\x80\x01\xff\xfd"\xb8\x91S\xaf3\xc5\x10\xd8_P\x18\x04\xff\xac\xc9\x00\x00GET / HTTP/1.1\r\nHost: 172.18.128.1:8888\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: en-US,en;q=0.9\r\n\r\n'
>>> ip_header = struct.unpack('!BBHHHBBH4s4s', packet[0:20])

现在是进行标题解析的时间:

>>> tcp_header = struct.unpack('!HHLLBBHHH', packet[20:40])
>>> tcp_header
(65533, 8888, 2438180659, 3306215519, 80, 24, 1279, 44233, 0)

现在可以简单地获得前4个值。这里的源端口是65533是本地客户端端口。 8888是目的地端口。鉴于此流程,可以认为这是服务器接收的数据包。序列号告诉服务器“这是我要发送的数据包中的该数据包适合的位置”,并且确认编号都可以让服务器知道您收到了一组数据,并且已经准备好了下一组。数据偏移量与IP标题中的IHL非常相似。构成标题的32位单词的数量。因为这是正确的转移可以使我们容易获得价值的前4位:

>>> tcp_header[4] >> 4
5

这里的值为5,因此没有TCP选项(尽管我稍后会讨论它们)。接下来是一系列标志,是1位值:

>>> format(tcp_header[5], '08b')
'00011000'
>>> cwr, ece, urg, ack, psh, rst, syn, fin = list(format(tcp_header[5], '08b'))
>>> cwr, ece, urg, ack, psh, rst, syn, fin
   ('0', '0', '0', '1', '1', '0', '0', '0')

我已经更改了输出,以使其更易于阅读。鉴于这些标志中有很多不错的数量,我将在以后的部分中更详细地介绍它们。目前,设置了ACK和PSH,这表明正在发送数据,并告诉收件人发送任何缓冲数据。窗口大小是字节中的大小(技术上窗口尺寸单元,但这是现代用法中的字节),在这种情况下,收件人可以收到:

>>> tcp_header[6]
1279

是1279个字节。请注意,由于网络的最大值的最大值为65536字节(65kb-ish),似乎在小尺寸上似乎有些略有。为了解决这是TCP选项之一,可以使用Window Scaling。这将告诉接收者左移窗口尺寸值一定的数量(这增加了2个值的功率降落)。此选项仅在三种握手中设置,并且在所有其他情况下都被忽略。接下来是校验和

>>> tcp_header[7]
44233

由于有点涉及,我将在专门的部分中对此进行解释。就目前而言,这是为了确保数据不会损坏。最后,仅在设置urg标志时(不是),这是有效的紧急指针。这意味着值本质上是0填充:

>>> format(tcp_header[8], '08b')
'00000000'

现在,TCP选项的工作方式相当有趣。我继续解析了一个具有TCP选项的数据包(数据偏移8)。因此,有4个字节的3个TCP选项段,这意味着TCP标头的最小尺寸为20个字节的12个字节:

>>> tcp_options = packet[40:52] 

现在,这种工作方式是您以单个字节读取的方式,这是选项类别:

>>> tcp_options[0]
1

现在看桌子:

TCP options table

这是一个没有选项长度和选项数据的非操作。因此,我们继续前进到下一个字节:

>>> tcp_options[1]
1

在这里,同一件事,请访问下一个字节:

>>> tcp_options[2]
8

这是根据表表示的时间戳选项,其中下一个字节为选项长度。还有两个包含时间戳的4个字节值:

>>> option_length, timestamp1, timestamp2 = struct.unpack('!BLL', tcp_options[3:])
>>> option_length, timestamp1, timestamp2
(10, 3008363081, 3008363081)

现在,两个1个字节NOOPS +选件类别和选项长度呈32位,并且两个时间戳都占32位。本质上,Noops充当填充物,因此一切都可以干净地融入三个32位段。如果您想了解更多有关时间戳的工作方式(实际上不是您想象的Unix时间戳),请查看this article

现在解释了选项,我们将返回数据包,没有选项并获取数据,该数据从40字节开始:

>>> print(packet[40:].decode('utf-8'))
GET / HTTP/1.1
Host: 172.18.128.1:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9


>>>

因此,这是我为根页面制作的快速Python HTTP服务器的请求。为了很好地打印它,我将字节解码为UTF-8,以便它们干净地打印出来。我将在后来的文章中详细介绍HTTP协议。

TCP标志

现在我们将更详细地查看TCP标志。

网络拥塞

CWR和ECE标志涉及TCP的网络拥塞功能。从本质上讲,当网络充血时,处理事物的正常方法是放下数据包。交通拥堵功能将使网络设备标记包的交通拥堵。然后,发件人将降低其传输率,以帮助减少对拥塞的影响。 CWR和ECE标志确定双方是否支持此功能以及是否发生拥堵。当该功能最初是作为RFC 3168在2001年的RFC 3168时,某些过时或故障的硬件会看到标志,并且不知道它们是什么,将数据包放下。值得庆幸的是,从那时起,这有所改善。

数据接收

urg标志表明应分析标题中的以后字段。此功能表明,会话的某些部分应以更优先的速率发送。仅ACK表示建立Syn/Ack握手(也称为三路握手),并响应Fin+Ack以关闭连接。 PSH告诉服务器立即发送任何缓冲数据。

连接下降

rst是一种强制连接下降的方式,可以在网络中的任何点注入。虽然使用量是为了让防火墙放下不需要的数据包或服务器拒绝连接,但它也有一个有趣的历史记录。康卡斯特(Comcast)是ordered by the FCC(PDF),以结束将RST数据包从客户中注入对等流量,以破坏流量。这可以进一步用于censorship purposes。考虑到所有这些research has been done尝试跟踪这种强制注射。

启动/停止连接

最后一个标志用于通用握手。服务器发送的SYN+ACK本质上是设置SYN和ACK标志的组合。鳍和ack也是如此。请注意,SYN和SYN+ACK的目的是简单地建立连接,并且将出现而无需附带任何TCP数据。

TCP校验和

现在是时候看看校验和工作方式了。不用担心它真的很简单:

"The checksum field is the 16-bit ones' complement of the ones' complement sum of all 16-bit words in the header and text. The checksum computation needs to ensure the 16-bit alignment of the data being summed. If a segment contains an odd number of header and text octets, alignment can be achieved by padding the last octet with zeros on its right to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing the checksum, the checksum field itself is replaced with zeros."

Female calculating complex math

现在,我可以在山上隔离自己,以冥想足以向您解释这一点,但我会做我们所谓的亲格拉姆的行动。没错,我们要去borrow code!因此,我们将继续导入:

from scapy.utils import checksum as witchcraft

Witchcraft

这真是太奇怪了,以至于整个RFC致力于checksum calculation。谢谢1980年代...现在,如果事情还不够糟糕,我们实际上必须创建一个“伪标头”,它是IP标头某些部分与TCP标头组合的组合,将校验和设置为0 和TCP数据:

Visual representation of a pseudo-header

不要弄乱原始数据包数据,我将进行深层副本:

>>> from copy import deepcopy
>>> pseudo_packet = bytearray(deepcopy(packet[20:]))

需要bytearray铸件,因为数据包类型是bytes,它是不变的,而bytearraybytearray是不变的,这几乎是相同的东西。我们仅对伪数据包的其余部分使用TCP数据,因此我们只能抓住它(被IP标头20字节抵消)。现在,打开包装和重新包装将是乏味的,因此我们将对数据包字节进行简单的阵列操纵。校验和值的偏移为16个字节,长度为2个字节,因此我们可以使用数组剪接来检索值:

>>> struct.unpack('!H', pseudo_packet[16:18])[0] == tcp_header[7]
True

所以现在我们可以将其设置为0:

>>> pseudo_packet[16] = 0x0
>>> pseudo_packet[17] = 0x0

现在是时候制作伪标题了:

>>> pseudo_header = struct.pack('!4s4sHH', ip_header[8], ip_header[9], socket.IPPROTO_TCP, len(pseudo_packet))

这是SRC IP地址,DEST IP地址,协议的数字值(TCP的6)以及Byte中数据包的TCP部分的长度的组合。现在一切都准备好了,是时候获得实际的校验和了:

>>> checksum = witchcraft(pseudo_header + pseudo_packet)
>>> checksum
44233
>>> tcp_header[7]
44233

因此,校验和匹配,我们可以放心数据是固体的。

UDP流

接下来是UDP或用户数据报协议。在这种情况下,我将进行DNS查询(DNS在UDP上运行):

$ dig mozilla.org

查看Wireshark流量:

556 1587.220073 172.18.139.193  172.18.128.1    DNS 94  Standard query 0x0748 A mozilla.org OPT
557 1587.236137 172.18.128.1    172.18.139.193  DNS 130 Standard query response 0x0748 A mozilla.org A 44.236.72.93 A 44.236.48.31 A 44.235.246.155

这就是一切。这将类似于送货驱动程序是否只是敲门并将包裹留在您家门口。它使送货人员的工作更快,但没有确认(让我们假装送货服务没有给您发送电子邮件/发短信),您实际上得到了包裹,并且您可能没有接受它。这也意味着直到您获得包裹,有人可以简单地将其从您家门口(数据包丢失)中窃取。

UDP标头布局

现在UDP非常非常简单。实际上,定义它的标准RFC768可能是我见过的最短的RFC之一。标题格式本身很短:

Visual display of UDP headers

与TCP不同,没有流量控制,对测序的关注以及任何三种握手。这是一个“火与忘记”协议。

UDP标题解析

首先,我将为我使用的数据包提供字节:

>>> packet = b'E\x00\x00t\n\x1a\x00\x00\x80\x11\xccw\xac\x12\x80\x01\xac\x12\x8b\xc1\x005\xc9\xe4\x00`Q\xf07\xa0\x81 \x00\x01\x00\x03\x00\x00\x00\x00\x07mozilla\x03org\x00\x00\x01\x00\x01\x07mozilla\x03org\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04,\xecH]\xc0\x1d\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04,\xeb\xf6\x9b\xc0\x1d\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04,\xec0\x1f'
>>> ip_header = struct.unpack('!BBHHHBBH4s4s', packet[0:20])

关于UDP解析的一件好事是您不必担心32位选项。标头始终是8个字节的固定宽度总数(64位)。这意味着解析就像:
一样简单

udp_header = struct.unpack('!HHHH', packet[20:28])
>>> udp_header
(53, 51684, 96, 20976)

其中51684是本地端口,而53是远程端口。由于53是源端口,这意味着从DNS服务器回来了回复。接下来是字节中的长度为96(是否32位单词计数):

>>> len(packet[20:])
96

校验和实际上是UDP的可选,但在计算上与TCP版本相似:

>>> udp_header
(53, 51684, 96, 20976)
>>> pseudo_packet = bytearray(deepcopy(packet[20:]))
>>> pseudo_packet[6] = 0x0
>>> pseudo_packet[7] = 0x0
>>> pseudo_header = struct.pack('!4s4sHH', ip_header[8], ip_header[9], socket.IPPROTO_UDP, len(pseudo_packet))
>>> checksum = witchcraft(pseudo_header + pseudo_packet)
>>> checksum
20976
>>>

在所有TCP工作之后,我希望有人让某人简单地解析协议是可以理解的。现在查看其余数据,该数据从IP标头(20个字节)和UDP标头(8个字节)开始:

>>> packet[28:]
b'7\xa0\x81 \x00\x01\x00\x03\x00\x00\x00\x00\x07mozilla\x03org\x00\x00\x01\x00\x01\x07mozilla\x03org\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04,\xecH]\xc0\x1d\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04,\xeb\xf6\x9b\xc0\x1d\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04,\xec0\x1f'

这是很多二进制数据,尽管Mozilla和org在几个地方显示。为了解决这个问题,我们将使用dnslib来解析结果:

>>> from dnslib import DNSRecord
>>> DNSRecord.parse(packet[28:])
<DNS Header: id=0x37a0 type=RESPONSE opcode=QUERY flags=RD,AD rcode='NOERROR' q=1 a=3 ns=0 ar=0>
<DNS Question: 'mozilla.org.' qtype=A qclass=IN>
<DNS RR: 'mozilla.org.' rtype=A rclass=IN ttl=0 rdata='44.236.72.93'>
<DNS RR: 'mozilla.org.' rtype=A rclass=IN ttl=0 rdata='44.235.246.155'>
<DNS RR: 'mozilla.org.' rtype=A rclass=IN ttl=0 rdata='44.236.48.31'>

查看结果,我们看到对mozilla.org查询的响应已返回并在结果中提供了3个IP地址。鉴于DNS对提出请求的关键是UDP标头和二进制格式的紧凑性的重要性。

TCP还是UDP?

在比较协议方面,这两者都经常在Internet中使用。当您想确保获得所有数据时,TCP会更加擅长。网页需要完整的HTML渲染。如果是UDP,则简单的数据包丢失可能意味着您的段落标签会丢失。文件传输也是如此,其中一些损坏的字节可能会破坏整个文件。

另一方面,

UDP在高性能方面表现出色。 DNS已经提到,但这对于VoIP和在线游戏也很重要。 VoIP可以显示一些文物,或者如果发生腐败发生,也许会有一些口吃。但是,等待三种握手会太贵了。如果您想了解更多有关在线游戏中使用UDP的信息,我强烈建议您推荐Glenn Fiedler's series on the topic。同样,您不想等待免费的方式握手来进行游戏动作以注册到服务器。

另一个有趣的功能是,UDP能够一次使用multicast一次访问多个客户端。 TCP无法做到这一点,因为三种握手强迫1:1的连接。多播对网络设备发现(例如Simple Service Discovery Protocol (SSDP))很有用。还有一个协议Real-Time Publish-Subscribe (RTPS)(是的,实际上称为OMG),该协议用于帮助达到UDP的鲁棒性问题之间的平衡,同时提供多播功能。这在their forums is a good resource上写了。

结论

这得出了对TCP和UDP的长期观察。如果有一件事,您肯定应该从中获得一件事,尽管这一切都是有趣的,而明智的做法是,如果您只想查看数据包数据,则最好使用Scapy进行现实世界使用或Wireshark之​​类的东西。犯错很容易(本文涵盖了几天的证明,尤其是校验和计算)。在下一个计划的分期付款中,我们将研究服务器的工作方式。