Skip to content

Latest commit

 

History

History
5923 lines (4356 loc) · 352 KB

221112a_network.md

File metadata and controls

5923 lines (4356 loc) · 352 KB

计算机网络

自底向上,讲述实用原理与细节为主。主要参考IETF RFC文档,目前的实际应用可能会有所出入

目录

1 协议栈分层模型

OSI分为7层,定义如下

名称
7 Application 应用层
6 Presentation 表示层
5 Session 会话层
4 Transport 传输层
3 Network 网络层
2 Datalink 数据链路层
1 Physical 物理层

TCP/IP(DoD/ARPANet)是事实上的网络分层标准,分为5层(一说4层,不包含物理层),定义如下

名称
5 Application 应用层
4 Transport 传输层
3 Network 网络层
2 Datalink 数据链路层
1 Physical 物理层

Physical物理层定义了网络传输的物理媒介与连接,如同轴电缆,双绞线,光纤,无线电信号等,定义了实际的编码方式以及信号类型(例如互联方式,无线信号还是电信号或光信号,全双工还是半双工,差分信号还是非差分信号,以及这些信号的调制解调方法)。由于物理层类型繁多,且物理层中数据链路层数据可以和其他许多专有协议数据共存,如果不研究硬件,在4层模型中我们可以不用关注该层

Datalink数据链路层定义了同一个网络中主机设备之间的数据传递,常见的协议有Ethernet(IEEE802.3),在Ethernet中每台设备使用MAC地址作为唯一标识。最传统的二层交换机工作于该层,它不需要MAC地址,只需学习、检查各个端口发来的数据包的MAC并查表发送到对应端口即可

Network网络层定义了网络中终端设备之间的数据传递,该层不提供可靠传输。IPv4以及IPv6是该层事实上的标准协议。路由器工作于该层,这些网络设备每一个端口都需要有单独的MAC地址对应

Transport传输层定义了终端设备上应用实例之间的数据传输,该层通常需要为数据传输的可靠性提供支持,提供例如数据纠错,缓冲,传输控制等功能,常见的协议有TCPUDPTCPUDP都使用端口机制。其中UDP由于不提供可靠传输,在实际中少有单独应用,通常都需要加上额外的包装才能为上层应用提供高效可靠的通信

Application应用层常见的协议有HTTPHTTPSSMTPIMAPSSH等。这些应用层协议会调用特定的Transport传输层协议来实现自己的功能

之所以叫协议栈,是因为上层需要依赖于下层提供的服务才能运行。从二层交换机到路由器再到主机,这些设备可以处理的网络层级也越高。数据包装是层层嵌套的

2 常用评估指标

2.1 带宽(Bandwidth)

用于衡量单位时间内传输的数据量,也即容量Volume。单位kbit/s Mbit/s。在通信中使用十进制,以bit为基本计量单位,1kb=1000b1Mb=1000kb

2.2 延迟(Latency)

数据从一个地方传送到另一个地方花费的时间,通常为ms级别。许多网络应用受延迟影响较大

2.3 错误率(Error Rate)

传输的误码率,如果网络通路良好,这个数值通常非常小,常见的计量单位有bit/Gb等。在实际应用中路由器和交换机等基础设施引入的处理延迟以及丢包才是影响普通用户使用体验的关键因素

3 数据链路层

3.1 Ethernet以太网

参考IEEE802.3

以太网是目前最为主流的链路层协议之一,数据使用以太网数据帧(数据包)的方式进行组织与传输

3.1.1 物理介质

以太网最早在1983年以IEEE802.3标准化。最早的以太网使用同轴电缆(coaxial)作为传输介质;后来发展出了如今最为常见的8芯双绞线,使用RJ45接口;以及光纤,现在光交换机也非常常见,常见光纤接口有SC,LC,ST等。以太网的基础设施主要有以太网交换机等。如果使用了光纤,在交换机、路由器、网卡会添加额外的光接口,这些设备经常会使用可拆卸的光模块(SFP)

国内运营商光纤入户最常用EPON以及GPON,这些光网络除上网数据外还需要搭载IPTV,语音(座机)以及运营商管理等服务,普通的上网数据在PON网络上走以太网协议。在GPON网络中,语音等服务会使用其他一些协议如ATM搭载。而EPON网络中语音也使用以太网数据帧搭载

多个邻近家庭/用户的网络一般通过分光器共享,分光器再向上连接运营商的OLT设备,这样就形成了多个ONT终端连接一个OLT的PON网络结构。这导致了上下行通信的不对称,从用户角度看下行使用广播的方式,而上行为了解决冲突问题使用TDMA时分复用

光纤的主要优点是抗干扰能力强,传输距离远。事实上光信号在光纤中的传导速度要慢于电信号在铜丝中的传导速度。实际应用中,有时各级路由器以及交换机带来的处理延迟才是更主要的影响因素

3.1.2 以太网帧格式

名称 长度(Byte) 注解
Preamble 前导 7 0x55,用于双方同步时钟
SFD 帧起始分隔符 1 0xD5,标志以太网帧的起始
Destination MAC 目标MAC 6 接收端的MAC地址。FF:FF:FF:FF:FF:FF为广播地址(此外还有组播MAC,这里省略),数据包会传输到网络上所有的机器并被接受
Source MAC 源MAC 6 发送端的MAC地址
802.1Q VLAN标签 4 可选,前2字节为TPID,通常为0x8100表示802.1Q,后2字节为TCITCI[15:13]PCP用于规定优先级,TCI[12]DEI表示数据包在拥塞时可以丢弃,TCI[11:0]VID表示VLAN ID,可取范围[1,4094]。VLAN用于在一个物理以太网络中虚拟出多个网络,这些网络互不干涉
EtherType/Size 上层协议/包大小 2 值取[42,1500]表示Payload的长度(单位Byte),值取1536及以上表示上层协议的类型
Payload 数据载荷 长度可变,保证从目标MAC到FCS长度不小于46字节(CSMA/CD规定的最小长度)。有VLAN标签时Payload最短需要42字节
FCS CRC校验码 4 使用CRC32,多项式0x04C11DB7
Inter Frame Gap 帧间隔 12 必须有的间隔,标志着数据帧的结束

以太网数据使用大端传输,但是单个Byte使用LSB在前的传输方法。例如上表中的0x55使用LSB在前的方式传输就会变成0xAA,分隔符0xD5会变成0xAB。只有FCS校验码使用MSB在前的传输方法

MAC地址

MAC地址由硬件厂商向IEEE申请后分配,在一个网络中用于标记一个唯一的接口。例如网卡,笔记本的有线和无线网卡拥有不同的MAC。有些网卡的MAC可以更改,如果一张网卡集成了多个网口,这些网口也会拥有不同的MAC。网络和网络之间可以使用路由器进行分隔,正常情况下一个网络内出现MAC冲突的几率非常小

MAC地址最高1字节的LSB(I/G)用于区分单播0多播1(传输时该位首先传输),而最高1字节的倒数第2位(U/L)用于区分该地址是全球唯一0还是本地唯一1,所以我们平常看到的网卡MAC地址最高1字节都是4的倍数。如果用户明确指定,U/L可以会被设为1,例如在iPhone上开启私有无线局域网地址,就会使用这样的一个MAC地址

在实际应用中,如果不信任网络内的交换机/路由器,可以使用随机MAC防止恶意跟踪

日常生活中,在一个局域网内,我们访问外网只能通过默认的路由器(默认网关)。路由器是第三层网络设备,我们通过抓取网页浏览产生的数据流量,可以发现无论访问的IP地址如何变化,数据包的MAC地址永远都是默认网关端口的MAC和我们的网口MAC。以太网数据包从一个网络到另一个网络需要路由器对MAC地址进行转换。而二层交换机只负责单个网络内的数据流通(启用VLAN时除外),交换机本身不需要MAC地址,只需根据学习到的MAC将数据发送到对应的物理接口就可以了

在一个网络中,如果需要嗅探所有主机间的数据包,就需要开启网卡的混杂模式,否则网卡只会将符合自己MAC或广播MAC的数据包提交给上层。但是这种数据包嗅探方式对于使用二层交换机的网络而言是无效的,在使用网桥或以太网HUB的网络中才可用。类似地,FCS域的CRC校验码也由网卡自行处理,发生错误的直接将数据包丢弃。区别是数据包的FCS不会递交给上层

CRC校验

IEEE802.3规定以太网的CRC32计算方法如下:

多项式0x04C11DB7,省略最高位实际33位0x104C11DB7

计算CRC的关键点如下:

CRC校验的区域包括2个MAC地址域,可选的VLAN标签域,EtherType/Size域,数据域以及数据域的占位符

校验区域的开头32位取补,数据左移32bit,进行计算

CRC结果取补码,并且传输时同样使用大端,但是MSB在前

实际应用中网卡固件通常使用查表的优化算法,或直接硬件实现

802.1Q优先级

802.1Q定义PCP优先级以及对应关系如下

优先级 数据类型
1 0 BK Background,最低优先级
0 1 BE Best Effort
2 2 EE Excellent Effort
3 3 CA Critical Applications
4 4 VI Video,要求小于100ms延迟
5 5 VO Voice,要求小于10ms延迟
6 6 IC Internetwork Control
7 7 NC Network Control

常用EtherType上层协议类型

协议 注释
0x0800 IPv4
0x0806 ARP MAC地址解析
0x0842 Wake-on-LAN 网络唤醒
0x8100 IEEE802.1Q VLAN
0x86DD IPv6
0x8808 Ethernet flow control 流控制
0x8863 PPPoE Discovery Stage
0x8864 PPPoE Session Stage
0x88CC LLDP Link Layer Discovery Protocol

3.1.3 以太网设施:Repeater和Bridge

Repeater相当于一个没有数据缓冲的模拟放大器,它只是将每个端口发来的信号再发送到其他所有端口。以太网HUB就是相当于Repeater,同时有两个主机发送数据会发生冲突

Bridge网桥有数据缓冲,它会将所有端口发来的数据进行缓存,之后再依次从所有网口发出。Bridge大大减小了冲突带来的影响

3.1.4 以太网设施:二层交换机Switch

二层交换机Switch是最常用的以太网设施。它本质是一种特殊的网桥,交换机可以理解以太网帧的MAC地址。它会学习各个端口发来的数据包的Source MAC源MAC并把对应关系记录到一张表中。如果端口发来数据包的Destination MAC存在于表中,那么交换机就会将这个数据包发往对应的端口。如果Destination MAC不在表中,这个数据包可能会从所有端口发出

算法较为保守的交换机会缓存整个数据包,进行CRC检错后才会将数据包发送往对应端口

算法较为激进的交换机会在收到目标MAC后立即开始数据包的转发(这也是将目标MAC放在开头的原因),此时由于发送方还未传输完毕所以无法进行CRC校验。这种交换机更为常用,需要更少的计算资源,包括缓存等,且拥有较小的延迟。但是纠错需要主机端来执行,且一次传输错误容易导致更多网络流量的浪费

3.1.5 CSMA/CD

CSMA/CD由网卡实现,全称载波侦听多路访问/冲突检测,这个协议广泛应用于通信领域,用于解决物理层面的冲突,尤其是半双工物理信道,比如同轴电缆。但是由于支持物理全双工RJ45接口的交换机的流行,信道争用少了许多,这个算法的作用已经弱化了。而在使用了无缓冲以太网HUB(Repeater)的网络中,这种算法依旧发挥重要作用

关于CSMA/CD,需要记住2个点:一个是载波监听,也就是一个设备发送前对信道进行监听,发现没有其他设备在发送时才会发送数据。而由于数据在信道上传播时延的存在,单独的监听无法完全避免冲突,冲突检测就是边发送边检测冲突。假设一个数据信号从信道一端到另一端的时间为 $\tau$ ,那么争用期长度为 $2\tau$

假设A和B使用一根长铜线进行通信,A发送了一个数据,这个数据在到达B之前B恰好也发送了一个数据(B在此之前未监听到A发送消息),那么两个信号在铜线上将会发生冲突。假设AB支持发送时冲突检测,B在发现自己发送的数据和监测到的铜线的电平不一致,立刻判定冲突停止发送。而A需要在B发送的电信号到达自己这一端时才能检测到电平的不一致问题。所以争用期长度为 $2\tau$。A需要至少等到 $2\tau$ 以后才能判定数据未发生冲突。如果长铜线还连接了其他设备,那么发生冲突的几率会更高

在检测到冲突以后,发送方需要首先发送一串干扰信号,以便让网络内设备知道发生了冲突

从冲突中恢复时,需要等待一段随机长度的时间,防止再次发生冲突

常用的8芯双绞线支持物理全双工通信,收发线路分开。一条线缆只能将2个端口连接,收发可以并行。而网络的组建依赖于交换机、网桥或HUB。在这种以太网环境下,只有在多个设备向同一个设备发送数据时才会产生冲突的问题

交换机以及网桥由于拥有数据缓冲,基本解决了物理通信冲突的问题(缓冲溢出除外)。如果一个发往目标MAC地址A的数据包还未发送完,此时其他目标地址为A的数据包就只能在缓冲区等待

无缓冲的HUB中如果多个设备同时发送数据就会导致冲突,设备主要通过载波监听(监听RJ45接收端)规避冲突

3.2 ATM Asynchronous-Transfer-Mode

目前普通消费级设备中已经很难再接触到ATM,原有的ATM应用领域大部分已经被Ethernet取代。ATM现今应用于一些有特殊要求的领域,在运营商的光网络中可以见到ATM,在GPON光网络中它用于搭载语音服务(从光猫连接出来的座机)

ATM的初衷就是降低通信的延迟,同时避免类似以太网中数据包乱序的问题。ATM基于虚拟线路Virtual Circuit设计,两个设备在通信之前需要网络建立一条虚拟通路。ATM使用48字节的定长数据包(这里称为Cell),Header长5字节,共53字节。之所以定为48字节是设计之初法国和美国的需求不同,最终在32字节和64字节取了中间数(结果谁也没得到好处

ATM数据包格式如下

ATM网络中用户到网络以及网络和网络之间使用的数据包是不相同的。用户到网络UNI数据包中GFC表示Generic Flow Control,没有实际作用永远为0VPIVirtual Path ID,长度1或1.5字节,VCIVirtual Channel ID,长度2字节,这2个数据域表示该数据包下一个目的地。PT表示数据包类型,0b1XX表示数据包用于网络管理,0b0XX表示用户数据包,0b01X表示网络拥塞。CLP为丢包优先级,只有2级。HECCRC,使用多项式0x107进行校验

ATM基于虚拟线路的设计导致其较为混乱,资源分配效率低,且带宽分配缺乏灵活性。最终其大部分普通应用被以太网替代

3.3 Avian Carriers

🖥  <--------  🕊✉️  <--------  🖥

RFC IPoAC April Fool's Day Series:

RFC1149 A Standard for the Transmission of IP Datagrams on Avian Carriers

RFC2549 IP over Avian Carriers with Quality of Service

RFC6214 Adaptation of RFC 1149 for IPv6

WWIII以后也许会用上,但那时候就不是搭载IP了

RFC1149

Status of this Memo

   This memo describes an experimental method for the encapsulation of
   IP datagrams in avian carriers.  This specification is primarily
   useful in Metropolitan Area Networks.  This is an experimental, not
   recommended standard.  Distribution of this memo is unlimited.

Overview and Rational

   Avian carriers can provide high delay, low throughput, and low
   altitude service.  The connection topology is limited to a single
   point-to-point path for each carrier, used with standard carriers,
   but many carriers can be used without significant interference with
   each other, outside of early spring.  This is because of the 3D ether
   space available to the carriers, in contrast to the 1D ether used by
   IEEE802.3.  The carriers have an intrinsic collision avoidance
   system, which increases availability.  Unlike some network
   technologies, such as packet radio, communication is not limited to
   line-of-sight distance.  Connection oriented service is available in
   some cities, usually based upon a central hub topology.

Frame Format

   The IP datagram is printed, on a small scroll of paper, in
   hexadecimal, with each octet separated by whitestuff and blackstuff.
   The scroll of paper is wrapped around one leg of the avian carrier.
   A band of duct tape is used to secure the datagram's edges.  The
   bandwidth is limited to the leg length.  The MTU is variable, and
   paradoxically, generally increases with increased carrier age.  A
   typical MTU is 256 milligrams.  Some datagram padding may be needed.

   Upon receipt, the duct tape is removed and the paper copy of the
   datagram is optically scanned into a electronically transmittable
   form.

Discussion

   Multiple types of service can be provided with a prioritized pecking
   order.  An additional property is built-in worm detection and
   eradication.  Because IP only guarantees best effort delivery, loss
   of a carrier can be tolerated.  With time, the carriers are self-
   regenerating.  While broadcasting is not specified, storms can cause
   data loss.  There is persistent delivery retry, until the carrier
   drops.  Audit trails are automatically generated, and can often be
   found on logs and cable trays.

Security Considerations

   Security is not generally a problem in normal operation, but special
   measures must be taken (such as data encryption) when avian carriers
   are used in a tactical environment.

4 网络层

网络层用于站点到站点之间的数据传输。目前几乎全部的设备在网络层都使用IPv4以及IPv6

IP协议工作于传输层协议如以太网更上一层。和MAC地址不同,一个数据包在不同网络之间传输,它的源IP和目标IP是不会改变的。经过NAT路由时除外

参考RFC791以及RFC1122

4.1 IPv4

IPv4数据包的以太网帧EtherType0x0800,使用32位表示一个站点,格式XXX.XXX.XXX.XXX

4.1.1 格式

在RFC791中定义如下

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                    Example Internet Datagram Header

Word 1

名称 长度(bit) 注解
Version 4 IP版本,这里是0x4
IHL 4 IP Header长度,使用4Byte为单位计数。如果IP Header没有Options那么这里为0x5,长20字节
ToS 8 服务类型。由于没有明确标准,不常用,为0x00。高3位表示数据包的传输优先级,接下来3位依次表示延迟、吞吐、可靠性要求
Total Length 16 总长度,使用字节计数,是包含IP Header的总长度(最长65535字节,64kB)

Word 2

名称 长度(bit) 注解
Identification 16 在分包时常用。一个数据包可以拆分成多个Fragment再传输,这个区域表示当前Fragment所属组(通常是同一个数据包)。大数据包传送到仅支持小数据包的网络中,如果网关(路由器)支持拆包就会分拆后传输
Flags 3 0b010表示禁止拆分,0b000表示最后一个Fragment,0b001表示还有后续Fragment。如果接收方不支持Fragment拼接,可以规定数据包不允许拆分
Fragment Offset 13 表示Fragment在数据包中的偏移(起始地址),以8Byte为单位计数

Word 3

名称 长度(bit) 注解
Time to Live 8 TTL,数据包每经过一个路由器都会减1。目前大部分操作系统默认使用64作为初始值。如果一个数据包在传输过程中TTL减小到0,说明经过了太多的路由器,路由器会直接丢弃该包并向发送方发送一个ICMP Time Exceeded报文。Linux下常用的tracepath就应用了TTL超时(逐次递增)来计算大致的路由路径
Protocol 8 数据包搭载的传输层协议。1ICMP6TCP17UDP
Header Checksum 16 Header的校验码,不包括之后的数据。这里的校验码计算非常简单(RFC1701),将原先的Checksum置0,Header以2Byte为单位分割后相加,单次加法后进位再加到最低位。最终结果取反码就是校验码。由于路由器需要更改TTL,所以路由器每次都要重新计算校验码

Word 4 & 5

名称 长度(bit) 注解
Source Address 32 源地址
Destination Address 32 目标地址

Word 5+

名称 长度(bit) 注解
Options 不定 基本不使用,会被一些路由器直接丢弃

在实际的应用中,分包不是很常用。一旦有一个Fragment出现错误整个数据包就不得不重新传输,IPv6废除了分包功能。且分包对于部分传输层的设备来说不友好。MTU表示Media Transmission UnitMaximum Transmission Unit,最大的传输单元

TTL的主要作用就是为网络中的数据包规定一个有限的生命周期,数据包如果在限定时间内未到达目的地会被丢弃而不是无限转发。没有TTL数据包就会永远留存在网络中

4.1.2 IPv4地址类型与保留地址

世界最大的网络是Internet。IPv4地址分为ABCDE共5大Class,其中有许多IP地址保留,不可分配用于公网IP

最早期的IPv4本没有Class之分,后来为满足不同机构、企业的需求才进行了5种Class的划分,且这种划分是针对于Internet而言的。离开了Internet谈Class也就失去意义了

现在Internet由于IPv4地址短缺问题又舍弃了这种划分,逐渐转向CIDR。尽管如此,很多当年分配到高等级IPv4网络的机构或公司依旧没有舍得让出这些地址

Class A

A类地址二进制以0开头,第1字节表示网络,后3字节表示主机。理论上可以提供128个Class A网络0.0.0.0/8127.0.0.0/8,每个网络2^24台主机

在上述地址空间内,实际上0.0.0.0/8保留表示当前网络,127.0.0.0/8保留用于回环loopback(通常使用地址127.0.0.1),10.0.0.0/8保留用于私有网络地址(例如通过NAT网关隔离的私有网络以及其中的主机)。100.0.0.0/8的子网络地址100.64.0.0/10保留用于运营商私有网络

Class B

B类地址二进制以10开头,前2字节表示网络,后2字节表示主机。理论上可以提供16384个Class B网络128.0.0.0/16191.255.0.0/16,每个网络2^16台主机

在上述地址空间内,实际上172.16.0.0/12(Class B网络172.16.0.0/16172.31.0.0/16)保留用于私有网络地址169.254.0.0/16保留用于link-local address(例如没有手动配置IP且DHCP失败,此时主机会默认给自己指定的IP。在网络故障时经常可以见到这样的IP)

Class C

C类地址二进制以110开头,前3字节表示网络,后1字节表示主机。理论上可以提供2^21个Class C网络192.0.0.0/24223.255.255.0/24,每个网络2^8台主机

在上述地址空间内,实际上192.0.2.0/24 198.51.100.0/24 203.0.113.0/24保留用于特殊用途(TEST-NET),192.88.99.0/24保留用于IPv6到IPv4中继,192.168.0.0/16(Class C网络192.168.0.0/24192.168.255.0/24)和192.0.0.0/24保留用于私有网络地址198.18.0.0/15(网络198.18.0.0/24到网络198.19.255.0/24)保留用于测试

Class D

D类地址二进制以1110开头,用于多播。地址范围224.0.0.0239.255.255.255

其中233.252.0.0/24用于特殊用途(MCAST-TEST-NET)

Class E

E类地址二进制以1111开头,保留用途。地址范围240.0.0.0255.255.255.255

其中地址255.255.255.255用于limited broadcast

4.1.3 子网划分和广播

在一个网络中,路由器和每台主机都会有自己的子网掩码,形式类似于255.255.240.0,前段全为二进制1表示IP地址网络字段域,后段全为二进制0表示IP地址主机字段域

在每一个子网中,主机字段0表示网络地址,全1表示广播地址,这两个地址不可以分配给主机或路由器接口。例如私有网络192.168.0.0/16配置子网掩码255.255.240.0,那么一共划分为子网192.168.0.0/20192.168.240.0/20,这些子网络拥有各自的广播IP和网络IP。注意IP广播数据包需要将目标MAC地址域也设为FF:FF:FF:FF:FF:FF广播地址,IP组播数据包需要将目标MAC地址设为对应的组播MAC地址

所谓广播就是一个网络中的所有设备都会接受的网络层广播数据包。过多的设备使用同一个广播地址(使用同一个子网)容易引发广播风暴,此时可以进行子网划分,并启用802.11Q VLAN,配合VLAN交换机使用

实际在同一个网络中,路由器端口以及各主机端口的子网掩码习惯上配置一致,否则会引发一些奇怪的问题。使用DHCP时会指定所有主机使用相同的子网掩码配置

子网掩码不是IP数据包的组成部分,它不会在设备之间传输。在一个网络中,主机和路由器还是有可能配置不同的子网掩码的,但是会出现很多问题,以下作例分析

假设这样一个典型的网络,网络通过交换机构建。有一台路由器的一个端口连接到了这个网络,此外还有主机A以及主机B连接到了这个网络

路由器设定自己的端口IP为192.168.1.1,子网掩码255.255.255.0。而A设定自己的端口IP为192.168.1.243,子网掩码255.255.240.0。B设定自己的端口IP为192.168.1.192,子网掩码255.255.255.240

首先,此时路由器广播地址192.168.1.255,A广播地址192.168.15.255,B广播地址192.168.1.207。此时任意两者之间无法接收对方发送的IP广播

此外,如果想要通过A访问B,A将B的地址和自己的子网掩码进行与运算后发现B和自己处于同一网段。于是A不再将数据发往自己的默认网关,而根据ARP协议发送一个数据链路层广播,要求询问192.168.1.192的MAC地址并告诉192.168.1.243,同时给出自己的MAC地址。此时B收到了这个广播,并将192.168.1.243和自己的子网掩码进行与运算,发现和自己不是同一个网段,于是B将ARP数据包丢弃。因此A是无法访问B的

反过来如果不为B配置默认网关,通过B也无法访问A。此时B认为A和自己不在一个网段,于是决定将数据包直接发送给路由器,但是当前B所属的子网中没有可用的路由

同一个交换机连接的设备可以属于不同的IP子网,但是没有路由器的情况下不同子网之间通常是无法通信的(除非子网掩码配置不同,导致不同大小的子网出现重叠。特定的地址之间可以进行单向或双向通信,只要双方发现对方和自己同一个网段)。ARP协议中主机会丢弃不属于自己网段的ARP广播数据包

在路由器的同一个端口是有可能绑定多个IP的,只要路由器支持,可以为不同的子网分别提供默认网关(和单臂路由工作原理类似),当然路由器中端口的IP也必须要有正确的子网掩码配置,否则主机ARP请求默认网关会失败

通常子网划分会和VLAN一起使用。有些家用路由就是单臂路由(现在很少见了,例如开发板厂商banana pi早期出品的BPI-R1就是单臂路由),单臂路由使用一个网络接口连接到一台支持VLAN的交换机,它使用VLAN分隔同一个接口的WAN和LAN的数据流

4.1.4 IPv4的问题和CIDR

历史原因,IPv4的地址没有得到高效的分配。许多A类B类网络地址被分配给了单个机构,如MIT,Stanford,惠普等,而这些机构通常没有能力消耗如此多的IP地址

IPv4诞生于1980年代,受冷战影响,Internet发展起来以后北美、西欧以外的地区普遍面临IP短缺的问题,在亚洲地区尤为严重。大部分国家如中国只能通过组建大型NAT局域网来缓解,这也是我们查看家庭光猫后台时发现IP地址为运营商局域网地址10.X.X.X100.X.X.X的原因,真正的公网IP分配给了运营商的上游网关。而在很多西欧、北美国家,普通的家庭宽带都可以分配到公网IP,在高校中甚至每台电脑和手机都可以分配到公网IP

因此,国内大部分家庭网在技术上就不可能通过公网访问,而访问家庭网内的设备需要一对公网、内网设备进行内网穿透,通过公网设备的代理服务才能访问

为解决ABC类网络主机数量过于悬殊无法合理分配的问题,以及路由表过大的问题,实际上现在Internet已经不再遵循ABC类网络的划分了,转而使用了类似于IPv6思路的CIDR(Classless Inter-Domain Routing)无Class划分,见RFC4632。为方便理解,本文依然遵照旧有的Classful网络讲述,这里只简单对CIDR进行一些说明

CIDR规定了IP地址由可变长度的网络地址以及随后的主机地址构成。例如122.23.4.252/12的网络前缀长12,对应的主机地址长20。由于使用CIDR后公网路由器无法确定一个IP的网络地址,所以要求在路由器协议中都要包含网络地址长度(或掩码),现在OSPF,IS-IS,RIPv2,EIGRP,BGP4路由协议都提供了CIDR技术支持。旧有的路由设备除非进行固件的升级,否则不能再用于Internet

现在公网IPv4地址由IANA统一分配给RIR后再分配到用户。最小的多主机网络需要/29/28/26是小型局域网,/25/24是大型局域网,/22为小型单位,/21/20给大型单位和小型运营商,/8是最大的可分配网络

4.2 IPv6

IPv6的出现主要是为了解决IPv4的枯竭问题,它的应用对于IP地址紧张的地区来说意义重大。IPv6使得网络中每一个设备都可以拥有公网IP。然而IPv6是面向未来设计的,它的工作机制和IPv4不相容;也由于网络基础设施的更新换代问题,在未来很长一段时间内IPv4将会和IPv6共存。兼容两种网络的工作量是巨大的

IPv6数据包的以太网帧EtherType0x86DD,使用128位表示一个站点。格式为16进制XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX省略前导0。如果有多个16位0,这片区域使用::代替,例如fe43:15dd:2e3f:32a:5c66::43eb中间::处省略了0000:0000

和IPv4将地址分配给网络中的节点略有不同,IPv6地址的分配对象是网络中的(逻辑上的)接口。也就是说,理论上可以给主机上的每一个进程分配一个IPv6地址。具体内容见4.2.2以及RFC4291

4.2.1 格式

在RFC2460中定义如下

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version| Traffic Class |           Flow Label                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Payload Length        |  Next Header  |   Hop Limit   |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                                                               +
   |                                                               |
   +                         Source Address                        +
   |                                                               |
   +                                                               +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                                                               +
   |                                                               |
   +                      Destination Address                      +
   |                                                               |
   +                                                               +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
名称 长度(bit) 注解
Version 4 IP版本,固定值6
Traffic Class 8 数据包等级,相当于ToS,没有权威定义不常使用。默认全0
Flow Label 20 数据流标签,标记相关的数据,路由器以此可以保证固定的传输路径,没有权威定义不常使用。默认全0
Payload Length 16 搭载的数据的长度,单位Byte,即目标地址后的数据长度
Next Header 8 搭载协议,同IPv4的Protocol,表示搭载的传输层协议。此外IPv6中还可以表示Routing header等非传输层相关数据
Hop Limit 8 同IPv4的TTL
Source Address 128 源地址
Destination Address 128 目标地址

相对IPv4来说,IPv6的数据帧结构简单了许多,且没有校验码。在IPv6网络中要求UDP数据包必须要有校验码

IPv6通常要求网络的MTU1280或以上

4.2.2 IPv6的地址分配机制

最早的IPv4本是没有NAT和私有网络的概念的,这些功能主要是为应对IPv4地址紧缺问题而扩展出来的(本质就是用传输层TCPUDP的端口号来扩展IP)。不要认为局域网一定要使用私有网络地址(如192.168.0.0),局域网和公网连通,就可以使用公网唯一的IP

IPv6地址多达128位,如此多的地址是严重过剩的。为了充分利用,IPv6规定其地址分配的最小细粒度是网络接口interface)而非节点node)。前64位/64节点地址,后64位为节点的网络接口地址。这里的接口同样是逻辑上的接口,不是物理上的接口

由于以上原因,IPv6事实上是64位地址协议

主机在分配到一个IPv6节点地址以后,可以使用任意的网络接口地址表示自己这台设备本身,从而自行填充剩余64位网络接口地址。例如直接使用网卡MAC填充,或对MAC进行哈希后填充。而主机上的每一个进程也可以拥有一个相同节点地址下的网络接口地址,但是这和TCPUDP的接口机制有部分功能上的重合

IPv6也支持类似IPv4的子网划分(prefix)。现在通常家庭网会分配prefix为/56/48的网络,分别允许25665536台主机,而企业会分配/48/44/40。IPv6下不需要私有网络以及NAT来解决公网地址不够的问题,每一台主机都可以拥有公网IP

4.2.3 保留地址

保留地址 用途
::/0 未指定时使用的默认网关配置
::/128 未初始化
::1/128 回环地址
::ffff:0:0/96 最后4字节网络接口地址,用于映射32位IPv4地址
::ffff:0:0:0/96 最后4字节网络接口地址,用于32位IPv4地址转译(转译算法有多种,各不相同)
fc00::/7 私有网络地址,其中fc00::/8保留,fd00::/8中prefix长/48,子网长/16
ff00::/8 多播地址
fe80::/10 Link-local address。这种地址用于单个网络中,路由器分隔的两个网络无法通过该类型地址通信,NDP以及DHCPv6协议都会使用到它。通常IPv6设备都会配置这样的一个地址,也是因此我们经常发现IPv6设备有2个地址
64:ff9b::/96 公网中用于IPv4IPv6互转
64:ff9b:1::/48 局域网中用于IPv4IPv6互转
100::/64 Discard
2001:0000::/32 Teredo tunneling
2001:20::/28 ORCHIDv2
2001:db8::/32 文档保留
2002::/16 弃用

4.2.4 IPv6多播地址

IPv6中只有多播以及任播(Anycast,传输到最近的1台设备),而没有广播。多播涵盖了广播的功能,IPv6组播时需要使用组播MAC地址而不是广播MAC地址

IPv6多播地址历史上经历过多次演变。定义如下,多播地址第一字节永远为ff

RFC4291

   |   8    |  4 |  4 |                  112 bits                   |
   +------ -+----+----+---------------------------------------------+
   |11111111|flgs|scop|                  group ID                   |
   +--------+----+----+---------------------------------------------+

Flags

   +-+-+-+-+
   |0|R|P|T|
   +-+-+-+-+

flags最高1位保留为0,后3位依次为R P T。其中R表示Rendezvous,Rendezvous Point字面意义汇合点简称RPR1时表示当前地址集成了RP地址(见下);P表示Prefix,为1表示由network prefix定义多播地址(同样见下);T表示Transient(Non-permanent),为1时表示不是由国际组织分配的多播地址。P1T也一定为1(固定的多播地址可以由IANA分配)

scope表示多播组的规模,group ID指定了scope网络范围内的组ID。scope定义如下

0  reserved
1  Interface-Local scope
2  Link-Local scope
3  reserved
4  Admin-Local scope
5  Site-Local scope
6  (unassigned)
7  (unassigned)
8  Organization-Local scope
9  (unassigned)
A  (unassigned)
B  (unassigned)
C  (unassigned)
D  (unassigned)
E  Global scope
F  reserved

常用IPv6组播地址如下

ff02::1 本网络内所有设备(节点)
ff02::2 本网络内所有路由
ff02::1:ffXX:XXXX 本网络内被请求对象的组播地址

组播地址ff02::1:ffXX:XXXX中最后3字节(24bit)取当前端口IP的后24位

需要首先计算出组播IP地址才能确定组播MAC地址。组播MAC地址格式为33:33:XX:XX:XX:XX,后4字节和组播IP地址的后4字节相同

RP地址计算方法如下

   |   8    |  4 |  4 |  4 |  4 | 8  |       64       |    32    |
   +--------+----+----+----+----+----+----------------+----------+
   |11111111|flgs|scop|rsvd|RIID|plen| network prefix | group ID |
   +--------+----+----+----+----+----+----------------+----------+

RP address

   +------------+---------------------+----+
   | network pre| 0000000000000000000 |RIID|
   +------------+---------------------+----+

R1PT也需要为10xff7x)。RP地址通常是一个IPv6路由器的地址,这个路由器负责数据源发来的多播数据包的分流转发。例如距离网络电视服务器最近的路由器就需要具备这种功能,而途径的路由器也需要支持这些功能。RP地址的构建需要plen network prefix RIID共三个部分,其中RIID放在128位IPv6的最后表示RPinterface,而plen指定network prefix的长度,截取后放在最前面。plen不大于64

4.3 路由原理

路由器是第三层网络设备,它的本质就是一种只能理解到网络层的特殊的主机(现在的路由器当然也可以理解更高层)。它和主机的不同点是它通常至少拥有2个网络接口,需要连接到不同的网络进行网络之间的数据转发,而主机只需要1个接口连接到网络就可以(家用路由器通常集成交换机,引出多个LAN接口,只能算一个接口)。路由器连接不同网络的每一个端口都拥有独立的MAC地址,数据包通过路由器转发时需要更改MAC地址为路由器转发接口的MAC。防火墙可以算一种特殊的路由器,主要实现过滤等保护功能

本章开头说过,如果没有NAT的特殊情况,数据包在不同网络之间,穿过路由器传输时源IP和目标IP是不会改变的,只有数据链路层的MAC地址会不断改变。网络中的主机会配置默认网关(路由器)IP,发送数据包时它会判断目标IP是否属于当前已经连接的网络,如果属于已连接网络只需设置好目标MAC地址后直接向网络中发送数据包;如果不属于已有网络,主机会将MAC地址设置为默认网关或对应静态路由器的MAC,不更改IP,将数据包发送给路由器转发处理

在日常生活中我们接触到的消费级路由器都是属于NAT网关设备。真正的核心路由器都由运营商管理,部署于运营商机房中,普通人难以接触。核心路由器需要搭载更多的功能,首先必须实现路由协议,这是几乎所有消费级设备所不具备的。所以狭义上说消费级路由器只能算网关,不能算真正的路由器

目前全球有许多网络设备厂商,但是拥有核心路由产品的厂家只有Cisco思科,Juniper瞻博,Huawei华为,Nokia诺基亚,Ericsson爱立信,ZTE中兴等少数几家。高端核心路由的单价都在六位数左右,体积巨大,使用专用的芯片,并且有极其强大的扩展性和稳定性

4.3.1 路由表

每台主机都拥有一个路由配置表,如下示例(省略了部分列)

示例1

$ netstat -nrv
Kernel IP routing table
Destination     Gateway         Genmask         Iface
0.0.0.0         81.187.150.214  0.0.0.0
10.92.213.0     10.92.213.242   255.255.255.0   eth0
81.187.150.208  81.187.150.216  255.255.255.240 eth1
127.0.0.1       127.0.0.1       255.255.255.255 lo

上表中共出现了3台路由器。访问私有网络10.92.213.0/24中的设备时需要通过eth0访问。当前配置访问10.92.213.0/24以外的网络时通过路由器81.187.150.214,而该路由器位于eth1连接的网络81.187.150.208/28内。此时如果10.92.213.0/24内的路由器10.92.213.242可以访问其他网络,我们也可以将第一行路由器地址更改为10.92.213.242

示例2

$ netstat -nrv
Kernel IP routing table
Destination     Gateway         Genmask         Iface
0.0.0.0         172.22.0.1      0.0.0.0         eth0
172.22.0.0      0.0.0.0         255.255.0.0     eth0

上表表明主机通过eth0连接了一个私有网络(如果可以访问上一层网络,就是NAT网络)。访问172.22.0.0/16局域网内的设备时无需通过网关,直接ARP后发送数据包即可

路由器中的路由表结构和上述类似,区别是可能会有其他的一些相关信息。动态路由通过学习获得,而静态路由由人工配置

通常路由器的路由表都会包含有目的网络地址网络掩码Metric(路径长度),Next hop(下一跳路由器地址)。路由器会自动选择Metric最短的路径

路由表还会包含其他信息,如Interface(上例的eth0),QoS Flags(例如U表示路由有效,G表示该路由表项指向一个网关,S表示是通过route命令配置的静态路由表项),以及安全配置(常见于防火墙)等

在现代路由器中,路由表需要转换为更高效的Forwarding table才能应用

4.3.2 路由协议

常见的路由协议有RIPRIPv2OSPFIS-IS等,路由器之间需要使用路由协议来交换路由信息,这些协议也称为IGPInterior Gateway Protocol)。公网中的路由器都依赖路由协议来寻找路径

RIP

RIPv2协议于1994年发布,作为RIPv1的继任者,定义于RFC2453,更新于RFC4822。RIP(RFC1058)是应用层协议,走UDP,端口520RIP的数据交换只发生在邻近路由器之间,是一种距离矢量类型的协议

RIP使用hops(跳转)作为metrics对路径长短进行评估,并包含在路由表中。路由器每隔一段时间(通常30秒,可能加上随机的正负offset)就会向邻近的路由器(adjacent)广播它的路由信息,邻近的路由器根据这些信息更新自己的路由信息。更新时会对metrics进行累加操作,如果发现同一目标IP对应的metrics变小了说明有更优的路径,路由器会使用对应的metrics以及网关地址替代原有的路由表项;如果发现同一个相邻网关发来的metrics变大了,那么就会将这个更大的metrics替换原来的metrics;此外每一个路由表项还有一个超时计时器,超过指定时间(180秒)未收到原有网关发来的更新,就认为已经失效,失效后再经过一段超时,此项就会被删除(后两种操作是面向网络故障而提出的应对方法,metrics既可以增也可以减)

路由表项的删除可能由超时或路由更新导致。旧的路由表项删除倒计时120秒,同时它的metric会被设为16,flag会对应表示该项已更改。之后会触发该路由器发送一次更新数据包

RIPv2使用了224.0.0.9组播地址而不是RIPv1255.255.255.255广播。此外RIPv2在路由信息中包含了网络掩码以支持CIDR,其路由表基于网络地址构建(也可以包含节点地址。RIPv1通常对每一个节点IP或网络建立一个路由表项,一个IP地址到来时选择其中最符合的),以及附加的Authentication

RIP系列协议为限制收敛时间,最多支持15个跳转,这意味着它不能用于距离相隔过远的网络。RIP的设计也导致网络故障时容易产生路由数据泛滥,收敛(稳定)速度慢,且容易产生振荡

RIP同时也提出了Split Horizon以及Triggered updates,在网络出现故障时有助于路由信息的收敛

RIPv2分享路由数据的格式如下。command1表示路由信息请求(例如新路由器上线),2表示回复。version表示RIP版本,为2AFI2表示IPv4,Next Hop为下一跳网关地址(对接收方来说是向后2个网关的地址)。一个包可以包含125个入口,每个长20字节。metric有效值最大只能取1516表示infinity

Header

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  command (1)  |  version (1)  |       must be zero (2)        |
   +---------------+---------------+-------------------------------+
   |                                                               |
   ~                         RIP Entry (20)                        ~
   |                                                               |
   +---------------+---------------+---------------+---------------+

Entry

    0                   1                   2                   3 3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   | Address Family Identifier (2) |        Route Tag (2)          |
   +-------------------------------+-------------------------------+
   |                         IP Address (4)                        |
   +---------------------------------------------------------------+
   |                         Subnet Mask (4)                       |
   +---------------------------------------------------------------+
   |                         Next Hop (4)                          |
   +---------------------------------------------------------------+
   |                         Metric (4)                            |
   +---------------------------------------------------------------+

OSPF

OSPF是一种链路状态类型的协议,全称Open Shortest Path First,同时支持IPv4和IPv6,支持CIDR,最初于1998年为IPv4设计(RFC2328),主要基于Dijikstra算法。由于OSPF可以单独写一篇长文章,这里不再讲解,有兴趣可以看相关文档

4.4 ARP协议

参考RFC826

ARP协议运行于IPv4同一层级,在IPv4 Ethernet网络中可以让一台设备获取IP对应的设备MAC地址。ARP数据包的EtherType0x0806

ARP除了可以获取MAC地址以外,还可以用于检测网络内的IP地址冲突

ARP请求方发送回复的被请求方使用相同的数据包格式。设计最初是作为一个通用协议,考虑了其他类型的链路层、网络层协议,除去以太网包装后ARP数据包的格式如下

名称 长度(Byte) 注解
HTYPE 2 数据链路层协议,1代表Ethernet
PTYPE 2 网络层协议,0x0800代表IPv4,定义和EtherType相同
HLEN 1 链路层协议地址(物理地址)长度(单位Byte)。Ethernet(MAC)地址长6
PLEN 1 网络层协议地址长度(单位Byte)。IPv4地址长4
OPER 2 操作类型,1表示Request,2表示Reply
SHA HLEN 发送方的物理地址(MAC地址)
SPA PLEN 发送方的网络地址(IPv4地址)
THA HLEN 目标的物理地址
TPA PLEN 目标的网络地址

在IPv4网络中,每一台主机的内存里都会缓存有一张ARP表,用于记录IP地址和以太网MAC地址的对应关系。每次主机想要将数据发往本地网络的一个指定节点(包括默认网关),它就会在表中查询这个IP对应的MAC。查询到以后就会直接将这个数据包发往对应的目的MAC

如果没有查询到,主机会将以太网目的地址设置为广播地址FF:FF:FF:FF:FF:FF,这样本地网络(通过交换机互联)的所有设备都会接收到这个ARP数据包。发送时ARP数据包类型OPER1请求,SHA为主机MAC,SPA为主机IP,THAFFTPA为目标主机的IP

本地网络中的其他主机接收到数据包以后,会依次检查HTYPE PTYPE,以及TPA。如果TPA符合,主机会将SHA SPA存入自己的ARP表中。如果确定OPER1,那么主机会进行回应,发送一个OPER2的数据包,同时将收发地址交换,将以太网数据包的目的MAC以及SHA更换为本机的MAC,送往原先的请求方

ARP的实现较为简单,缺点是非常不安全,容易劫持

4.5 NDP协议

参考RFC4861

NDP协议用于IPv6网络,运行于IPv6更高一层级,包含了类似ARP的地址解析功能,但是走ICMPv6ICMPv6通过IPv6数据包搭载,Next Header值为58

NDP定义了如下几个ICMPv6数据包。Solicitate就是Query的意思

名称 ICMPv6 Type 作用
RS Router Solicitation 133 主机请求本地网络中路由器信息
RA Router Advertisement 134 路由器在收到RS后,或每隔一段时间广播自己,表明存在,附带一些其他参数信息例如网络MTU
NS Neighbor Solicitation 135 邻居请求,用于请求指定IPv6主机的MAC地址或验证其在线
NA Neighbor Advertisement 136 邻居广播,用于回复NS报文等用途
Redirect 137 重定向,路由器通知主机可以连接到另一个路由器,速度更快

以上报文通常需要搭配多播IPv6使用,ff02::1网络内所有节点,ff02::2网络内所有路由

除了MAC地址获取以及IP冲突检测外,NDP还可以寻找网络内路由,获取网络MTU、hop limit参数,地址分配获取,重定向,可用性检测(reachability)功能等

NDP协议主要使用NSNA报文来进行MAC地址的请求,地址冲突检测和可用性检测

NSNA数据包格式分别如下

Neighbor Solicitation

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                           Reserved                            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                                                               +
   |                                                               |
   +                       Target Address                          +
   |                                                               |
   +                                                               +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |   Options ...
   +-+-+-+-+-+-+-+-+-+-+-+-

Neighbor Advertisement

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |R|S|O|                     Reserved                            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                                                               +
   |                                                               |
   +                       Target Address                          +
   |                                                               |
   +                                                               +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |   Options ...
   +-+-+-+-+-+-+-+-+-+-+-+-

Options (MAC)

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |    Length     |    Link-Layer Address ...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

NS报文的IPv6头中源地址通常为本机IP地址或0:0:0:0:0:0:0:0(如未分配地址),目的地址为多播/组播地址(如ff02:1)或直接就是目的IP地址(例如在检测对方可用性时)。而NS报文包含的目的地址是表示请求的IP地址,不能是多播地址或无效地址。Code0。在多播报文中必须在Options包含发送方(本机)的MAC地址

NA报文的IPv6头中源地址是本机IP地址,目的地址为原先请求方的IP地址或多播/组播地址(如请求方未分配地址)。R表示发送方是一台路由器,S表示是应请求发送的回复报文,而不是自主发送的报文(此时目标IP必须非多播,在可用性检测中有用),O表示覆盖原有的MAC地址缓存。通常NA回复报文包含的目的地址和原先对应的NS报文相同。Code0Options为本机的MAC地址,在多播报文中同样需要包含

OptionsType1表示源MAC(NS报文常用),为2表示目标MAC(NA报文常用)

主机请求指定IP的MAC地址时,主机首先需要发送一个组播ICMPv6 NS报文,这个NS报文使用组播目标MAC,IPv6头的源地址为本机IP,目标地址为被请求方的组播IP地址ff02::1:ffXX:XXXXNS报文body的目标地址为被请求方的单播IP地址。并且在NS报文最后附上本机的MAC,Type1

被请求方回复MAC地址请求时,发送一个NA报文,这个报文的源MAC和目标MAC分别为被请求方MAC以及请求方的MAC(非组播),源IP和目标IP同理。NA报文body的目标地址依然是自己的单播IP地址。在NA最后附上自己的MAC地址,Type2

4.5.1 SLAAC

4.6 NAT

参考RFC1918

NAT全称Network Address Translation,由网关实现,它可以使得私有网络内的多台主机共用一个公网IP地址,本质上是利用传输层的端口号(长16位)来弥补IPv4地址数量不足的问题。IPv4网络下我们日常用的局域网都使用了私有IP地址。同时NAT在某种程度上为IPv4私有网络提供了天生的安全保护。但是由于端口号的限制,一个NAT网络中同时访问同一公网主机的进程数量也有所限制。虽然应用广泛,事实上NAT打破了一些原有的设计规则,它并不被一些人看好

NAT网络需要网关理解传输层端口。家用路由器都是NAT网关

在NAT网络中,访问当前私有网络以外网络的数据包会被直接发送往NAT路由。假设当前NAT网关的WAN口直接连接了公网并拥有公网IP(假设194.47.156.230)。我们访问一台公网主机产生一个数据包。那么在私有网络内,该数据包:

源IP为主机在私有网络内的IP(假设192.168.1.23),目标IP为被访问公网主机的IP(假设为17.253.144.10)(MAC的修改省略)

源Port为主机发送数据的传输层Port,目标Port为公网主机提供服务的Port(假设访问http,端口80,源端口10320

NAT网关接收到上述数据包以后,进行以下操作后发送:

记录两个IP和端口(192.168.1.23:10320, 17.253.144.10:80)。将源IP修改为WAN口IP,再随机分配一个未使用过的上行端口(例如12004)。发送的数据包的源IP和Port变为194.47.156.230:12004,最终形成192.168.1.23:10320 -> WAN Port 12004 -> 17.253.144.10:80的映射

公网主机收到后回复一个数据包,网关进行以下操作:

发现WAN端口12004收到了来自17.253.144.10:80的数据包。经过查表将目标IP和Port改为192.168.1.23:10320,发送给私有网络内的主机

NAT的应用场景不限于以上示例。Outbound NAT时,数据从私有网络发送往上层网络,不同的内网host:port访问相同的外网host:port,必须分配不同的WAN端口(如果访问不同外网host:port,可以分配相同WAN端口,但是没必要这么做。而相同内网host:port访问不同外网host:port,也可以分配相同的WAN端口)。Inbound NAT时,NAT网关直接查询之前的表格就可以确定内网的host:port

对于TCPUDP来说NAT的端口保留机制有所不同

TCP中使用<src addr, src port, dest addr, dest port>定义一个唯一的连接,一个公网端口需要在TCP通信过程中全程保留。每建立一个新的连接,网关需要为该连接分配一个公网port,该连接未释放前其他从私有网络访问同一<dest addr, dest port>TCP连接不得使用该端口,所谓保留本质就是避免这种冲突。网关需要监视TCP连接状态,断开连接后才可以释放该端口(可能会在一段较长的延时之后),该端口归入可用端口池,可以重新用于其他TCP连接

UDP由于是无状态协议,网关无需跟踪连接状态。一个UDP数据包发送时网关立即分配端口并保留一段较短时间即可释放(可能几秒到十几秒),这段时间私有网络内从其他<src addr, src port>访问同一<dest addr, dest port>UDP数据包不可使用该端口

4.7 ECN

RFC3168

在实际的网络应用中,很多时候网络的瓶颈不是主机性能,而是途经的路由器。在以往的应用中,路由器在面对过多的数据包时会选择丢弃一部分数据包(这是一种AQM机制,Active Queue Management),而后面讲述的类似TCP这样的传输层协议就会通过丢包情况,控制数据包的发送速度来缓解路由器压力(例如调节窗口)。此外,路由器为了在真正无法处理数据包之前使得通信双方减小数据流量,它通常会提前开始丢弃一些数据包

这种被动的检测丢包的阻塞控制方法在很多时候是非常低效的,刻意的丢包造成的代价太大,由此便产生了ECNExplicit Congestion Notification)。ECN需要IP协议和支持阻塞控制的可靠传输层协议如TCP QUIC的支持,它需要利用IPToS标志位和传输层协议的标志位来提供显式的阻塞控制。由此路由器就可以不通过提前丢包而是通过数据包中的ECN标记来使双方降低流量。ECN目前已经有较为广泛的应用

4.7.1 IP数据包中的ECN标记位

IPv4IPv6数据包中,ECN标记位占据ToS的最高2bit

         0     1     2     3     4     5     6     7
      +-----+-----+-----+-----+-----+-----+-----+-----+
      |          DS FIELD, DSCP           | ECN FIELD |
      +-----+-----+-----+-----+-----+-----+-----+-----+

新的RFC标准定义ECN中的两个位作用是对称的,定义如下

ECN 定义
0b00 发送方在其发送的数据包中使用该值表示它不支持ECN
0b010b10 发送方表示它支持ECNECT状态,ECN-Capable Transport
0b11 数据包途经一台路由器,如果ECN0b010b10,路由器将ECN域置为该值表示发生阻塞(CE状态,Congestion Experienced)。如果没有阻塞或ECN已经为0b11那么不更改ECN

发送方发送的数据包不可能使用0b11。接收方可能接收到这4个值中的任意一个。如果接收到0b11,就表示它需要和发送方进行协调减小数据流量

我们要记住双方收发数据不一定会按照同一条路径,可能数据包从主机1发到主机2会经过路径A,但是主机2发送到主机1可能使用另一条路径B,不是原路返回。两个方向的阻塞控制也是分开的,支持ECN的接收方发现IP数据头中的ECN0b11,它在紧接的回复中依然回复ECN0b010b10

在旧的ECN定义中(RFC2481)ToS的bit7CECongestion Experienced标志位),bit6ECTECN-Capable Transport标志位)。只有0b10ECT置位)才表示支持ECN的意思,而0b01没有定义。新的定义虽然允许0b100b01但还是建议使用0b10

4.7.2 TCP支持

TCP的标志位中,CWRCongestion Window Reduced)和ECEECN-Echo)两个标志位用于ECN机制

        0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15
      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
      |               |               | C | E | U | A | P | R | S | F |
      | Header Length |    Reserved   | W | C | R | C | S | S | Y | I |
      |               |               | R | E | G | K | H | T | N | N |
      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

数据包接收方的网络层一旦发现ECN0b11,它会通知上层的TCP将下一次回复的TCP数据包中ECE标志位置1再发往原先的数据发送方,发送方才能知道发生拥塞。此时数据接收方和发送方会采用对待丢包一样的拥塞调节措施(例如降窗快速恢复),这通常意味着窗口的减小,接收方发送TCP数据包中的Window值会减小

该回复被发送方接收后,发送方会对应地调节发送窗口,并在下一个发送的数据包中将CWR置位。这样接收方知道发送方已经调节了发送流量(CWRRReduced而不是Reduce),如果此时IP数据头中的ECN不再为0b11,那么接收方无需再将回复数据包的ECE置位

在一个方向的TCP数据传输中,在一个RTT时间内不允许多次因为ECN而减小窗口,否则容易导致窗口一下子变得太小

在接收到ECE后回复CWR时,该数据包可能丢失。重传时CWR不能置位

同样的,ECE也可能丢包。因此接收方检测到ECN时通常需要连续回复多个ECE置位的数据包以充分考虑丢包的情况,直到接收到发送方回复的CWR

任何重传的数据包的ECN域都必须0b00

TCP初始化时的ECN协商

TCP在三次握手中双方发送的SYN数据包中就完成了ECN的协商

发起连接的一端(客户端)如果想要使能ECN,它发送的TCP数据包中SYNCWR ECE必须都置位。而服务器收到该数据包后知道了客户端支持ECN,如果它也可以并打算使能ECN,服务器回复的数据包会将SYNACK ECE置位(CWR不置位),客户端接收到服务器的这个SYN-ACK后就意味着ECN协商成功,双方在通信时可以使用ECN,也可以不使用(例如仅发送无数据负载的ACK时,通常IP数据头中的ECN0b00)。它们无论作为发送方还是接收方在遇到ECN时都要做出相应的正确措施

如果协商不成功(例如服务器不支持ECN),那么后续双方都不可能会使用到ECN

这些SYN数据包的IP数据头中ECN必须0b00,因为此时还未协商完成所以不能使用IP数据头中的标志位,只有协商完成后才能使用(对于一台主机来说,即发送/接收了一个SYN并接收/发送了一个SYN-ACK。我们默认IP中的ECN是给路由器看的,并假设路由器无法理解传输层及以上的内容)

只有上述数据包与协商过程才可以使能ECN。规定以外的SYN CWR ECE ACK组合都不起作用

两台主机之间每次TCP连接都需要重新协商ECN

4.7.3 QUIC支持

TCP类似的,QUIC也是由数据包的接收方检测IP数据头的ECN标记位并通过ACK帧(Type = 0x03)中的ECN Count来反馈

ECN Counts {
  ECT0 Count (i),
  ECT1 Count (i),
  ECN-CE Count (i),
}

ECT0ECT1ECN-CE都采用可变长整数编码,它们分别代表接收方接收到ECN标记位为0b100b01以及0b11的次数

由于QUIC支持一个UDP数据包中封装多个数据包,且不同类型的数据包可能会使用不同的数据包序号空间。对于ECN有效的UDP数据包中的每一个QUIC数据包,其对应的数据包序号空间的Count都要+1

假设一个UDP数据包ECN标志位为0b10,它包含了1Initial,一个Handshake,以及21-RTT数据包,那么该数据包的接收方需要在回复时将InitialECT0 Count1,将HandshakeECT0 Count1,将1-RTTECT0 Count2,并集成于对应数据包类型中的ACK帧中进行反馈。可以推断ECN-CE Count值增加时就意味着路由器阻塞的发生

ECN验证

QUIC连接迁移中提到过在连接迁移同时需要进行ECN的验证。QUIC在建立连接时同样需要进行ECN的验证

对于每一条新的网路,通信双方在该网路上开始发送数据包时可以将IP数据头中的ECN置为ECT0,随后通过对方的反馈来判断网路是否支持ECN。有些路由器会直接丢弃ECN0b00的数据包,这样就不会得到任何反馈。通过对比发送的数据包数量以及对方回复的各个Count就可以验证该网路是否支持ECN(通常只有数据包全部丢失才表示网路不支持ECN

如果知道网路是可用的,只是不支持ECN导致的丢包,通信双方可以只在发送的最初10个数据包中置位ECT0,这样后续依然可以利用该网路,而不是等待出现新的可用网路

ECN Counts校验在以下情况中会失败:

  1. 本应当有对应数据包ECN CountsACK帧没有搭载ECN Counts

  2. 对方ACK回应的ECT0 Count + ECN-CE Count增量小于最新被ACKECT0置位数据包数量;ECT1数据包同理(考虑到ACK丢包的情况,允许Count之和大于已经被ACK的数据包;考虑到ACK乱序的情况,Count可能会减小,需要容许这种情况)

  3. 网络设备有可能会更改ECN,将ECT1改为ECT0(新的设备通常不会这么做)。此时ACK回复中ECT0 Count出现了变化,而不是ECT1 Count。这可以触发错误,并且这种错误也代表了网路上更改ECT设备的存在

一旦出现ECN校验失败的情况,双方需要立即停用ECN(置ECN0b00)。但是后续如果条件允许(例如该网路ECN验证成功),双方可以重启ECN功能

5 传输层

传输层协议提供了主机上进程之间的数据传输支持,以端口号Port作地址,以TCPUDP为主导,ICMP用于网络控制管理等特殊功能。其他大部分传输层协议都是基于TCPUDP设计而来的。对于应用层来说,TCP传送过来的数据是有序的,而UDP传送过来的数据可能是乱序的

5.1 TCP

RFC793

RFC9293

5.1.1 TCP通信原理

TCP是一种可靠的传输层协议,基于滑动窗口设计,主要实现了数据传输的流控制,纠错,重传等。TCP有多种实现。这里只对TCP共通的基本原理进行讲解

概览

TCP是基于连接的有状态协议,需要使用状态机控制。在基于TCP的socket程序中,对于每一个<src addr, src port, dest addr, dest port>,OS都需要创建一个socket与之对应,这样两台主机间的一个连接使用一对专用的socket进行通信

为方便理解,之后会使用Wireshark对TCP数据流进行抓包实验

连接建立和释放

TCP三次握手建立连接的过程如下

发起连接的客户端首先向被请求方(服务器)发送一个TCP数据包,其中SYN标记置位,Seq0(相对值,具体看抓包)。被请求方收到后返回一个SYNACK置位的数据包,Seq0(相对值),而Ack值为上一个数据包Seq+1,为1(相对值)。发起连接一方接收到后再返回一个ACK置位的数据包,Ack值为上一个数据包Seq+1。此时连接建立,可以开始传输数据

这个过程可以描述为SYN SYN-ACK ACK

在连接建立时的SYN包中还会在TCP Options携带上一些配置信息,例如最大Segment大小MSS(Option 2),允许SACK(Option 4),Timestamp时间戳(Option 8),Window Scaling系数(Option 3)等

TCP四次挥手释放连接的过程如下

连接的终止通常由请求方(客户端)发起。首先发送一个FIN数据包,被请求方接收到以后返回一个ACK包,此时请求方不可再主动发送数据。之后由被请求方发送一个FIN数据包,请求方接收到后返回一个ACK包。此时连接释放

以上过程可以描述为FIN ACK FIN ACK。为节省数据包发送次数,被请求方通常会将中间两个数据包合并为1个。所谓的四次挥手变为三次,形成FIN FIN-ACK ACK

滑动窗口原理

TCP除了发送窗口接收窗口,还有拥塞窗口的概念,它的大小需要根据拥塞控制随时调节,而发送窗口的大小在拥塞窗口接收窗口之间取较小值(发送窗口根据拥塞窗口调节,但不能大于接收窗口

网络层数据包可能会在途中任意方向,任何时间,任何地点丢失,也经常会出现数据包乱序,TCP对这些问题提供了解决方案。此外,TCP流水线式的数据传输,数据包的批量ACK方式,避免了批量传输受往返延迟的影响

为区分发送的各个TCP数据包,TCP中每个数据包都拥有一个序号Sequence Number。对于一个方向的数据流,发送方和接收方各有一个窗口(每个端口都对应有一对发送和接收窗口)。发送方的窗口只有在接收到当前窗口中开头数据包对应的ACK回复之后才会向前移动,否则保持不动。而接收方的窗口每接收到一个数据包并回复一个ACK包,窗口就可以向前移动一格。也就是说,发送方的窗口开头位于当前最早的未被ACK的数据包,而接收方的窗口开头位于当前最早的未接收到的数据包

TCP数据传输过程中,发送方接收方分别使用SeqAck作为计数器,对传输的数据进行计数,由于是双向传输,双方数据包的ACK都置位。TCP通过三次握手建立连接后,双方SeqAck初始值都为1(注意是相对值。实际上可能初始值是一个随机值)

由于TCP采用流水线形式,发送方有时会一次批量发送多个数据包。在发送方,每下一个数据包的Seq'的值为当前数据包的Seq加上当前TCP数据包的Payload长度Len,即Seq' = Seq + Len

接收方对于每一个接收到的数据包都必须发送一个对应的ACK包,使得发送方知晓数据包已成功送达。ACK包的Ack值为对应Seq包的Seq + Len。也就是说,ACK包的Ack值对应下一个数据包的Seq'

上述行为在使用TCPCumulative ACK特性时除外。ACK回复在现在很多具体的TCP实现中其实是十分灵活的,可以允许多个发送方数据包对应一个接收方ACK回复,也可以反过来一对多

数据传输过程中,发送方和接收方需要检查发来数据包的AckSeq,来判断传输过程出现的错误。这些情况会在下面进行讲解

窗口大小和Window Scaling扩展

每个TCP数据包都会包含一个2字节的Window域,用于表示发送该数据包的主机目前接收窗口的大小

如果不启用TCP的Window Scaling扩展,窗口默认最大65535字节。如果启用,在TCP建立连接的阶段,双方发送SYN包时可以使用专用的TCP Option(3)设置Window Scaling左移位数,最大值为14,窗口最大65535 * 2^14。Window Scaling要到连接建立以后才会生效,默认情况下都是1

数据传输:乱序、超时和丢包

由于IP协议栈的特性,尽管路由器会尽力保持路径的一致性,数据包还是可能走不同的路径(例如负载均衡)。TCP本身基于滑动窗口的设计提供了缓冲的功能,所以数据包的乱序不属于数据传输错误

数据包乱序代表着先前未到达的数据包最终还是到达了。接收方接收到乱序数据包后续的一个数据包后,回复的ACK中Ack相较上一个ACK不变,这就是Dup ACK,此时发送方将丢失数据包之后的一个数据包标记为发送成功,发送窗口不移动。乱序的数据包到达后,接收方发现空缺已经补全,回复ACK时将Ack置为接下来期望数据包的Seq。发送方接收该ACK后也知晓数据已经补全,直接发送接下来的数据包

如果乱序的两个数据包是连续到来的,也是同上,接收方回复的两个ACK中第一个Ack设为迟到数据包的Seq,另一个Ack设为下一个期望数据包的Seq

用一句话概括,接收方的ACK回复中Ack为接收方期望接收到数据包的Seq最小序号。对于发送方来说,数据包是否已成功传输,一切以接收方的回复为准

丢包是导致传输错误的最常见原因,在这些情况下TCP需要为其提供纠错和重传机制

丢包可能在任何阶段发生。可以是发送方的数据包,也可以是接收方的ACK回复

发送方的数据包如果丢失,接收方通过将接收到数据包的Seq和上次发送ACK回复的Ack比对,就可以监测到(此时是无法判断数据包是丢失还是乱序,要将后续成功接收到的数据包放入到接收窗口,但是窗口不移动)。接收方发送ACK回复时会一直重复上一个Ack值(Dup ACK),这样发送方也会知晓丢包的情况

需要注意的是,在上述丢包发生后,接收方发送的那些Dup ACK依然ACK了丢失数据包之后的数据包。此时发送方不能移动发送窗口,但是可以将之后的数据包标记为已发送成功。如果丢包未成功重传的情况下接收方发现再次发生了丢包,其发送的Dup ACK依旧是原来的Ack(即最小的Ack)。直到原先丢失的包重传成功后,接收方首先回复该数据包对应的ACK;之后发送的Dup ACK对应下一个丢失的数据包的Ack

这种情况下,通常在一定时间以后发送方会触发快速重传,通常在十几个Dup ACK以后(开启了SACK的非实时应用中也可能会立即重传)

丢包是导致TCP队头阻塞问题的主要原因,如果队头数据包丢失,那么包括之后的数据在内,直到重传完成TCP才有可能将数据递交给上层。这也是新一代HTTP/3体系抛弃TCP使用QUIC的原因之一

而接收方回复的ACK包一旦出现丢失情况,发送方的发送窗口就无法向前移动。发送方将会对窗口中未ACK的数据包设定一个定时器,超过一定时间还未收到也会对数据包进行重传,通常对应Wireshark中的Spurious重传

数据传输时使用的超时时间RTO是可变的,需要基于RTT(Round Trip Time,通常指发送端发送数据包到接收到ACK为止这段时间)来调整,计算方法在RFC6298(Jacobson / Karels算法)

计算RTO需要用到两个数据,一个是SRTT(Smoothed RTT),一个是RTTVAR(RTT Variation)。RTO初始值为1s,最小值也不能低于1s

传输时,获取到第一个RTT后,设SRTT = RTT, RTTVAR = RTT/2, RTO = SRTT + max(G, 4*RTTVAR)

获取到后续RTT后,设RTTVAR = (1 - b) * RTTVAR + b * abs(SRTT - RTT), SRTT = (1 - a) * SRTT + a * RTT,再使用同样方法计算得到RTO。通常取a = 0.125, b = 0.25

如果一个数据包在重传后依然失败,那么下一次重传的超时时间RTO'需要目前的RTO乘上一个数n,例如*2RTO'翻倍,使得下一次重传超时时间更长,但是最长通常要求不能超过120s

时间戳扩展Timestamp

现在大部分的TCP通信默认都开启了时间戳功能来提高可靠性,每个数据包都会包含时间戳信息,方便RTT测量以及PAWS(Protection Against Wrapped Sequences,在超大带宽的网络中32位Seq可能不够用,在上一个Seq = x还在窗口中就传输了超过4GiB数据从而导致Seq = x在窗口中重复出现)

Timestamp Option长度10字节,通常TCP通信中如果建立连接时选择了Timestamp,那么之后每个数据包都会有Timestamp,格式如下

    +-------+-------+---------------------+---------------------+
    |Kind=8 |  10   |   TS Value (TSval)  |TS Echo Reply (TSecr)|
    +-------+-------+---------------------+---------------------+
        1       1              4                     4

TSval表示发送该数据包一方的TCP时钟,该时钟相比数据包的传送速度慢许多。TSecr必须在ACK置位时才有效(ACK未置位必须置TSecr = 0),此时TSecr的值等于对方最近一个数据包的TSval时钟

SACK扩展

SACK即选择性ACK。SACK是为更高效地提供丢包信息而设计,尤其是接收方的ACK回复丢失时,可以减少多余的Spurious重传,这种重传下面会讲到。SACK目前有较广泛的应用

注意SACK只是一个扩展,它只是附加的提示信息,没有对TCP通信的ACK回复进行本质的更改

SACK信息由TCP数据头中的可选的SACK Option搭载,格式如下

                      +--------+--------+
                      | Kind=5 | Length |
    +--------+--------+--------+--------+
    |      Left Edge of 1st Block       |
    +--------+--------+--------+--------+
    |      Right Edge of 1st Block      |
    +--------+--------+--------+--------+
    |                                   |
    /            . . .                  /
    |                                   |
    +--------+--------+--------+--------+
    |      Left Edge of nth Block       |
    +--------+--------+--------+--------+
    |      Right Edge of nth Block      |
    +--------+--------+--------+--------+

一个TCP数据包通常最多只能搭载4组SACK数据项

SACK选项中每一个数据项由一对Seq构成,每个Seq长4字节,它们分别表示接收方已接收到数据包区间的左界和右界(注意一个数据包本身是一个Seq区间,例如35650 36843 36843 38126合并为35650 38126

TCP规定1st Block必须是最近的一个Block(也就是导致接收方发送当前ACK的Block)。TCP要求尽量多地包含Block,其余较近的Block可以乱序,但是不要重叠,且建议依旧遵照由近及远排列

发送方的数据包丢包时,之后数据包中的Ack会小于最远区块的左边界,这表示第一个丢失数据包的Seq区间。之后丢包的Seq区间在SACK Option中体现

接收窗口满和零窗口

在Wireshark中,接收窗口满后会出现[ TCP Window Full ]的提示。这通常是丢包迟迟未进行重传导致的,此时接收方窗口无法再向前移动,而此时发送方发送的最后一个数据包会填满接收方窗口。接收方窗口满不会显式表示在TCP数据包中,而是需要发送方自行根据接收方的Win以及发送的数据进行推断

零窗口Zero Window由接收端主动发送一个Win = 0的ACK触发,此时发送端知晓接收端已经来不及处理数据,立即暂停发送数据包。之后由接收端再发送一个Win不为0的ACK触发恢复数据传输(Window Update窗口更新)

零窗口可能会在窗口满附近出现。有些TCP实现可能难以捕捉到零窗口

Spurious重传

Spurious重传通常由ACK回复丢失引发,最终通过发送方定时器超时触发重传。结果就是接收方发现了重复的数据包

快速重传

快速重传由多个Dup ACK触发,此时是接收方未接收到发送方的数据包(通过Seq的连续性判断)。此时发送方发现Dup ACK后就会开始计时,超时后就会进行快速重传(通常为几个或十几个Dup ACK以后,具体多少数据包根据dupthresh来判断。dupthresh的初值很小,一旦检测到数据包乱序就会增大)

普通重传

Wireshark定义快速重传中,上一个Dup ACK必须出现在20mS以内,且之前有至少3个对应的Dup ACK。由于不同TCP协议栈的实现也会有所不同,有时抓取到的快速重传数据包可能不满足Wireshark对于快速重传的判定要求,这些快速重传可能会显示为普通重传

普通重传还有可能在发送窗口满时触发(这种情况很少)。此时发送方不能再发送新数据,为继续传输数据只能将原先未ACK的数据包进行重传。此外还有更罕见的情况,可能由接收端自身问题接收数据包而没有发送ACK引发等

窗口大小更新

接收方可以在传输过程中更改接收窗口Win大小,通常情况下数据传输时接收窗口大小一直会随RTT变化(无论是否使能Window Scaling)。Wireshark中如果检测到当前Win和先前ACK的Win不等,且发送方的数据包发生了丢失(出现了Dup ACK),此时说明接收端有必要扩大接收窗口,此时Wireshark会判定Window Update

TCP Keep-Alive

Keep-Alive数据包常见于idle连接,此时没有正在接收的数据,也没有任何需要发送或未ACK的数据,且已经经过一段这样的空闲时间。双方需要确认连接是否还有效。这种数据包通常不搭载数据,且最大特征Seq相比下一次传输时使用的Seq会减1

收到Keep-Alive的一端可以回复一个Keep-Alive ACK表明连接的有效,这个数据包通常紧随Keep-Alive数据包,且SeqAck都维持前后一致不变化

TCP规定Keep-Alive的实现是可选的

连接重置

连接重置可以由发送方或接收方发起,此时RST置位。如果在发送RST后另一端发来了数据包,就需要再发送多个RST确保数据传输和连接的终止

TCP状态机

如下

                            +---------+ ---------\      active OPEN
                            |  CLOSED |            \    -----------
                            +---------+<---------\   \   create TCB
                              |     ^              \   \  snd SYN
                 passive OPEN |     |   CLOSE        \   \
                 ------------ |     | ----------       \   \
                  create TCB  |     | delete TCB         \   \
                              V     |                      \   \
          rcv RST (note 1)  +---------+            CLOSE    |    \
       -------------------->|  LISTEN |          ---------- |     |
      /                     +---------+          delete TCB |     |
     /           rcv SYN      |     |     SEND              |     |
    /           -----------   |     |    -------            |     V
+--------+      snd SYN,ACK  /       \   snd SYN          +--------+
|        |<-----------------           ------------------>|        |
|  SYN   |                    rcv SYN                     |  SYN   |
|  RCVD  |<-----------------------------------------------|  SENT  |
|        |                  snd SYN,ACK                   |        |
|        |------------------           -------------------|        |
+--------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +--------+
   |         --------------   |     |   -----------
   |                x         |     |     snd ACK
   |                          V     V
   |  CLOSE                 +---------+
   | -------                |  ESTAB  |
   | snd FIN                +---------+
   |                 CLOSE    |     |    rcv FIN
   V                -------   |     |    -------
+---------+         snd FIN  /       \   snd ACK         +---------+
|  FIN    |<----------------          ------------------>|  CLOSE  |
| WAIT-1  |------------------                            |   WAIT  |
+---------+          rcv FIN  \                          +---------+
  | rcv ACK of FIN   -------   |                          CLOSE  |
  | --------------   snd ACK   |                         ------- |
  V        x                   V                         snd FIN V
+---------+               +---------+                    +---------+
|FINWAIT-2|               | CLOSING |                    | LAST-ACK|
+---------+               +---------+                    +---------+
  |              rcv ACK of FIN |                 rcv ACK of FIN |
  |  rcv FIN     -------------- |    Timeout=2MSL -------------- |
  |  -------            x       V    ------------        x       V
   \ snd ACK              +---------+delete TCB          +---------+
     -------------------->|TIME-WAIT|------------------->| CLOSED  |
                          +---------+                    +---------+

5.1.2 TCP数据包

参考RFC9293

TCP数据包定义如下

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |          Source Port          |       Destination Port        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                        Sequence Number                        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Acknowledgment Number                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  Data |       |C|E|U|A|P|R|S|F|                               |
    | Offset| Rsrvd |W|C|R|C|S|S|Y|I|            Window             |
    |       |       |R|E|G|K|H|T|N|N|                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Checksum            |         Urgent Pointer        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                           [Options]                           |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                                                               :
    :                             Data                              :
    :                                                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
名称 长度(Byte) 注解
Source Port 2 源端口
Destination Port 2 目标端口
Sequence Number 4 简称seq,数据域第1字节的序号(计数单位Byte)。如果SYN置位,该域表示初始序号ISN,数据域第1字节序号为ISN+1
Acknowledgment Number 4 简称ack,接收方发送数据包时ACK置位,该域表示(接收方认为的)发送方此时期望的下一个seq
Data Offset TCP头长度(单位4Byte),没有Options通常为5。也表明数据域起始位置
CWR Congestion Window Reduced,阻塞窗口减小
ECE ECN-Echo
URG UrgentUrgent Pointer域有效,已弃用
ACK AcknowledgmentAcknowledgment Number域有效
PSH Push
RST Reset,连接重置
SYN Synchronizeseq计数器同步
FIN Finish,表示发送端没有数据传输了
Window 2 发送该数据包的主机目前接收窗口的大小(单位Byte)
Checksum 2 校验码(计算时Checksum0),计算方法同IPv4Header的校验码
Urgent Pointer 2 Urgent数据的位置(相对于当前seq,指向Urgent数据之后的1字节),已弃用
Options 4 选项,可以是1Byte长度Kind,也可以是1ByteKind加上1ByteLength加上后续Option数据
Data 数据

TCPChecksum校验码需要涵盖Pseudo-header,Header以及Data三个连续的部分。计算时Header中Checksum本身全置0,而Pseudo-header包含了网络层的一些信息,如下,其中PTCL表示ProtocolIPv4下为4IPv6Pseudo-header40字节

IPv4 Pseudo-header

    +--------+--------+--------+--------+
    |           Source Address          |
    +--------+--------+--------+--------+
    |         Destination Address       |
    +--------+--------+--------+--------+
    |  zero  |  PTCL  |    TCP Length   |
    +--------+--------+--------+--------+

IPv6 Pseudo-header

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                                                               |
    +                                                               +
    |                                                               |
    +                         Source Address                        +
    |                                                               |
    +                                                               +
    |                                                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                                                               |
    +                                                               +
    |                                                               |
    +                      Destination Address                      +
    |                                                               |
    +                                                               +
    |                                                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                   Upper-Layer Packet Length                   |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                      zero                     |  Next Header  |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

常用的Options如下

Kind Length 注解
0 End-of-Option,表示Options的结束
1 No-Operation,作占位符使用,进行对齐
2 4 Segment最大大小MSS,通常只在连接初始化时使用
3 3 Window Scaling系数
4 2 允许SACK(Selective ACK)
8 10 Timestamps

5.1.3 TCP实验

使用Wireshark进行抓包,更便于理解TCP的通信过程

注意事项:Wireshark虽然可以方便抓包分析,但是经过后续一些实际研究,发现Wireshark对于TCP事件的判断算法有较多设计不合理的地方,许多特定条件下容易导致事件的误判。建议理解TCP不要过于依赖Wireshark的提示信息

测试URL:

$ curl URL > /dev/null

IPv4 LAN address 10.80.193.210

[ Linux host 1 ] IPv4 45.76.35.230

http://tcpdynamics.uk:4000/8M

[ Linux host 2 ] IPv4 39.155.141.16

https://mirrors.bfsu.edu.cn/slackware/slackware-13.1/kernels/hugesmp.s/bzImage

[ FreeBSD host ] IPv4 213.138.116.73

http://pkg.freebsd.org/FreeBSD:13:aarch64/release_1/packagesite.txz

连接建立和释放

[ FreeBSD host ]连接建立,三次握手

[]括号内表示该数据包中置位的标记位。点击本机发送的第一个SYN,可以观察到一些Options

本机支持MSS = 1460,使能SACK功能以及时间戳,启用Window Scaling,系数为7(左移7位)。此时SYN置位ACK不置位,Seq实际值2675542447TCP使用这个值作为初值,当作Seq = 0看待。注意Wireshark显示的SeqAck都是相对值)。接下来观察服务器发送的SYN-ACK

Options的顺序有所不同。此时SYNACK都置位,初始值Seq = 0, Ack = 1(这里Ack需要相对对方发来的Seq1),服务器端Window Scaling系数为11。可以发现TSecr等于本机先前发送SYN的TSval

本机回复一个ACK数据包,Options只剩下Timestamp。此时SYN复位,ACK置位,Seq = 1, Ack = 1Seq等于对方发来的AckAck相对对方发来的Seq1

[ FreeBSD host ]连接释放,四次挥手

以上是最典型的TCP连接释放操作

服务器发来的HTTP OK数据包Seq = 6395817, Len = 4331。此时本机在ACK该最后一个数据包后才发送FIN

本机发送的第一个FIN中FIN, ACK置位,而Ack相比上一个ACK数据包不会变化,Ack = 6400148(这两个数据包中间没有任何来自服务器的数据)。

服务器发送的ACK回复中Ack等于接收到数据包的Seq + 1,值为125

下一步由服务器发送FIN,相比上一个服务器发送的ACK最大的区别是FIN置位

最后本机回复ACK,Ack同样需要1,连接断开

连接释放过程中数据包的Options没有特殊的注意点,和正常数据传输一样都只有Timestamp

[ Linux host 1 ]连接建立

过程和[ FreeBSD host ]同理

[ Linux host 1 ]连接释放

这里的连接释放和[ FreeBSD host ]有所不同,可以看到服务器端将ACK和FIN压缩为一个数据包,此时为三次挥手,Ack需要加187(相对值),最后本机回复的Ack同样加1

正常数据传输

[ FreeBSD host ]数据传输

正常的TCP数据传输中,发送方的Seq以及接收方的Ack应当都是单调增的。如上示例,发送方(服务器)发送了Seq = 233129Seq = 236025(相对值)两个数据包,长度分别为289613032,接收方(本机)回复了Ack = 236025 = 233129 + 2896Ack = 249057 = 236025 + 13032两个数据包分别作为上述两个数据包的ACK。之后的Seq = 249057同理

我们可以观察到接收方(本机)的接收窗口大小Win一直在变化(由RTT统计数据得来)

TCP是流水线协议,接收方会在接收到数据后尽早ACK。以上抓包结果并不一定说明服务器的前2个数据包和后1个数据包是间隔发送的。TCP中发送方应当控制单位时间发送的数据量,尽量保证数据以最高效率传输

[ Linux host 1 ]数据传输,端口4000(Window Scaling & SACK Enabled, Packet loss set to 0%)

数据的传输也是同理。这里的区别是发送端经常会置位PSH。在TCP中,如果数据包的PSH置位,就表示发送方要求接收方在接收到数据包后,立即将数据递交给上层应用。这在发送端也表示数据包需要立即从发送缓冲区发出

传输是否使用PSH视情况而定。实时性要求较高的场合通常需要配置PSH置位

数据包乱序

[ Linux host 1 ],端口4030(Window Scaling & SACK Disabled, Packet loss set to 0%)

在上述警告中,发送方连续发送了Seq = 44644737 44647633 44649081 44650529四个数据包,其中数据包4464908144647633出现了倒序。相应的,接收方发送了四个ACK分别为Ack = 44647633 44647633 44650529 44653425,其中前两个ACK为Dup ACK

丢包与快速重传

[ Linux host 1 ],端口4030(Window Scaling & SACK disabled, Packet loss set to 0%)

出现以上现象(许多个同Ack的Dup ACK)基本就表示出现了丢包。此时接收方也知道发现了丢包。超时后会进行如下所示的快速重传

通过计算我们可以得知之前丢失了大小为4344字节的数据。但是在重传中使用了3个1448字节的数据进行回复,这三个数据包分别为Seq = 40625089, 40626537, 40627985。而发送方在快速重传还未完成时就进行了之后新数据包的传输(Seq = 40626537之后的40690249

这里的着色规律可能有点难以理解,只要记住之前提过的原则:接收方回复的ACK永远只表现当前其期望的最小Seq数据包(即便是陆续丢失了好几个不连续的包),而不必过度关心Wireshark的提示信息。事实上以上三个快速重传数据包都应当标记为Fast Retransmission,而由于Wireshark的标记算法问题有2个数据包被误标记为Out-Of-Order

尽管类似上面的丢包现象很多时候重传会只使用一个数据包,这个示例的情况在实际应用中也较为常见,需要注意甄别

以上丢包重传过程在打开Wireshark的Statistics -> TCP Stream Graphs -> Time Sequence (tcptrace)可以观察到如下现象

tcptrace有三条线,蓝线为数据包的Seq,黄线为Ack,绿线是黄线加上Win的结果,绿线黄线之间的距离就表示当前接收窗口的大小。由于当前没有启用Window Scaling,并且传输的是单个大文件,显然接收窗口Win一直维持在最大值65535。这也从一定程度上说明64kB窗口是不太足够的

当黄线变水平就意味着丢包的发生,此时接收方回复的Ack不再增长

我们将后半部分放大看

上图是已经做好标记的重传过程。数字对应蓝色标记处数据包的Seq,粉色箭头表示数据传输的顺序,上方三个蓝色标记为正常传输的数据,下方三个蓝色标记是重传的数据包。蓝色圆圈内为丢包后继续传输的后续数据包

接下来我们可以观察一下启用了Window Scaling和SACK后的效果

[ Linux host 1 ],端口4001(Window Scaling & SACK Enabled, Packet loss set to 5%)

启用Window Scaling后首先就是突破了65535字节的Win大小限制,其次启用SACK后可以使发送方对丢包情况有更多了解。首先我们在实际测试中就已经感觉到性能的大幅提升

以下为第一次出现丢包时的数据包和tcptrace图表

上图中标记粉色圈的位置就是发生第一次丢包的地方,可以发现蓝线段高度在此处不连续。此时Ack不再增长,但是Win还在增长(体现为[ TCP Window Update ]),接收方为后续数据包的到来提供足够的空间

同时我们观察到本机回复的ACK包大小从66字节变成了78字节。变大的ACK包意味着此时SACK开始发挥作用,上图中一条红色线段就是对应一个ACK数据包的SACK区间。我们可以对比一下前后的Options变化

变化后的Options,增加了12字节。NOP为数据对齐而存在

此时有SACK,同时发送方发送的是Dup ACK。但是同样由于Wireshark的事件判断算法问题,这些Dup ACK(Ack = 28961)数据包没有高亮(疑似和[ TCP Window Update ]判断冲突)

由于该TCP传输被配置为偏实时性任务,该丢失的数据包最终还是超时后通过快速重传传输成功,如果立即重传将会导致后面的数据滞后。在某些情况下(例如配置为低实时性任务),为保证更高的总体效率,该丢失的数据包可能会被立即重传(具体需要看该环境下TCP协议栈的实现和配置),而不是等到超时后快速重传

超时重传

最常见的超时重传在Wireshark中通常会显示为Spurious Retransmission。此时接收方接收到了数据包并回复了ACK,但ACK在途中丢失。超时计时由发送方触发,并进行重传。启用SACK后Spurious重传会大大减少

[ Linux host 1 ],端口4031(Window Scaling & SACK Disabled, Packet loss set to 5%)

通过Analyze -> Expert Information,我们可以查找一些Spurious Retransmission数据包的Seq,并在过滤条件中添加该Seq(例如tcp.seq == 76182

通过以上方法(tcp.seq == 842737),我们可以发现如上两个Seq相同的数据包。正常传输时是不会出现两个相同的数据包的。第一个数据包接收方确确实实收到了(否则Wireshark就捕捉不到它了)。这说明接收方没有接到对应ACK,最后超时进行了重传

接收窗口满

未启用Window Scaling时非常容易导致接收窗口满,启用后几乎不会出现窗口满的情况

[ Linux host 1 ],端口4031(Window Scaling & SACK Disabled, Packet loss set to 5%)

如上。[ TCP Window Full ]出现的位置蓝线和绿线触碰,在本数据包传输完毕后接收方窗口就满了。发送方需要通过接收方的Win以及之前发送过的数据、接收到的ACK进行自动的计算,发现接收窗口满时暂时停止发送

有些TCP使能了Zero Window功能。在接收方发现数据快要满时,发送ACK,将Win置为0,强制发送方停止发送

连接复位

[ Linux host 2 ]

接收方的连接复位可以在curl未完成时通过Ctrl-C触发。得到如下结果

类似的在[ Linux host 1 ]得到如下结果

可以观察到RST时,通常第一个RST维持了上一个ACK包的SeqAck值。后续的RST每接收到一个发送方的数据包就会回复一个,同时只有RST置位,WinLen都为0

TCP复位可以由应用触发。在网络极度不稳定时,TCP有时也会自动复位

Keep-Alive

高丢包率最终会导致双方之间数据包交换阻塞时间越来越长。在一段时间没有数据交换以后,可能出现Keep-Alive数据包。捕捉较耗费时间,不再测试

5.1.4 TCP附加说明

窗口大小对传输距离的限制

在没有启用TCP的Window Scaling扩展的情况下,数据通信双方的收发窗口大小最大为65535字节,这意味着必须将通信的RTT限制在一定范围内,否则非常容易导致瓶颈

假设我们使用速度为1000Mbps的以太网,中间没有任何路由或交换机带来的延时,只计算电信号传播(光速)带来的延迟。设接收方在接收到数据以后立即发送ACK。通过该网络发送完65535B数据需要65535*8 / 1000M = 524.28us (RTT),也就是说第一个ACK回复需要在该RTT时间限制之内到达,否则窗口会满导致阻塞。假设电信号速度3e8m/s,为了避免瓶颈,那么双方之间最大的距离为524.28us * 3e8m/s / 2 = 76.842km

实际应用中路由以及交换等操作会引入比上述大的多的延迟,所以65535字节的窗口是远远不够的,只能启用Window Scaling扩展来规避瓶颈

5.1.5 TCP拥塞控制特性

RFC5681

本小节讲述TCP慢启动(slow start)与快速恢复(fast recovery)特性

最早的TCP实现是没有慢启动和快速恢复特性的。后来Tahoe TCP首先实现了慢启动,但是没有实现快速恢复,它针对丢包的情况一律重新执行慢启动的阻塞控制策略。而Reno TCP改进了Tahoe,使得TCP发送方在检测到Dup ACK后执行快速重传,并使用快速恢复的方法及时回到正常的数据传输状态。NewReno相对于Reno又改进了同时丢多个包的处理方式

本文主要讲述Reno。其他常见的还有Cubic,Bic,Westwood等,不再讲述

TCP数据传输中,有几个关键的虚拟变量用于TCP的拥塞控制。一个是cwndCongestion Window),它表示的是前文提到的发送方的窗口大小。一个是rwnd,它表示接收方窗口大小,接收方发送数据包中的Window就取自该值。一个是ssthresh,它决定使用slow start还是congestion avoidance进行数据控制

上述三个变量中,我们规定cwnd的计量单位为多少个MSS,即TCP在该网络上一个数据包能发送的最大数据量,由MTU计算得来(可以近似看作cwnd相当于多少个最大数据包大小)。而其他变量我们默认就是实际的数据量(单位字节)

有些TCP实现中cwnd单位就是MSS,而其他有些TCP实现中cwnd单位依旧按字节算

慢启动

慢启动本质就是通过一定手段在刚开始传输时使用较小的cwnd来限制数据的发送

为了防止在新的TCP连接中一次发送太多的数据造成太大的冲击导致丢包等问题,TCP采用先slow startcwnd通常为指数级增长)后congestion avoidancecwnd通常为线性增长)的方法逐步试探网络的容量,如下图。在发生丢包后TCP会降低数据吞吐,降低ssthresh,重复上述步骤逐步提高数据流量

TCP规定,在cwnd < ssthresh时使用慢启动slow start,而在cwnd > ssthresh使用congestion avoidance。在TCP刚建立完连接时,一定有cwnd < ssthresh,并使用慢启动。之后如果传输一直顺利,随着cwnd的增长会过渡到congestion avoidance阶段

TCP要求在一开始将ssthresh设定为一个绝对高的值,之后遇到阻塞再减小ssthresh

RFC5681定义在建立连接时,cwnd的初始值称为IW,总大小通常在4k字节左右。MSS表示Maximum Segment Size,它基于当前的网络MTU计算得来。但是如果MSS太大可能导致cwnd的初始值过大,依旧可能导致大量小数据包冲击,所以这种情况下需要额外限制cwnd大小

If (MSS <= 1095 bytes)
    then win <= 4 * MSS;
If (1095 bytes < MSS < 2190 bytes)
    then win <= 4380;
If (2190 bytes <= MSS)
    then win <= 2 * MSS;

在目前实际应用中很多实现并没有采取以上做法。例如Linux的TCP实现就将cwnd初值直接设定为10MSS

slow start阶段时,我们规定每收到一个ACK就将cwnd+1。假设我们的cwnd初值就是10MSS,那么假设我们按1MSS的大小发送10个数据包,那么通常我们将会收到10ACK。由此cwnd会每经过1个RTT左右的时间翻番。cwnd随时间以2的指数级增长的说法由此得来

由于实际应用有些许差异(例如数据包小于MSS等),我们不一定会观察到很理想的指数曲线。而网络上许多将cwnd增长描述为简单的*2翻倍的说法也是不严谨的

RFC5681建议每次接收到一个ACK时,cwnd所代表的数据量的增长值取min(1MSS, N),其中N为本次ACK实际新确认的数据量。这样做是考虑到TCP中接收方有可能会对一个发送方数据包回复多个ACK,这容易被攻击者利用,故意放大cwnd

在有Path MTU Discovery (PMTUD)的情况下,过大的数据包会被丢弃并从路由器得到一个ICMP回复。发送端知道了数据包过大,cwnd所代表的数据量也会随MSS减小

cwnd超过ssthreshTCP就会进入到congestion avoidance阶段,我们规定该阶段中通过定时,每隔1RTT的时间才将cwnd+1,这样cwnd随时间会呈线性递增

实际一次增长的大小可能会小于MSS,但绝对不能超过MSS

在具体的实现中,congestion avoidance也可以仿照slow start阶段,由ACK触发cwnd的更新而不是通过RTT定时器。但是此时cwnd递增量需要为MSS*MSS/cwnd(此处cwndMSS单位字节)

RFC5681要求TCP的发送方在检测到超时丢包(注意不是快速重传,请看下文快速恢复。Reno中它们使用不同的处理方式)后,它需要降低ssthreshmax(FlightSize / 2, 2*SMSS)(即ssthresh最小不能低于2MSSFlightSize即还在网络上奔跑的数据,已发送而未接收到ACK的数据,可以大致看作是上一个阻塞窗口发送的数据。新的ssthresh数值大致为原先的cwnd减半)。同一个数据包再次超时丢包时无需再更改ssthresh

同时,cwnd也需要减小到1MSS,并重新开始slow start

超时丢包经常由接收方的ACK回复丢包导致。而发送方发送的数据包一旦丢包,它会收到Dup ACK,这通常使用快速重传处理

快速恢复

TCP所谓的快速恢复(fast recovery),就是在发现丢包进行数据包快速重传后,在此期间控制新数据包发送的拥塞控制算法,以期望尽早地恢复到正常的传输(直到不再接收到Dup ACK)。它相当于快速重传发生后的慢启动方法(尽管它不采用slow start,而是直接进入到congestion avoidance阶段)

快速恢复不适用于超时的情况

RFC5681中定义快速恢复算法如下,这也是较早期的一种Reno快速恢复实现。目前较新的TCP实现常使用PRR等算法,这里不再讲述

  1. 发送方数据包发生乱序或丢包时,它会收到Dup ACK。收到第3个Dup ACK时判定为丢包,在此之前按照原先步调继续发送新数据包

  2. 发送方按照超时重传一样的定义调节ssthreshmax(FlightSize / 2, 2*SMSS)

  3. 发送方对丢失的数据包立即进行快速重传,并调节cwndssthresh + 3MSS;发送方随后每收到一个Dup ACK,就将cwnd增加1MSS。发送方此时需要照常按1MSS的大小发送新数据包

  4. 收到丢失数据包对应的ACK(非Dup ACK)就意味着丢包的解决,快速重传结束。此时发送方将cwnd设为2中ssthresh值来缩小cwnd(称为降窗window deflating操作),并退出快速恢复状态,进入到正常状态继续使用congestion avoidance发送新数据

RFC5681的方法有利于在快速重传同时尽量多的发送数据包,但这是一种偏激进的做法,可能加重丢包情况。目前大部分TCP实现都要更保守一些,快速重传时及后续正常传输时的cwnd会有更严格的限制以及更平滑的过渡

Reno在发生快速重传cwnd的变化(近似,实际快速恢复期间会有毛刺,cwnd会有多次赋值)

RFC6582对Reno进行改进提出了NewReno,主要面向阻塞窗口cwnd中有多个丢包导致的cwnd变得过小的情况(Reno中每个丢包都会导致cwnd变为原来1/2左右大小)。它可以检测到多个丢包,并且在Dup ACK后接收到新的ACK时进行判断,如果还有丢包,NewReno不会退出快速恢复状态,而是立即对新的丢包进行重传,并且cwnd保持快速恢复期间的变化规律,而不是退出快速恢复时重置为ssthresh

在之前对于TCP的讲述中,我们说过无论发送方的数据包丢失多少个,接收方Dup ACK的Ack值会保持不变,永远只对应最早的丢失的数据包,如果不使用SACK,发送方对后续的丢包情况知之甚少。在Reno中,每个丢失的数据包都会导致cwnd的减小,后续丢失的数据包甚至会因为没有及时快速重传导致超时。如果超时了就只能触发超时重传,并且从slow start开始从头再来了

NewReno针对的是没有SACK支持的情况

NewReno使用一个辅助变量recover来对多次丢包情况进行判断。recover的初始值等于发送方的第一个Seq。NewReno的改进如下:

前文介绍过Ack的值为接收方期望的下一个Seq值,例如Seq = 1836, Len = 464,它对应的ACK回复的Ack = 2300。下文依照此基础进行分析讲解

正常情况下,发送方每检测到3个Dup ACK会判定为丢包,此刻就是快速恢复(包含快速重传)的入口,发送方将此时它发送数据包的最大Seq值赋值到recover记录下来,并立即执行快速重传,设定ssthresh = max(FlightSize / 2, 2*SMSS)

在快速重传后的一段时间内老的Dup ACK还会持续一段时间,这些Dup ACK即便超过3个也不能再次触发重传(这是RFC6582原文中一开始将Dup ACK的Ackrecover进行比较的意义)

旧的丢包如果重传成功,返回的ACK被发送方接收,它的Ack值相对先前的Dup ACK一定会变化。发送方此时知道最老的丢包重传成功,它需要复位重传超时定时器,但是这还不够,它不能立即退出快速恢复状态(下文也是NewReno改进的关键)

发送方需要再次将新的Ack和此时的recover进行对比,判断从发送丢失的数据包快速重传为止这段时间内是否再次发生了丢包。如果Ack > recoverFull acknowledgments),说明没有发生丢包,此时可以执行退出快速恢复状态的步骤(退出时降窗,cwnd可以设为ssthreshmin(ssthresh, max(FlightSize, SMSS) + SMSS),总之最大不超过ssthresh

如果Ack <= recoverPartial acknowledgments),就说明此期间再次发生了丢包,需要再立即对该Ack所指的数据包进行快速重传,同时按上文的定义更新recover的值为最大的Seq。此时cwnd也需要更新,如果新ACK的数据量大于1MSS,那么cwnd = cwnd + 1MSS。如果小于1MSS,新的被ACK的数据量为n,那么cwnd = cwnd - n

上述过程需要一直执行直到丢失数据包重传完成,并退出快速恢复

NewReno中,无论何时出现超时重传,都一定要退出快速恢复状态,并将此时的最大Seq更新到recover

5.2 UDP

5.2.1 UDP通信原理

相比于TCPUDP要简单的多,它是一种无状态、不可靠的传输层协议,仅仅是网络层协议的简单包装。现在的实际应用中很少直接使用UDP,而是将UDP作为基础设施,再次包装以后使用,例如目前流行的传输层协议QUIC。直接利用UDP的典型应用层协议有DNSSNMP

由于UDP的特性,对于接收方来说UDP数据包是突然到来的,并没有建立连接与状态机的概念,系统也就没有必要为每一个<src addr, src port, dest addr, dest port>都创建一个socket。此时socket只以接收端地址<dest addr, dest port>区分。发往同一主机同一端口的UDP数据包实际上在接收端由同一个socket接收,而不是像TCP一样由OS的协议栈针对不同发送端地址创建多个socket分别接收

5.2.2 UDP数据包

参考RFC768

UDP数据包格式如下

     0      7 8     15 16    23 24    31
    +--------+--------+--------+--------+
    |     Source      |   Destination   |
    |      Port       |      Port       |
    +--------+--------+--------+--------+
    |                 |                 |
    |     Length      |    Checksum     |
    +--------+--------+--------+--------+
    |
    |          data octets ...
    +---------------- ...

UDP数据包的数据头只有源端口,目标端口,长度(包含数据头长度)和校验码。计算16位校验码时使用和TCP相同的算法,需要包含如下所示的UDP伪数据头

通常UDP不允许网络层分包。因此在IPv4中,UDP所在IPv4数据头通常需要将Flags置为0b010

     0      7 8     15 16    23 24    31
    +--------+--------+--------+--------+
    |          source address           |
    +--------+--------+--------+--------+
    |        destination address        |
    +--------+--------+--------+--------+
    |  zero  |protocol|   UDP length    |
    +--------+--------+--------+--------+

5.3 ICMP

参考RFC792

ICMP通过IPv4数据包搭载,主要用于IPv4网络中的一些特殊的控制和管理用途。我们日常使用的pingtracepath都使用到了ICMP

IPv4ICMP数据包的Protocol类型为1,源地址为生成该ICMP数据包的路由器或主机

5.3.1 目标不可达

Destination Unreachable格式如下,Type3

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             unused                            |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Internet Header + 64 bits of Original Data Datagram      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

计算Checksum时数据从Type开始算,校验算法和IP相同。最后的Internet Header是触发该ICMP数据包的原数据包IPv4数据头,后接该IPv4数据包的开头64字节数据

常见的Code定义如下

Code 名称 定义 发送方
0 net unreachable 网络不可达 通常为路由
1 host unreachable 主机不可达 通常为路由
2 protocol unreachable 协议未启用 通常为主机
3 port unreachable 端口未启用 通常为主机
4 fragmentation needed and DF set 通常由于MTU不够大,要求分块,通常用于网络MTU检测与反馈
5 source route failed 源路由失效 通常为路由

5.3.2 超时

Time Exceeded格式和目标不可达相同,Type11traceroute就使用了TTL Exceeded功能来追踪路径

常见的Code定义如下

Code 作用 发送方
0 time to live exceeded in transit,TTL超时(降为0) 通常为路由
1 fragment reassembly time exceeded

5.3.3 参数错误

Parameter Problem格式如下,Type12。常见的Code0

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |    Pointer    |                   unused                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Internet Header + 64 bits of Original Data Datagram      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Pointer表示IPv4数据头中出现问题的地方。如果对方(可能是主机或路由器)无法依照IPv4数据头中的参数处理该数据包,就会接收到这样的错误

5.3.4 重定向

Redirect格式如下,Type5

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                 Gateway Internet Address                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Internet Header + 64 bits of Original Data Datagram      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Gateway Internet Address指示主机更改路由表将数据发往该路由。这通常是因为主机和该指定路由在同一子网下,具备更短路径

常见的Code定义如下

Code 作用 发送方
0 Redirect datagrams for the Network,发往该网络的数据包都重定向 路由
1 Redirect datagrams for the Host,发往该主机的数据包都重定向 路由
2 Redirect datagrams for the Type of Service and Network 路由
3 Redirect datagrams for the Type of Service and Host 路由

5.3.5 Echo和回复

Echo or Echo Reply格式如下,Type0表示echo reply8表示echoCode0ping使用了该功能

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Identifier          |        Sequence Number        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Data ...
    +-+-+-+-+-

Identifier用于区分多个ping(通常从0开始分配,用户执行一次ping命令就会加1)。Sequence Number作用和TCP的类似,每次加1,用于匹配一对EchoEcho reply(和TCP不同点是一对EchoEcho replySequence Number是相同的)。Data开头通常有8字节的时间戳,后接长度48字节的任意数据,但是一对EchoEcho reply搭载的数据需要相同

可以向路由器或主机发送Echo并获取回复

5.3.6 时间戳

Timestamp or Timestamp Reply用于时间同步(大部分应用已经被NTP等取代),格式如下,Type13表示timestamp14表示timestamp replyCode0

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |      Code     |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Originate Timestamp                                       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Receive Timestamp                                         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Transmit Timestamp                                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

IdentifierSequence Number作用和Echo类似。请求方发送数据包时将Originate Timestamp设置为最后一次touch数据包的时刻(自UT Midnight开始算,单位ms),其余设置为0。接收方回复时Originate Timestamp不变,Receive Timestamp为接收方第一次touch到数据包的时刻,Transmit Timestamp为接收方发送前最后一次touch数据包的时刻

5.4 ICMPv6

5.4.1 目标不可达

Destination Unreachable格式如下,Type1

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             Unused                            |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    As much of invoking packet                 |
    +                as possible without the ICMPv6 packet          +
    |                exceeding the minimum IPv6 MTU [IPv6]          |

最后需要尽量多的包含触发该ICMPv6的源数据包内容

常见Code定义如下

Code 作用
0 No route to destination,路由表内没有合适的网关,通常产生于未配置默认网关的路由/主机
1 Communication with destination administratively prohibited,访问禁止,通常产生于防火墙等
2 Beyond scope of source address,超出源地址域,例如源地址是一个IPv6 Link-local,而目标地址是一个广域网地址
3 Address unreachable,如果其他情况都不匹配,默认的Code
4 Port unreachable,端口无效(例如未在监听),通常由目标地址主机回复
5 Source address failed ingress/egress policy,该源地址数据包被禁止
6 Reject route to destination,路由禁止访问特定地址

5.4.2 超时

Time Exceeded格式和目标不可达相同,Type3

Code定义和ICMPv4相同,为0表示超过Hop limitTTL

5.4.3 数据包过大

Packet Too Big格式如下,Type2。该信息在网络的MTU检测中有重要作用,类似ICMPfragmentation needed and DF set

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             MTU                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    As much of invoking packet                 |
    +               as possible without the ICMPv6 packet           +
    |               exceeding the minimum IPv6 MTU [IPv6]           |

MTU为下一跳的MTU。该数据包通常由路由器发送,告知来方下一跳的MTU不足以使得该数据包通过

5.4.4 参数错误

Parameter Problem格式如下,Type4

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                            Pointer                            |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    As much of invoking packet                 |
    +               as possible without the ICMPv6 packet           +
    |               exceeding the minimum IPv6 MTU [IPv6]           |

Pointer指向引发问题的字节

常见Code定义如下

Code 作用
0 Erroneous header field encountered
1 Unrecognized Next Header type encounteredNext Header类型问题
2 Unrecognized IPv6 option encounteredOption问题

5.4.5 Echo和回复

Echo RequestEcho Reply格式如下,Type128表示Echo Request129表示Echo Reply

     0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Type      |     Code      |          Checksum             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Identifier          |        Sequence Number        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Data ...
    +-+-+-+-+-

在多播的情况下,每一个符合多播地址的主机都应该以自己的单播地址进行回复

5.4.6 NDP

NDP属于ICMPv6一部分

NDP

5.5 TLS

参考RFC5246

其他参考文章

https://www.cnblogs.com/xiaxveliang/p/13183175.html

RFC文档对于TLS的描述不适合初学,建议先参考其他资源

先看SSL对证书有一个基本的了解

TLS全称Transport Layer Security,工作建立于已有的传输层协议以上,对传输的数据进行加密,为网络数据传输提供安全保护和可信保障

TLS由网景开发的SSL演变而来,经历了1.0 1.1 1.2到最新的1.3版本的进化。其中版本1.0SSL 3.1)和1.1在2021年的RFC8996中已经正式淘汰,本文不再讲述。目前大部分网站使用1.2(2008)或1.3(2018),并且在不断向1.3迁移。TLS不仅用于HTTPS,它同样可以用于其他加密通信,例如tor

目前TCP TLS协议栈最常用TLS 1.2TLS 1.3。IETF强制要求QUIC使用TLS 1.3

本章基于TLS 1.2进行讲解

额外阅读:扩展欧几里德算法

5.5.1 TLS基本概念

TLS基于TCP运行,具备两种基本功能:一个功能是验证通信时对方身份是否合法(是否是我们期望的站点,数据包是否经过中间人篡改),另一个功能是对通信数据进行加密。由于现有非对称加密相比对称加密通常耗费资源更多,所以我们使用非对称加密协商对称密钥,来实现安全的数据传输

TLS中基本的数据传输协议是TLS record protocol记录协议,它会接收数据并分块,可能会对数据进行压缩,签名,加密,然后发送数据;而接收端会进行相反的操作,对数据进行解密,验证,解压缩等操作,并将完整的数据递交给应用层

TLS记录协议分为四大类型,分别为handshakealertchange cipher spec以及application data,它们都是建立在记录协议数据头之上的

记录协议格式如下

struct {
    uint8 major;
    uint8 minor;
} ProtocolVersion;

enum {
    change_cipher_spec(20), alert(21), handshake(22),
    application_data(23), (255)
} ContentType;

struct {
    ContentType type;
    ProtocolVersion version;
    uint16 length;
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

记录协议数据头中的TLS版本version在客户端刚刚发起握手(ClientHello)时为0x0301表示TLS 1.0系列协议,后续服务器回复的ServerHello等数据包会确定使用的TLS版本,在这些数据包中version值通常为0x0303表示TLS 1.2

TLS中的MAC是Message Authentication Code的缩写,它用于验证数据发送方是否为我们期望的发送方,以及数据的完整性(是否被更改)

5.5.2 TLS handshake

TLS握手是我们最为感兴趣的一个步骤,它最后会建立一个会话。一个会话的环境通常包含以下要素

session identifier,会话ID,由服务器选定,用于标记一个active或resumable的会话

peer certificate,证书

compression method,加密前的压缩算法

cipher spec,包含了生成密钥的伪随机算法(PRF),数据的对称加密算法(例如AES等),以及MAC算法(例如HMAC-SHA1)等

master secret,客户端和服务端共享的机密,通常是对称密钥等

is resumable,表示当前会话是否可以初始化新连接

握手步骤示意图

RFC5246的描述如下所示,其中带*的是可选的信息。一个箭头代表一次数据传输,而一次传输视TCP分包情况可以分为多个TCP数据包传输

      Client                                               Server

      ClientHello                  -------->
                                                      ServerHello
                                                     Certificate*
                                               ServerKeyExchange*
                                              CertificateRequest*
                                   <--------      ServerHelloDone
      Certificate*
      ClientKeyExchange
      CertificateVerify*
      [ChangeCipherSpec]
      Finished                     -------->
                                               [ChangeCipherSpec]
                                   <--------             Finished
      Application Data             <------->     Application Data

             Figure 1.  Message flow for a full handshake

handshake数据包格式

以下是握手数据包(除去记录层数据头)handshake格式以及常见Type编码

enum {
    hello_request(0), client_hello(1), server_hello(2),
    certificate(11), server_key_exchange (12),
    certificate_request(13), server_hello_done(14),
    certificate_verify(15), client_key_exchange(16),
    finished(20), (255)
} HandshakeType;

struct {
    HandshakeType msg_type;    /* handshake type */
    uint24 length;             /* bytes in message */
    select (HandshakeType) {
        case hello_request:       HelloRequest;
        case client_hello:        ClientHello;
        case server_hello:        ServerHello;
        case certificate:         Certificate;
        case server_key_exchange: ServerKeyExchange;
        case certificate_request: CertificateRequest;
        case server_hello_done:   ServerHelloDone;
        case certificate_verify:  CertificateVerify;
        case client_key_exchange: ClientKeyExchange;
        case finished:            Finished;
    } body;
} Handshake;

以下分别对上述数据包进行详细讲述,除去记录协议和上述handshake数据头

HelloRequest数据包

struct { } HelloRequest;

由服务器发往客户端,要求客户端发送ClientHello重新握手。客户端如果想要拒绝,它可以回复一个no_renegotiationAlert。如果服务器没有接收到新的ClientHello,它可以使用Alert直接关闭连接

ClientHello数据包

struct {
    uint32 gmt_unix_time;
    opaque random_bytes[28];
} Random;

opaque SessionID<0..32>;

uint8 CipherSuite[2];

enum { null(0), (255) } CompressionMethod;

struct {
    ProtocolVersion client_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suites<2..2^16-2>;
    CompressionMethod compression_methods<1..2^8-1>;
    select (extensions_present) {
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ClientHello;

struct {
    ExtensionType extension_type;
    opaque extension_data<0..2^16-1>;
} Extension;

ClientHello是客户端发起TLS握手时发送的数据包,它会在其中包含一个随机数以及支持的加密校验套件的列表;在extensions中会包含椭圆曲线格式,session ticket,签名算法(extension_type = 0x000d,见后文定义)等重要信息。客户端在发送过该请求之后就会等待服务端回复ServerHello

client_version数据定义和记录协议中的version相同,不同的是它表示当前连接中客户端希望使用的TLS版本。TLS 1.20x0303,包括客户端发送的ClientHello数据包中client_version也为该值而不是0x0301

random的前4字节是UNIX时间,后28字节是一个随机数

session_id格式为1字节length加上032字节长度的idTLS的Session机制可以用于两台主机之间安全参数的复用,SessionID由服务器分配,而客户端ClientHelloSessionID可能来自于先前的连接,来自于当前连接或此时有效的其他连接;它在TLS握手完成,双方交换Finished数据包以后就生效,并会在错误发生或过期后失效。有了SessionID,客户端可以选择性的更新部分参数,也可以在客户端与服务器同时发起多个TLS连接时,省略重复的握手步骤。如果客户端发现没有目标站点的缓存,或者客户端想要重新协商新的参数,那么这里session_idlength0

cipher_suites给出了客户端支持的加密与校验算法套件,格式为2字节length加上22^16-2字节长度的CipherSuite。每一个CipherSuite长度2字节,定义见后

compression_methods格式为1字节length加上12^8-1字节长度的CompressionMethod。每一个CompressionMethod长度1字节。为节省CPU,不启用压缩,此时length0x01CompressionMethod0x00

extensions用于搭载所有额外的信息(例如QUIC传输参数就是一个重要的TLS Extension),格式为2字节length加上最少4字节大小的Extension列表。单个Extension包含2字节的extension_type2字节的length(可以为0)以及最多2^16-1字节的数据

服务器和客户端可以使用的Extension是不对称的,有双方都可使用的,也有服务器或客户端专用的。此外,在新Session、旧Session的上下文中同一个Extension也可能是有不同作用的,并且在这些不同的上下文中也会有专用的Extension

Extension只能在处理完成compression_methods之后通过检测后续是否还有数据来判断(实际应用中都会有Extension

CipherSuite定义如下。一个CipherSuite包含了密钥交换算法,对称加密算法和对称密钥长度,MAC算法,以及PRF

RFC5246 (TLS 1.2)
CipherSuite TLS_NULL_WITH_NULL_NULL               = 0x0000;
CipherSuite TLS_RSA_WITH_NULL_MD5                 = 0x0001;
CipherSuite TLS_RSA_WITH_NULL_SHA                 = 0x0002;
CipherSuite TLS_RSA_WITH_NULL_SHA256              = 0x003B;
CipherSuite TLS_RSA_WITH_RC4_128_MD5              = 0x0004;
CipherSuite TLS_RSA_WITH_RC4_128_SHA              = 0x0005;
CipherSuite TLS_RSA_WITH_3DES_EDE_CBC_SHA         = 0x000A;
CipherSuite TLS_RSA_WITH_AES_128_CBC_SHA          = 0x002F;
CipherSuite TLS_RSA_WITH_AES_256_CBC_SHA          = 0x0035;
CipherSuite TLS_RSA_WITH_AES_128_CBC_SHA256       = 0x003C;
CipherSuite TLS_RSA_WITH_AES_256_CBC_SHA256       = 0x003D;
CipherSuite TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA      = 0x000D;
CipherSuite TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA      = 0x0010;
CipherSuite TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA     = 0x0013;
CipherSuite TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA     = 0x0016;
CipherSuite TLS_DH_DSS_WITH_AES_128_CBC_SHA       = 0x0030;
CipherSuite TLS_DH_RSA_WITH_AES_128_CBC_SHA       = 0x0031;
CipherSuite TLS_DHE_DSS_WITH_AES_128_CBC_SHA      = 0x0032;
CipherSuite TLS_DHE_RSA_WITH_AES_128_CBC_SHA      = 0x0033;
CipherSuite TLS_DH_DSS_WITH_AES_256_CBC_SHA       = 0x0036;
CipherSuite TLS_DH_RSA_WITH_AES_256_CBC_SHA       = 0x0037;
CipherSuite TLS_DHE_DSS_WITH_AES_256_CBC_SHA      = 0x0038;
CipherSuite TLS_DHE_RSA_WITH_AES_256_CBC_SHA      = 0x0039;
CipherSuite TLS_DH_DSS_WITH_AES_128_CBC_SHA256    = 0x003E;
CipherSuite TLS_DH_RSA_WITH_AES_128_CBC_SHA256    = 0x003F;
CipherSuite TLS_DHE_DSS_WITH_AES_128_CBC_SHA256   = 0x0040;
CipherSuite TLS_DHE_RSA_WITH_AES_128_CBC_SHA256   = 0x0067;
CipherSuite TLS_DH_DSS_WITH_AES_256_CBC_SHA256    = 0x0068;
CipherSuite TLS_DH_RSA_WITH_AES_256_CBC_SHA256    = 0x0069;
CipherSuite TLS_DHE_DSS_WITH_AES_256_CBC_SHA256   = 0x006A;
CipherSuite TLS_DHE_RSA_WITH_AES_256_CBC_SHA256   = 0x006B;
CipherSuite TLS_DH_anon_WITH_RC4_128_MD5          = 0x0018;
CipherSuite TLS_DH_anon_WITH_3DES_EDE_CBC_SHA     = 0x001B;
CipherSuite TLS_DH_anon_WITH_AES_128_CBC_SHA      = 0x0034;
CipherSuite TLS_DH_anon_WITH_AES_256_CBC_SHA      = 0x003A;
CipherSuite TLS_DH_anon_WITH_AES_128_CBC_SHA256   = 0x006C;
CipherSuite TLS_DH_anon_WITH_AES_256_CBC_SHA256   = 0x006D;

RFC8446 (TLS 1.3)
CipherSuite TLS_AES_128_GCM_SHA256                = 0x1301;
CipherSuite TLS_AES_256_GCM_SHA384                = 0x1302;
CipherSuite TLS_CHACHA20_POLY1305_SHA256          = 0x1303;
CipherSuite TLS_AES_128_CCM_SHA256                = 0x1304;
CipherSuite TLS_AES_128_CCM_8_SHA256              = 0x1305;

RFC8422
CipherSuite TLS_ECDHE_ECDSA_WITH_NULL_SHA           = 0xC006;
CipherSuite TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA   = 0xC008;
CipherSuite TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA    = 0xC009;
CipherSuite TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA    = 0xC00A;
CipherSuite TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B;
CipherSuite TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C;
CipherSuite TLS_ECDHE_RSA_WITH_NULL_SHA             = 0xC010;
CipherSuite TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA     = 0xC012;
CipherSuite TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA      = 0xC013;
CipherSuite TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA      = 0xC014;
CipherSuite TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256   = 0xC02F;
CipherSuite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384   = 0xC030;
CipherSuite TLS_ECDH_anon_WITH_NULL_SHA             = 0xC015;
CipherSuite TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA     = 0xC017;
CipherSuite TLS_ECDH_anon_WITH_AES_128_CBC_SHA      = 0xC018;
CipherSuite TLS_ECDH_anon_WITH_AES_256_CBC_SHA      = 0xC019;

客户端使用签名算法扩展(signature_algorithms Extension)告诉服务器它支持的签名算法组合列表,服务器不可发送该Extension。定义如下

enum {
    none(0), md5(1), sha1(2), sha224(3), sha256(4), sha384(5),
    sha512(6), (255)
} HashAlgorithm;

enum { anonymous(0), rsa(1), dsa(2), ecdsa(3), (255) }
SignatureAlgorithm;

struct {
    HashAlgorithm hash;
    SignatureAlgorithm signature;
} SignatureAndHashAlgorithm;

SignatureAndHashAlgorithm
supported_signature_algorithms<2..2^16-2>;

supported_signature_algorithms格式同样为2字节length加上列表的格式

ServerHello数据包

struct {
    ProtocolVersion server_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suite;
    CompressionMethod compression_method;
    select (extensions_present) {
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ServerHello;

服务器收到客户端ClientHello请求后回复的数据包,其中包含了另一个随机数,选择使用的单个加密套件(注意这里不是列表),以及少量附加的扩展信息,例如应用层协议类型等

server_versionclient_version一样为0x0303。由于此时服务器确定了使用的TLS版本,记录层数据头的version不能依旧为0x0301,需要更改为TLS 1.20x0303

session_id可能和ClientHello中的相同,也有可能不同。如果服务器发现ClientHello中的session_id存在于自己的缓存中,服务器可以选择沿用旧的Session;否则服务器会返回一个新的session_id,告诉客户端使用一个新的Session。如果沿用旧的Session,双方必须沿用原先的加密套件;如果使用新Session,那么必须进行完整的TLS握手。ServerHello中的session_id同样可以为空,即便ClientHellosession_id不为空。此时表示服务器规定该新的SessionID为空,不希望当前Session被客户端缓存

Certificate数据包

opaque ASN.1Cert<1..2^24-1>;

struct {
    ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;

在服务器回复的数据包中该数据包永远紧跟ServerHello之后,用于向客户端出示自己的证书链,格式为3字节length总长度加上证书数据。证书数据可以包含多张证书,而单张证书格式为3字节length证书长度加上证书本体(x509v3格式)。所有证书依照证书依赖关系的顺序存放,站点(即服务器)的证书永远放在第一位,之后存放的是CA次级证书。CA根证书由于存储于客户端所以可能会忽略

在高安全性的应用场合中(例如银行软件)服务器可能会要求客户端也出示证书,客户端同样使用Certificate回复证书数据包

在复用先前的Session的情况下,服务器无需出示证书,也就不会观察到Certificate数据包

补充说明:CertificateStatus数据包

CertificateStatus数据包的type22,上文未给出。CertificateStatus由发送证书的一方发送,通常紧随Certificate数据包之后,其中搭载OCSP信息说明证书的状态,例如已吊销等(OCSP stapling,RFC6961。这里不再详述)

ServerKeyExchange数据包

enum { dhe_dss, dhe_rsa, dh_anon, rsa, dh_dss, dh_rsa
    /* may be extended, e.g., for ECDH -- see [TLSECC] */
} KeyExchangeAlgorithm;

struct {
    opaque dh_p<1..2^16-1>;
    opaque dh_g<1..2^16-1>;
    opaque dh_Ys<1..2^16-1>;
} ServerDHParams;     /* Ephemeral DH parameters */

struct {
    select (KeyExchangeAlgorithm) {
        case dh_anon:
            ServerDHParams params;
        case dhe_dss:
        case dhe_rsa:
            ServerDHParams params;
            digitally-signed struct {
                opaque client_random[32];
                opaque server_random[32];
                ServerDHParams params;
            } signed_params;
        case rsa:
        case dh_dss:
        case dh_rsa:
            struct {} ;
            /* message is omitted for rsa, dh_dss, and dh_rsa */
        /* may be extended, e.g., for ECDH -- see [TLSECC] */
    };
} ServerKeyExchange;

ServerKeyExchange专门用于有椭圆曲线的密钥协商过程(可以看后文对于椭圆曲线的讲解),用于给出曲线类型,通过曲线生成的Diffie-Hellman公钥,签名等信息。通常只有DHE_DSS DHE_RSA DH_anon等方法中可以使用ServerKeyExchange,其他方法如RSA DH_DSS DH_RSA都不允许出现该类型数据包

不同的密钥协商方法中ServerKeyExchange定义不同,这里不再详述

CertificateRequest数据包

enum {
    rsa_sign(1), dss_sign(2), rsa_fixed_dh(3), dss_fixed_dh(4),
    rsa_ephemeral_dh_RESERVED(5), dss_ephemeral_dh_RESERVED(6),
    fortezza_dms_RESERVED(20), (255)
} ClientCertificateType;

opaque DistinguishedName<1..2^16-1>;

struct {
    ClientCertificateType certificate_types<1..2^8-1>;
    SignatureAndHashAlgorithm
    supported_signature_algorithms<2^16-1>;
    DistinguishedName certificate_authorities<0..2^16-1>;
} CertificateRequest;

该数据包很少见,只有在高安全要求场合中,服务器需要验证客户端的身份时才会发送CertificateRequest请求客户端的证书,通常紧跟服务器发送的CertificateCertificateStatusServerKeyExchange(如果有)。客户端使用Certificate数据包进行回复

certificate_types格式为1字节length加上服务器可以接受的证书类型列表。例如rsa_sign表示包含RSA密钥的证书,dss_sign表示包含DSA密钥的证书,rsa_fixed_dh表示包含静态Diffie-Hellman密钥的证书,等

supported_signature_algorithms定义和ClientHellosignature_algorithms Extension的相同。由于SSL实现的历史原因,有些certificate_types会同时定义签名算法。在TLS 1.2中允许supported_signature_algorithms覆盖certificate_types定义的签名算法

certificate_authorities表示服务器可以接受的CA的DN列表,可以为空。为空时表示服务器可以接受任意已知CA签发的证书

ServerHelloDone数据包

struct { } ServerHelloDone;

服务器在发送完所有ServerHello以及相关数据包后会发送ServerHelloDone,通常也和这些数据包共用一个TCP数据包,此时服务器会开始等待客户端的回应。客户端接收到该数据包后会对服务器的证书进行验证(如果给出),并处理服务器发来的密钥相关数据,最后进行相应的回复

ClientKeyExchange数据包

struct {
    select (KeyExchangeAlgorithm) {
        case rsa:
            EncryptedPreMasterSecret;
        case dhe_dss:
        case dhe_rsa:
        case dh_dss:
        case dh_rsa:
        case dh_anon:
            ClientDiffieHellmanPublic;
    } exchange_keys;
} ClientKeyExchange;

ServerKeyExchange不同,一次完整的TLS握手(不包括复用Session的情况)中一定有ClientKeyExchange数据包。该数据包通常为客户端回复的第一个数据包(客户端需要回复Certificate时除外)。根据密钥协商方法的不同,ClientKeyExchange中可能会包含椭圆曲线生成的DH公钥,或使用非对称密钥加密的对称密钥。该数据包的发送意味着premaster secret的形成,之后双方就可以根据premaster secret计算出最终的主密钥master key)与会话密钥session key

即便客户端在Certificate中使用了rsa_fixed_dh类型的证书,其中已经包含了密钥,它还是必须发送一个空的ClientKeyExchange数据包

TLS要求ClientKeyExchange的数据负载开头要有一个2字节的length表示数据大小(也就是说紧跟记录层、Handshake数据头中的length以外里面还有一个2字节length

不同的密钥协商算法中ClientKeyExchange拥有不同的定义,主要分为RSADiffie-Hellman两种情况,见下文介绍

RSA加密的预备密钥RSA-Encrypted Premaster Secret Message

struct {
    ProtocolVersion client_version;
    opaque random[46];
} PreMasterSecret;

struct {
    public-key-encrypted PreMasterSecret pre_master_secret;
} EncryptedPreMasterSecret;

在使用RSA进行密钥协商时,ClientKeyExchange仅仅包含了经过服务器证书中公钥加密后的pre_master_secretRSAPreMasterSecret长度为48字节,其中包含了2字节的TLS版本(指客户端可以支持的版本而不是当前使用的版本,TLS 1.20x0303)以及客户端通过安全的方法生成的一个随机数。将client_version包含进来是为了防范version rollback attacks

TLS要求RSA时的ClientKeyExchange必须遵从上述定义与格式。为了防范各种攻击,解密后遇到长度非48字节的,服务器需要使用自行生成的46字节随机数对client_version以外的数据进行替换使得后续再出现问题进行异常处理,此时程序继续执行而不是异常退出以防止时间攻击;如果正常,服务器需要绝对确保pre_master_secretclient_version是正确的。如果客户端发来的pre_master_secret内容和先前声明的client_version不一致,密钥协商一定失败

Diffie-Hellman公共密钥Client Diffie-Hellman Public Value

enum { implicit, explicit } PublicValueEncoding;

struct {
    select (PublicValueEncoding) {
        case implicit: struct { };
        case explicit: opaque dh_Yc<1..2^16-1>;
    } dh_public;
} ClientDiffieHellmanPublic;

dh_Yc就是指代Diffie-Hellman密钥交换法中会出现在网络传输中的公共密钥。如果客户端也发送了Certificate并且其中已经包含了Diffie-Hellman公共密钥,那么ClientKeyExchange就无需再重复传输该密钥,为空即可(implicit

CertificateVerify数据包

struct {
    digitally-signed struct {
        opaque handshake_messages[handshake_messages_length];
    }
} CertificateVerify;

由于和客户端发送的Certificate有关,该数据包同样较为少见,需要紧跟ClientKeyExchange发送,客户端通过发送该数据包实现显式的客户端证书自证(客户端给出的证书必须是有签名功能的)。其中handshake_messages是从ClientHello到现在为止客户端发送的完整数据包,首位相接(不包含记录层数据头)

该数据包实际包含的是将handshake_messages经过前面服务器在CertificateRequest中要求的一个SignatureAndHashAlgorithm算法套件进行哈希、签名以后输出的结果,且通常使用客户端证书相关的密钥,以此证明客户端证书的合法性

采用不同SignatureAndHashAlgorithm时该数据包的定义也不同

Finished数据包

struct {
    opaque verify_data[verify_data_length];
} Finished;

verify_data
    PRF(master_secret, finished_label, Hash(handshake_messages))
    [0..verify_data_length-1];

finished_label
    For Finished messages sent by the client, the string
    "client finished".  For Finished messages sent by the server,
    the string "server finished".

Finished通常在ChangeCipherSpec后出现,它意味着密钥交换与握手的成功,也是第一个使用正式的会话密钥加密的数据包,在Wireshark中显示为Encrypted Handshake MessageFinished接收方必须对数据包进行校验后才能开始后续的应用数据传输

PRF指伪随机函数,该函数以master_secret,字符串finished_label,以及目前为止所有的Handshake数据包(HelloRequest除外)的哈希值为输入参数。verify_data通常为12字节(verify_data_length = 12),视具体CipherSuite而定

TLS版本协商

handshake需要负责TLS版本的确认。所以在客户端发送的ClientHello数据包的记录层中version0x0301表示TLS 1.0系列协议,而在该数据包的fragment负载中(也就是handshake的数据结构中)才会给出具体的TLS版本(TLS 1.20x0303)。Type = 1 (ClientHello)1字节,Length3字节,Version2字节

密钥协商算法

TLS有多种密钥协商算法,例如RSA(非对称加密),ECDHElliptic Curve Diffie-Hellman,椭圆曲线DH),使用不同算法时的握手步骤也是不同的。但是这些算法最终的目的都是获取一个服务器和客户端公认的对称加密密钥

基于RSA的交换算法过程如下

  1. 客户端发送ClientHello,其中包含会话ID(Session ID),客户端支持的TLS版本以及加密压缩算法套件,以及一个随机数Rc(明文)

  2. 服务器回复ServerHello,其中包含会话ID,服务器选择的加密压缩套件,以及另一个随机数Rs(明文)

  3. 服务器回复其站点证书Certificate

  4. 服务器发送ServerHelloDone,客户端接收到以后进行签名的验证,以确认服务器的身份,同时知晓了服务器公钥(明文)

  5. 客户端随机生成一个预备对称密钥Premaster Secret,指Premaster Key),并使用服务器的公钥进行加密,发送给服务器。服务器接收到以后使用自己的私钥对其进行解密

  6. 此时双方根据之前交换的随机数RcRs以及预备密钥,使用伪随机函数PRF各自计算出主密钥,再使用同样的方法使用主密钥计算出最终的会话密钥Session Key(结果应当相同)。该密钥用于正式的数据交换

  7. 客户端发送经过Session Key加密的ChangeCipherSpec(通知服务器之后都使用当前选定的算法套件和密钥)和Finished信息,服务器回复ChangeCipherSpecFinished信息

  8. 加密数据流传输开始

RSA基于大数设计,该大数通常只有两个很大的因数,这两个因数都为素数。通常基于这三个数字先选取公钥再计算得到私钥

基于ECDH的交换算法过程如下

  1. 客户端发送ClientHello,其中包含会话ID,客户端支持的TLS版本,加密压缩算法套件,以及一个随机数Rc(明文)

  2. 服务器端回复ServerHello,其中包含会话ID,服务器选择的加密压缩套件,另一个随机数Rs(明文)

  3. 服务器回复其站点证书Certificate。外加ServerKeyExchange,其中包含一个随机生成的临时公钥ECDH的各项参数(明文),以及这些参数的签名(使用之前的随机数对参数进行哈希)

  4. 服务器发送ServerHelloDone,客户端开始验证服务器身份,同时验证DH参数的签名,并获取临时公钥和DH参数

  5. 客户端发送ClientKeyExchange,其中同样包含了另一个临时公钥(明文,且没有DH参数。DH以服务器参数为准)

  6. 双方根据约定的DH参数各自在本地计算出预备对称密钥(结果应当相同)

  7. RSA,使用Rc Rs和预备对称密钥经过两次计算得出最终的会话密钥

  8. RSA,双方交换ChangeCipherSpec并各发送Finished

ECDH的方案相当于在RSA的基础上又添加了一层密钥

ECDH的非对称原理(RFC6090)

ECDH本身表示两种算法,是椭圆曲线和Diffie-Hellman交换算法的结合

椭圆曲线算法基于y^2 = x^3 + ax + b形式的椭圆曲线(形状类似于横放的章鱼,关于x轴对称,如下图。实际可能使用其他函数),在其上取一个公认的点G作切线交曲线于另一点G1'G1'关于x轴的对称点为G1,我们定义运算1*G=G1,那么n*G=Gn。假设给一个G以及一个Gn,我们很难计算出到底经过了多少次运算n

经典示例:假设Alice和Bob公认一个曲线上的点G,Alice有一个私有密钥a,Bob有b。此时Alice使用自己的密钥计算a*G并发送给Bob,Bob同样回复一个b*G。之后Alice可以计算a*b*G,Bob可以计算b*a*G,最终得到的结果是相同的。这个相同的结果就可以作为对称加密密钥使用

Diffie-Hellman算法的基本思想是给定Alice的密钥a,Bob的密钥b,以及一个公认的参数F和配套的转换算法e,双方交换a' = e(a,F) b' = e(b,F)。双方最终可以在本地通过计算d(b,a',F)d(a,b',F)得到相同的结果,作为对称加密密钥。上述椭圆曲线算法正好符合这种性质,并且远比RSA难破解

通过以上介绍,我们可以发现两者最大的区别在于预备密钥的生成方法。RSA的预备密钥在客户端随机生成,并且需要在加密后直接通过网络传输。而ECDH方案的预备密钥由双方自行计算得出,无需通过网络传输,这是ECDH的优势之一

此外,ECDH相比RSA还具备前向安全性RSA中预备密钥仅仅由服务器内SSL证书的公钥加密,一旦某一天服务器的私钥泄漏(暴力计算非常耗时,更多的是通过一些手段直接获取),客户端之前发送的所有预备密钥都将暴露,从而使得所有会话中传输过的数据都暴露。ECDH每个会话都会使用临时密钥,即便服务器的私钥泄漏,也只能暴露客户端的临时公钥,不会影响其他传输过的数据(同时直接获取私钥也几乎不可能)

在最新的TLS 1.3中已经废弃了普通的RSA交换算法

TLS 1.3为了提高效率,也可能直接复用之前使用过的对称密钥(需要通过一些算法进行变换,得到预共享密钥PSK,牺牲一定安全性)。这种方法称为0-RTT,在5.6QUIC中有重要应用,此时ClientHello会表明这种复用情况并直接传输加密后的数据

Wireshark抓包实验

TCP连接建立以后就立刻开始了TLS握手,客户端发送Client Hello,如下。可以看到该记录协议数据包的层次,其中包含了前文所述的各项内容

这里补充说明一下TLS中的参数Extension格式

每一个扩展参数都由2字节的类型,2字节的长度以及后续的实际数据构成。例如上图中0x0023表示参数为session_ticket,后面的0x0000表示长度为0,没有搭载数据。紧接下来就是renegotiation_info(类型65281 = 0xff01,长度1

服务器回复的Server Hello格式如下,其中约定了使用的算法等各项基本参数

此后服务器同时发送了证书CertificateServer Key Exchange,以及Server Hello Done

客户端同时发送的Client Key Exchange,以及随后的Change Cipher Spec。最后的Finished是第一条加密信息。服务器回复Change Cipher Spec和加密的Finished,这里不再展示

5.5.3 TLS data

握手后所有的数据都通过会话密钥加密。每个数据包依然包含记录协议的header,其中有ContentTypeProtocolVersionlength以及包含的加密数据

Wireshark抓包

5.5.4 TLS alert

Alert用于交换一些错误信息,其本身也会被加密,在连接过程中出现错误时会发挥作用,分为warningfatal以及255几个等级,其中遇到fatal双方必须直接关闭连接,并销毁所有该连接相关的密钥和状态。格式如下

enum { warning(1), fatal(2), (255) } AlertLevel;

enum {
    close_notify(0),
    unexpected_message(10),
    bad_record_mac(20),
    decryption_failed_RESERVED(21),
    record_overflow(22),
    decompression_failure(30),
    handshake_failure(40),
    no_certificate_RESERVED(41),
    bad_certificate(42),
    unsupported_certificate(43),
    certificate_revoked(44),
    certificate_expired(45),
    certificate_unknown(46),
    illegal_parameter(47),
    unknown_ca(48),
    access_denied(49),
    decode_error(50),
    decrypt_error(51),
    export_restriction_RESERVED(60),
    protocol_version(70),
    insufficient_security(71),
    internal_error(80),
    user_canceled(90),
    no_renegotiation(100),
    unsupported_extension(110),
    (255)
} AlertDescription;

struct {
    AlertLevel level;
    AlertDescription description;
} Alert;

Alert分为正常关闭连接的Closure Alerts(仅close_notify一个)以及报错的Error Alerts

Closure Alerts

TLS会使用Alertclose_notify正常关闭连接,表示本发送方不会再发送数据,如下

Error Alerts

Error Alerts等级定义如下

Alerts 等级 解释
unexpected_message fatal 正常的TLS实现中不会出现
bad_record_mac fatal 正常的TLS实现中不会出现
decryption_failed_RESERVED not used
record_overflow fatal 正常的TLS实现中不会出现
decompression_failure fatal 正常的TLS实现中不会出现
handshake_failure fatal Alert发送方无法协商出可用的安全参数
no_certificate_RESERVED not used
bad_certificate 证书验证失败
unsupported_certificate 不支持的证书类型
certificate_revoked 证书已吊销失效
certificate_expired 证书已过期
certificate_unknown 证书无效(其他原因)
illegal_parameter fatal handshake数据包中参数错误
unknown_ca fatal 未知的CA
access_denied fatal 证书有效,但该Alert发送方不想再继续协商
decode_error fatal 正常的TLS实现中不会出现,网络传输错误时除外
decrypt_error fatal handshake出现加解密错误,例如签名验证失败或finish数据包验证失败等
export_restriction_RESERVED not used
protocol_version fatal (version not supported) 客户端协商的TLS版本服务器可以识别,但不支持(例如强制要求新版TLS 1.3
insufficient_security fatal (more secure cipher required) handshake_failure的特殊情况,服务器要求客户端提供更安全的加解密套件选项
internal_error fatal (buggy implementation) 内部错误
user_canceled warning 提示性警告信息,后面通常会跟一个close_notify
no_renegotiation warning 在握手完成后再接收到Client HelloServer Hello时,本机如果不想再次重新协商,可以发送no_renegotiation
unsupported_extension fatal 客户端接收到的Server Hello中包含了它不支持的扩展

5.5.5 TLS change cipher spec

Change Cipher Spec格式如下

struct {
    enum { change_cipher_spec(1), (255) } type;
} ChangeCipherSpec;

Change Cipher Spec通常出现在重新协商密钥以及加密套件之后。在协商的过程中依然使用了旧的加密套件和密钥,而通信双方交换Change Cipher Spec之后的信息需要使用新的加密套件和密钥

5.5.6 附加说明:OpenSSL

主要讲解SSL中的证书

SSL有多种实现,目前应用最广泛的就是OpenSSL。其他还有LibreSSL等

当前最新的OpenSSL处于3.0版本。有关OpenSSL的原理和最新的API编程教程推荐阅读Alexei Khlebnikov的Demystifying Cryptography with OpenSSL 3.0

密码学相关见笔记201230a

SSL证书类型

pem格式文件定义见RFC7468,证书格式见RFC5280 Section 4

我们可以总结如下(可以先看后面的实验):

SSL生成的密钥文件中包含了公钥和私钥。公钥需要从该文件提取

SSL证书格式为x509,主要分为CA根证书,CA次级证书,以及普通证书。所有这些证书构成一个证书依赖树,树的根节点就是CA根证书,而叶子节点为普通证书。其余节点都为CA次级证书。通常每一个证书都有且只有一个签发方Issuer和一个持有方Subject,签发方和持有方使用DN名称作为唯一标识(Distinguished Name)。证书依赖链也是基于DN进行匹配连接

CA根证书是最权威的证书。根证书是自签名的,它的IssuerSubject都是持有方本身。CA根证书可以用于签署其他的CA次级证书或普通证书,而为了规避风险很少会直接签署普通证书,普通证书通常由CA次级证书签署。客户端(如浏览器)会事先安装一些可信的CA根证书,通过https访问一个网站时会获取这个网站的证书以及额外的次级证书。客户端如果通过解析最终无法追踪到本地已安装的根证书,就代表网站证书不合法

CA次级证书不可以自签名,它的Issuer可以是CA根证书或其他CA次级证书持有方。CA次级证书同样可以签署其他的CA次级证书或普通证书。服务器会在回复自己的证书同时包含对应的CA次级证书

普通证书同样不可以自签名,它的Issuer只能是CA证书持有方。它不能用于签署其他证书。这是我们通过https访问网站时网站发送给我们的证书

SSL证书结构

            -- Certificate --
tbsCertificate
    version
    serialNumber
    signature (Identical to signatureAlgorithm below)
    issuer
    validity
        notBefore
        notAfter
    subject
    subjectPublicKeyInfo
        algorithm
        subjectPublicKey
    issuerUniqueID (optional)
    subjectUniqueID (optional)
    extensions (optional)
signatureAlgorithm
signatureValue

SSL证书的层次结构如上。一张证书包含了证书签发方的签名算法,以及计算得到的签名,外加其他被签名的内容(RFC5280称为TBSCertificateTBS means To-Be-Signed三大部分

tbsCertificate包含了一些明文信息。包括x509版本(v3对应2),CA给该证书分配的序列号(最多20字节),签名算法,签发方名称,有效期,持有方名称,以及持有方的公钥,公钥对应的非对称算法

签发方和持有方名称通常包含国家,省/州/郡,单位,DN,CN(Common Name)以及身份序列号,还可以加上域名

由此,证书链都是层层签发,反过来也是层层校验

证书签发时,CA首先生成非对称密钥,用于根证书并自签名。根证书中包含公钥,以及使用对应私钥对tbs哈希值进行加密得到的签名

CA再重新生成非对称密钥,用于生成CA次级证书的CSR(Certificate Signing Request,使用pkcs格式),该CSR包含了新密钥的公钥,以及使用新密钥私钥生成的tbs签名。使用根证书签名时签发方首先会通过CSR给出的公钥解密验证CSR签名,之后填写Issuer等信息,重新计算tbs哈希,并使用根证书私钥签名,生成次级证书(x509格式,注意此时tbs内公钥依旧是新密钥的公钥)

站点生成自己的非对称密钥并提交CSR给CA签名。同样CA首先使用CSR中站点的公钥验证CSR签名,之后进行Issuer等信息的填充(此时填充的是CA次级证书对应的信息,同时需要包含获取次级证书的URI),最后重新算哈希并使用次级证书私钥签名,生成证书返还站点

证书校验时,客户端预先安装了根证书。客户端首先获取了站点的证书以及对应的CA次级证书,并分别提取公钥,对所有证书进行解密与哈希校验。之后客户端依据次级证书的Issuer提示在本地找到了根证书,并提取公钥对次级证书签名进行了校验。此时校验成功,站点证明了合法身份

此外CA还有吊销次级证书的操作(例如私钥泄漏等原因)。这样的操作通常使用CRLOCSP实现,CA会在次级证书中包含证书吊销列表CRL的请求链接以供查验

生成RSA密钥

随着量子计算的发展,RSA将会淘汰

首先生成RSA密钥,输出格式为pem,我们习惯使用.key后缀。不加长度默认2048bit(建议可以4096bit)

openssl genrsa -out prv.key 2048
cat prv.key

得到如下格式的密钥(公钥需要提取)

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

通过如下命令查看密钥的私钥以及各项参数

openssl rsa -text -noout -in prv.key

通过如下命令生成公钥

openssl rsa -in prv.key -pubout -out pub.key
cat pub.key

输出

-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----

生成自签名根证书

实际应用中我们通常需要将我们的CSR发给CA进行签名,而不是自签名。但是我们可以在本地安装我们自签名的根证书,使应用信任我们的站点

证书为x509(也是pem的一种)格式,习惯使用.crt后缀,可以指定证书有效期。之后会提示输入你的具体地址,名称和邮箱等。实际应用通常需要指定证书对应的域名,该证书只对该域名有效

openssl req -x509 -key prv.key -out prv.crt -days 16 -subj "/CN=domain.com"
cat prv.crt

如果已经有密钥和对应的CSR,那么操作如下

openssl x509 -req -in prv.csr -signkey prv.key -out prv.crt -days 16 -subj "/CN=domain.com"
cat prv.crt

输出

-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

可以通过以下命令查看该根证书具体的信息,之前填写的地址,名称,邮箱都可以直接解码出来,也会显示证书的公钥和签名信息

openssl x509 -noout -text -in prv.crt

输出,我们可以发现该证书为可以签署其他证书的CA证书(x509扩展域指出该证书为CA:TRUE)。同时IssuerSubject完全相同,这证实了该证书为根证书

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            ...
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = XX, ST = XX, L = XX, O = XXX, OU = XX, CN = XXXXX, emailAddress = [email protected]
        Validity
            Not Before: Jan  X 01:39:36 2023 GMT
            Not After : Feb  X 01:39:36 2023 GMT
        Subject: C = XX, ST = XX, L = XX, O = XXX, OU = XX, CN = XXXXX, emailAddress = [email protected]
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    ...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                ...
            X509v3 Authority Key Identifier: 
                ...
            X509v3 Basic Constraints: critical
                CA:TRUE
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        ...

使用其他证书签名非根证书

生成密钥

openssl genrsa -out server.key 2048

首先为该密钥生成CSR,习惯使用.csr后缀,之后会提示输入该密钥对应的地址,名称和邮件(server.csr文件会包含server.key中提取的公钥,外加地址,名称,邮件等信息,都是明文。最后会有一个自签名)

openssl req -new -key server.key -out server.csr -subj "/CN=domain.org"
cat server.csr

输出

-----BEGIN CERTIFICATE REQUEST-----
...
-----END CERTIFICATE REQUEST-----

可以使用以下命令,类似x509一样查看server.csr包含的信息,可查看信息内容和.crt基本相同

openssl req -text -noout -verify -in server.csr 

输出

Certificate request self-signature verify OK
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = XX, ST = XX, L = XX, O = XXX, OU = XX, CN = XXXXX, emailAddress = [email protected]
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    ...
                Exponent: 65537 (0x10001)
        Attributes:
            challengePassword:
            Requested Extensions:
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        ...

最终使用原先的根证书prv.crt和密钥prv.keyserver.csr签名,生成x509格式server.crt

openssl x509 -req -in server.csr -CA prv.crt -CAkey prv.key -CAcreateserial -out server.crt -days 10

使用之前相同的方法解码server.crt,可以看到之前我们为server.csr设定的邮件等信息,此外多出了证书方的prv.crt内容

Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            ...
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = XX, ST = XX, L = XX, O = XXX, OU = XX, CN = XXXXX, emailAddress = [email protected]
        Validity
            Not Before: Jan  X 02:37:09 2023 GMT
            Not After : Feb  X 02:37:09 2023 GMT
        Subject: C = XX, ST = XX, L = XX, O = XXX, OU = XX, CN = XXXXX, emailAddress = [email protected]
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    ...
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        ...

实际应用中,为防止机器的密钥被盗取导致泄露,我们通常会在生成密钥时使用加密,例如openssl genrsa -des3 -out server.key 2048,并且修改密钥文件权限为400r--------)。也可以用其他更安全的加密方法如-aes256。这样每次使用该密钥时都需要手动输入密码

可以将证书pem转换为der格式导入到某些软件,例如浏览器

openssl x509 -in server.crt -outform der -out server.der

Linux系统中的证书

内置的根证书位于/etc/ssl/certs,也是x509格式,其中大部分证书使用xxxxxxxx.0的文件命名。xxxxxxxx是一个哈希值,可以通过以下命令生成

openssl x509 -inform der -subject_hash_old -in cert.der | head -1

Chromium中的证书

Chromium中根证书在Settings -> Privacy and security -> Security -> Manage Certificates -> Authorities。我们随意打开一个根证书如下,可以看到一些基本信息

我们打开 github.com,点击地址栏的锁就可以查看当前网站的证书

可以发现github使用了来自DigiCert的一个二级证书,并在证书中指明了CA次级证书的URI。我们随意选一张证书,例如我们点击Export保存一下CA次级证书,并使用openssl解码(x509)得到如下内容

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            07:f2:f3:5c:87:a8:77:af:7a:ef:e9:47:99:35:25:bd
        Signature Algorithm: sha384WithRSAEncryption
        Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
        Validity
            Not Before: Apr 14 00:00:00 2021 GMT
            Not After : Apr 13 23:59:59 2031 GMT
        Subject: C = US, O = DigiCert Inc, CN = DigiCert TLS Hybrid ECC SHA384 2020 CA1
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:c1:1b:c6:9a:5b:98:d9:a4:29:a0:e9:d4:04:b5:
                    db:eb:a6:b2:6c:55:c0:ff:ed:98:c6:49:2f:06:27:
                    51:cb:bf:70:c1:05:7a:c3:b1:9d:87:89:ba:ad:b4:
                    13:17:c9:a8:b4:83:c8:b8:90:d1:cc:74:35:36:3c:
                    83:72:b0:b5:d0:f7:22:69:c8:f1:80:c4:7b:40:8f:
                    cf:68:87:26:5c:39:89:f1:4d:91:4d:da:89:8b:e4:
                    03:c3:43:e5:bf:2f:73
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Subject Key Identifier: 
                0A:BC:08:29:17:8C:A5:39:6D:7A:0E:CE:33:C7:2E:B3:ED:FB:C3:7A
            X509v3 Authority Key Identifier: 
                03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            Authority Information Access: 
                OCSP - URI:http://ocsp.digicert.com
                CA Issuers - URI:http://cacerts.digicert.com/DigiCertGlobalRootCA.crt
            X509v3 CRL Distribution Points: 
                Full Name:
                  URI:http://crl3.digicert.com/DigiCertGlobalRootCA.crl
            X509v3 Certificate Policies: 
                Policy: 2.16.840.1.114412.2.1
                Policy: 2.23.140.1.1
                Policy: 2.23.140.1.2.1
                Policy: 2.23.140.1.2.2
                Policy: 2.23.140.1.2.3
    Signature Algorithm: sha384WithRSAEncryption
    Signature Value:
        47:59:81:7f:d4:1b:1f:b0:71:f6:98:5d:18:ba:98:47:98:b0:
        7e:76:2b:ea:ff:1a:8b:ac:26:b3:42:8d:31:e6:4a:e8:19:d0:
        ef:da:14:e7:d7:14:92:a1:92:f2:a7:2e:2d:af:fb:1d:f6:fb:
        53:b0:8a:3f:fc:d8:16:0a:e9:b0:2e:b6:a5:0b:18:90:35:26:
        a2:da:f6:a8:b7:32:fc:95:23:4b:c6:45:b9:c4:cf:e4:7c:ee:
        e6:c9:f8:90:bd:72:e3:99:c3:1d:0b:05:7c:6a:97:6d:b2:ab:
        02:36:d8:c2:bc:2c:01:92:3f:04:a3:8b:75:11:c7:b9:29:bc:
        11:d0:86:ba:92:bc:26:f9:65:c8:37:cd:26:f6:86:13:0c:04:
        aa:89:e5:78:b1:c1:4e:79:bc:76:a3:0b:51:e4:c5:d0:9e:6a:
        fe:1a:2c:56:ae:06:36:27:a3:73:1c:08:7d:93:32:d0:c2:44:
        19:da:8d:f4:0e:7b:1d:28:03:2b:09:8a:76:ca:77:dc:87:7a:
        ac:7b:52:26:55:a7:72:0f:9d:d2:88:4f:fe:b1:21:c5:1a:a1:
        aa:39:f5:56:db:c2:84:c4:35:1f:70:da:bb:46:f0:86:bf:64:
        00:c4:3e:f7:9f:46:1b:9d:23:05:b9:7d:b3:4f:0f:a9:45:3a:
        e3:74:30:98

5.5.7 TLS1.3

QUIC-TLS

5.6 QUIC

参考RFC9000

https://www.chromium.org/quic/

UCLA - A Quick Look at QUIC

QUIC是一个非常复杂的协议。学习QUIC之前可以先尝试一下Wireshark抓包粗略了解一下数据包的层次结构

QUIC(发音同quick,Quick UDP Internet Connection)是面向未来应用的协议,最初由Google开发,用于新一代HTTP/3协议栈,作为TCP TLS HTTP/2应用体系的替代品提升用户体验(目前QUIC通常结合TLS 1.21.3版本使用)。现在主流浏览器都已支持QUIC。但是QUIC的目的并不是替代TCP

目前IETF成立的QUIC小组正在致力于QUIC的标准化,以方便未来的广泛应用。除RFC9000系列已完成外,其余有很多文档依旧在完善中

5.6.1 简介

QUIC是有状态协议,需要建立连接。QUICTCP一样支持拥塞控制(Congestion Control),可以缓冲数据,保证数据的顺序。也可以支持UDP一样的无保证传输

QUIC是一种建立于UDP之上的传输层协议(之所以叫User Datagram Protocol,就是方便用户在这个基础上开发其他协议)

QUIC还不能算是成熟的协议,它的应用存在许多问题,但广泛应用QUIC是未来的总趋势

目前QUIC依旧处于重复造轮子的阶段,有多种实现且集成于各应用中,例如符合IETF标准的就可以称为IETF QUIC,而Google开发的版本就称为Google QUIC。现在的IETF QUIC相比Google QUIC已经有了很多的改动,基本可以算是一个完全不同的协议了。使用Wireshark抓取不同软件的QUIC包可以观察发现许多的差异

未来QUIC有可能会像TCP一样成为操作系统的一个可用组件。但由于QUIC是一种复杂的协议,这需要大量的工作,包括网络设施的更新,操作系统的更改,API接口的标准化。QUIC规范化可能需要耗费很多年时间。即便是现在的TCP相比最早期的版本也有了很多的更改,而其最终还是由Berkeley/POSIX sockets统一了接口

TCPUDP的同号端口是不相关的。目前依然有很多网站并未使用QUIC,为解决兼容问题,目前的浏览器访问一般网站时都是先基于TCP TLS协议栈握手以后再由服务器通知浏览器发起QUIC连接的。这样在使用浏览器时并不能发挥QUIC0-RTT应有的优势,仅仅是利用了QUIC的数据并行能力(TCP TLS HTTP/2同样能做到数据流并行,但是长远看来新协议栈会有优势。此外专用软件如APP可以不用考虑这样的兼容问题)

由于QUIC基于UDP,运营商网络设施对UDP数据流的处理方式不太友好。这会限制QUIC的性能发挥

虽然QUIC是传输层协议,但是QUIC实际上包含了以往HTTP应用体系中传输层和应用层的部分功能。下图截自Wikipedia

为实现数据传输的并行化,解决TCP的队头阻塞问题,QUIC引入了数据流(Stream)的概念,一个QUIC连接可以有多个并行数据流。通常一个QUIC数据包会携带有多个数据流的数据,这些帧称为流帧(Stream Frame),每个流帧都标记有其所属流的ID(Stream ID)。每一个QUIC数据包正是由数据头(Header)和(Frame)构成(还有其他类型的帧,具体见后文

一个UDP数据包可能包含多个QUIC数据包(但是除最后一个数据包以外都需要包含Length域,具体解释见5.6.16

现在的网页通常包含许多独立的资源,不同的资源可以通过一个QUIC连接中不同的数据流进行传输。数据传输的并行化可以大大提高网络浏览的体验。原先这部分工作是由HTTP/2负责的

QUIC相比TCP TLS的方案可以更快的建立一个安全连接(0-RTT1-RTT)。和以往的协议栈不同,在架构上QUIC调用了TLS的功能并直接和上层应用交互,而不像往常的协议栈中HTTP直接依赖于TLS运行

5.6.2 QUIC数据包:长数据头

单个QUIC数据包由HeaderFrame构成。QUIC处于不同状态下时(例如建立连接时、连接建立成功后),数据包会有不同程度的加密保护

QUIC数据包的数据头有长短两种。其中长数据头通常用于连接建立过程中,这些数据包又分为Version NegotiationInitial0-RTTHandshakeRetry共计5种。而短数据头用于后续的数据传输,此时连接已建立,使用1-RTT数据包传输数据

不同版本的QUIC数据包的处理方式不同,通信前首先需要通过Version Negotiation进行协商。下文对这5种数据包分别作解释

下文如果不作特殊说明,数据包中的整数可变长编码定义如下

2MSB(最高2bit) 编码长度(Byte) 可表示范围
00 1 [0..2^6-1]
01 2 [0..2^14-1]
10 4 [0..2^30-1]
11 8 [0..2^62-1]

Version Negotiation

版本协商数据包由服务器发往客户端,特征是Version域为0x00000000。客户端接收到以后无需回复ACK

Version Negotiation Packet {
  Header Form (1) = 1,
  Unused (7),
  Version (32) = 0,
  Destination Connection ID Length (8),
  Destination Connection ID (0..2040),
  Source Connection ID Length (8),
  Source Connection ID (0..2040),
  Supported Version (32) ...,
}

()括号内的数字表示该域长度bit。各部分定义如下

名称 解释
Header Form 恒为1表示长数据头
Unused 可以设置为任意值。通常最高位可以置10x40)以和其他非QUIC协议共存(RFC7983)
Version 必须全0
Destination Connection ID Length 接收端的连接ID长度Byte,最大255
Destination Connection ID DCID,接收端的连接ID,等于先前客户端发来数据包的Source Connection ID
Source Connection ID Length 发送端的连接ID长度Byte,最大255
Source Connection ID SCID,发送端的连接ID,由服务器生成
Supported Version 服务器支持的一些QUIC版本的列表

Initial

初始包用于服务器、客户端之间协商密钥,其中搭载了CRYPTO帧,同时也可能搭载ACK帧进行相应的回应。通常由客户端首先发送Initial包(CRYPTO的offset为0),之后才开始协商。此外,Initial包中还可以搭载PINGPADDINGCONNECTION_CLOSE帧等

Initial数据包只能使用Initial数据包进行对应的ACK

Initial Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 0,
  Reserved Bits (2),
  Packet Number Length (2),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Token Length (i),
  Token (..),
  Length (i),
  Packet Number (8..32),
  Packet Payload (8..),
}
名称 解释
Header Form 恒为1表示长数据头
Fixed Bit 1
Long Packet Type 0b00表示Initial
Reserved Bits 0b00
Packet Number Length 后面Packet Number长度(Byte)为该域+1(也即14字节)
Version QUIC版本
Destination Connection ID Length 接收端的连接ID长度Byte,最大20
Destination Connection ID DCID,接收端的连接ID
Source Connection ID Length 发送端的连接ID长度Byte,最大20
Source Connection ID SCID,发送端的连接ID
Token Length Token长度(Byte)。采用可变长编码。如果没有Token,那么为0。服务器发出的Initial包没有Token
Token 先前的Retry包或NEW_TOKEN帧提供的Token
Length 该数据包后续剩余字节数(Packet NumberPacket Payload)。采用可变长编码
Packet Number 数据包序号,长度对应上面的Packet Number Length,需要在4字节以内,见5.6.16
Packet Payload 搭载了一些帧

当前QUIC版本为0x00000001

0-RTT

0-RTT包用于在握手完成之前客户端向服务器传送数据包。0-RTTQUIC的一大特性,牺牲了一定的安全性换取更高的响应速度。客户端只有在握手完成以后才会收到0-RTT数据包对应的ACK帧(搭载于后续的1-RTT数据包中),也是因此0-RTT不能有ACK

0-RTT中不能有ACKCRYPTOHANDSHAKE_DONENEW_TOKENPATH_RESPONSE以及RETIRE_CONNECTION_ID帧,否则触发PROTOCOL_VIOLATION

0-RTT Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 1,
  Reserved Bits (2),
  Packet Number Length (2),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Length (i),
  Packet Number (8..32),
  Packet Payload (8..),
}
名称 解释
Header Form 恒为1表示长数据头
Fixed Bit 1
Long Packet Type 0b01表示0-RTT
Reserved Bits 0b00
Packet Number Length 同上,略
Version
Destination Connection ID Length
Destination Connection ID
Source Connection ID Length
Source Connection ID
Length
Packet Number
Packet Payload

Handshake

握手包用于搭载加密握手消息与相应的ACKHandshake通常由服务器发起,客户端在接收到服务器发来的Handshake以后需要相应的进行回复。Handshake包拥有独立的包序号(从0开始)。Handshake中搭载了CRYPTO帧,也可能搭载PINGPADDINGACKCONNECTION_CLOSE

Handshake数据包同样只能使用Handshake数据包进行对应的ACK

Handshake Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 2,
  Reserved Bits (2),
  Packet Number Length (2),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Length (i),
  Packet Number (8..32),
  Packet Payload (8..),
}
名称 解释
Header Form 恒为1表示长数据头
Fixed Bit 1
Long Packet Type 0b10表示Handshake
Reserved Bits 0b00
Packet Number Length 同上,略
Version
Destination Connection ID Length
Destination Connection ID
Source Connection ID Length
Source Connection ID
Length
Packet Number
Packet Payload

Retry

Retry由服务器发送,其中包含了服务器生成的Address Validation Token。服务端可以在接收到客户端发来的0-RTTInitial数据包之后回复Retry数据包。而客户端在接收到服务端发来的InitialRetry以后就不能再处理后续接收到的Retry除非重新建立一个连接;此外,Retry后客户端不能擅自复位任何Packet Number

如果客户端无法验证服务器发来的Retry Integrity Tag,它必须丢弃这个数据包

客户端在接收到服务器的Retry数据包后回复Initial数据包以继续建立连接,其中需要包含之前Retry数据包中的Retry TokenRetry后客户端发送的所有Initial数据包都需要包含最新的Token);或者可能会尝试0-RTT。在后续如果再次接收到服务器发来的Initial数据包,往往意味着服务器连接ID的更新(InitialRetry中的Connection ID会被验证)

Retry数据包无法被显式ACK

Retry Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 3,
  Unused (4),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Retry Token (..),
  Retry Integrity Tag (128),
}
名称 解释
Header Form 恒为1表示长数据头
Fixed Bit 1
Long Packet Type 0b11表示Retry
Unused 可以为任意值
Version
Destination Connection ID Length
Destination Connection ID 等于客户端发送Initial包的Source Connection ID
Source Connection ID Length
Source Connection ID
Retry Token 用于验证客户端地址的Token
Retry Integrity Tag 见QUIC-TLS

5.6.3 QUIC数据包:短数据头

QUIC版本协商以及1-RTT密钥交换完成后就会使用短数据头数据包。短数据头格式通用,如下

1-RTT Packet {
  Header Form (1) = 0,
  Fixed Bit (1) = 1,
  Spin Bit (1),
  Reserved Bits (2),
  Key Phase (1),
  Packet Number Length (2),
  Destination Connection ID (0..160),
  Packet Number (8..32),
  Packet Payload (8..),
}
名称 解释
Header Form 恒为0表示短数据头
Fixed Bit 1
Spin Bit 见下
Reserved Bits 必须为0b00
Key Phase 用于辅助接收方识别密钥
Packet Number Length 同上,略
Destination Connection ID 接收方指定的连接ID。注意这里不再有源连接ID
Packet Number
Packet Payload

Latency Spin Bit

Latency Spin Bit使得网络通路上的设备(通常为运营商的路由器)可以对客户端、服务器之间的RTT进行估算,以便对数据的传输进行控制,是QUIC中一个相对较新的特性,并没有被普遍支持

RFC要求即便在支持QUIC的平台中,即便用户没有禁用Spin Bit,每16个连接中也必须要有一个连接禁用Spin Bit。禁用Spin Bit后该位的值通常是随机的

Spin Bit只有在连接建立后的1-RTT数据包中起作用,双方初始值都为0(包括更改Connection ID以后)。服务器、客户端都会在内存中维护一个Spin Bit值,在接收到对方发来的1-RTT数据包后首先会对内存中的Spin Bit值进行更新,随后发送的数据包都使用该值

Spin Bit的使用需要考虑到数据包乱序的问题

当服务器接收到客户端的数据包后,如果发现Packet Number是新的,那么就会将自己的Spin Bit值设为接收到的Spin Bit

当客户端接收到服务器的数据包后,如果发现Packet Number是新的,那么就会将接收到的Spin Bit值取反并设置到当前的Spin Bit

通过以上操作,我们简单画图便可知,取一个方向的数据包,只要匹配最近的一对01,这个时间间隔大致就是RTT。实际应用中会使用更复杂的算法根据Spin Bit来评估网络性能

Stateless Reset

5.6.15

Stateless Reset数据包是一种特殊的数据包,它的结构如下,末尾包含了16字节的Stateless Reset Token,而Unpredictable Bits中包含的是无意义的随机数据

Stateless Reset {
  Fixed Bits (2) = 1,
  Unpredictable Bits (38..),
  Stateless Reset Token (128),
}

5.6.4 QUIC数据包:帧

QUIC数据包中Packet Payload由帧Frame构成。帧的类型有许多,使用1字节的Type指示其类型,以下先列表后依次讲解

可以使用的数据包中,I表示InitialH表示Handshake0表示0-RTT1表示1-RTT

备注中N表示仅包含该类型数据帧的数据包可以不回复ACK(注意不一定是不回复,是否对该数据包回复ACK以及如何回复视具体上下文而定,有些需要和其他包一起连带ACK。而例如单独的CONNECTION_CLOSE就可以不回复ACK

C表示仅包含该类型数据帧的数据包不参与流量统计与阻塞控制

P表示仅包含该类型数据帧的数据包是连接迁移时使用的probing数据包

F表示该数据帧搭载的数据需要参与流控制

由于Type采用可变长编码,Type需要采用最短的编码,否则可能触发PROTOCOL_VIOLATION

名称 Type 作用 可以使用的数据包 备注
PADDING 0x00 占位符 IH01 NP
PING 0x01 用于测试对方是否仍然可达,可以用于保持连接防止连接超时。接收方只需回复ACK即可 IH01
ACK 0x02 0x03 接收方向发送方表示数据包已经成功接收并处理(仅限于有Packet Number的) IH_1 NC
RESET_STREAM 0x04 通常由发送方发送,用于重置一个指定的数据流(在双向数据流中只能重置一个方向的数据流 __01
STOP_SENDING 0x05 用于主动向发送方表示停止发送数据,当前接收方无法接收数据。接收到STOP_SENDING的数据发送方需要停止发送数据并回复一个RESET_STREAM __01
CRYPTO 0x06 用于搭载加密握手信息 IH_1
NEW_TOKEN 0x07 由服务器发送往客户端的一个Token,供之后使用 ___1
STREAM 0x08..0x0f 主要的上层数据承载媒体 __01 F
MAX_DATA 0x10 全局流控制,向对方表示总的数据允许接收量 __01
MAX_STREAM_DATA 0x11 单个数据流流控制,向对方表示指定数据流的允许接收量 __01
MAX_STREAMS 0x12 0x13 流数量控制,用于向对方表示当前连接中允许的累计数据流数量(注意不是允许的并行数据流数量) __01
DATA_BLOCKED 0x14 全局流控制,发送方主动通知接收方想要发送更多数据,但被MAX_DATA限制,希望调整参数 __01
STREAM_DATA_BLOCKED 0x15 单个数据流流控制,发送方通知由于MAX_STREAM_DATA指定数据流被限制 __01
STREAMS_BLOCKED 0x16 0x17 流数量控制,发送方通知接收方由于MAX_STREAMS限制无法新建数据流 __01
NEW_CONNECTION_ID 0x18 用于声明本机的新连接ID(连接ID缩写为CID) __01 P
RETIRE_CONNECTION_ID 0x19 本机主动请求对方淘汰对方提供的CID(本机发送数据包的Destination Connection ID __01
PATH_CHALLENGE 0x1a 用于测试网路是否连通(在连接迁移Connection Migration中也会用到) __01 P
PATH_RESPONSE 0x1b 用于回复PATH_CHALLENGE ___1 P
CONNECTION_CLOSE 0x1c 0x1d 连接关闭 IH01IH仅限于0x1c0x1d由于是应用触发的连接关闭所以不能用于InitialHandshake N
HANDSHAKE_DONE 0x1e 服务器客户端表示握手成功 ___1
Extension 其他扩展类型

PADDING帧

PADDING Frame {
  Type (i) = 0x00,
}

PING帧

PING Frame {
  Type (i) = 0x01,
}

ACK帧

QUIC在处理完对方发来的数据包并提交到应用接收缓冲区时就可以对该数据包回复ACK

ACK的是对方数据包的序号。ACK帧有两种Type分别为0x020x03。在使能QUICECN特性后(可用于控制阻塞状态)需要使用0x03类型的ACK回复,其中相比0x02类型的数据包多出了当前已经接收到相应ECN mark的数据包的累计数量

ACK中可以包含一个或多个ACK range,和TCPSACK特性有点类似,不同点是QUICACK的作用效果是不可逆的。同时为了限制ACK占用的数据流量,QUIC要求将ACK控制在有限长度范围,并在必要时可以省略一些有用的ACK range,代价是更多的多余重传(Spurious Retransmission)

由于QUIC中一个连接会有多个数据包编号空间,不同的数据包可能会有相同的编号。同一个编号空间的数据包只能包含本编号空间对应的ACK,例如Initial只能对Initial进行ACK0-RTT不能包含ACK0-RTT必须由服务器端使用1-RTT数据包进行ACK。考虑到服务器回复的HandshakeInitial可能丢失,这会形成一定的限制

ACK Frame {
  Type (i) = 0x02..0x03,
  Largest Acknowledged (i),
  ACK Delay (i),
  ACK Range Count (i),
  First ACK Range (i),
  ACK Range (..) ...,
  [ECN Counts (..)],
}

Largest Acknowledged中的(i)表示前文所述的可变长整数编码(下同),但是不去除前导0。该值表示当前被ACK的最大数据包序号

ACK Delay表示接收方接收到数据包到回复ACK之间的这段时间,可以用于更精准地估计实际的RTT。该域需要在解码得到实际整数后,向左移动ack_delay_exponent位(该值在本机先前的TLS握手中定义,是一个transport parameter)。如果不使用到ACK Delay通常认为没有延迟

ACK Range Count表示后面ACK Range列表数据项的数量

First ACK Range可以用于计算当前被ACK的最小数据包序号,将Largest Acknowledged减去该值就是最小序号

ACK RangeECN Counts格式见下

每一个ACK Range定义如下

ACK Range {
  Gap (i),
  ACK Range Length (i),
}

TCPSACK类似的,ACK Range也采用由近及远的排列方式

ACK Range Length表示Largest Acknowledged之前已经被ACK的连续数据包数量(从Largest Acknowledged之前一个开始算第1个),那么该连续域中最小的数据包序号为Largest Acknowledged - ACK Range Length

Gap + 1表示该连续区间之前未被ACK的数据包数量,同样从最小序号数据包之前一个开始算起

上图中,ACK Range 2Acked域最大序号为Largest Acknowledged - ACK Range Length - Gap - 2

如果QUIC发现对方ACK了一个自己没有发送过的数据包,必须触发PROTOCOL_VIOLATION

ECN Counts只有在0x03类型的数据包中会有,定义如下

ECN Counts {
  ECT0 Count (i),
  ECT1 Count (i),
  ECN-CE Count (i),
}

以上三个值分别表示已经接收到ECT0 ECT1 ECN-CE codepoint的数据包数量

RESET_STREAM帧

RESET_STREAM Frame {
  Type (i) = 0x04,
  Stream ID (i),
  Application Protocol Error Code (i),
  Final Size (i),
}

RESET_STREAM由数据发送方发送,此后发送方将不会继续重传对应数据流的数据。而接收方在接收到该帧后需要将对应数据流已经接收到的数据全部丢弃

双向数据流中该帧只能重置由发送方到接收方的单向数据流,反方向的数据流不受影响

Stream ID指定想要终止的数据流ID

Error Code为错误码,指明发生重置的原因,见5.6.5

Final Size表示该数据流到重置为止传输的字节数

STOP_SENDING帧

STOP_SENDING Frame {
  Type (i) = 0x05,
  Stream ID (i),
  Application Protocol Error Code (i),
}

STOP_SENDING通常由数据接收方在一个数据流处于RecvSize Known状态下发送,要求停止发送,通常是因为不再想要该数据流已经接收到的数据

如果接收到STOP_SENDING的数据发送方处于SendReady状态,它必须回复一个RESET_STREAM。如果此时发送方已经处于Data Sent状态,它可以推迟发送RESET_STREAM直到知晓已发送数据包的下落(接收到对应ACK或丢失)

通常RESET_STREAM中的错误码需要和STOP_SENDING中的一致(也可以不同)

关闭双向数据流时,通信一方可以将RESET_STREAMSTOP_SENDING一并发送,使对方也停止发送并回复一个RESET_STREAM

CRYPTO帧

CRYPTO Frame {
  Type (i) = 0x06,
  Offset (i),
  Length (i),
  Crypto Data (..),
}

加密握手数据也是一个数据流。CRYPTO可能出现在0-RTT以外的所有数据包类型中。作为加密握手数据的载体,它和STREAM帧的区别是没有流控制机制,也没有Stream ID

Offset表示当前CRYPTO帧搭载的数据在整个加密握手数据流中的偏移

Length表示搭载数据的长度

Crypto Data为握手加密相关数据

不同的加密阶段会使用单独的数据流进行握手信息的传输。这些单独的数据流的Offset都从0开始

NEW_TOKEN帧

NEW_TOKEN Frame {
  Type (i) = 0x07,
  Token Length (i),
  Token (..),
}

NEW_TOKEN只能由服务器生成并发送往客户端。客户端在之后的Initial数据包中需要包含该Token

Token Length表示令牌数据的字节长度

STREAM帧

STREAM Frame {
  Type (i) = 0x08..0x0f,
  Stream ID (i),
  [Offset (i)],
  [Length (i)],
  Stream Data (..),
}

由于显而易见的限制,QUIC中一个数据流传输的数据长度不可能超过2^62-1

服务器和客户端之间的应用数据(QUIC上层的数据)主要就是通过STREAM帧搭载

STREAM中有可选的OffsetLength域。STREAM一共占用8Type0b00001xxx),其中Type的低3bit依次为OFF LEN FIN,置位时分别表示Offset域存在,Length域存在,以及数据流的结束

FIN1时,数据流最终传输的数据长度为该帧中Offset + Length + 1

Offset0开始算,表示该帧搭载的数据在当前数据流累计数据中的偏移。如果OFF0,那么Offset不存在,为0,可以表示该帧搭载了数据流的开头表示数据流的结束

Length表示之后Stream Data的长度。如果LEN0,那么Length不存在,Length之后原本需要搭载数据的Stream Data需要自动延伸至满足数据包大小要求;此时Offset的值可能为该数据流中下一个帧在数据流中的偏移

Stream Data搭载了该数据流的数据

MAX_DATA帧

MAX_DATA Frame {
  Type (i) = 0x10,
  Maximum Data (i),
}

全局流控制即所有数据流的总和。发送方发送数据时必须保证符合最大数据限制,否则接收方会立即终止连接(FLOW_CONTROL_ERROR

MAX_STREAM_DATA帧

MAX_STREAM_DATA Frame {
  Type (i) = 0x11,
  Stream ID (i),
  Maximum Stream Data (i),
}

该帧只能在对应数据流处于Recv状态时发送。如果一个数据流是receive-only的,如果接收到对方发来对应该数据流的MAX_STREAM_DATA,说明对方的数据流配置存在错误,需要立即终止连接(STREAM_STATE_ERROR)。如果MAX_STREAM_DATA对应数据流仅仅初始化还未创建,需要触发错误(STREAM_STATE_ERROR

类似的,如果接收到的数据超出该数据流的限制,接收方需要关闭连接并触发FLOW_CONTROL_ERROR

通常接收数据的计数和溢出检测需要依赖Largest Received(和ACK帧中的Largest Acknowledged有点类似,区别是该变量在内存中维护。一个数据流的收发双方都需要分别维护这样的一个变量)

MAX_STREAM_DATA和下面的MAX_STREAMS由于只能增加不能减小,这两种数据帧发送次数不能过多

MAX_STREAMS帧

MAX_STREAMS Frame {
  Type (i) = 0x12..0x13,
  Maximum Streams (i),
}

指定当前连接生命周期中允许的累计数据流数量。Type0x12指定双向数据流数量,0x13指定单向数据流数量。由于QUIC中有4种数据流,MAX_STREAMS表示的是一种数据流不能超过的数量

通常认为一个连接的生命周期内单一数据流数量不能超过2^60。如果超过,接收方触发FRAME_ENCODING_ERROR

流控制机制可能在数据传输的过程中更新MAX_STREAMS限制。但是最大数据流数量不能减小,否则难以决定抛弃哪些数据流

DATA_BLOCKED帧

DATA_BLOCKED Frame {
  Type (i) = 0x14,
  Maximum Data (i),
}

Maximum Data表示当前引起阻塞的全局数据量限制

STREAM_DATA_BLOCKED帧

STREAM_DATA_BLOCKED Frame {
  Type (i) = 0x15,
  Stream ID (i),
  Maximum Stream Data (i),
}

Stream ID为指定流ID。如果一个数据流是send-only的,却接收到了对应的STREAM_DATA_BLOCKED,说明对方的数据流配置存在错误,需要立即终止连接(STREAM_STATE_ERROR

STREAMS_BLOCKED帧

STREAMS_BLOCKED Frame {
  Type (i) = 0x16..0x17,
  Maximum Streams (i),
}

Type0x16用于双向数据流0x17用于单向数据流Maximum Streams为当前引发限制的值,不可能超过2^60

NEW_CONNECTION_ID帧

NEW_CONNECTION_ID Frame {
  Type (i) = 0x18,
  Sequence Number (i),
  Retire Prior To (i),
  Length (8),
  Connection ID (8..160),
  Stateless Reset Token (128),
}

通信时双方的CID都需要通过协商才能确定,双方都会向对方提供自己可用的CID,作为对方发送数据包时的DCID

由于NEW_CONNECTION_ID的存在,如果一台主机想要更改CID,它无需再进行一次完整的握手。NEW_CONNECTION_ID用于Connection Migration功能中,这可能发生在网络IP更改(这在移动端经常发生)或主动的网路切换时,可以防止被潜在的监视者跟踪

Sequence Number为新指定CID的序号(QUIC中每个连接ID都有对应的序号,用于检测CID的更新)

Retire Prior To取的是一个Sequence Number值,表示淘汰小于等于Sequence Number的CID

Length1字节,表示CID的长度,可以取120

Stateless Reset Token表示该CID对应的Reset Token

QUIC中由于可以使用0长度CID,此时不可更新CID,本机在使用0长度CID时不能发送NEW_CONNECTION_ID,否则触发PROTOCOL_VIOLATION

同一个新CID由于丢包超时等原因可能被发送多次,如果这些NEW_CONNECTION_ID帧中Sequence NumberStateless Reset Token不同,也可以触发PROTOCOL_VIOLATION

RETIRE_CONNECTION_ID帧

RETIRE_CONNECTION_ID Frame {
  Type (i) = 0x19,
  Sequence Number (i),
}

关于QUIC中连接更新淘汰的机制可以见5.6.9

Sequence Number指定的是淘汰的单个CID对应序号,不能和当前数据包的Destination Connection ID相等,否则接收方触发PROTOCOL_VIOLATION错误

和前文类似的,如果对方此时使用0长度的CID,那么本机就不能发送RETIRE_CONNECTION_ID请求

由于数据包传输乱序的存在,本机接收到的NEW_CONNECTION_ID新CID的序号有可能已经淘汰,此时接收方需要主动发送一个RETIRE_CONNECTION_ID来请求对方淘汰该CID

PATH_CHALLENGE帧

PATH_CHALLENGE Frame {
  Type (i) = 0x1a,
  Data (64),
}

测试对方是否可达,或用于连接迁移时验证网络通畅

Data8字节任意数据

PATH_RESPONSE帧

PATH_RESPONSE Frame {
  Type (i) = 0x1b,
  Data (64),
}

接收到PATH_CHALLENGE一方必须使用PATH_RESPONSE进行回复,其中的Data需要和先前PATH_CHALLENGE的相同

CONNECTION_CLOSE帧

CONNECTION_CLOSE Frame {
  Type (i) = 0x1c..0x1d,
  Error Code (i),
  [Frame Type (i)],
  Reason Phrase Length (i),
  Reason Phrase (..),
}

Type0x1c时仅仅终止传输层的QUIC,而0x1d支持向上层应用递交Error Code(应用层的Error CodeQUIC的错误码不同,由应用层定义)

0x1d类型的CONNECTION_CLOSE只能用于0-RTT1-RTT数据包;而如果在握手过程中想要关闭连接,可以在HandshakeInitial数据包中发送0x1c类型的CONNECTION_CLOSEError CodeAPPLICATION_ERROR

一个QUIC连接关闭时,先前未显式关闭的数据流也随之关闭

Error Code各取值的定义见后一小节

Frame Type只有Type0x1c的数据帧才有,表示触发错误的数据帧类型。如果类型未知那么Frame Type0

Reason Phrase Length表示后面Reason Phrase的长度。如果不想提供详细信息那么可以为0

Reason Phrase是一个UTF-8编码的字符串,提供导致连接关闭的提示性信息

HANDSHAKE_DONE帧

HANDSHAKE_DONE Frame {
  Type (i) = 0x1e,
}

HANDSHAKE_DONE只能由服务器发送

5.6.5 错误码

QUIC中错误码有两种:一种是Transport Error Codes,它用于0x1c类型的CONNECTION_CLOSE中,只表示QUIC传输层的错误。另一种是Application Protocol Error Codes,它用于RESET_STREAM STOP_SENDING0x1d类型的CONNECTION_CLOSE中,可以向上层应用提供错误信息

Transport Error Codes为62位无符号整数,定义如下

名称 定义
NO_ERROR 0x00 连接正常关闭,没有错误
INTERNAL_ERROR 0x01 其他内部错误
CONNECTION_REFUSED 0x02 服务器拒绝连接
FLOW_CONTROL_ERROR 0x03 流控制错误,超出了MAX_DATAMAX_STREAM_DATA限制
STREAM_LIMIT_ERROR 0x04 超出了MAX_STREAMS限制
STREAM_STATE_ERROR 0x05 收到了数据流在指定状态下不可能接收到的对应数据帧。例如前文中收到了对应本机receive-only数据流或初始化数据流的MAX_STREAM_DATA;或收到了对应本机send-only数据流的STREAM_DATA_BLOCKED
FINAL_SIZE_ERROR 0x06 最终的流数据统计发现错误。只会在接收到STREAM帧或RESET_STREAM帧后触发。RESET_STREAM会携带一个Final Size,而STREAM如果FIN置位同样可以计算出最终接收到的数据字节数,多发生在丢包严重时。例如(1)数据流在确定Final Size后接收到了超出该数量的数据(2)最终计算Final Size时发现数据超出(3)Final Size确定后又接收到同一数据流的Final Size,并且与已有的不相符
FRAME_ENCODING_ERROR 0x07 帧格式错误(例如帧Type未知,或ACK帧中Range过多等)
TRANSPORT_PARAMETER_ERROR 0x08 握手时transport parameter参数错误,包括非法参数,无效参数等
CONNECTION_ID_LIMIT_ERROR 0x09 对方提供的连接ID过多,超出了active_connection_id_limit
PROTOCOL_VIOLATION 0x0a 其他各类未归类的特殊错误
INVALID_TOKEN 0x0b 客户端发来的InitialToken无效
APPLICATION_ERROR 0x0c 表示上层应用决定了关闭连接
CRYPTO_BUFFER_EXCEEDED 0x0d CRYPTO帧过大,超出接收缓存
KEY_UPDATE_ERROR 0x0e 更新密钥时发生错误
AEAD_LIMIT_REACHED 0x0f 达到AEAD算法限制(AEADAuthenticated Encryption with Associated Data,起加密、签名与验证的作用)
NO_VIABLE_PATH 0x10 主机判定网路不支持QUIC。很少出现,除非网络MTU过小
CRYPTO_ERROR 0x0100-0x01ff 加密握手(TLS握手)失败,一共有256个可用值

错误处理

QUIC要求在数据传输时发生错误,只要连接状态还在,就需要尽量发送错误码(无论Transport Error Codes还是Application Protocol Error Codes)向对方提供有用信息。如果无法对错误类型进行定性,可以发送PROTOCOL_VIOLATIONINTERNAL_ERROR代替

在连接状态还未销毁时,只能使用CONNECTION_CLOSE关闭一个连接,而这个数据包可能丢失。因此主机在发现关闭连接后如果对方还在发送数据包,需要再发送CONNECTION_CLOSE

QUIC允许接收方丢弃Initial

QUIC的数据流控制需要由上层应用支持,数据流的传输发生错误也需要由上层应用发起RESET_STREAM(通常也需要伴随STOP_SENDING使用)。一个QUIC实现需要为上层应用提供该功能的接口

5.6.6 数据流ID

之前已经提到过一个QUIC连接中可以有多个并行的数据流,每一个搭载上层数据的STREAM帧都会包含一个Stream ID。一个QUIC数据包可以同时包含多个数据流的STREAM帧,而一个UDP数据包可能包含多个QUIC数据包。一个数据流可能需要多次传输才能传输完成

数据流的创建

双方连接建立后,一个数据流直接通过发送对应流ID的帧就已经表示创建。该数据包通常为短数据头类型,STREAM帧中包含的Stream ID就是新流ID。STREAM帧可以表示创建数据流,搭载数据,或终结一个数据流(FIN1

流ID表示的数据流类型

QUIC数据流分为单向(Unidirectional)和双向(Bidirectional)两种。单向数据流只能从数据流的发起方发往接收方;而双向数据流支持双向传输(即双方收发时,使用同一个Stream ID

QUIC中数据流ID采用前文所述的变长编码,不同的数据流必须采用不同的流ID,可取值02^62-1。ID的LSB为0表示该数据流由客户端发起并创建,为1表示服务端发起;2ndLSB为0表示双向流,为1表示单向流。定义如下

    +======+==================================+
    | Bits | Stream Type                      |
    +======+==================================+
    | 0x00 | Client-Initiated, Bidirectional  |
    +------+----------------------------------+
    | 0x01 | Server-Initiated, Bidirectional  |
    +------+----------------------------------+
    | 0x02 | Client-Initiated, Unidirectional |
    +------+----------------------------------+
    | 0x03 | Server-Initiated, Unidirectional |
    +------+----------------------------------+

            Table 1: Stream ID Types

同一类型数据流ID按先后顺序严格递增。例如服务器发起的双向流ID为0x01,那么服务器发起的下一个双向流的ID为0x05。如果数据包传输发生了乱序,例如0x09先于0x05到达,那么此时0x05数据流其实也已经开启了

STREAM帧中会包含其搭载数据的OffsetLength。一个数据流在传输过程中同一处Offset的数据可能会被传输多次,但接收方不得更改已经接收过的数据,只能更新新到的数据

API实现功能

QUIC要求API至少实现以下数据流功能:

数据流发送:写数据,并可检查数据是否已发送;终结数据流,发送FIN置位的STREAM;数据流重置,发送RESET_STREAM

数据流接收:读数据;停止读数据,发送STOP_SENDING

5.6.7 数据流状态机

            o
            | Create Stream (Sending)
            | Peer Creates Bidirectional Stream
            v
        +-------+
        | Ready | Send RESET_STREAM
        |       |-----------------------.
        +-------+                       |
            |                           |
            | Send STREAM /             |
            |      STREAM_DATA_BLOCKED  |
            v                           |
        +-------+                       |
        | Send  | Send RESET_STREAM     |
        |       |---------------------->|
        +-------+                       |
            |                           |
            | Send STREAM + FIN         |
            v                           v
        +-------+                   +-------+
        | Data  | Send RESET_STREAM | Reset |
        | Sent  |------------------>| Sent  |
        +-------+                   +-------+
            |                           |
            | Recv All ACKs             | Recv ACK
            v                           v
        +-------+                   +-------+
        | Data  |                   | Reset |
        | Recvd |                   | Recvd |
        +-------+                   +-------+
        Figure 2: States for Sending Parts of Streams



            o
            | Recv STREAM / STREAM_DATA_BLOCKED / RESET_STREAM
            | Create Bidirectional Stream (Sending)
            | Recv MAX_STREAM_DATA / STOP_SENDING (Bidirectional)
            | Create Higher-Numbered Stream
            v
        +-------+
        | Recv  | Recv RESET_STREAM
        |       |-----------------------.
        +-------+                       |
            |                           |
            | Recv STREAM + FIN         |
            v                           |
        +-------+                       |
        | Size  | Recv RESET_STREAM     |
        | Known |---------------------->|
        +-------+                       |
            |                           |
            | Recv All Data             |
            v                           v
        +-------+ Recv RESET_STREAM +-------+
        | Data  |--- (optional) --->| Reset |
        | Recvd |  Recv All Data    | Recvd |
        +-------+<-- (optional) ----+-------+
            |                           |
            | App Read All Data         | App Read Reset
            v                           v
        +-------+                   +-------+
        | Data  |                   | Reset |
        | Read  |                   | Read  |
        +-------+                   +-------+
        Figure 3: States for Receiving Parts of Streams

单向流双方各只需一个状态机,而双向流双方各需要一对发送状态机和接收状态机

在一个数据流的传输中,发送方能发送STREAM STREAM_DATA_BLOCKED RESET_STREAM共3种数据流相关的Frame,这些帧的定义见5.6.4。在结束状态下(Data RecvdReset Recvd)不得发送这些数据包。在Reset Sent状态下不得发送STREAMSTREAM_DATA_BLOCKED

接收方能回复MAX_STREAM_DATA STOP_SENDING2种Frame。其中MAX_STREAM_DATA只能在Recv状态下发送,而STOP_SENDING通常只在RecvSize Known状态下发送。发送STOP_SENDING时通常意味着接收方不再想要该数据流中接收到的数据,这种情况下发送方如果处于SendReady状态下就必须回复RESET_STREAM,其中包含了先前STOP_SENDING中的错误码

对于接收方来说,一个数据流中传输的数据总和在Size KnownReset Recvd状态下就已经知道了。不管最终数据流是以STREAM帧还是RESET_STREAM帧结束的,如果最终对比发现数据包表示的数据量和实际接收到的数据量不同,接收方需要发送FINAL_SIZE_ERROR错误(通常伴随着连接的关闭,因此FINAL_SIZE_ERROR通常搭载于CONNECTION_CLOSE帧)。这是QUIC的安全特性之一,用于防范off-by-one攻击

以下为收发双方在各状态下的系统状态表

      +===================+=======================+=================+
      | Sending Part      | Receiving Part        | Composite State |
      +===================+=======================+=================+
      | No Stream / Ready | No Stream / Recv (*1) | idle            |
      +-------------------+-----------------------+-----------------+
      | Ready / Send /    | Recv / Size Known     | open            |
      | Data Sent         |                       |                 |
      +-------------------+-----------------------+-----------------+
      | Ready / Send /    | Data Recvd / Data     | half-closed     |
      | Data Sent         | Read                  | (remote)        |
      +-------------------+-----------------------+-----------------+
      | Ready / Send /    | Reset Recvd / Reset   | half-closed     |
      | Data Sent         | Read                  | (remote)        |
      +-------------------+-----------------------+-----------------+
      | Data Recvd        | Recv / Size Known     | half-closed     |
      |                   |                       | (local)         |
      +-------------------+-----------------------+-----------------+
      | Reset Sent /      | Recv / Size Known     | half-closed     |
      | Reset Recvd       |                       | (local)         |
      +-------------------+-----------------------+-----------------+
      | Reset Sent /      | Data Recvd / Data     | closed          |
      | Reset Recvd       | Read                  |                 |
      +-------------------+-----------------------+-----------------+
      | Reset Sent /      | Reset Recvd / Reset   | closed          |
      | Reset Recvd       | Read                  |                 |
      +-------------------+-----------------------+-----------------+
      | Data Recvd        | Data Recvd / Data     | closed          |
      |                   | Read                  |                 |
      +-------------------+-----------------------+-----------------+
      | Data Recvd        | Reset Recvd / Reset   | closed          |
      |                   | Read                  |                 |
      +-------------------+-----------------------+-----------------+

5.6.8 数据流控制

QUIC使用并行数据流的形式,流控制相较TCP要更复杂,需要分别从单个数据流以及单个连接(也就是全局)进行流控制,分别为Stream flow controlConnection flow control

QUIC握手时,通信双方会对所有数据流的接纳容量进行协商(transport parameters)。后续如果需要更改为更大的接纳容量,需要通过以下方式进行通知:

单个数据流中接收方使用MAX_STREAM_DATA帧来表明对应流的接纳容量,该帧包含了最大的byte offset

单个连接中通信一方使用MAX_DATA帧来表明所有流的接纳容量

单个数据流单个连接中如果有任意一个超出接纳限制(可能是发送方未检测到接收缓冲满或乱序、丢包等原因),接收方需要立刻关闭连接,并触发FLOW_CONTROL_ERROR错误

相对应的,如果发送方由于流控制算法导致单个数据流连接发生阻塞,那么需要发送STREAM_DATA_BLOCKEDDATA_BLOCKED帧来向接收方表明这种情况。这种情况下接收方需要根据RTT自动发送MAX_STREAM_DATAMAX_DATA表示增大接收容量,使得发送方不至于被阻塞(原理和TCP的窗口调节机制相同)

但是当接收方收到STREAM_DATA_BLOCKEDDATA_BLOCKED时,往往意味着此时再增大缓冲已经晚了。所以接收方需要根据实际情况,主动提前增大接收缓冲并告知发送方

此外还需要控制一个连接中数据流的数量,类似地:

QUIC握手时通信双方就会通过transport parameters告知对方自己的最大累计数据流数量

而在数据传输时允许的累计数据流数量通过MAX_STREAMS帧规定。由于Stream ID的最低两位表示本数据流类型MAX_STREAMS表示的是每个数据流类型中的最大累计数据流数量。只有Stream ID小于(max_streams << 2) + first_stream_id_of_type的数据流才被允许(也就是说max_streams不能大于2^60

如果接收方发现累计数据流数量大于其设定,需要触发STREAM_LIMIT_ERROR错误。如果发送方由于限制无法建立新的数据流,那么需要发送STREAMS_BLOCKED

注意,CRYPTO帧不参与QUIC的流控制机制

5.6.9 QUIC连接

建立一个QUIC连接首先需要进行握手,再使用TLS协商公共对称密钥等安全传输必要的参数,并协商上层应用。同时,QUIC中引入了0-RTT传输机制,使用0-RTT数据包可以在未进行握手或握手未完成时就进行数据的传输(利用之前连接中缓存的加密密钥等参数)。这种机制适当牺牲安全性,提高了网络应用的响应性能,而传统的TCP TLS协议栈只能在繁杂的握手完成以后开始数据的传输

一个UDP端口需要同时支持许多个QUIC连接,一个UDP数据包可以搭载多个QUIC数据包。而一个QUIC连接可以有多个CID,一个CID可以同时用于传输许多个并行数据流

连接ID基本概念

QUIC中使用连接ID(CID)来唯一标识一个连接,一个连接的生命周期中可以使用多个CID。由于UDP是无状态协议,UDP的同一个端口可以用于不同的逻辑数据连接,但是在系统层面使用同一个socket。QUIC的CID相当于在端口机制上又加了一层,用于区分不同的连接。同时由于移动设备会频繁更改IP地址,网路会随之更改,此时服务器发送数据的目标IP也必须随之更改,QUIC中的CID可以为连接的迁移提供支持,防止数据被发送到错误的地方

每一个CID还对应一个序号见前NEW_CONNECTION_IDRETIRE_CONNECTION_ID),发放CID时对应的序号以1递增

通信双方会通过NEW_CONNECTION_ID帧互相提供可用的CID,发送方必须使用接收方提供的CID作为目标ID。同时为了防范潜在的监视行为,QUIC使用了变更CID的方法来提高跟踪难度,同时在双方的通信过程中绝对禁止两个不同的连接使用相同的CID

回顾:QUIC长数据头中包含了目标连接ID和源连接ID;而短数据头通常只包含了目标连接ID

由服务器发往客户端的Version Negotiation数据包会使用客户端提供的CID,用于确认网络可达,同时服务器可以向客户端证明该数据包是针对客户端请求的回复

CID长度可以为0。但是同为0长度CID的连接不允许用于同一IP和端口

数据传输时连接ID的分配

在连接建立时,初始的CID包含在握手过程的长数据头中,该CID对应序号为0(服务器的transport parameter中有preferred_address参数时除外,从1开始)。具体描述见握手

连接建立之后的CID分配使用NEW_CONNECTION_ID以及RETIRE_CONNECTION_ID帧处理,每个新CID序号+1。所有已经分配但还未Retire的CID都是active的,在此期间接收方必须接收携带该CID的数据包

本机对于对方提供的可用的CID数量是有限制的。本机在transport parameter中可以包含active_connection_id_limit表明本地允许维护的CID数量,对方不能提供超过该数量的CID。如果对方提供的CID超出了本机的限制,本机必须关闭连接,错误码CONNECTION_ID_LIMIT_ERROR

发起连接迁移时,本机也需要确保提供给对方的CID充足

使用0长度ID时在后续通信中无法再分配新CID

数据传输时连接ID的消耗与淘汰

QUIC中的CID可能会在任何时候更改,并且不再使用时需要及时淘汰

QUIC的CID都需要本机向对方提供,通信双方(客户端和服务器)各自需要接收并维护对方提供的CID,并作为发送数据包时的DCID使用

CID的更新淘汰有两种机制,通信双方都可以导致CID的更新,分别由CID提供方发起或CID维护方发起。两种机制都会使用NEW_CONNECTION_IDRETIRE_CONNECTION_ID

当CID的提供方想要更新CID时,它会主动向对方(即维护方)发送一个NEW_CONNECTION_ID,其中包含了一个相比之前增大的Retire Prior To值,表示淘汰到该序号为止的所有CID。此时对方接收到NEW_CONNECTION_ID后发现Retire Prior To增大,必须依次使用RETIRE_CONNECTION_ID进行回复,并将所有指定CID淘汰。发起CID更新操作的提供方在接收到相应RETIRE_CONNECTION_ID前应该接收刚刚淘汰CID的数据包,而不是丢弃。而维护方在将新CID加入到可用CID之前必须先进行淘汰操作,防止CID超出本机限制发生CONNECTION_ID_LIMIT_ERROR,或导致无CID可用

在这种CID更新机制下,CID提供方需要保证一次更新的CID数量不会太多,否则对方需要回复的RETIRE_CONNECTION_ID就会过多。此外,在上一个NEW_CONNECTION_ID中被更新的CID未得到RETIRE_CONNECTION_ID回复之前,本机不允许再发送新的NEW_CONNECTION_ID

相反的,当CID的维护方想要更新CID时,它会向CID提供方发送一个RETIRE_CONNECTION_ID表示希望淘汰指定的CID。此时对方必须使用NEW_CONNECTION_ID回复进行CID的更新操作,并淘汰到指定CID为止的所有CID

数据包和连接的匹配

一台主机接收到QUIC数据包后,需要根据CID将其与已有的连接匹配之后才能进一步处理。此外对于服务器来说也可能是一个新连接的建立

QUIC中可以使用0长度CID,即仅仅基于来源IP和Port来建立一个连接。在接收到0长度CID的数据包以后主机可以根据源IP和Port来匹配一个连接。但是0长度CID连接有较多缺陷,尽量避免使用

对于客户端来说,如果接收到的数据包无法和任何一个已有的连接匹配(例如丢包等原因导致两边连接状态不一致,对方未及时销毁连接),本机需要丢弃这些数据包,同时回复一个Stateless Reset数据包(见前),对方接收到Stateless Reset后需要立即销毁对应连接

数据包乱序可能导致本机接收到后续数据包时密钥还没计算出来。遇到这种数据包通常直接丢弃或缓存

接收到的数据包中如果QUIC版本和当前设定不一致也必须丢弃

长数据头中包含了版本Version

对于服务器来说,如果不支持客户端发来的Initial数据包中包含的版本,无法区分版本,服务器需要发送一个Version Negotiation来协商版本

如果对方(客户端)发来的Initial包符合要求,服务器需要使用Handshake继续握手过程

如果服务器想要立即关闭连接,需要发送一个包含CONNECTION_CLOSE帧以及CONNECTION_REFUSED错误码的Initial数据包

服务器接收到客户端发来的0-RTT以后可以进行缓存。在服务器没有对客户端的请求进行回应(Handshake)之前客户端不得发送Handshake

API实现功能

QUIC要求API至少实现以下数据流功能:

客户端:打开连接并进行握手;提供0-RTT相关功能;知晓0-RTT数据包是否被成功接收

服务器:监听端口,检测新连接;提供0-RTT相关功能;通知客户端0-RTT是否成功接收

共通的功能:在transport parameter中配置流控制参数;判断握手状态;使用PING Frame或要求发送数据的方法防止超时连接关闭;使用CONNECTION_CLOSE关闭连接

5.6.10 版本协商

服务器对于会初始化QUIC连接的数据包可能会回复一个Version Negotiation数据包

一个QUIC连接由客户端发送Initial数据包发起;Initial数据包中会有QUIC的版本号Version以及作为Payload的帧。各个QUIC版本要求的最小数据包大小可能不同,客户端需要取不同版本之间的最大值。客户端如果支持多个QUIC版本,它需要在Initial数据包中添加PADDING Frame来满足要求

服务器对于客户端发来的Initial数据包,如果发现自己不支持客户端使用的QUIC版本,服务器必须发送Version Negotiation数据包,向客户端提供可用的QUIC版本列表。客户端收到后绝对不得使用Version Negotiation进行回复

在客户端的Initial数据包到来之前,服务器可能先接收到了0-RTT数据包,此时无需发送Version Negotiation,可以等到收到Initial数据包以后再发送。在此期间的0-RTT数据包先缓存

当前版本中,如果客户端收到的Version Negotiation数据包包含了当前使用的版本,或者已经成功处理了先前的Version Negotiation,客户端应当忽略该Version Negotiation数据包。

在其余情况下,客户端必须停止当前的连接,并更换版本重新发送连接请求

5.6.11 QUIC握手

QUIC握手同样是我们最为感兴趣的过程。QUIC握手相比TCP TLS来说所需RTT少很多

QUICCRYPTO可能用于多个独立的数据包序列空间,各自使用独立的Offset

QUIC握手需要确定以下内容:

  1. 双方密钥交换,验证身份(客户端可以不验证)。每个连接使用的密钥不同

  2. 交换transport parameters

  3. 确定应用协议(使用ALPNApplication-Layer Protocol Negotiation

握手过程总述

简化的QUIC握手过程如下

Client                                               Server

Initial (CRYPTO)
0-RTT (*)              ---------->
                                           Initial (CRYPTO)
                                         Handshake (CRYPTO)
                       <----------                1-RTT (*)
Handshake (CRYPTO)
1-RTT (*)              ---------->
                       <----------   1-RTT (HANDSHAKE_DONE)

1-RTT                  <=========>                    1-RTT

            Figure 4: Simplified QUIC Handshake

(*)的是不一定会有的数据包

QUIC在握手阶段检测对方发来的ACK帧判定对方是否支持ECNExplicit Congestion Notification)。此外在需要进行Version Negotiation的场合下,需要先协商版本后再进行握手

具体的0-RTT握手和1-RTT握手见下文

1-RTT握手

Client                                                  Server

Initial[0]: CRYPTO[CH] ->

                                 Initial[0]: CRYPTO[SH] ACK[0]
                       Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
                                 <- 1-RTT[0]: STREAM[1, "..."]

Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[0]: STREAM[0, "..."], ACK[0] ->

                                          Handshake[1]: ACK[0]
         <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0]

                Figure 5: Example 1-RTT Handshake

注意:在目前的Google QUIC中,最常见的是双方可能不会在最后向对方发送InitialHandshake数据包对应的ACK(即没有上图中的Initial[1]: ACK[0]Handshake[1]: ACK[0]两个数据包),握手实际使用1.5RTT就已经完成,这在之后的抓包示例中有展现。0-RTT同理

一次发送的Initial数据包和Handshake数据包可能合并到同一个UDP数据包,也有可能会分多个数据包发送

此外,为满足网络传输对于数据包大小的要求,过小的数据包通常会使用PADDING帧进行填充。这些PADDING可能位于握手数据包,也可能位于同一个UDP数据包中单独用于占位的QUIC数据包

0-RTT握手

Client                                                  Server

Initial[0]: CRYPTO[CH]
0-RTT[0]: STREAM[0, "..."] ->

                                 Initial[0]: CRYPTO[SH] ACK[0]
                                  Handshake[0] CRYPTO[EE, FIN]
                          <- 1-RTT[0]: STREAM[1, "..."] ACK[0]

Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[1]: STREAM[0, "..."] ACK[0] ->

                                          Handshake[1]: ACK[0]
         <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1]

                Figure 6: Example 0-RTT Handshake

0-RTT握手相比1-RTT,其使用了先前已缓存的参数,服务器的回复中少了CERTCV

0-RTT中客户端直接向服务器发送0-RTT数据包(由于搭载数据,通常使用单独的UDP数据包而不是和Initial共用数据包。0-RTT可以有多个)。而之后客户端的第一个1-RTT数据包序号为1而非0

连接ID:协商

前文讲述的只是数据传输过程中CID的分配与更新。本段对握手过程中连接ID的初始化方法进行讲述

路由器可以利用QUIC数据包中的DCID来保证一致的路由(这是同一个QUIC数据连接的唯一标识)

下图展示了CID的协商过程,以及各数据包CID的关系

Client                                                  Server

Initial: DCID=S1, SCID=C1 ->
                                  <- Initial: DCID=C1, SCID=S3
                             ...
1-RTT: DCID=S3 ->
                                             <- 1-RTT: DCID=C1
        
        Figure 7: Use of Connection IDs in a Handshake

下图中多出了一步Retry

Client                                                  Server

Initial: DCID=S1, SCID=C1 ->
                                    <- Retry: DCID=C1, SCID=S2
Initial: DCID=S2, SCID=C1 ->
                                  <- Initial: DCID=C1, SCID=S3
                             ...
1-RTT: DCID=S3 ->
                                             <- 1-RTT: DCID=C1
    
    Figure 8: Use of Connection IDs in a Handshake with Retry

握手过程中的CID直接通过长数据头中的SCIDDCID进行初始化。SCID给出了对方应当使用的CID,而DCID就是当前本机发送数据包选择使用的(对方的)CID

双方发送的Initial会使用SCID给出自己的CID,而后发送数据包的DCID都需要设置为对方提供的CID,使用什么CID需要由对方回复决定

CID可以为0长度,看上去非常像是长数据头中没有给出SCID。记住此时一定会有SCID,不要误解了

发起连接时,如果客户端在向服务器发送Initial之前没有收到过服务器的InitialHandshake(即该连接为新连接,不是未建立完成的连接),客户端发送Initial时还需要设置DCID为一个至少8字节长的值,服务器使用该CID辨别本Initial数据包的保护密钥packet protection keys。此时客户端在得到服务器回复之前DCID需要一直保持该值,包括0-RTT(客户端的0-RTTInitial共用SCIDDCID

服务器回复的InitialDCID需要设置为客户端的SCID

接收到服务器回复的InitialRetry后,客户端发送的数据包的DCID才会确定(通常会更改SCID,但是数字可能变化不大,例如只更改了最高1字节),客户端必须将后续发送数据包的DCID改为服务器回复的SCID。后续的数据传输只能使用NEW_CONNECTION_ID更新CID

通信一方一次发送的多个Initial数据包的SCID需要一致。如果出现不一致的情况,接收方必须丢弃这些不一致的数据包

连接ID:验证

为了便于使用TLS查验,CID除了需要包含于数据头以外,还需要包含于TLStransport parameter扩展参数中

目前很多的QUIC实现并不完全符合该条,这些参数可能缺失

以下需要结合上文连接ID:协商理解

QUIC规定通信双方发送的第一个Initial数据包中,需要有一个参数initial_source_connection_id,其值就是该数据包的SCID;而服务器回复的第一个Initial数据包还需要包含一个original_destination_connection_id,其值就是先前客户端发来的InitialDCID,也就是先前说的最少8字节的CID。如果此时服务器想要发送Retry而不是回复Initial,除以上参数还需要包含retry_source_connection_id,其值为该Retry数据包的SCID

QUIC要求接收方校验上述参数以及对应的CID值,如果上述参数丢失或查验错误,必须触发错误(TRANSPORT_PARAMETER_ERRORPROTOCOL_VIOLATION)。这样可以防止攻击者在握手过程中注入

传输参数缓存

传输参数介绍见5.6.19

0-RTT需要依赖已经缓存的transport parameters。对于客户端来说,服务器会给它提供session tickets,客户端需要将相应的transport parameters与之关联并存储(此外还有加密参数以及ALPN相关参数),下次使用该session tickets时客户端需要加载已缓存的参数。而服务器本地也会有各个session tickets的参数,当接收到对应的0-RTT时也会加载这些参数

在握手结束后再发送1-RTT就会使用新的transport parameters,新的加密参数。旧参数的寿命至此结束

客户端本地缓存的这些transport parameters是固定的,不会受到后续例如MAX_DATA MAX_STREAM_DATA的更新影响

对于0-RTT来说,缓存有些transport parameters是没有意义的。每个transport parameters都必须说明其0-RTT缓存属性,可以是mandatory, optional, prohibited

此外,服务器不得擅自减小部分transport parameters(主要和流控制相关),以防客户端发来的0-RTT超限。这些参数有:active_connection_id_limit initial_max_data initial_max_stream_data_bidi_local initial_max_stream_data_bidi_remote initial_max_stream_data_uni initial_max_streams_bidi initial_max_streams_uni

CRYPTO缓冲

由于CRYPTO帧没有流控制,它的传输的乱序的(下面的实验中会展现)。接收CRYPTO时需要使用足够大的缓冲并排序。使用大的CRYPTO缓冲便可以容纳更多的参数,证书以及密钥等。如果超出了CRYPTO缓冲,需要立即关闭连接,触发CRYPTO_BUFFER_EXCEEDED错误

Wireshark示例

客户端:Chromium(Google QUIC示例,和IETF QUIC有所不同)

抓包如下。其中标记的就是握手相关的数据包,分别为客户端发送的Initial,服务端回复的InitialHandshake,以及客户端回复的Handshake

显然这是一次0-RTT。首先由客户端发起请求,发送的UDP数据包中仅有一个Initial数据包。此时使能了0-RTT,有额外的0-RTT数据包(使用另外的UDP搭载,独立的数据包序号空间)。Initial包含了多个CRYPTO帧以及多个PING帧,乱序传输。观察数据头如下

这里客户端使用了0长度SCID,并且将DCID设置为8字节长度的值。我们使用的QUIC版本为0x00000001。继续往下看

所有的CRYPTO按顺序拼接以后才会得到完整的CRYPTO,打开最后一个CRYPTO才会看到。其内含数据格式全部由TLS定义(明文的Client Hello),具体需要见下一章。TLS除握手类型、长度、版本、会话ID、随机数、加密套件等基本参数以外,还包含了许多必要的扩展(TLS Extensions,格式和前文所述相同),尤其是transport parameters扩展中明文搭载的许多关键参数

之后服务器回复的UDP数据包中会有两个长数据头类型的QUIC数据包,分别为Initial(明文的Server Hello)以及Handshake;此外还有一个1-RTT数据包,共计3个数据包共用一个UDP,它们使用相同的SCID(图中未展示。长度8字节,值为0xc48123092878ac58,和先前客户端的DCID只有最高1字节不同)。其中Handshake中的数据全部加密,所以显示为黄色。我们看到服务器的Initial数据包中包含了ACKACK了客户端的Initial数据包,包序号1)。服务器回复的InitialCRYPTO只有很少的一些扩展参数

最终客户端发送的UDP数据包中只有一个Handshake数据包,而没有对应服务器Initial数据包的ACK(其他有些实现是有的,和IETF所述有一个额外的Initial数据包)。Handshake全部加密

最终服务器也没有发送对应客户端Handshake数据包的ACK,握手使用1.5RTT完成

目前对于部分QUIC实现Wireshark无法对其加密数据进行解密

数据包解密方法:设置环境变量SSLKEYLOGFILE,启动Chromium会自动创建文件,并到Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename设定。此时Wireshark会显示解密后的数据包

5.6.12 连接建立时的地址验证

QUIC中引入了地址验证机制,是为了防范放大攻击(DoS的一种)

QUIC会在连接建立时以及连接迁移的情况下进行地址验证,本小节讲述连接建立时的情况,如果是连接迁移5.6.13。连接建立时使用Token,而连接迁移时需要使用PATH_CHALLENGEPATH_RESPONSE进行网路验证

在地址验证完成之前,不得向未经验证的地址发送超过3倍于原数据包的数据(主要是一开始发送的Initial0-RTT。地址验证对于0-RTT来说很重要)

以上规则我们称之为3倍数据原则

对于客户端来说,它首先发送Initial来发起连接。客户端必须在数据包中添加PADDING帧来满足数据包大小要求(使UDP数据包达到1200字节),更大的客户端Initial可以允许服务器发送更多的回复

建立连接时,QUIC可以在所有握手开始之前进行地址的验证,使用Token。此时客户端发送的第一个Initial中需要包含一个Token。这个Token服务器提供,可以在上一个连接中使用NEW_TOKEN发送到客户端;也可以在客户端发送Initial请求后立即发送一个Retry数据包,该Token包含于数据头中的Retry Token

所谓地址验证,本质就是验证数据包的发送方是否位于其所声明的地址。握手过程本身就是地址验证。在QUIC中,我们认为如果接收到了对方的Handshake数据包并成功处理,说明对方接收并处理了我们发送的Initial数据包,这样可以算一种地址验证。但是在实际应用中还是使用Token机制进行地址验证

此外,对方发来的DCID(至少长8字节)如果和我们给出的相符,也是间接验证了地址

对于客户端来说,它在发起连接时的Initial中除了自己的SCID也包含有一个DCID。服务器回复的Initial中携带的初始密钥就需要依赖该DCID生成。而该DCID还会在服务器的Version Negotiation数据包中使用,或在Retry数据包中的Integrity Tag中出现

由于未完成地址验证时服务器端发送数据有限制,如果服务器回复的InitialHandshake丢包可能导致死锁。此时客户端需要在一段指定时间以后回复Initial(没有Handshake密钥的情况下)或Handshake数据包来重新请求

基于RetryToken的地址验证

服务器通过发送Retry来立即向客户端提供新Token并要求重新连接

Client                                                  Server

Initial[0]: CRYPTO[CH] ->

                                                <- Retry+Token

Initial+Token[1]: CRYPTO[CH] ->

                                 Initial[0]: CRYPTO[SH] ACK[1]
                       Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
                                 <- 1-RTT[0]: STREAM[1, "..."]
            
            Figure 9: Example Handshake with Retry

在服务器发送Retry以及其中的Retry Token要求重新连接后,客户端需要在后续所有Initial数据包的Token域中包含服务器指定的Token。服务器收到这些Initial后便不可再发送Retry,只能决定继续通信或直接关闭连接(例如发现发来的Token无效等,触发INVALID_TOKEN错误)

基于NEW_TOKEN的地址验证

在一个QUIC连接终结后,客户端只能使用服务器在上一个连接的NEW_TOKEN中提供的Token来发起新的Initial连接请求,而不能使用Retry中提供的Token。客户端和服务器都需要缓存这些Token

服务器可以根据这些Token的首尾关系将不同的连接关联,除非客户端不使用先前提供的Token。但是对于客户端来说,只要有Token可用,它就需要发送该Token,否则服务器会回复Retry给出新Token,这会额外消耗时间

和前文所述类似的,如果服务器发现客户端发来的第一个InitialToken无效或不符,同样需要进行错误处理,通常发送Retry,但不会像前文一样直接关闭连接触发错误

NEW_TOKEN帧中包含的Token需要集成一个到期时间,过期需要销毁。可以是一个时间戳也可以直接是一个到期时间

客户端只有在确定对方(服务器)身份是正确的情况下才能在Initial中发送Token

服务器可以一次提供多个NEW_TOKEN,这样可以允许多次的连接请求(请求可能失败),也可以尽早替换即将到期的Token。而客户端可能会缓存有多个同一服务器的Token,下次建立一个新连接时选一个即可。同版本QUICToken可以通用

Token的设计要求

QUIC要求Token长度至少8字节,且有完整性保护(加密和签名),很难碰撞

Retry Token需要支持客户端IP和Port一致性的检验,有效期较短(因为收到Retry后客户端会立即回复,且该Token不能用于下一次连接)

NEW_TOKEN需要支持客户端IP一致性的检验。如果发现客户端IP地址变更,服务器需要限制回复,遵守前文所述的3倍数据规则。NEW_TOKEN有效期相比Retry Token需要更长,但是同一个有效的Token服务器不能接收多次

Token中不得包含敏感信息,服务器、客户端尽量不要分配、使用重复的Token

5.6.13 网路验证

网路验证和前文所述的地址验证不是一回事,它是用于连接迁移的地址验证机制

网路验证目的在于测试两个IP+Port之间的网路是否连通,并确保对方发来的数据包中的地址不是欺骗地址

网路验证会使用到PATH_CHALLENGE以及PATH_RESPONSE帧,其中发起方使用PATH_CHALLENGE,而对方使用PATH_RESPONSE进行回应,此外可能需要添加PADDING来满足数据包大小要求(UDP数据包1200字节),否则无法验证网路的MTU(如果没有验证MTU需要再发送大数据包重新验证一遍)。如果对方收到后立即发起反向的网路验证,PATH_CHALLENGEPATH_RESPONSE可以放在同一个数据包中。接收方如果接收到过小的PATH_CHALLENGE PATH_RESPONSE数据包不得丢弃

PATH_CHALLENGE中会搭载8字节的数据负载,该数据负载是一串难以预测的数据,PATH_CHALLENGE数据包的接收方必须立即使用PATH_RESPONSE进行回复,其中包含了先前发过去的8字节数据

PATH_CHALLENGE可以发送多次以防丢包的情况,但接收方对于每个PATH_CHALLENGE只能回复一个PATH_RESPONSE

网路验证证明的是PATH_CHALLENGE对方可以正常接收,不管本机以何种方式接收PATH_RESPONSE;并且也只有在接收到PATH_RESPONSE时才代表验证的成功

放弃验证代表了网路验证的失败,只由发起方决定。发起方可以设置一个合适的超时时间(需要考虑到新网路可能有更大的延迟),超出时间没有接收到PATH_RESPONSE以后就算失败。验证失败只表示该网路不可用,并不代表连接的失败,如果当前或未来有新的可用网路就可以再次发起验证(也可以关闭连接。实际应用中为防范攻击,通常在失败后返回到原来使用的网路)。如果一直没有可用网路,可以触发NO_VIABLE_PATH错误

网路验证的失败也有可能是因为当前有了更好的新网路,此时立即切换就会导致旧网路验证的失败

一次网路验证只能证明本机到对方的网路是正常的(即PATH_CHALLENGE走过的路径)。因此通信双方需要分别发起网路验证才能知晓双向通信的安全。双方可以在任何必要的时候发起网路验证

对于网路验证的非发起者来说,它只需对发来的PATH_CHALLENGE原路返回一个PATH_RESPONSE即可。但是对于网路验证的发起者来说,它不能认为PATH_RESPONSE一定从原先的路径回来,具体可以看后文的Off-Path Packet Forwarding攻击,见5.6.14

为了防止追踪,在客户端IP和Port地址更改后,客户端必须使用新的CID发起网路验证的probingprobing定义见5.6.14)数据包。客户端还必须保证服务器此时还拥有可用的DCID(即客户端提供的CID),否则服务器无法回应。客户端可以发送PATH_CHALLENGE外加NEW_CONNECTION_ID来保证服务器有足够的DCID(但不超出active_connection_id_limit限制)

5.6.14 连接迁移

移动设备切换网络以及NAT都会导致数据传输过程中IP和Port的变化。QUIC为应对这种变化,提供了连接迁移的功能。QUIC要求IP和Port变化以后必须进行连接迁移处理,连接迁移通常由地址变化的一端发送数据包触发(服务器提供了preferred_address地址选择时除外)。在连接迁移时,可以进行网路验证,具体情况见下文解释

为方便表述,下文开始我们默认地址发生变化的一方是客户端,而服务器默认代指地址固定的一方。但实际应用中QUIC也可以允许服务器地址的改变,两端的地址迁移功能应当是对称的

探查帧(probing frames)

探查帧probing frames共有4种PATH_CHALLENGE PATH_RESPONSE NEW_CONNECTION_ID PADDING,其余所有类型的帧都是non-probing frames

QUIC规定只要是含有non-probing frames的数据包都是non-probing数据包,而probing数据包要求其所有帧必须都是probing frames

发送连接迁移请求

连接迁移请求由地址变化的一端(客户端)发起,客户端通过向服务器发送地址变化non-probing数据包正式标志连接的迁移。而在此之前或之后会使用probing数据包进行网路验证

连接迁移分为主动和被动两种情况:它可能是客户端主动切换网路导致的,也有可能是因为NAT网关的公网端口变化(NAT Rebinding)等原因被动触发。如果不是因为网络问题导致的切换,主动的网路切换不能过于频繁

在先前的QUIC握手过程中,如果对方在transport parameters中包含了disable_active_migration参数禁用主动连接迁移,本机在IP和Port发生变化时就不允许再向对方发送任何数据包。除非对方提供了preferred_address参数,此时客户端依旧可以和新的服务器地址进行连接的迁移

如果本机发现对方违反了disable_active_migration,它必须丢弃所有的数据包但不能发送Reset或关闭连接。同时本机可以进行本小节讲述的连接迁移操作

服务器在收到客户端的non-probing数据包前不得向该地址发送non-probing数据包。如果服务器违反了该规定,客户端需要丢弃这些数据包

客户端连接迁移时不需要再次验证服务器的地址,因为此时服务器地址没有变化。但是为了验证新网路的可用性,客户端可以在新网路上发送probing进行网路验证。客户端网路验证通常在连接迁移之前,也有可能会延后至服务器回复non-probing后再进行。在连接迁移之前进行网路验证有助于提前确认新连接的可用性

连接迁移时需要重置阻塞控制以及RTT状态,以及测试ECN的支持情况,因为不同的网路可能有不同的要求

处理连接迁移请求

服务器在接收到客户端发送的non-probing数据包后会知晓客户端连接迁移的发生

一旦服务器决定允许连接迁移,之后所有的服务器数据包必须发送往新的客户端地址,同时如果服务器还未发起过网路验证,它需要立即发起(某些情况可以省略,见后文),以检测客户端地址是否为真。此外,由于连接迁移必须使用新的CID,此时客户端需要保证服务器有CID可用,否则需要通过NEW_CONNECTION_ID补充

客户端发来的数据包可能会有乱序,尤其在连接迁移时,服务器发现客户端的数据包地址反复变化。为了解决这个问题,服务器需要比对发来数据包的序号,如果序号是目前最大并且地址改变,那么就更改发送数据包使用的目标地址;如果序号不是最大,那么就不更改

对应的,服务器在发现客户端连接迁移时,也可以在验证客户端的地址之前直接向客户端发送数据包(尤其是在该地址最近出现过的情况下,甚至允许直接省略验证),但是需要注意防范后文所述的前两种攻击(Peer Address SpoofingOn-Path Address Spoofing)。在发生攻击时(例如发送很多假的数据迁移non-probing来影响正常数据传输),服务器在返回到原先的网路时,省略地址验证可以最大限度降低攻击对数据传输的影响。服务器一旦开始向客户端发送non-probing就必须终止其他网路上的网路验证

服务器成功验证客户端地址后,它需要向客户端发送新的Token

Peer Address Spoofing 地址欺骗攻击

这种攻击由客户端发起,客户端可以伪造自己non-probing数据包的地址,使得服务器向其他主机发送数据包,如果不限制服务器的回复,服务器很容易被利用于放大攻击流量,构成DoS攻击

为了解决这个问题,服务器在成功验证客户端地址前需要遵守3倍数据原则。而在省略网路验证的情况下,我们认为网路以及地址已经是可信的(这也是省略验证的前提),所以无需限制流量

On-Path Address Spoofing 中间人地址欺骗攻击

这种攻击由中间人引发,而非中间人可以通过后文所述的攻击成为中间人。中间人可以读取客户端发来的数据包,更改其地址并通过更快的路径先于真正的客户端数据包发送到服务器。按照前文所述的处理连接迁移请求中,服务器会认为这是一个连接迁移并向新的地址发送数据包,而由于该假数据包和真正的数据包序号相同,服务器会丢弃真正的客户端数据包,并继续向错误的地址发送数据,这会导致服务器无法向真正的客户端发送数据包,这也是一种攻击

但是错误地址处的主机没有和服务器进行过握手,因此它无法解析服务器发来的数据包,服务器发起的PATH_CHALLENGE网路验证注定无法成功

解决方法是服务器一旦发现网路验证失败,它需要立即切换到原先使用的网路(即原先的客户端地址),而不是维持原样。此外,只要真正的客户端一直发送新的数据包过来更新数据包序号的最大值,由于假地址的网路验证会被终止,这种攻击就很难奏效,尤其是攻击者只发送单个数据包的情况下

但是如果先前的网路状态已经清除,服务器别无选择只能关闭连接,对于后续真正的客户端数据包只能回复Stateless Reset

Off-Path Packet Forwarding 网路外包转发

这种攻击使得网路之外的攻击者可以成为网路上的中间者,并进行上文所述的攻击

网路上的攻击者不一定是这样转变来的,也有可能是一台被劫持的路由器

假设这样的一个场景:网络中有一个攻击者,它可以看到客户端和服务器之间的所有数据包,并且它有更快的路径可以将数据发送到客户端以及服务器。如果它劫持了双方的数据包,更改地址后继续推送,由于PATH_CHALLENGE以及PATH_RESPONSE搭载的8字节数据没有防范地址篡改的功能,且数据包序号、数据包到达的先后顺序会导致真正的数据包被丢弃,攻击者很容易就会变成中间人。这在双方直通路径有丢包发生的情况下更难以防范。除非双方真正的数据包可以先于攻击者的数据包到达,及时将网路切换至原先的直通网路

为了解决这个问题,QUIC要求通信双方在接收到连接迁移时必须先对老的网路进行验证(在老的网路上发送PATH_CHALLENGE)。这个网路验证可能成功也可能失败。而从有效老网路接收到PATH_CHALLENGE的一方需要回复一个non-probing,争取再次切换到老网路,避免攻击者成为中间人

这种解决方法不是很完美,但是还是有用的。并且攻击者很少会有比双方直通更快的路径

前文5.6.13说过网路验证发起方不能认为PATH_RESPONSE一定会从原路径返回,这就体现在这种攻击中

攻击者可以劫持并转发任何数据包,包括那些包含PATH_CHALLENGE PATH_RESPONSE的,以及non-probing,它没有密钥看不到这些内容,它只能对所有的数据包进行地址更改

假设这样的场景:服务器同时对老网路以及新网路进行验证,它并不能判断新网路是否被劫持。从老网路发送的PATH_CHALLENGE一定会从攻击者处得到PATH_RESPONSE回应,也会从原有网路得到回应(可能丢包)。发起方只要接收到数据包解密,发现8字节随机数相同,就可以证明老网路依旧可用。而如果发起方因为不是从原来网路发送的原因丢弃了该数据包,而真正的PATH_RESPONSE也丢包了,服务器就会使用攻击者提供的新网路,此时攻击者也很容易就可以变成中间人,上文提出的方法也就没有什么作用了

丢包检测与阻塞控制

在连接迁移中,除非地址只是更改了端口,否则先前的网路状态需要丢弃,不能用于新的网路,例如RTT,阻塞控制等。在连接迁移时尽管数据包可能通过多条网路发送,但是通常使用一个丢包重传与阻塞控制上下文即可。而probing数据包(例如PATH_CHALLENGEPATH_RESPONSE)通常需要独立的丢包(超时)与重传控制,不能改变数据传输的流控制,影响到正常的数据传输

连接迁移时可能会有较多的数据包乱序,此时服务器会发现数据包的地址反复变化。在数据包乱序的情况下,接收方依然需要发送ACK来进行回复

连接迁移的CID

QUIC中的CID无论在时间还是在空间层面都不能重复使用。尤其是对方使用0长度CID的情况下,不允许进行连接迁移

连接迁移时,在不同的网路上传输数据包必须使用不同的CID,以防止不同的网路被潜在的监视者关联并跟踪(QUIC在正式的数据传输中会对数据头进行加密保护,监视者无法得知CID,这已经增加了跟踪的难度)。而在NAT的情况下可以允许不同的地址使用相同的CID,这在NAT端口变化时无法避免

一台服务器在和不同的客户端通信时也不允许使用相同的CID

在较长时间没有交换数据以后,客户端可能也需要变更端口,从而更换CID

连接迁移之前需要保证有足够的CID可用

服务器地址选择

在客户端向服务器发起QUIC连接时,服务器可以在它的TLS扩展参数transport parameters中包含preferred_address,通常有一个IPv4,一个IPv6地址。客户端根据自己的实际情况,在握手结束以后选择其中一个地址作为服务器地址发起连接迁移,同时客户端需要发起新网路的网路验证。和普通的连接迁移一样,服务器只有在接收到客户端的non-probing时才标志着连接的迁移,在这之前服务器需要通过旧网路发送所有的non-probing;在连接迁移之后服务器仍旧需要接收旧网路上迟到的数据包(但需要丢弃本应该在新网路发送的数据包)。类似的,服务器在preferred_address接收到PATH_CHALLENGE时需要从原路(新网路)返回一个PATH_RESRONSE

服务器指定preferred_address往往意味着它会一直使用该IP地址,不会变化。preferred_address只对当前连接有效

本次连接迁移使用的新CID可以使用已有的,也可以从preferred_address中提取,或使用服务器发来的NEW_CONNECTION_ID。如果连接迁移失败,客户端需要使用原来的地址继续和服务器交换数据

客户端地址改变时,客户端可能会同时进行preferred_address和新地址的连接迁移,选择验证成功的一条网路即可,条件允许尽量选preferred_address。如果客户端迁移到了新地址,它需要根据该地址的IP类型(v4v6)重新选择preferred_address。这种情况下服务器发现客户端地址改变,也必须遵守3倍数据原则,防范前文所述的两种攻击,对客户端的新地址进行验证

IPv6

由于IPv6拥有流标签QUIC要求这个流标签需要同CID一样不能重复使用,否则连接迁移时即便遵守前文所述的CID使用规则也无法防止监视跟踪

5.6.15 连接终止

QUIC可以有三种方法终止连接:idle timeoutimmediate closestateless reset。下文分别讲解

idle timeout 空闲超时

idle timeout依赖于一个空闲超时定时器。双方在握手时会分别在transport parameters包含max_idle_timeout,在两个值中取较小的值。如果双方在超过max_idle_timeout的时间内没有数据交换,那么该QUIC连接就会静默关闭(即关闭时不会有任何数据包交换)。该定时器会在成功接收并处理对方数据包时复位,也会在接收数据包后向对方发送第一个数据包时复位(会引发ACK的包)

如果想要测试QUIC连接是否还存活,主机需要在超时前提前一段时间发送PING(可以外加其他一些引发ACK的数据帧)。如果发得太晚,数据包到达对面时可能已经超时,对方会丢弃该数据包

发送PING也可以用于主机不想关闭连接,但又无数据可发送时,发送PING可以保持连接,但是不能滥用

QUIC建议每隔30秒发送一次PING,主要是为了防止网络路由丢失UDP状态(这通常比idle timeout超时先发生)

immediate close 立即关闭

idle timeout之前想要关闭连接需要通过immediate close

immediate close可以由上层应用调用。immediate close会向对方发送一个CONNECTION_CLOSE帧(类型0x1c传输层或0x1d应用)来立即关闭连接,此时所有未关闭的数据流都被关闭。CONNECTION_CLOSE包含一个错误码Error Code表示关闭连接的原因

immediate close中,连接关闭以后CONNECTION_CLOSE的发送方会进入到closing状态,而接收方进入draining状态(通常保持idle timeout长度的时间,此时连接状态依旧保存),在这些状态下双方对于连接关闭后延迟到达的数据包不会发送Stateless Reset。而在之后接收到这些数据包时,会发送Stateless Reset

closing状态下,发送方对于所有接收到的对应已关闭连接的数据包都回复一个CONNECTION_CLOSE,但是回复速度不能过快。此时允许发送方丢弃密钥并对于每个接收到的数据包发送一个完全相同的CONNECTION_CLOSE,但是这可能导致放大攻击,所以回复CONNECTION_CLOSE同样需要遵守3倍数据规则。此外,发送方可能在closing状态接收到不同地址的数据包(例如在最后发生了连接迁移),这种情况下可以直接丢弃,或遵照遵守3倍数据规则对该新地址进行回复

通常PROTOCOL_VIOLATION错误会引发immediate close

draining状态下接收方不得发送任何数据包,密钥也不必保留。而接收到CONNECTION_CLOSE后进入draining之前,接收方也可以向对方发送一个CONNECTION_CLOSE,错误码NO_ERROR。发送方接收到该数据包以后也进入draining状态

CONNECTION_CLOSE可以在1-RTT中发送,也可以在握手未完成时发送

由于服务器通常不知道客户端是否有Handshake keys,在握手未完成时服务器需要同时在InitialHandshake中包含CONNECTION_CLOSE才能保证客户端能处理immediate close

有时服务器不会接受0-RTT数据包,如果客户端在0-RTT中发送CONNECTION_CLOSE可能没有用,需要在Initial中发送才会起作用

如果此时握手进行到Handshake1-RTT之间,需要同时在这两个数据包中发送CONNECTION_CLOSE

InitialHandshake数据包中不能发送0x1d类型的CONNECTION_CLOSE,否则会暴露上层应用类型。这里只能使用0x1c类型,错误码APPLICATION_ERROR

QUIC建议如果在InitialHandshake中接收到了非法的信息,不是立即发送CONNECTION_CLOSE,而是直接忽略这些数据包,以防范DoS攻击

stateless reset 无状态重置

QUIC是由状态机控制的,每一个QUIC连接都有状态上下文。如果当前主机无法将接收到的数据包和目前有效的连接关联,也就是无法获取对应的QUIC连接的状态(例如已经丢弃,或者连接已经崩溃),那么它只能使用stateless reset进行回复。stateless reset不能用于当前还未销毁的连接(只能用immediate close

stateless reset由本机发送Stateless Reset数据包实现,其独占一个UDP数据包,末尾包含16字节的Stateless Reset Token明文),该token由对方新建连接时通过NEW_CONNECTION_IDstateless_reset_token参数提供。对方接收到以后需要立即关闭对应的连接

每一个CID都会对应有一个Stateless Reset Token,在连接活跃时通信双方会使用NEW_CONNECTION_ID顺便提供一个CID对应的token。而对于服务器来说它还能在握手时通过stateless_reset_token参数提供该token。这些token都是加密保护的,只有双方互相知道对方的token。一个CID淘汰以后其对应的token随之淘汰

因此,在QUIC握手完成之前由于CID对应的token可能还未给出,双方没有任何有效token的情况下是无法进行Stateless Reset的。此时接收到无效的数据包直接忽略即可,无需任何回复

通信双方需要记忆所有最近使用过的对方的CID(需要是未淘汰的)以及对应的token和目标地址(我们称表1),还有自己发布过的CID以及对应的token和使用过该CID的主机地址(我们称表2),分别存储。QUIC建议不要直接存储token明文,而是对token明文进行哈希或加密后存储。匹配token时尽量耗费相同长度的时间

对于Stateless Reset发送方来说,它是因为无法将收到的数据包与当前有效的QUIC连接进行关联处理才会发送Stateless Reset。由于此时状态已经丢失,该发送方并不能知晓接收到数据包的内容,包括DCID。所以发送方只需根据接收到数据包的地址,在表2中任意挑选一个有效的CID对应的token,并将该token放在Stateless Reset中发送。此外由于需要伪装成短数据头Stateless Reset中对应短数据头的DCID域需要填充为不可预测的随机值

对于Stateless Reset接收方来说,它发现该数据包无法依照当前打开的连接解密得到有意义的数据。在此同时它需要提取该数据包的地址和token,并在表1中匹配该地址对应的token。如果匹配到了有效的CID对应的token,它判定该数据包为Stateless Reset,需要将对应连接立即切换到draining状态,并停止发送任何数据包

但是如果Stateless Reset接收方既无法解密得到有效数据,也没有在表1中该地址下找到对应的token,那么它也会回复一个Stateless Reset,这会导致无限循环。因此QUIC要求所有Stateless Reset数据包必须比触发该重置的数据包小,循环直到数据包小于一定长度后停止回复Stateless Reset(也可以计数,达到一定数量后便不再回复)。如果出现了以上机制也无法解决的情况,最终就只能通过设定一个定时器,超时结束

QUIC要求5.6.3所示的Stateless Reset数据包需要混淆,其特征无法和普通的短数据头进行区分(防范监视者),在Stateless Reset Token之前的数据需要至少5字节长(共计21字节,最短的有效QUIC数据包长度,更短的数据包视为无效会直接丢弃)。但是考虑到5字节可能比一些CID还短,QUIC建议本机发送的Stateless Reset长度至少需要是对方提供的CID长度+22字节

此外,发送Stateless Reset也需要防范放大攻击,本机回复的Stateless Reset需要遵守3倍数据原则;而如果对方发来的数据包小于等于43字节,本机回复的Stateless Reset长度在此基础上减1

QUIC要求将所有末尾拥有有效16字节token的UDP数据包都视为Stateless Reset,因为有些不符合IETF规范的实现可能使用长数据头发送Stateless Reset

token的生成

由于token是CID发布方(通常也就是接收到无效数据包后的Stateless Reset发送方)随之提供的,所以它需要由CID发布方生成。CID发布方在和同一台主机建立连接通信时,可以使用同一个key来生成对应的token。它将当前自己的CID以及key作为生成函数的输入,就可以得到一个CID对应的token,并发送到对方

由于以上原因,token只由CID和key生成,而不同的主机可能会使用相同的key。在一台主机上被该主机Stateless Reset过的CID不允许用于另一台使用相同key的主机,否则会产生不同主机相同token的情况,有可能容易引发DoS攻击

5.6.16 数据包保护简述

具体的数据包保护与加密握手见QUIC-TLS

QUIC中,Version Negotiation没有加密保护,Retry数据包有AEAD保护,Initial数据包同样有AEAD保护(但是用于AEAD密钥的参数使用明文传输)。以上数据包没有有效的加密保护,只有有限的防篡改能力

Handshake0-RTT1-RTT都有加密保护。其中0-RTT1-RTT传输的数据拥有最强的保密性和完整性,使用的是握手过程中商定的密钥。数据头中的数据包序号(Initial0-RTTHandshake1-RTT)也可能有另外的保护方法,属于header protection的一部分。数据头中的敏感信息部分都有加密保护

InitialHandshake0-RTT1-RTT使用的保护密钥都不同,称为cryptographic separation

关于共用UDP数据包的问题

前文已经说过单个UDP数据包可以包含多个含Length的数据包。这里再解释一遍,由于Length只有在解密之后才能知晓,所以QUIC要求一个UDP数据包中的QUIC数据包需要按顺序存放,例如Initial需要在Handshake前面,等等。同时多个QUIC共用一个UDP时,数据包末尾是可以有一个1-RTT数据包的,前提是前面的数据包都是含Length的,这样才能知道最后一个数据包的开头位置

QUIC要求同一个UDP数据包中的QUIC数据包必须属于同一个CID,否则接收方只认第一个数据包的CID,后续不属于该CID的统统丢弃

在同一个UDP数据包或在不同UDP数据包中发送多个QUIC数据包是等价

RetryVersion Negotiation数据包永远独占一个UDP数据包

关于数据包序号

由于数据包序号采用可变长编码,其值可以取02^62-1但是这仅限于ACK帧中

我们已经定义了数据头中Packet Number Length长度不能超过4字节,这和上面的描述不相符。因此,如果数据包序号超出了4字节,需要截取低4字节(从解包结果来看,会出现重复的数据包序号。这和TCP中的序号溢出类似)

QUICVersion NegotiationRetry数据包没有序号

同一连接的其他类型数据包中,Initial数据包使用同一个序号空间,Handshake使用同一个序号空间,0-RTT1-RTT共用一个序号空间(为了更好地支持重传)。由于序号空间存在的原因,ACK只能由同类型的数据包回复,例如Initial数据包只能由Initial数据包进行对应的ACK

正常的数据传输中数据包序号不能重复使用。如果接收方接收到了重复序号的数据包,且之前该数据包已经成功接收且处理,那么它必须丢弃该数据包

5.6.17 分包与可靠传输机制

为避免产生过多的小数据包,QUIC要求一个QUIC数据包需要尽量多的包含数据帧,尤其是在数据传输时,可以等到上层应用提交了足够的数据帧后再一并发送;但同时需要权衡多数据帧导致的等待,不能让上层应用等待太长的时间;此外,由于QUIC的并行数据流机制,QUIC要求尽量不要在同一个QUIC数据包中包含太多不同数据流的数据,否则一旦发生丢包这些数据流全都需要重传,相比TCP难以发挥优势(head-of-line blocking问题)

ACK的发送

前文已经指出了数据包类型,我们定义只包含N类型帧的数据包为non-ack-eliciting,即它们可以不被接收方ACK;反之我们称该数据包为ack-eliciting,这种数据包必须被ACK。对应每个ack-eliciting数据包接收方回复的ACK不得超过1个(因为ACK不受流控制限制)

QUIC要求接收到ack-eliciting类型的InitialHandshake数据包后立即回复ACK

此外,在数据包出现乱序时也必须立即回复ACK,无论对于提前到达的数据包还是推迟到达的数据包。及时处理有助于防止多余的重传

QUIC中,通信双方还会给出自己的max_ack_delay传输参数,表示自己在接收到ack-eliciting类型的0-RTT1-RTT数据包后最多不超过多长时间(数值大致为max_ack_delay + RTT)会回复ACK。如果发送方发现ACK超时,那么它就会重发该数据包,并重新计算RTT

ACK中还包含一个ACK Delay,用于表示当前该主机从处理数据包到回复ACK的时间。max_ack_delay只是主机估计的自己的ACK Delay最大值,如果ACK Delay超过了max_ack_delay应当如实回复给对方

由于Handshake0-RTT1-RTT数据包是需要解密的,接收到这些数据包时可能密钥还没计算出来,此时可以先缓冲这些数据包,等到密钥可用并成功解密这些数据包后再回复ACK。此时的ACK Delay可能会比较大

QUIC不允许使用non-ack-eliciting数据包回复non-ack-eliciting数据包,否则可能导致无限loop。即,只有在等到我们收到新的ack-eliciting后使用non-ack-eliciting回复,或在收到non-ack-eliciting后使用ack-eliciting回复(例如添加一个PING帧)

要注意发送方对于接收方发来的单独的ACK数据包也需要进行ACK回复,但是发送方不能直接发送ACK回复,否则就会出现上面的矛盾。发送方只能在其他ack-eliciting数据包中包含对应的ACK进行回复,例如下一个包含STREAM的数据包中

关于ACK发送的频次

发送ACK的频次不能过高也不能过低。QUIC建议接收方至少每接收到2ack-eliciting数据包才回复一个ACK

目前大部分的QUIC配置依旧使用了TCP一样的滑动窗口阻塞机制来控制数据包的发送,如果接收方回复ACK频次过低将不利于丢包检测,也会大大影响性能

而如果接收方发送单独的ACK包频次过高,这会增大发送方数据传输和处理的负担

关于ACK Range

前文已经说过ACK帧需要被限制在一定大小范围,因此接收方不能发送太多的ACK Range,并且省略一些当前有用的ACK Range。和TCP类似的,丢包最终是由发送方检测并决定进行重传,这些省略的ACK Range最终会由发送方进行多余的重传。但是接收方在真正接收到ACK Range对应的数据包之前必须标记未接收到的数据包,并在条件允许的情况下依然需要发送对应的ACK Range

重传机制

QUIC中丢失的数据包以及数据帧不一定会按原样重传一遍,这些丢失的数据帧有可能在新的数据包中依照目前的上下文按需发送

只要是发送方已经确认接收方已经成功接收的数据(已经被ACK的),它就应当尽量避免再重传这些数据,造成多余的重传

QUIC要求出现丢包时,发送方需要采取相应的阻塞控制措施

以下是数据包中各种帧的重传规则

QUIC要求握手时CRYPTO丢包需要重传

STREAM发生丢包时,丢失数据部分在后续新的STREAM中传输。一旦发送方发送了RESET_STREAM,后续便不可再发送该数据流的STREAM

ACK比较特殊。QUIC要求ACK的发送不能推迟过多时间,也不能重复发送旧的ACK,否则可能导致RTT波动,或导致ECN的失效

RESET_STREAM(见5.6.7)用于发送方撤销一个数据流传输,它也需要接收方使用ACK进行回复,在没有接收到对应ACK时需要不断重传。RESET_STREAM在重传时内容不可改变

STOP_SENDING用于接收方撤销一个数据流,接收方在接收到发送方发来的RESET_STREAM或所有数据包成功接收之前也需要在必要时重传STOP_SENDING

CONNECTION_CLOSE由于是关闭了连接,它丢包时不会重传

由于MAX_DATAMAX_STREAM_DATA只能增加不能减小,每次传输时无论是因为丢包还是何种原因它的值需要比旧值更大。所以这两种数据帧传输(包括重传)次数不能过多。但是接收方必须要能够处理MAX_DATAMAX_STREAM_DATA减小的状况

DATA_BLOCKEDSTREAM_DATA_BLOCKED,以及STREAMS_BLOCKED在丢包时也需要重传。其中DATA_BLOCKED对应的是整个连接,STREAM_DATA_BLOCKED对应的是一个数据流,STREAM_BLOCKED对应的是一类数据流。在重传时这些数据帧中包含的限制数值需要为当前值

PATH_CHALLENGE在接收到对应PATH_RESPONSE或撤销网路测试之前也需要不同重发,其中搭载的随机数据负载每次都要不一样。接收方对于每一个PATH_CHALLENGE回复一个PATH_RESPONSE即可,无需多次发送

NEW_CONNECTION_IDRETIRE_CONNECTION_ID需要丢包重传。其中NEW_CONNECTION_ID多次重传时其中的新CID序号不变

NEW_TOKEN需要重传

PINGPADDING无需重传

HANDSHAKE_DONE在得到ACK之前需要不停重传

ECN

4.7.3

5.6.18 数据包大小

QUIC通常要求网络至少支持1280字节长度的数据包(包含IPUDP数据头在内)。UDP数据包搭载数据多少可以通过QUICmax_udp_payload_size传输参数进行限制

数据包大小需要遵守三倍数据限制,以防止放大攻击。此外也需要遵守PMTUD的限制

如果QUIC数据包太小,例如Initial,它需要添加PADDING帧满足1200字节的最小UDP负载。如果接收到的QUIC数据包太小,UDP负载小于1200字节,它需要丢弃该包。例如Initial过小,接收方也可以直接发送CONNECTION_CLOSE关闭连接,并触发PROTOCOL_VIOLATION

IPv4IPv6中,如果数据包超过了网络的MTU大小,路由器会丢弃过大的数据包并反馈一个ICMPICMPv6消息。ICMPv4可以携带原数据包的开头8字节数据,而ICMPv6消息通常较大,它会包含原先数据包的大部分数据

为了防止ICMP注入导致QUIC主机误以为MTU变小,QUIC主机会对ICMP数据包进行校验,包括CID,IP,Port等

5.6.19 传输参数

QUIC传输参数quic_transport_parametersTLS的一个Extension,如下

它是一个列表,格式定义如下

Transport Parameters {
  Transport Parameter (..) ...,
}

Transport Parameter {
  Transport Parameter ID (i),
  Transport Parameter Length (i),
  Transport Parameter Value (..),
}

每一个参数有自己的IDLength(都采用可变长编码),以及Value

所有ID31 * N + 27的传输参数都是保留参数,它们没有定义

参数名称 ID 数据类型 定义
original_destination_connection_id 0x00 可变长整数 用于握手,通常是一个8字节数据,由服务器在接收到客户端的第一个Initial后发送,反馈客户端发送的Initial中的DCID(服务器此时会变更它的CID)
max_idle_timeout 0x01 可变长整数 用于空闲超时关闭连接,单位ms
stateless_reset_token 0x02 可变长整数 服务器在握手期间加密发送的无状态复位令牌,通常是一个16字节值
max_udp_payload_size 0x03 可变长整数 接收方允许接收的最大UDP数据包负载大小,见5.6.18
initial_max_data 0x04 可变长整数 初始MAX_DATA值,见5.6.4
initial_max_stream_data_bidi_local 0x05 可变长整数 本机发起的双向数据流MAX_STREAM_DATA初始值。在客户端发送的包中包含该参数,它限制的是Stream ID末位为0b00的数据流;在服务器发送的包中包含该参数,它限制的是Stream ID末位为0b01的数据流
initial_max_stream_data_bidi_remote 0x06 可变长整数 对方发起的双向数据流MAX_STREAM_DATA初始值。在客户端发送的包中包含该参数,它限制的是Stream ID末位为0b01的数据流;在服务器发送的包中包含该参数,它限制的是Stream ID末位为0b00的数据流
initial_max_stream_data_uni 0x07 可变长整数 对方发起的单向数据流MAX_STREAM_DATA初始值。在客户端发送的包中包含该参数,它限制的是Stream ID末位为0b11的数据流;在服务器发送的包中包含该参数,它限制的是Stream ID末位为0b10的数据流
initial_max_streams_bidi 0x08 可变长整数 对方最多允许发起的双向数据流累计数量,MAX_STREAMS0x12)初始值。如果没有该参数或该参数为0表示对方不允许发起双向数据流
initial_max_streams_uni 0x09 可变长整数 对方最多允许发起的单向数据流累计数量,MAX_STREAMS0x13)初始值。如果没有该参数或该参数为0表示对方不允许发起单向数据流
ack_delay_exponent 0x0a 可变长整数 ACKACK Delay数值左移位数,不得超过20。不指定该值默认为3ACK Delay8
max_ack_delay 0x0b 可变长整数 最大可能出现的ACK Delay,单位ms,见5.6.17。不得超过2^14。如果没有给出该参数,默认值为25ms
disable_active_migration 0x0c Length0,无Value 禁用主动的连接迁移,如果服务器提供了该参数,客户端在握手后发生地址变化时就不可以再向服务器发送数据包,有preferred_address时除外。见5.6.14
preferred_address 0x0d 见后文解释
active_connection_id_limit 0x0e 可变长整数 本机愿意维护的对方CID的最多数量。这些CID可以来自于对方发来的NEW_CONNECTION_ID,也可以来自于握手过程中协商的CID,或是来自于preferred_address中的CID。最小值为2。如果没有给出该参数,默认值为2。该参数在使用0长度CID时无效
initial_source_connection_id 0x0f 可变长整数 用于握手时发送的第一个Initial数据包,值和SCID相同
retry_source_connection_id 0x10 可变长整数 用于服务器发送的Retry数据包,值和SCID相同

单数据流控制参数initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, initial_max_stream_data_uni如果缺失,数据流控制默认从0开始

客户端不可使用的传输参数有original_destination_connection_id, preferred_address, retry_source_connection_id, stateless_reset_token

通常传输参数有关的错误都是触发TRANSPORT_PARAMETER_ERROR

preferred_address定义

服务器地址选择见5.6.14

定义如下

Preferred Address {
  IPv4 Address (32),
  IPv4 Port (16),
  IPv6 Address (128),
  IPv6 Port (16),
  Connection ID Length (8),
  Connection ID (..),
  Stateless Reset Token (128),
}

preferred_address由服务器发送。客户端一开始可能使用一个多播地址或服务器拥有的其中一个地址访问服务器,此时服务器可以通过preferred_address给出它偏好使用的地址。握手完成后不久客户端就可以发起连接迁移,和服务器给出的地址通信

preferred_address包含了服务器的IPv4IPv6地址各一个。如果服务器没有对应地址,将该地址数据域设置为全0即可

preferred_address中的CID序号为15.6.9)。Connection IDStateless Reset Token的作用定义和NEW_CONNECTION_ID中的相同(可以看作就是一个NEW_CONNECTION_ID)。给出一个新的CID是为了保证客户端向新服务器地址发起连接迁移时有足够的CID可以用

QUIC的服务器地址选择特性和0长度CID不相容,服务器使用0长度CID时不得提供该参数

5.6.20 QUIC拥塞控制特性

RFC9002

5.7 QUIC-TLS

参考RFC9001 RFC8446

5.7.1 基本概念

5.7.2 数据包保护

5.8 KCP

6 应用层

6.1 HTTP

HTTP/1.1参考RFC2616 RFC2818,HTTP/2参考RFC9113,HTTP/3参考RFC9114。HTTP综述参考RFC9110

参考《HTTP权威指南》

HTTP是最流行的应用层协议。它可以用于传输各种文本以及非文本数据。同时HTTP也是一种无状态协议,基于TCP传输,服务器在80端口监听

HTTP的三个主要版本1.1 2 3目前都被广泛使用,对于不同应用环境来说各有优劣,所以它们并不完全是新版替换旧版的关系

6.1.1 HTTP基本概念

网络服务器上提供的资源非常多样,有.svg .jpg .mp4等静态文件,也有软件程序服务,例如我们在网络购买了一件衣服,就是调用了网站的服务资源。HTTP需要传输所有以上类型的数据

HTTP沿用了邮件系统的MIME类型,所有通过HTTP传输的对象都拥有自己的MIME属性。这在服务器响应中体现为Content-Type。例如html类型为text/htmltxt类型为text/plainjpg类型为image/jpg,二进制字节流为application/octet-stream

一个HTTP事务由客户端发送的请求命令和服务端回复的响应结果构成

参与HTTP通信常用的组件有以下几种

代理是客户端和服务器(不直接通信)的中间人,它通常负责转发数据,同时可以对数据进行修改(例如屏蔽不安全的内容)

缓存是一种特殊的代理,它的主要作用是缓存常用的资源,使得客户端访问速度更快,同时减轻服务器负担

网关gateway是一种特殊的代理,它主要用于协议的转换,例如接收HTTP请求并使用FTP协议到其他服务器获取资源,Web服务器主要就提供了网关的功能

隧道tunnel也可以算一种特殊的代理,通常需要两台中间服务器。两台服务器之间的数据相当于在原有的数据流上再添加了一层HTTP包装(它们分别负责打包和还原),这样就可以使得TLS流量流过仅允许HTTP流量的线路了

Agent是客户端发起HTTP请求的应用程序,例如curl和浏览器等

6.1.2 URI

每一个URI在全球范围内唯一表示一个信息资源。URI包含了URLURN,其中URL将资源的具体位置表现了出来,而URN是资源的一个名字,而不是具体位置

以下是一个URL。我们通过该URL向HTTP服务器请求服务,查询symphony x这个乐队

http://www.metal-archives.com:80/search?searchString=symphony+x&type=band_name

其中http:+//表示访问协议类型,使用其他协议时也可以是ftp smb等。www.metal-archives.comhost主机名,80为端口,使用:分隔。/search为请求的资源(绝对路径形式),?之后的内容表示请求服务对应的查询内容,使用=键值对形式,并且使用&分隔

URN只在磁力链接等地方有应用

magnet:?xt=urn:btih:8402E328F819AADD68A333A75729DE890F8...

6.1.3 HTTP/1.1报文结构

HTTP报文主要由start lineheaderbody三大部分组成。而HTTP报文按照方向分为InboundOutbound,是相对于服务器而言的。而数据发送方永远位于接收方上游Upstream,相反接收方位于发送方下游Downstream

HTTP/1.1中,start line起始行以及header都是直接使用字符行形式,每一项都以\r\n\x0d\x0a)回车换行结尾。而body就是该HTTP数据包搭载的必要数据(实体)。客户端发送的请求中,通常只有POSTPUT会拥有body,而有bodyGET数据包是不符合规范的

客户端的请求报文

请求报文的一般格式如下

<method> <request-URL> <version>
<headers>

<body>

在客户端,start line通常包含请求的方法资源路径以及HTTP的版本,如下所示,字符串GET /16M HTTP/1.1\r\n就是start line,使用空格 分隔

header的每一行都是一个文本形式的键值对,使用: 冒号加空格定义。例如上图中的User-Agent指发起请求的应用,这里是curl/7.86.0,而Accept表示客户端可以接收的数据类型(MIME),这里为*/*\r\n表示任意类型。注意在header之后一定会有一个空行\r\n,接下来才是body。而GET请求没有body,所以到这里也就结束了

headers中的一个值可以拥有多个延续行(为了可读性),该延续行开头必须要有 空格或\t制表符

headers按含义和作用分为通用,请求,响应,Body,扩展共五大类

服务器的响应报文

响应报文的一般格式如下

<method> <status> <reason-phrase>
<headers>

<body>

在服务器端,start line通常包含回复的HTTP版本,以及状态码,如下为HTTP/1.1 200 OK\r\n,同样使用空格 分隔

header的格式相同。这里的Content-TypeMIME)为application/octet-streamContent-Length16777216(数据总长度)。这里的服务器回复有body(我们请求大小16M文件的最后一些字节),所以空行后面还有数据

日期格式是统一的,Date: Mon, 09 Jan 2023 13:29:40 GMT\r\n

6.1.4 HTTP请求方法以及返回状态码

客户端发送的HTTP请求方法分为以下几种

方法 解释 Side-effects Body
GET 请求服务器把资源发送过来(仅通过URL请求)
POST 请求服务器处理发过去的数据,包含待处理的数据以及其他一些必要参数,例如请求的内容,调用的服务等。常用于HTML表单数据等 通常有
PUT 请求服务器存储发过去的数据到指定路径
HEAD 仅发送被请求资源的头部。通常用于检查资源是否存在,资源类型,修改时间等
DELETE 请求服务器删除指定资源
OPTIONS 询问可用的请求类型(例如Allow: GET, POST, OPTIONS
TRACE 请求最终的接收方重复一下我们发送的请求内容,查看途中进行了哪些更改
CONNECT 用于tunnel

Side-effects表示该种请求是否会导致服务器端数据的改变,它不是绝对的,行为可以由开发者定义。但实际应用中还是需要遵守这些方法的应用场合

服务器返回状态码如下

状态码 可读格式 解释
100 Continue 继续请求。客户端发送数据包前可以先发送一个试探性的数据包,其中header包含了Expect: 100 Continue,得到服务器的100响应后再发送数据(如果响应超时,客户端无论是否收到响应都要立即发送数据包)。取决于试探性数据包的body是否已经包含了部分数据,服务器会回应100(不包含)或不回应(包含)。有些服务器的100响应会有bug,在实际应用中需要注意
101 Switching Protocols 服务器按要求正在切换HTTP协议版本
200 OK 请求成功,本数据包body包含了响应数据
201 Created 主要响应PUT,需要包含Location(服务器端创建的URL
202 Accepted 已接受请求但未处理。body需要包含附加信息,例如预计时间等
203 Non-Authoritative Information 通常在有缓存服务器的场合中出现,表明缓存服务器暂时无法验证它响应的信息是否可信
204 No Content 响应没有body,但是客户端需要根据headers进行一些本地操作
205 Reset Content 清除所有(HTML)元素
206 Partial Content 部分请求(客户端只请求了文档的一部分)成功
300 Multiple Choices 同一URL对应多资源
301 Moved Permanently URL已经移除,通过Location指示现在的可用URL(以后都使用新的)
302 Found URL暂时不可用,通过Location指示现在的可用URL(以后仍然使用旧的)。只用于HTTP/1.0
303 See Other 使用Location指定URL访问,主要用于POST(重定向访问使用GET
304 Not Modified 资源未修改
305 Use Proxy 必须通过代理Location访问
306
307 Temporary Redirect 作用同302,用于HTTP/1.1
400 Bad Request 请求错误
401 Unauthorized 未输入用户名和密码
402 Payment Required 保留
403 Forbidden 拒绝访问
404 Not Found 服务器没有URL对应资源
405 Method Not Allowed 不支持该请求方法
406 Not Acceptable 无法接收
407 Proxy Authentication Required 类似401,用于代理服务器
408 Request Timeout 客户端发送请求时间过长(一般很少出现)
409 Conflict 资源冲突
410 Gone 类似404,但是资源曾经存在
411 Length Required 服务器要求客户端包含Content-Length
412 Precondition Failed 客户端请求中的条件失败
413 Request Entity Too Large 请求过大
414 Request URI Too Long URI过长
415 Unsupported Media Type 服务器不支持该类型文件
416 Request Range Not Satisfiable 非法的范围请求
417 Expectation Failed 无法满足客户端的Expectation
500 Internal Server Error 服务端程序出错,无法提供服务
501 Not Implemented 不支持的请求
502 Bad Gateway 代理服务器背后的链路故障
503 Service Unavailable 服务不可用
504 Gateway Timeout 类似408,客户端向代理服务器发送请求时间过长(一般很少出现)
505 HTTP Version Not Supported 不支持的HTTP协议

常见Headers

通用Headers

请求headers

响应headers

Body headers

6.1.5 HTTP/2

6.1.6 HTTP/3

6.1.7 Token

6.1.8 Session

6.1.9 Cookie

6.2 HTTPS

HTTP/1.1参考RFC2818

6.3 DNS

参考RFC1034 RFC1035

DNS全称Domain Name System,负责域名到地址的解析

IP地址难以记忆。且如果一个网站更换了IP,访问它的客户端就不得不更改访问配置。DNS应运而生,并且已经成为了最老的应用层协议之一。此外DNS也可以起负载均衡的作用

为了方便理解DNS数据包格式,可以结合Wireshark抓包观察

6.3.1 域名基本概念

以域名www.metal-archives.com为例,这个域名中的www metal-archives com称为标签label,每个长度不超过63字节,总长不超过255字节。所有的域名最后都有一个长度0null标签,表示根域root,域名在此结束

根域root下的顶级域Top Level Domains.com .org .edu .gov .mil .net .info .uk .jp .de .cn .es .fi等,主要包括了常用机构,以及国家名。而二级域在顶级域之下,常见的例如.cn下的.com .edu.jp下的.co .ac。上例中metal-archives.commetal-archives也是直接挂靠在.com顶级域下的一个二级域

域名的概念和域不同。metal-archives.com这个域名本身是一个顶级域名,而zol.com.cn是一个二级域名

邮件域名格式和普通站点域名有所不同,格式为<local-part>@<mail-domain>,需要将@前后进行拆分合并,例如[email protected]变成mastermail.outlook.com再进行处理

6.3.2 DNS系统组成

DNS系统由边缘服务器(称为Recursive ServerCaching Server),权威服务器(称为Authoritative ServerIterative Server)以及客户端Client)三大角色构成。客户端向边缘DNS服务器发送域名查询请求,如果边缘服务器没有在缓存中查找到对应信息,就需要代客户端向权威服务器发送请求,获取到信息以后回复给客户端

权威服务器有主服务器Master(或Primary)和从服务器Slave(或Secondary)之分。每台服务器都会存储一张信息表,称为Resource RecordsRR),其中包含该服务器知晓的域名信息,一台服务器的RR的集合称为一个Zone。更高级别的服务器中包含的RR可能指向更低级别服务器地址,告知边缘服务器询问该更低级别的权威服务器(等级是相对的,两台服务器的等级不一定有绝对的高低之分。高等级服务器将RR信息传送给低等级服务器的行为称为下放delegation

世界上存在最高等级的DNS服务器,这就是根服务器。它们必须知晓所有的顶级域信息(无论是IP地址还是指向其他DNS服务器),例如.com .org

边缘服务器在接收到客户端的DNS请求后,首先会查找缓存是否已经有对应的记录。如果没有,再按一定顺序(例如从高级权威服务器到低级权威服务器的顺序)进行询问,直到有权威服务器给出具体的IP地址

递归查询需要由边缘DNS服务器执行全部任务,最终将结果发送回客户端。一次查询过程中,客户端和边缘服务器之间只有一来一回两个数据包。同时边缘服务器也承担了缓存查询结果的任务,以尽量降低权威服务器的压力

权威服务器通常不会同时提供递归查询服务,客户端也就无法直接询问权威服务器。每一台权威服务器都只能回答有限的问题

DNS服务器监听TCPUDP53端口,由于UDP数据包不能超过512字节,更大的数据包需要使用TCP传输。通常边缘服务器客户端之间都是使用UDP进行数据传输,而边缘服务器权威服务器之间的数据会使用TCP传输

这里我们只讨论客户端和边缘服务器之间交换的数据。而DNS协议本身是设计成可以同时用于客户端-边缘服务器、边缘服务器-权威服务器之间的数据传输

6.3.3 Resource Records

在DNS协议中,一条RR数据可以看成是由如下内容组成的(这里描述的不是数据包格式。格式见下一小节)

owner 域名,即Name
type RR数据类型,可以为AIPv4地址,AAAAIPv6地址,PTR反向解析(地址到域名),CNAME别名(别名,先请求一次获取到的真正域名,之后再次请求并返回结果),MX为该域名相关的邮件服务域名+优先级,NS为下一级DNS服务器地址(域名形式),SOA表示Authority Zone起始(Start Of a zone of Authority),TXT表示普通字符,HINFO为处理器和系统类型
class 永远为IN,指Internet
TTL Time to live,该RR的有效期,单位秒。不同的RR会有不同的TTL
RDATA 数据存储区,内容取决于type

PTR反向解析需要在RR中使用固定的in-addr.arpa作为Name

6.3.4 DNS数据包格式

DNS数据包中,通常只有边缘服务器回复的数据包会使用RR(放在AnswerAuthorityAdditional中)搭载信息;而客户端发送的请求数据包会将请求要素都记录在Question中,客户端发送的数据包经常是没有RR

DNS数据包格式如下

    +---------------------+
    |        Header       |
    +---------------------+
    |       Question      | the question for the name server
    +---------------------+
    |        Answer       | RRs answering the question
    +---------------------+
    |      Authority      | RRs pointing toward an authority
    +---------------------+
    |      Additional     | RRs holding additional information
    +---------------------+

所有的DNS数据包都有数据头Header,而之后的QuestionAnswerAuthorityAdditional Information区域都是可选的,视具体情况而定

Question包含了查询请求的QTYPEQCLASSQNAME

后面的三个域AnswerAuthorityAdditional格式相同,都由RR组成

Answer用于存放直接对应Question询问内容的回复列表(RR)。这里的RR类型可以是MX(指出域名对应的邮件域名)

Authority通常用于给出NS信息(给出下一级DNS的域名),可能包含SOA

Additional information搭载额外的有用信息,通常对应上面MXNS的补充性信息,例如下一级DNS服务器域名对应的IP地址(类型为AAAAA

数据包开头的Header格式如下,请求和回复数据包都使用这个格式

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

定义

作用
ID Transaction ID每一对请求和回复使用相同的Transaction ID
QR 0表示请求,1表示回复
Opcode 操作类型,0为正向解析请求,1为反向解析请求,2为状态请求(2不常用)
AA Flag位Authoritative Answer权威服务器回复的数据包(服务器有该请求域名对应的RR项)中该位通常会置位。由于我们请求的是边缘服务器,在抓包中基本不出现
TC Flag位TrunCation,表示信息长度超过UDP512字节限制,需要重新使用TCP传输。同上,基本不出现
RD Flag位Recursion Desired,通知边缘服务器进行递归式请求。大部分请求都会置位(对于一个RD置位的请求,其对应的回复中RD也要置位)
RA 指示位Recursion Available,在服务器的回复中表示它自己是否支持递归式解析
Z 保留
RCODE 回复状态码。0正常,1请求格式错误Format error2服务器错误Server failure3权威服务器未找到域名Name error4不支持的请求Not implemented5服务器拒绝请求Refused
QDCOUNT ANCOUNT NSCOUNT ARCOUNT 分别表示Question Answer Authority Addtional入口数量

6.3.5 Question数据格式

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

在域名的数据表示中,每一个label(每一个由.分隔的域)都有一个字符串长度前缀(占1字节)。www.metal-archives.comDNS中使用\x03www\x0Emetal-archives\x03com\x00表示,长度24字节

长度前缀只能取0x000x3F192及以上的前缀为特殊前缀0xC0开始

QNAME为上述格式的域名数据。长度可以是奇数,且无需对齐

QTYPERRtype,表示请求边缘服务器回复的数据类型,定义见下

QCLASSRRclass,恒定为IN1

QTYPE号 定义
1 A,IPv4地址
2 NS,下一级DNS服务器地址(域名形式)
3 淘汰
4 淘汰
5 CNAME,Canonical Name,别名,获取到真正域名后需要再次请求并返回结果。可以将一个提供单一服务的子域名映射到一个更高级的域名,例如将git.example.com映射到example.com,由该主机提供服务
6 SOA,zone列表的起始
7 实验
8 实验
9 实验
10 实验
11 WKSWell known service描述
12 PTR,反向解析地址
13 HINFO,主机信息
14 MINFO,邮箱信息
15 MX,优先级+邮箱服务域名
16 TXT,文本
28 AAAA,IPv6地址
252 AXFR,请求一个zone(通常是权威服务器之间)
253 实验
254 淘汰
255 *,请求所有RR

6.3.6 RR数据格式

该数据格式用于AnswerAuthorityAdditional,是这些数据域的基本组成单元

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                                               /
    /                      NAME                     /
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     CLASS                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TTL                      |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                   RDLENGTH                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
    /                     RDATA                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

前面部分定义和Question格式相同。相比添加了TTL表示RR的寿命,而RDLENGTH表明RDATA数据域的长度

AAAAA类型数据长度分别4字节和16字节(IPv4长度和IPv6长度)

HINFO格式如下

    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    /                      CPU                      /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    /                       OS                      /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

MX格式如下,开头有2字节的优先级。如果有多个MX,那么优先级数字最小的域名将会被选择

    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                  PREFERENCE                   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    /                   EXCHANGE                    /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

SOA格式如下,MNAMERNAME分别表示zone的发送方和邮箱,SERIAL用于区分新旧zone,REFRESH定义刷新时间

    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    /                     MNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    /                     RNAME                     /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    SERIAL                     |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    REFRESH                    |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     RETRY                     |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    EXPIRE                     |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    MINIMUM                    |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

域名字符串压缩机制

同样的域名在一个DNS数据包中通常只需出现一次即可。这使用到了之前介绍域名表示格式时的长度前缀特殊值。这个特殊值之后的1字节表示域名字符串的起始位置,可以通过下面的抓包理解

$ dig @10.80.192.1 netflix.com

netflix.com第一次出现的位置

第二次出现的位置,直接使用0xc0 0x0c代替。0x0c正是字符串netflix.com开头的0x07DNS数据中的偏移地址

6.3.7 使用命令行发送DNS请求与抓包

使用dig命令

Linux下可以使用dig命令向指定DNS服务器发送请求

$ dig @8.8.8.8 github.com A

得到如下结果

dig发送的请求数据包

服务器8.8.8.8回复的数据包

使用dig命令,默认只会请求IPv4,也就是说RR类型为A

$ dig @10.80.192.1 github.com

也可以请求其他类型的RR,示例

$ dig @8.8.8.8 github.com NS

; <<>> DiG 9.18.21 <<>> @8.8.8.8 github.com NS
...
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 1
...
;; QUESTION SECTION:
;github.com.			IN	NS

;; ANSWER SECTION:
github.com.		3153	IN	NS	dns1.p08.nsone.net.
github.com.		3153	IN	NS	dns2.p08.nsone.net.
github.com.		3153	IN	NS	dns3.p08.nsone.net.
github.com.		3153	IN	NS	dns4.p08.nsone.net.
github.com.		3153	IN	NS	ns-1283.awsdns-32.org.
github.com.		3153	IN	NS	ns-1707.awsdns-21.co.uk.
github.com.		3153	IN	NS	ns-421.awsdns-52.com.
github.com.		3153	IN	NS	ns-520.awsdns-01.net.
...

$ dig @8.8.8.8 github.com AAAA

; <<>> DiG 9.18.21 <<>> @8.8.8.8 github.com AAAA
...
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
...
;; QUESTION SECTION:
;github.com.			IN	AAAA

;; AUTHORITY SECTION:
github.com.		1469	IN	SOA	dns1.p08.nsone.net. hostmaster.nsone.net.
...

上述我们请求的AAAA并没有得到直接回答,是一次失败请求,只是通过SOA给出了权威服务器的域名,最后的Additional中会包含出错原因

使用nslookup命令

nslookup默认会请求A(IPv4)和AAAA(IPv6)

$ nslookup github.com 8.8.8.8

同样可以和dig一样指定请求内容

$ nslookup -type=NS github.com 8.8.8.8
Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
github.com	nameserver = dns1.p08.nsone.net.
github.com	nameserver = dns2.p08.nsone.net.
github.com	nameserver = dns3.p08.nsone.net.
github.com	nameserver = dns4.p08.nsone.net.
github.com	nameserver = ns-1283.awsdns-32.org.
github.com	nameserver = ns-1707.awsdns-21.co.uk.
github.com	nameserver = ns-421.awsdns-52.com.
github.com	nameserver = ns-520.awsdns-01.net.

...

使用host命令

host默认会请求IPv4、IPv6以及邮件

host github.com

6.3.8 现代操作系统环境的DNS查询流程

受博客文章 https://nova.moe/rethink-type-url-dns/ 启发,结合一些实际考据,进行复述

有关NSS:一些事实

首先需要说明,glibc集成了NSS(Name Service Switch facility),它为这个操作系统的应用提供了各种名字服务的调用方法,例如主机名,网络域名,用户名等。NSS可以访问/etc/passwd/etc/group/etc/hosts等本地文件,也可以直接发起DNS解析请求(使用nss-dns插件)

所有基于glibc的Linux发行版,以及FreeBSD,NetBSD(基于或曾经基于glibc),会依赖NSS,它们都有一个/etc/nsswitch.conf文件,其中指定了各项服务对应的数据库,例如group名字服务对应file数据库实际是查询/etc/group;而hosts名字服务(主机名解析服务)对应resolve数据库是向systemd-resolved发送查询请求

对于DNS域名解析支持,glibc给了getaddrinfo()供应用程序调用,而glibc的域名解析服务会读取/etc/resolv.conf来确定DNS服务器地址,从而发起请求

操作系统的网络管理守护进程可能会根据配置自动更改/etc/nsswitch.conf/etc/resolv.conf

ArchLinux下可以使用getent命令来调用/etc/nsswitch.conf中列出的名字服务

$ getent group root
root:x:0:brltty,root

$ getent hosts github.com
64:ff9b::8c52:7903 github.com

$ getent hosts localhost
::1             localhost

浏览器的DNS记录缓存

对于DNS解析结果,现在的浏览器可以支持缓存(host resolver cache),在Chromium中最多会缓存1000条DNS记录

根据实际测试,以github.com为例,我们的DNS服务器返回的A记录的TTL37秒,如下

此次刷新页面37秒后,我们再次刷新页面,通过Wireshark发现浏览器又发送了一次DNS请求查询github.com的地址。而如果在DNS给出的TTL时间之内刷新,则不会出现新的DNS请求

Chromium的DNS缓存记录可以通过页面chrome://net-export导出,并按照指示查看

NSS的查询功能

浏览器只有DNS缓存没有命中,才会调用glibc函数getaddrinfo()来发起DNS请求,此时这个请求就会被Wireshark捕获到(如果通过nss-dnsnss-resolve发起)。这里开始就涉及到getaddrinfo()是如何查询域名的问题

不同的Linux主机因为会使用不同的网络管理程序,网络配置,会安装不同的软件(例如容器、虚拟机),/etc/nsswitch.conf也是不同的

以下是博客作者nsswitch.confhosts行配置

hosts: files myhostname mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns

而在本机上nsswitch.confhosts行配置如下

hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns

mymachines是查询本机已经注册的虚拟机/容器名,resolve表示向systemd-resolved发起请求,files会直接查询本地文件例如/etc/hostsmyhostname表示查询本机主机名,dns表示由glibcnss-dns插件直接发起DNS请求

更早期的glibc中是使用gethostbyname()(deprecated)。而较新的getaddrinfo()在解决gethostbyname()不可重入问题的同时,也集成了getservbyname()的功能(man getaddrinfo

getaddrinfo()使用struct addrinfo存储socket相关信息。以下Demo程序使用getaddrinfo()发起对域名github.com的查询,输出对应的IPv4地址

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>

static const char name[] = "github.com";
static const struct addrinfo hint = {
    .ai_flags = AI_V4MAPPED | AI_ADDRCONFIG,
    .ai_family = AF_INET,
    .ai_socktype = SOCK_STREAM,
    .ai_protocol = 0,
    .ai_addrlen = 0,
    .ai_addr = NULL,
    .ai_canonname = NULL,
    .ai_next = NULL 
};

int
main(int argc, char* argv[]) {
    struct addrinfo *ai_arr, *p;
    char *addr;
    if (getaddrinfo(name, NULL, &hint, &ai_arr)) {
        fprintf(stderr, "Error occurred while getting address\n");
        return 1;
    }
    p = ai_arr;
    while (p != NULL) {
        if (p->ai_family == AF_INET) {
            addr = p->ai_addr->sa_data;
            fprintf(stdout, "IPv4 address: ");
            for (int i = 2; i < 6; i++) {
                fprintf(stdout, "%d ", (uint8_t)addr[i]);
            }
            fprintf(stdout, "\n");
            break;
        } else {
            p = p->ai_next;
        }
    }
    freeaddrinfo(ai_arr);
    return 0;
}

nsswitch.conf配置文件中各个数据源的含义

这里将本机配置再放一遍

hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns

systemd没有造出systemd-resolved这个轮子之前,glibc使用nss-dns(配置文件中为dns)插件来直接发起DNS查询请求。而在systemd-resolved出现以后,glibc给它专门设计了一个nss-resolve模块,如果在/etc/nsswitch.conf中配置了查询resolve,在查询过程执行到这里时就会向systemd-resolved发起请求。应用程序可以通过D-Bus向该守护进程发送请求,可以通过nss-resolve访问,也可以直接向地址127.0.0.53发送请求

考虑到systemd-resolved未启动的情况,[!UNAVAIL=return]可以及时返回,执行后面的查询

在配置文件中,resolve应当放在较前的位置(在files dns之前),因为通过files直接读/etc/hosts文件速度较慢(尤其在文件较大的情况下);而systemd-resolved不仅可以发起DNS查询请求,也会读取/etc/hosts以及查询本机hostname,同时也提供了缓存功能;dns放在靠后的位置是作为最终的解决方案

mymachines通常放在resolve之前。systemd提供了一个systemd-machined服务,虚拟机或容器管理工具可以将本机创建的实例的名称注册到systemd-machined。这样就可以在本机通过NSS得到这些容器/虚拟机实例的地址了。放在resolve之前是为了优先查找本地的这些实例

myhostname的功能已经被systemd-resolved包含。NSS可以通过直接调用gethostname获取本机主机名(/etc/hostname中配置的名称)、/etc/hostslocalhost_gateway_outbound对应的IP地址。这里依旧包含myhostname同样是考虑systemd-resolved未启动的情况,它的行为和systemd-resolved查询结果一致。它通常放在dns之前,files之后

总结

如果是通过浏览器发起一次DNS查询请求,首先需要经过浏览器的DNS查询结果缓存

如果浏览器缓存未命中,浏览器会调用glibc的NSS功能,根据/etc/nsswitch.confhosts项配置的顺序逐个进行查询,直到获得有效的回复。查询的途径包括:

本机的虚拟机/容器实例名

systemd-resolved提供的查询服务(包含DNS请求,/etc/hosts文件,本机主机名等,有缓存功能)

直接读取本地/etc/hosts等文件

通过gethostname得到本机主机名,localhost等名称,并查询对应的地址

直接的DNS查询请求

6.4 DHCP

参考RFC2131

DHCP协议由BOOTP发展而来,现在主要用于网络内IP地址的协商和分发。在一个网络内的主机可以通过DHCP请求获取支持其他支持正常工作的参数,包括掩码,默认网关,DNS服务器地址等。此外,DHCP服务器会尽量保证每次分配IP地址时,对于同一个MAC分配同一个IP地址。客户端申请IP地址时可以选择有限长或无限长(0xffffffff)的租期(lease),客户端可以申请延长租期

DHCP服务器也相当于存储网络中主机相关配置的数据库,其中的数据使用key-value形式存储

6.4.1 DHCP数据包格式

DHCP数据包传输基于UDP,服务器在端口67监听,客户端在端口68监听

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     op (1)    |   htype (1)   |   hlen (1)    |   hops (1)    |
   +---------------+---------------+---------------+---------------+
   |                            xid (4)                            |
   +-------------------------------+-------------------------------+
   |           secs (2)            |           flags (2)           |
   +-------------------------------+-------------------------------+
   |                          ciaddr  (4)                          |
   +---------------------------------------------------------------+
   |                          yiaddr  (4)                          |
   +---------------------------------------------------------------+
   |                          siaddr  (4)                          |
   +---------------------------------------------------------------+
   |                          giaddr  (4)                          |
   +---------------------------------------------------------------+
   |                                                               |
   |                          chaddr  (16)                         |
   |                                                               |
   |                                                               |
   +---------------------------------------------------------------+
   |                                                               |
   |                          sname   (64)                         |
   +---------------------------------------------------------------+
   |                                                               |
   |                          file    (128)                        |
   +---------------------------------------------------------------+
   |                                                               |
   |                          options (variable)                   |
   +---------------------------------------------------------------+
位域 作用
op 消息类型,1REQUEST(客户端发送),2REPLY(服务器发送)
htype 硬件地址类型,1表示MAC
hlen 硬件地址长度,MAC为6
hops 通常不使用为0DHCP中继会使用
xid Transaction ID,用于匹配一对请求和回复
secs 用于客户端消息,表示客户端从开始获取地址到目前的时间
flags 见下
ciaddr 用于客户端消息,表示客户端目前的IP地址(只在BOUND RENEW REBINDING状态下有效,此时客户端有可用的IP地址)
yiaddr 通常用于服务端,告知客户端分配的IPyour IP
siaddr 通常用于服务端的DHCPACK DHCPOFFER消息,告知客户端下一次请求时的DHCP服务器地址
giaddr 中继地址
chaddr 客户端硬件地址
sname 服务器名称,0x00结尾的字符串,不再使用
file Boot file name,不再使用
options 可选附加参数,开头有4字节的99 130 83 99魔法数,最后有一个end option。一定包含DHCP message type

flags定义如下

                         1 1 1 1 1 1
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |B|             MBZ             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    B:  BROADCAST flag

    MBZ:  MUST BE ZERO (reserved for future use)

有些TCP/IP协议栈不支持在没有IP地址时从数据链路层接收带有目标IP的数据包。B标记位就是用于解决这个问题,可以告知服务器发送IP分配通知时使用广播IP还是单播IP。现在已经很少使用,置0

6.4.2 DHCP消息类型

类型 代码 解释
DHCPDISCOVER 1 客户端发送的广播消息,请求可用的DHCP服务器
DHCPOFFER 2 服务器对于DHCPDISCOVER的回复,包含了一些配置参数
DHCPREQUEST 3 用于客户端回应确认服务器发送的参数,或证实IP地址的一致,或延长IP地址的租期
DHCPACK 5 服务器的回复,会包含配置参数
DHCPNAK 6 服务器通知客户端IP配置不正确,或IP租期到期
DHCPDECLINE 4 客户端告知服务器IP已被使用
DHCPRELEASE 7 客户端主动告知服务器释放为其分配的IP
DHCPINFORM 8 客户端已经手动配置IP后向服务器请求本地配置

6.4.3 DHCP工作过程

一次完整的DHCP过程可以描述为DHCPDISCOVER-DHCPOFFER-DHCPREQUEST-DHCPACK,最后可以使用DHCPRELEASE释放IP,如下图

首先客户端广播一个DHCPDISCOVER,等待DHCP服务器回应(ci yi si gi都为0)。之后DHCP服务器根据客户端要求回复一个广播或单播DHCPOFFER数据包(仅yi si有效),其中包含了一些配置数据

接下来DHCP客户端广播一个DHCPREQUEST数据包(仅si有效),并必须在其中的server identifier中指定其选择的DHCP服务器(一个网络内可以有多台DHCP服务器,但是通常不会这样做),以及requested IP address(和之前服务器中的yiaddr相同),外加一些可能还需要的配置参数请求。DHCPREQUEST中的secs必须和之前DHCPDISCOVER的相同

最后由DHCPREQUEST选中的服务器回复DHCPACK(仅yi si有效),并且在服务器内部建立一个chaddr-ipaddr格式的数据项,用于维护对应客户端的配置。如果服务器不能满足DHCPREQUEST,必须发送一个DHCPNAK

客户端接收到DHCPACK以后需要最后检查一下包含的配置参数,例如使用ARP检测IP是否已经被使用。如果是,那么需要广播一个DHCPDECLINE,表示发生问题

客户端释放IP时发送一个DHCPRELEASEci si有效),其中需要包含其硬件地址以及IP地址

DHCPREQUESTDHCPDISCOVER都需要包含客户端想要的参数列表(parameter request list选项)。DHCPDISCOVER也可以包含客户端想要的特定requested IP addressIP address lease time

DHCP也可以省略一些步骤,直接使用之前的配置。此时只会有DHCPREQUEST以及之后的步骤

在这种情况下,如果客户端移动到了另一个子网下(尤其是使用以太网的VLAN时),服务器要发送一个DHCPNAK。此后客户端必须重新请求

如果手动进行了IP地址的配置,之后使用DHCP时可以使用DHCPINFORM来请求其他本地参数,ciaddr有效。服务器回复DHCPACK

6.4.4 常用Options

每一个Option格式如下,长度可变

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |    code (1)   |  length (1)   |             data              |
   +---------------+---------------+                               +
   |                                                               |
   +                              ...                              +

常用Option如下

Option 作用 Length
0 Pad,占位符 0
1 Subnet mask,子网掩码 4
2 Time offset,相对UTC时间偏移 4
3 Router,可用的路由器IP,优先级高到低 4n
4 Time server,可用的时间服务器IP 4n
6 Domain name server,可用的DNS 4n
12 Host name,客户端主机名 >=1
15 Domain name,域名 >=1
42 NTP,可用的NTP时间同步服务器IP 4n
50 Requested IP address,客户端请求的IP 4
51 IP address lease time,IP租期 4
52 Option overload 1
53 DHCP message type,消息类型 1
54 Server identifierDHCP服务器IP 4
55 Parameter request list,参数请求列表 >=1
56 Message >=1
57 Maximum DHCP message size 2
58 Renewal (T1) time value 4
60 Vendor class identifier >=1
61 Client-identifier >=2
100 Time zone,时区
101 Time zone,tz database style
119 Domain search
121 Classless static route
255 End,结束 0

其中服务器端Options如下

Option                    DHCPOFFER    DHCPACK            DHCPNAK
------                    ---------    -------            -------
IP address lease time     MUST         MUST (DHCPREQUEST) MUST NOT
                                       MUST NOT (DHCPINFORM)
DHCP message type         DHCPOFFER    DHCPACK            DHCPNAK
Message                   SHOULD       SHOULD             SHOULD
Client identifier         MUST NOT     MUST NOT           MAY
Vendor class identifier   MAY          MAY                MAY
Server identifier         MUST         MUST               MUST
All others                MAY          MAY                MUST NOT

客户端Options如下

Option                     DHCPDISCOVER  DHCPREQUEST      DHCPDECLINE,
                           DHCPINFORM                     DHCPRELEASE
------                     ------------  -----------      -----------
Requested IP address       MAY           MUST (in         MUST
                           (DISCOVER)    SELECTING or     (DHCPDECLINE),
                           MUST NOT      INIT-REBOOT)     MUST NOT
                           (INFORM)      MUST NOT (in     (DHCPRELEASE)
                                         BOUND or
                                         RENEWING)
IP address lease time      MAY           MAY              MUST NOT
                           (DISCOVER)
                           MUST NOT
                           (INFORM)
DHCP message type          DHCPDISCOVER/ DHCPREQUEST      DHCPDECLINE/
                           DHCPINFORM                     DHCPRELEASE
Client identifier          MAY           MAY              MAY
Vendor class identifier    MAY           MAY              MUST NOT
Server identifier          MUST NOT      MUST (after      MUST
                                         SELECTING)
                                         MUST NOT (after
                                         INIT-REBOOT,
                                         BOUND, RENEWING
                                         or REBINDING)
Parameter request list     MAY           MAY              MUST NOT
Maximum message size       MAY           MAY              MUST NOT
Message                    SHOULD NOT    SHOULD NOT       SHOULD
Site-specific              MAY           MAY              MUST NOT
All others                 MAY           MAY              MUST NOT

6.5 Telnet

6.6 FTP

6.7 MQTT

6.8 CoAP

6.9 SMTP

6.10 SNMP

6.11 SSDP

6.12 SSTP

6.13 BGP

7 附录

7.1 Wireshark中部分TCP分析的定义

由于Wireshark只能从本机的角度进行TCP数据流的分析,所以是有局限性的,许多分析结果并不准确,实际使用时需要注意

TCP ACKed unseen segment

Set when the expected next acknowledgment number is set for the reverse direction and it’s less than the current acknowledgment number.

TCP Dup ACK

Set when all of the following are true:

  • The segment size is zero.
  • The window size is non-zero and hasn’t changed.
  • The next expected sequence number and last-seen acknowledgment number are non-zero (i.e., the connection has been established).
  • SYN, FIN, and RST are not set.

TCP Fast Retransmission

Set when all of the following are true:

  • This is not a keepalive packet.
  • In the forward direction, the segment size is greater than zero or the SYN or FIN is set.
  • The next expected sequence number is greater than the current sequence number.
  • We have more than two duplicate ACKs in the reverse direction.
  • The current sequence number equals the next expected acknowledgment number.
  • We saw the last acknowledgment less than 20ms ago.
  • Supersedes “Out-Of-Order” and “Retransmission”.

TCP Keep-Alive

Set when the segment size is zero or one, the current sequence number is one byte less than the next expected sequence number, and none of SYN, FIN, or RST are set.

Supersedes “Fast Retransmission”, “Out-Of-Order”, “Spurious Retransmission”, and “Retransmission”.

TCP Keep-Alive ACK

Set when all of the following are true:

  • The segment size is zero.
  • The window size is non-zero and hasn’t changed.
  • The current sequence number is the same as the next expected sequence number.
  • The current acknowledgment number is the same as the last-seen acknowledgment number.
  • The most recently seen packet in the reverse direction was a keepalive.
  • The packet is not a SYN, FIN, or RST.

Supersedes “Dup ACK” and “ZeroWindowProbeAck”.

TCP Out-Of-Order

Set when all of the following are true:

  • This is not a keepalive packet.
  • In the forward direction, the segment length is greater than zero or the SYN or FIN is set.
  • The next expected sequence number is greater than the current sequence number.
  • The next expected sequence number and the next sequence number differ.
  • The last segment arrived within the Out-Of-Order RTT threshold. The threshold is either the value shown in the “iRTT” (tcp.analysis.initial_rtt) field under “SEQ/ACK analysis” if it is present, or the default value of 3ms if it is not.

Supersedes “Retransmission”.

TCP Port numbers reused

Set when the SYN flag is set (not SYN+ACK), we have an existing conversation using the same addresses and ports, and the sequence number is different than the existing conversation’s initial sequence number.

TCP Previous segment not captured

Set when the current sequence number is greater than the next expected sequence number.

TCP Spurious Retransmission

Checks for a retransmission based on analysis data in the reverse direction. Set when all of the following are true:

  • The SYN or FIN flag is set.
  • This is not a keepalive packet.
  • The segment length is greater than zero.
  • Data for this flow has been acknowledged. That is, the last-seen acknowledgment number has been set.
  • The next sequence number is less than or equal to the last-seen acknowledgment number.

Supersedes “Fast Retransmission”, “Out-Of-Order”, and “Retransmission”.

TCP Retransmission

Set when all of the following are true:

  • This is not a keepalive packet.
  • In the forward direction, the segment length is greater than zero or the SYN or FIN flag is set.
  • The next expected sequence number is greater than the current sequence number.

TCP Window Full

Set when the segment size is non-zero, we know the window size in the reverse direction, and our segment size exceeds the window size in the reverse direction.

TCP Window Update

Set when the all of the following are true:

  • The segment size is zero.
  • The window size is non-zero and not equal to the last-seen window size.
  • The sequence number is equal to the next expected sequence number.
  • The acknowledgment number is equal to the last-seen acknowledgment number.
  • None of SYN, FIN, or RST are set.

TCP ZeroWindow

Set when the receive window size is zero and none of SYN, FIN, or RST are set.

The window field in each TCP header advertises the amount of data a receiver can accept. If the receiver can’t accept any more data it will set the window value to zero, which tells the sender to pause its transmission. In some specific cases this is normal — for example, a printer might use a zero window to pause the transmission of a print job while it loads or reverses a sheet of paper. However, in most cases this indicates a performance or capacity problem on the receiving end. It might take a long time (sometimes several minutes) to resume a paused connection, even if the underlying condition that caused the zero window clears up quickly.

TCP ZeroWindowProbe

Set when the sequence number is equal to the next expected sequence number, the segment size is one, and last-seen window size in the reverse direction was zero.

If the single data byte from a Zero Window Probe is dropped by the receiver (not ACKed), then a subsequent segment should not be flagged as retransmission if all of the following conditions are true for that segment: * The segment size is larger than one. * The next expected sequence number is one less than the current sequence number.

This affects “Fast Retransmission”, “Out-Of-Order”, or “Retransmission”.

TCP ZeroWindowProbeAck

Set when the all of the following are true:

  • The segment size is zero.
  • The window size is zero.
  • The sequence number is equal to the next expected sequence number.
  • The acknowledgment number is equal to the last-seen acknowledgment number.
  • The last-seen packet in the reverse direction was a zero window probe.

Supersedes “TCP Dup ACK”.

TCP Ambiguous Interpretations

Some captures are quite difficult to analyze automatically, particularly when the time frame may cover both Fast Retransmission and Out-Of-Order packets. A TCP preference allows to switch the precedence of these two interpretations at the protocol level.

TCP Conversation Completeness

TCP conversations are said to be complete when they have both opening and closing handshakes, independently of any data transfer. However, we might be interested in identifying complete conversations with some data sent, and we are using the following bit values to build a filter value on the tcp.completeness field :

1 : SYN

2 : SYN-ACK

4 : ACK

8 : DATA

16 : FIN

32 : RST

For example, a conversation containing only a three-way handshake will be found with the filter 'tcp.completeness==7' (1+2+4) while a complete conversation with data transfer will be found with a longer filter as closing a connection can be associated with FIN or RST packets, or even both : 'tcp.completeness==31 or tcp.completeness==47 or tcp.completeness==63'

7.2 CDN