简介
Quic 全称 quick udp internet connection [1],“快速 UDP 互联网连接”,是由 Google 提出的使用 udp 进行多路并发传输的协议
当前quic协议是已经是目前正在itef拟规范的 http3协议 的底层实现协议,quic它最大的特点是基于udp。
Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势:
- 减少了 TCP 三次握手及 TLS 握手时间;
- 改进的拥塞控制;
- 避免tcp队头阻塞(head of line)的多路复用;
- 连接迁移(如移动4g网络移动至WIFI);
- 前向冗余纠错。
传统的https网络通信实现需要基tcp + tls + http ,tls的增加了网络传输中的rtt时长。特别是在弱网和移动端网络环境下,传统tcp+tls所带来的性能损耗不可忽略。
当前quic协议和http3协议仍旧在草案阶段,目前quic协议已经正式过审,http3协议已经到了34版本
传输层+会话层(udp + tls):https://datatracker.ietf.org/doc/draft-ietf-quic-transport/
应用层:(http3) https://datatracker.ietf.org/doc/draft-ietf-quic-http/
quic协议基础概念
quic协议可以看做是 udp + tls + http3协议的集合。下面的基础概念仅限传输层的部分,暂不包含http3实现的部分。大部分内容是基于IETF-29版本的内容进行翻译和基于quic-go的源码梳理。
-
connection
连接,用途在客户端和服务器之间建立连接。和tcp不同的是,quic是通过连接id(connectionId)来标识一个连接的,tcp是通过连接四元组(client ip、client port、server ip、server port)来确定一个连接的。这导致了如果其中有一个因素变化会导致在传输层层面上重新建链。而quic通过连接id可以实现在传输层层面上连接不变。即连接迁移。
和tcp一样,quic的每一个连接的建立都是开始于握手。只是quic的握手会附带上加密的quic-tls协议(quic-tls)
-
packet
包,是承接一个stream字节流的传输层单位,QUIC规定packet取决于至少1280字节的最小IP包大小。这个长度在ipv4的情况下默认是1252字节,在ipv6的情况下默认是1232字节。
一个packet由header及data组成。QUIC规定了两个类型的packet header。当前版本(29)的QUIC在建立连接时使用长包头(Long packet Header)。具有长报头的数据包是Initial,0-RTT,Handshake和Retry类型。版本协商使用与版本无关的独立Long packet header。
QUIC中规定的4中Long Packet Header
// PacketTypeInitial is the packet type of an Initial packet PacketTypeInitial PacketType = 1 + iota // PacketTypeRetry is the packet type of a Retry packet PacketTypeRetry // PacketTypeHandshake is the packet type of a Handshake packet PacketTypeHandshake // PacketType0RTT is the packet type of a 0-RTT packet PacketType0RTT
Long packet Header 的数据
Long Packet Header用于在建立1-RTT密钥之前发送的包。一旦1-RTT键可用,发送端切换到使用Short Packet Header发送数据包。Long Packet Header允许其他特殊的信息包(例如版本协商)以这种统一的固定长度信息包格式表示。
Long Packet Header的具体格式如下:
Long Header Packet { Header Form (1) = 1, //区分是LongPacket还是 shortPacket Longpacket时设置为1 Fixed Bit (1) = 1, Long Packet Type (2),// 区分长包类型,类型就是上面的4种 Type-Specific Bits (4), Version (32), //QUIC 版本号 Destination Connection ID Length (8),//目标connectionId 长度 Destination Connection ID (0..160), //目标connectionId Source Connection ID Length (8),//发送端connectionId 长度 Source Connection ID (0..160),//发送端connectionId }
版本协商(version negotiation) Packet header 如下:
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) ..., }
Short Header packet 的常见类型主要是 1-rtt packet
Short Packet Header具体格式如下:
Short Header Packet { Header Form (1) = 0,//区分是LongPacket还是 shortPacket Longpacket时设置为1 Fixed Bit (1) = 1, Spin Bit (1), Reserved Bits (2),//保留2个bit,一般用来保护header的(quic-tls) Key Phase (1),//quic-tls的phase Packet Number Length (2), Destination Connection ID (0..160), Packet Number (8..32), Packet Payload (..),//真正的数据 }
其他类型的LongPacketHeader格式在下文讨论细节是再详细讨论
-
frame (帧)
当收到packet后,packet的payload通过tls层的对称解密后,可以解析成一系列(≥1)的完整的frame(Version Negotiation、Stateless Reset、Retry Packet不含有frames)。在QUIC中定义了一系列的frame,分别来做上层不同的事情。
-
流,是一个有序字节数组的抽象。流既可以是单向的,也可以是双向的。stream在一个connection(连接) 里通过streamId来标识。在同一个连接里streamId不能重复。QUIC允许任意数量的流并行地发送。另外一种观点为:“可以把stream当做一个无限制长度的”消息““
QUIC的地址验证
地址验证最主要的目的是为了确保端点(endpoint)不会被流量放大(traffic amplification)攻击。在UDP的世界里,最常见的攻击就是流量放大攻击。例如Memcached反射型流量放大攻击。发起攻击者伪造成受害者的 IP 对互联网上可以被利用的 Memcached 的服务发起大量请求,Memcached 对请求回应。大量的回应报文汇聚到被伪造的 IP 地址源,形成反射型分布式拒绝服务攻击。
基于retry-packet帧的地址验证
QUIC针对流量放大攻击的主要防御措施是验证对端是否能够在其申明的传输地址接收数据包。大致原理是当quic 服务器收到一个新的连接请求时(一般是收到initial packet时),会先发送重试包(Retry Packet) ,重试包会携带一个token,客户端收到重试包后,会再次发起initial包并在包中带上这个token。由此来确定地址是真实发送的。在地址验证之前,为预防流量放大攻击,quic规定服务端发送的字节数不能超过它接收到的字节数的三倍。上述所说的quic收到新的连接请求一般会有以下两种场景,一是QUIC在建立连接时,另外是在连接迁移时。在这两种场景下均会出发地址验证
另外,quic还规定:
- 客户端必须确保包含初始数据包(Initial packets)的 UDP 数据报(datagrams)的 payload 至少为 1200 字节,如果少于 1200 字节则可以添加 PADDING 帧填充。
- 客户端丢失来自服务端的初始(Initial)或握手(Handshake)数据包,可能导致死锁(deadlock)。为了防止这种死锁,客户端必须在探测超时(PTO,probe timeout)时发送数据包。如果客户端没有握手(Handshake)密钥,它应该发送一个包含 初始(Initial)包的 UDP 数据包(至少 1200 字节)。如果客户端有握手(Handshake)密钥, 则它应该发送一个握手(Handshake)数据包。
可以看到,基于retry-token的方式会增加建链时的1个rtt,这对于支持0-rtt的quic来说在性能上还有有一定瓶颈。所以quic支持了在后续连接中使用NEW_TOKEN帧来进行处理
基于new-token帧的地址验证
服务端可以在一次连接中向客户端提供地址验证令牌(token),该令牌可用于后续的连接。这对于 0-RTT 尤其重要,后续的新连接可以直接使用该令牌进行地址验证,而无需额外的 1-RTT。这里可以看出,这个机制有点类似tls1.3中的session ticket机制。但是这也存在一定问题。攻击者可以重放令牌(replay tokens)从而使服务端做为 DDoS 攻击的放大器(amplifiers)。为了防止此类攻击,服务端必须确保防止或限制令牌的重放(replay)。服务端应确保在重试数据包(Retry packets)中发送的令牌仅在短时间内被接受。在 NEW_TOKEN 帧中提供的令牌需要更长的有效期,但不应在短时间内多次接受,鼓励服务端只允许令牌使用一次。如果可能的话,令牌可以包含有关客户端的附加信息,以进一步缩小适用性或重用。
newToken帧的发送一般是在首次建链成功握手完成后发放的,从图中可以看出,它的发放其实是和tls1.3的session ticket是在一个packet中发送的
后续还会介绍quic关于连接建立、流量控制、重传机制以及连接迁移的过程。