QUIC协议解析与DDoS攻击分析

QUIC(Quick UDP Internet Connections)协议是一种基于UDP的新型可靠传输协议,目前主要用于HTTP/3.0。近年来,现网中逐步发现了一些基于QUIC协议的DDoS攻击痕迹,并且友商也已经开始了关于QUIC协议的DDoS攻防相关研究。因此,ADS团队针对该协议进行了一系列的分析和实验,为相关的DDoS攻击防御技术研究打下基础。

一、QUIC协议分析

1.1 协议简介

QUIC协议最早由Google于2013年提出,旨在创造一种新型的低延时、高可靠性的传输协议。之后,IETF在2021年正式发布RFC9000与一些相关的补充RFC,将QUIC协议标准化,并定义当前的协议版本为QUIC Version 1。

通常情况下,将早期Google自研的QUIC版本称为gQUIC。gQUIC经过了长时间的 发展与多次演进,与当前标准的QUIC Version 1版本有较大的差异,并且已经基本不再使用。因此本文将仅讨论RFC9000所定义的QUIC Version 1版本,不再涉及gQUIC版本。

QUIC协议相关的协议栈如上图所示,这是一种基于UDP实现的协议。实际上,QUIC的本质是一种传输层协议,因此原理上是可以直接基于网络层协议(例如IP协议)实现的。然而,当前网络中存在大量能够识别并且处理传输协议的设备,新增一种新的传输层协议会导致出现大量的兼容性问题。这样做会导致当前网络中所有的设备都需要进行升级。因此,为了兼容性考虑,才将其设计为基于UDP协议实现。

QUIC协议具有以下优势:

  • 高可靠性:虽然使用了UDP协议作为底层协议,但是QUIC采用了基于连接的通信方式,并且实现了类似于TCP协议的ACK确认、SEQ序号、排序、重传、差错控制、流量控制、拥塞控制等等机制,提供了一种具有高可靠性的通信方式。
  • 高安全性:QUIC协议天然集成了TLS3协议,并且对通信中几乎所有的报文都进行了加密,提供了极高的通信安全性。
  • 低延时:QUIC协议简化了传统TCP+TLS通信方式中的握手流程,支持1-RTT甚至0-RTT握手,大大降低了握手所带来的通信延时。
  • 彻底解决队头阻塞问题:在HTTP2协议中,就已经实现了基于流的并行通信,提升通信效率。但是,受限于TCP协议的队头阻塞问题,各个流之间并不能完全实现并行。QUIC协议基于UDP协议实现,彻底解决了队头阻塞问题,真正实现了多请求的并行处理。
  • 连接迁移:QUIC支持连接迁移特性。当通信双方的IP地址或者通信端口发生变化时,该特性能够继续维持之前的连接,继续使用该连接进行数据通信,不需要再重新建立连接。
  • 改进的流量控制与拥塞控制机制:QUIC支持Stream和Connection两级流量控制机制,能够采用更加灵活的流量控制算法。同时,由于QUIC协议的所有处理全部在用户态完成,应用程序可以很方便的选用与升级所使用的拥塞控制算法。

1.2 一些概念

QUIC协议中涉及到一些基本的概念,为了方便后续的阅读,本章将其中一些概念预先进行简单的介绍,以便后续阅读中更容易理解。

1.2.1 基础概念

  • 报文:QUIC的报文承载于UDP的载荷部分,具有独立的报文结构。因此,一个UDP报文中可以承载一个或者多个QUIC报文。
    • 根据协议要求,部分QUIC报文必须延续至一个UDP报文的结尾,即后续不能再承载更多的QUIC报文。
    • 部分情况下,协议对UDP报文的长度有要求。如果QUIC报文的长度不满足要求,可以使用全零的数据对UDP报文的后续内容进行填充。
    • 后文中,所描述的报文均指QUIC报文。如果指定的为UDP报文,会特别说明。
  • 帧:QUIC报文的载荷部分实际上是由一个个具有不同含义的结构体组成,这些结构体被称为帧,是构成QUIC报文载荷的基本单位。QUIC通信中,所有的应用数据均由特定的帧承载,大部分的控制信息也由不同类型的帧来表示。
  • 流:为了实现应用数据的多路并发,QUIC协议中所有的应用数据均在一条条流上进行通信。不同的流通过流ID来区分,各个流之间互不影响。
  • 连接:QUIC是一种基于连接的通信协议,所有的数据通信均在一个连接中进行。其中建立连接的过程被称为握手。

1.2.2 连接ID

通常情况下,在TCP与UDP协议中,均使用五元组(即传输协议、源IP、目的IP、源端口、目的端口)来标识一个连接,其中UDP虽然不使用连接的概念,但仍然可以据此来识别一次业务交互流程。

而QUIC协议引入了一个新的概念,即连接ID,来标识一个连接。如果使用了连接ID,则不能再仅依靠五元组来标识连接,而是可能出现以下两种情况:

  • 相同的五元组可以承载多个QUIC连接,通信双方能够根据连接ID来确认报文到底属于哪个QUIC连接。
  • 当通信双方的五元组发生变化时,不需要再重新建立连接,而是可以使用连接ID来继续在之前的连接上进行通信。

连接ID本质上来说,是一个长度不超过20字节(仅限QUIC Version 1版本)的整数,或者说字节序列。在一个连接中,实际上存在着两组连接ID,分别由客户端与服务器自行选择并发布。这其中每一个连接ID均可以用于标识该连接。这些连接ID被称为活动连接ID。

对于通信的每一方来说,在通信时,需要从对端发布的活动连接ID中,选取一个作为目的连接ID,并且使用一个本端发布的连接ID作为源连接ID(可能,通信中并非所有报文都需要携带源连接ID)。

活动连接ID在连接期间并非是不变的,通信双方可以通过以下方案发布或者退役活动连接ID:

  • 每端发送的第一个Initial报文中携带的源连接ID,默认为本端发布的第一个活动连接ID。
  • 服务器可以在传输参数中携带preferred_address(优先地址)参数,在该参数中需要向客户端发布一个新的连接ID。
  • 通信过程中,可以通过NEW_CONNECTION_ID帧向对端发布新的连接ID。
  • 通信过程中,可以通过RETIRE_CONNECTION_ID帧,向对端声明退役一个或者多个活动连接ID。
    • 注意,本端发布的退役声明是用于退役由对端发布的连接ID的。
    • 发布此声明,表示本端在之后的通信过程中,不会再使用已经被退役的连接ID作为目的连接ID来发送报文。而对端之后如果再收到目的连接ID为已经退役的连接ID的报文的话,可以当作异常报文来处理。
    • 通过NEW_CONNECTION_ID帧发布新的连接ID时,也可以指定需要退役的连接ID,要求对端对指定的连接ID进行退役。但这本质上可以理解为一种必须采纳的建议,并不能表示连接ID已经失效,仍然需要对端采纳并且正式发布RETIRE_CONNECTION_ID帧后,退役才真正生效。

此外,在通信过程中,还会使用一些临时的连接ID。这些连接ID不会作为活动连接ID被记录,仅在特定情况下被使用:

  • 客户端发送的第一个Initial报文,由于还未收到服务器发布的活动连接ID,客户端会自行选择一个临时连接ID作为目的连接ID,直到从服务器收到第一个响应报文为止。
  • 在源地址验证流程中,服务器在收到第一个Initial报文后,会回复一个Retry报文进行验证。Retry报文中携带的源连接ID,客户端需要在后续的Initial报文中带回,但这并不是一个活动连接ID,在后续的通信中,该连接ID不会再被使用。

需要注意的是,虽然目前观察到的绝大部分实现都使用了连接ID,但从协议上,连接ID可以被认为是一种可选的特性。具体为,通信中的一方或者双方可以在Initial报文中发布长度为零的连接ID,这实际上相当于宣布本端放弃使用连接ID特性,后续仅通过五元组来唯一标识该连接。使用零长度的连接ID后,后续将不能再通过NEW_CONNECTION_ID帧来发布新的连接ID,并且一些依赖于连接ID的特性(例如连接迁移)在本次连接中也将无法使用。

1.2.3 包号

QUIC通信中的大部分报文均会携带一个编号,即报文的包号。包号类似于TCP协议中的SEQ号,同样由发送端选择,接受端通过确认对应的包号来对报文进行确认。但与SEQ号不同的是,包号的选择与报文的长度没有关系,仅用于表示报文在发送序列中所处的位置。实际上,QUIC协议本身并没有严格规定每个报文的包号选取方式,仅仅要求后发送的报文,其包号必须大于先发送的报文。不过在观察到的绝大部分实现中,都采用了从0开始,每次递增1的做法。

另一个与SEQ号不同的地方在于,在一次连接中,并不是将所发送的所有报文依次排序。实际上,不同类型的报文有不同的包号空间,主要为以下三种:

  • Initial报文
  • Handshake报文
  • 0-RTT/1-RTT报文

一次连接中,不同的包号空间中的报文进行包号选择时彼此独立,互不干扰,仅需要在同类型的报文之间满足包号选取的要求即可。对应的,每个报文也只能由位于相同包号空间的报文进行确认,例如,一个Handshake报文,其包号选取与之前发送的Initial报文没有任何关系,并且也只能由对端发送的Handshake报文进行确认。

1.3 报文

QUIC的报文大体上可以分为两类:长报文头报文与短报文头报文。其中长报文头报文主要在握手阶段使用,而短报文头报文主要用于后续的数据通信过程。

1.3.1 变长编码整数

在后续的报文结构描述中,可能会看到如下的参数描述:

  • Name(n):表示该参数的长度为n比特位
  • Name(m..n):表示该参数的长度可变,范围为m到n比特位
  • Name(..):表示该参数长度可变,范围不固定。
  • Name(i):表示该参数为一个变长编码整数。

上述的变长编码整数,是QUIC协议中大量使用的一种对非负整数的编码方式。具体为,使用参数的最高两位(网络序)来表示参数的长度,而使用剩余的部分来表示参数的值。这种方式能够节省用于描述参数长度所需要的空间。具体的表示方式为:

1.3.2 长报文头

长报文头报文通常用于连接握手阶段,其主要结构如上图所示,具体参数含义为:

  • Header Form:固定为1,表示这是一个长报文头报文。
  • Fixed Bit:固定为1,这主要是为了在与一些其他协议共存的场景下使用。
  • LongPacket Type:表示该报文的具体类型,含义如下:
    • 0x00:Initial报文
    • 0x01:0-RTT报文
    • 0x02:Handshake报文
    • 0x03:Retry报文
  • Type-Specific Bits:根据报文类型不同,各有不同的含义。
  • Version:表示QUIC协议的版本号,当前版本(由RFC9000定义的版本)固定为1。
  • Destination Connection ID Length:目的连接ID字段的长度。
  • Destination Connection ID:目的连接ID。
  • Source Connection ID Length:源连接ID字段的长度。
  • Source Connection ID:源连接ID。
  • Type-Specific Payload:根据报文类型不同,各有不同的含义。

1. Initial报文

Initial报文是通信双方发送的第一个报文。Initial报文用于开启一个连接,并承载Client Hello以及Server Hello信息。

Initial报文的报文格式如上图所示,其中的特有字段为:

  • Packet Number Length:表示Packet Number字段的长度。需要注意的是,实际包号字段的长度,为该参数的值加一。
  • TokenLength:Token字段的长度。
  • Token:令牌,用于源地址验证。
  • Length:从该字段开始(不包含),当前报文的剩余长度。
  • PacketNumber:包号。

2. 0-RTT报文

0-RTT报文主要用于0-RTT握手流程,该流程依赖于TLS1.3的0-RTT握手机制。使用该报文可以在连接握手完成之前,就进行应用数据的发送。其报文结构如下:

3. Handshake报文

Handshake报文用于连接握手,承载了主要的TLS握手信息。具体来说,除了Client Hello与Server Hello外的所有其他TLS握手信息都由Handshake报文发送。其报文格式如下:

4. Retry报文

Retry报文属于一种比较特殊的长报文头报文,此类报文并不携带应用或者握手数据,而是专用于源地址认证机制。

Rerty报文的格式如上图所示,其特有字段为:

  • RetryToken:源地址认证令牌,用于进行源地址认证。
  • RetryIntegrty Tag:完整性标签,本质上是一种摘要数据,用于验证Retry报文的完整性和真实性。

Retry报文的作用,以及完整性标签的计算方式,将在源地址认证章节(1.7.1)中详细介绍。需要注意的是,该报文中并未携带Retry Token字段的长度信息,原因是根据协议要求,Retry报文必须延续至当前UDP报文的载荷末尾,后续不能再携带其他QUIC报文。

1.3.3 短报文头

QUIC协议中的短报文头报文即为1-RTT报文,其功能与0-RTT报文类似,只不过该报文用于在握手完成,协商出通信密钥后进行应用数据的发送。

1-RTT报文格式如上图所示:

  • HeaderForm:固定为0,表示短报文头报文
  • FixedBit:固定为1,作用与长报文头报文中对应字段相同。
  • SpinBit:延迟自旋位,用于一种测量网络延迟的机制。
  • KeyPhase:用于加密处理的标识,方便接收方识别用于保护该报文的密钥。

剩余的字段含义与长报文头报文相同,因此不再赘述。需要注意的是,短报文头中并不包含目的连接ID长度字段。这要求接收方有办法能够识别出目的连接ID的长度,常用的方案为发布固定长度的连接ID,或者采用特殊的编码方式来表示连接ID等。

1.3.4 版本协商报文

版本协商报文是一种特殊的报文格式。该报文格式并不在当前版本中定义,而是在版本无关的协议特性(RFC8999)中定义。该特性考虑了多版本演进与兼容的问题。

具体来说,如果服务器收到了一个QUIC报文,但是其指定的QUIC协议版本服务器并不支持,则可以通过发送一个版本协商报文通知客户端,并告知本端支持的版本列表。如果客户端能够支持对应的版本,则会重新使用对应的版本发起连接,否则无法进行通信。

版本协商报文的格式如下:

  • Version:版本号字段,固定为0。客户端正是依据该字段来判断这是一个版本协商报文。
  • Destination Connection ID与SourceConnection ID:目的与源连接ID,为接收到的报文中携带的源于目的连接ID,需要将其带回。
    • 与QUICVersion 1不同的是,该报文支持最多2040bit的连接ID,这是为了兼容后续的版本。也即,协议要求后续所有QUIC版本的连接ID字段不应该超过2040bit。
    • 版本协商报文只能由长报文头报文触发。
  • SupportedVersion:服务器支持的协议版本列表。

1.4 连接

1.4.1 握手

1. 1-RTT握手

QUIC协议通过握手流程来建立一个新的连接。最常规的握手流程为1-RTT握手流程,其主要流程为:

  • 客户端通过Initial报文发起连接,并携带ClientHello报文。
  • 服务器回复Initial报文,携带ServerHello报文。
  • 双方通过Handshake报文,通过TLS3协议协商数据加密的细节。
  • 完成握手后,通过1-RTT报文进行后续的应用数据通信。

以下为一次完整的1-RTT握手报文交互流程示例:

2. 0-RTT握手

除了1-RTT握手流程之外,QUIC还支持0-RTT握手流程,其主要流程如下:

由上图所示,0-RTT握手流程与1-RTT握手流程基本一致。实际上,0-RTT握手流程唯一的区别,即可以在握手完成之前,通过0-RTT报文预先进行应用数据的发送,该流程中依赖TLS1.3的0-RTT握手流程,使用了之前建立过的连接中协商好的密钥对数据进行加密。因此,这种做法不具有足够的前向安全性,建议谨慎使用。

1.4.2 连接关闭

QUIC的连接关闭流程与TCP协议有一定的类似之处,但也有许多不同。具体来说,QUIC连接具有三种关闭方式。

1. 正常关闭

QUIC协议中使用CONNECTION_CLOSE帧来进行连接的关闭。与TCP不同的是,QUIC使用了一种单工关闭机制,即只要有一方关闭连接,就无法在进行应用数据通信了。大致流程如下图所示;

如上图所示,当连接中的任何一方希望关闭连接时,会通过CONNECTION_CLOSE帧通知对端。之后,本端进入closing状态,不会再发送应用数据,并且在接收到任何应用数据之后,都只会回复CONNECTION_CLOSE。而在收到对端响应的CONNECTION_CLOSE帧后,则会彻底关闭连接。

得益于这种机制,QUIC的连接关闭动作只需要CONNECTION_CLOSE一种指令,不需要ACK,因此也不需要重传。

2. 空闲超时

空闲超时机制类似于TCP协议中的Keep Alive机制。通信双方可以约定一个空闲超时时间,并且各自维护一个定时器。当定时器超时时,即放弃本端的连接,立即进入draining状态。

该超时时间通过传输参数中的max_idle_timeout参数来进行协商,当通信双方中至少一方发布该参数时,该机制将被激活。如果只有一方发布该参数,超时时间为该参数指定的时间;而如果双方都发布了该参数,则超时时间为其中的较小值。

超时定时器将在以下情况下被重置,防止在正常通信过程中出现超时:

  • 从对端成功接收并且处理一个报文后。
  • 从上次接收到报文开始,本端发送第一个需要被确认的报文时。

3. 无状态重置

如果本端收到一个QUIC报文,其既不是Initial报文,又无法匹配上任何一个已经存在的QUIC连接,可以使用该机制通知对方出现异常需要关闭连接。这中情况通常发生在报文发送错误,或者本端应用程序异常关闭,导致连接信息丢失时。

在这种情况下,本端可以发送一种特殊的无状态重置报文,用于通知对方关闭连接。无状态重置报文的格式如下图所示:

无状态重置报文在设计上被伪装成了一个1-RTT报文,其中的Unpredictable Bits为无意义的随机填充,这种做法的主要目的是为了防止三方观察者察觉到这是一个无状态重置报文进而获取到无状态重置令牌。而该报文中实际有意义的,是最后的128bit,即为无状态重置令牌。

具体来说,本端在发布任何一个连接ID时,都应该使用特殊的方法计算一个相应的无状态重置令牌,并与连接ID一并发布给对端。这样,一旦本端收到了无法匹配连接的报文,就可以根据报文中的目的连接ID,根据相同的方式计算出对应的无状态重置令牌,并通过无状态重置报文发送给对端。

需要注意的是,无状态重置机制通常是由服务器在使用。这是因为通过Initial报文发布的第一个连接ID,其对应的无状态重置令牌需要通过传输参数stateless_reset_token进行发送。而客户端的传输参数是在Initial报文中进行发送的,这不够安全,所以客户端不允许发送stateless_reset_token传输参数。

对端收到无状态重置报文后,由于该报文被伪装成了一个1-RTT报文,因此对端无法直接解析该报文。实际的处理流程为,对端在收到任何1-RTT报文后,都会先取其最末尾的128bit数据,作为无状态重置令牌,并与当前所有活动连接ID的无状态重置令牌进行匹配。一旦成功匹配,则确定该报文是一个无状态重置报文,关闭对应的连接。如果匹配失败,则将当前报文作为一个1-RTT报文进行解析处理。

由以上机制可以看出,无状态重置报文比较类似于TCP协议中的RST报文,但二者也存在一些不同。主要为,无状态重置报文不能被用于关闭一个当前仍然存在的连接,这种情况只能使用CONNECTION_CLOSE帧进行关闭。

无状态重置机制本身也存在一定的风险,可能引发一些问题:由于无状态重置报文被伪装成了一个1-RTT报文,因此,如果该报文被发送往了一个错误的对端,则该报文本身也会引发一次无状态重置响应,这可能会导致无状态重置报文在两个服务器之间无限循环。针对该问题,协议提出了一些解决方案:

  • 协议要求端点发送的无状态重置报文,其报文大小必须小于触发它的那个报文。这会导致上述循环中能够发送的无状态重置报文越来越小,最终无法发送。
  • 此外,端点不应该在短时间内为相同的连接ID发送过多的无状态重置报文,这可能会引发风险。

1.4.3 传输参数

连接握手的主要作用,是进行以下信息的协商:

  • 通信密钥协商,以及对服务器和客户端(可选)的身份验证。主要通过QUIC-TLS(TLS3)实现。
  • 应用层协议协商,主要使用ALPN协议进行。
  • 传输参数协商。

传输参数是由端点在握手阶段发布的一系列通信参数,对端在通信过程中必须遵守由本端发布的传输参数,否则就会触发通信错误。其中,客户端通过Client Hello中的扩展字段quic_transport_parameters来发布传输参数;服务器则通过Multiple Handshake Messages中的该扩展字段来发布。传输参数主要包含以下几种:

传输参数 含义
original_destination_connection_id 客户端Initial中的目的连接ID
max_idle_timeout 最大空闲超时时间
tateless_reset_token 无状态重置令牌
max_udp_payload_size UDP报文最大载荷,类似MSS
initial_max_data Connection级别流控
initial_max_stream_data_bidi_local 本端双向流Stream级别流控
initial_max_stream_data_bidi_remote 对端双向流Stream级别流控
initial_max_stream_data_uni 单向流Stream级别流控
initial_max_streams_bidi 双向流量数限制
initial_max_streams_uni 单向流数限制
ack_delay_exponent ACK延迟指数
max_ack_delay 最大ACK延迟
disable_active_migration 禁用活动地址迁移
preferred_address 服务器首选地址
active_connection_id_limit 活动连接ID数上限
initial_source_connection_id 本端第一个Initial中的源连接ID
retry_source_connection_id Retry报文中的源连接ID

由上表中可以看出,除了用于发布通信限制的传输参数外,还有三个特殊的传输参数:original_destination_connection_id、initial_source_connection_id、retry_source_connection_id。这三个参数主要用于连接ID验证,该机制要求通信双方必须要在该参数内填入指定的连接ID,用于验证握手流程。

1.5 流

QUIC协议中的所有应用数据,均在一条一条的流上进行发送。不同的流通过流ID标识,不同的流可以同时进行数据传输。各个流之间的数据传输相互独立,互不影响。

流ID用于标识一条流,这是一个逐渐递增的非负整数。同一个流ID,在一次连接中只能被使用一次。

流ID的最后两位,被用于表示流的类型,主要分为以下几类:

  • 0x00:客户端创建的双向流
  • 0x01:服务器创建的双向流
  • 0x02:客户端创建的单向流
  • 0x03:服务器创建的单向流

需要注意的是,原则上同一个类型的流ID必须从小到大依次使用。如果直接开启一个高ID的流,那么相同类型且ID较小的流也会被默认开启。

此外,流仅用于发送应用数据以及与流相关的一些管理帧。而与连接相关的管理帧以及握手帧(CRYPTO)均不在流上传输。

1.5.1 流状态机

QUIC的流状态机比较简单,总体上来说可以分为发送状态机与接收状态机两种。单向流中,两端分别运行发送与接收状态机;而双向流中,两端均会同时运行两种状态机。在同一端同时运行的两种状态机之间存在一定的联系,但并不密切,因此可以独立介绍与理解。

1. 发送状态机

流的发送状态机如图所示,其状态转移关系为:

  • 当应用程序向QUIC要求发送数据时,会开启一条流并自动进入Ready状态,准备进行数据发送。
  • 之后,发送方开始进行数据发送,并转入Send状态。而当数据发送完毕后,会转入DataSent状态。该状态下,发送端将不再发送新数据,而是专心进行数据重传。
  • 之后,会等待对端对全部数据进行确认。一旦收到了所有的确认,则进入DataRecvd状态。这是一个最终状态,表示该流不会再被使用。
  • 如果在未收到所有的确认前,应用程序放弃了数据发送,或者收到了对端发送的STOP_SENDING帧,则本端会放弃发送数据,向对端发送RESET_STREAM帧并转入ResetSent状态。
  • 而在ResetSent状态下收到对端的确认后,会转入Reset Recvd状态,这同样是一个最终状态。

2. 接收状态机

流的接收状态机如图所示,其状态转移关系为:

  • 接收到对端发送的数据后,流会自动进入Recv状态并且开始接收数据。
  • 当接收到FIN标志位(STREAM帧中携带)时,表示对端已经完成了数据发送。此时,本端已经知晓对端需要发送的数据大小,进入SizeKnown状态。之后本端只接收重传数据,不再接收新的数据。
  • 当所有数据都成功接收后,进入DataRecvd状态。之后如果应用程序成功读取完所有数据,则进入Data Read状态,该状态为一个最终状态。
    • 接收过程中,如果发送了某些错误,或者本端应用程序放弃接收,则需要发送STOP_SENDING帧通知对方停止发送。但这并不会导致本端发送状态转移 。
  • 如果收到了对端发送的RESET_STREAM帧,则进入ResetRecvd状态。之后当应用程序读取到了这一次reset信息后,进入Reset Read状态,该状态为一个最终状态。
  • 比较特殊的是,如果在DataRecvd状态下收到RESET_STREAM帧,或者在Reset Recvd状态下接受完了所有的数据,QUIC协议的实现可以自行选择是将这些数据提交给应用程序,还是通知应用程序当前流已经终止。

1.5.2 流控

与TCP协议类似,QUIC也具有流量控制机制。但不同的是,QUIC只提供相关的机制,并不提供相对应的流控算法。因此,应用的实现者可以根据业务的特性,自行选择所使用的流量控制算法,也能根据实际情况进行灵活调整。

QUIC的流控机制简单来说,就是通过发布一个限制值,限制对端能够向本端发送的数据总量。具体来说,这个限制分为两个级别,Stream和Connection,分别用于限制在一个流以及当前连接上,对端能够发送的数据总量。具体来说,双方可以通过对应的传输参数为对方设置初始的流控值。之后如果需要,则可以通过MAX_STREAM_DATA或者MAX_DATA帧,来扩大Stream或Connection级别的流控限制。

流控的大体流程如下:

  • 接收方通过对应的传输参数,向对端发布初始的流控限制。
  • 发送方在发送数据时,必须同时遵循对端发布的Stream与Connection级别的流控限制。如果发送的数据量达到了限制值,则必须停止发送。
  • 此时,如果发送方还有数据需要发送,可以通过向对方发送一个STREAM_DATA_BLOCKED帧(对应Stream级别)或者DATA_BLOCKED帧(对应Connection级别),告知对方这一情况。注意,该行为并不是一个必须的行为。
  • 接收方在有需要时,可以根据自己的流控算法,扩大对应的流控限制,并通过MAX_STREAM_DATA或者MAX_DATA帧来给对方发布新的限制值。

由于同时存在Stream和Connection级别的流控,为了保证计算准确,通信双方需要就某条流上到底发送了多少数据达成一致,防止因为丢包和重传机制导致双方计算的数据量不一致,进而导致流控算法出现问题。协议中将一条流上发送的实际数据量称为最终大小,Connection基本的流控值将根据该大小进行计算。最终大小的计算方式如下:

  • 如果当前流正常终止,则其最终大小依据携带FIN标志位的STREAM帧进行计算,为其中的offset参数与length参数值之和。
  • 如果当前流异常终止,则其最终大小为RESET_STREAM帧中finalsize参数的值。

除了流量控制之外,QUIC协议还可以对对端能开启的流数量进行限制。其具体的处理机制与流量控制机制类似:

  • 通过传输参数发布初始限制。
  • 当启动的流数量达到上限后,可以通过STREAMS_BLOCKED帧通知对方(可选)。
  • 接收方可以通过MAX_STREAMS帧发布新的流控限制。

1.6 帧

帧是QUIC协议进行数据和控制信息传输的主要方式。其本质是一些结构各异的结构体。所有的帧都由一个变长编码整数表示的帧类型字段开头,后续则根据帧类型的不同附加不同的数据字段。当前QUIC协议所支持的帧类型如下,由于篇幅问题,本章仅简单列出各种帧类型以及其用途,不再做更详细的介绍。

帧类型 用途
PADDING 填充
PING 存活探测
ACK 确认报文
RESET_STREAM 终止一个流
STOP_SENDING 通知对端停止在流上发送数据
CRYPTO 密钥协商,TLS交换都通过该帧进行
NEW_TOKEN 向对端发送一个地址验证令牌
STREAM 携带应用数据
MAX_DATA 发布新的Connection级别流控
MAX_STREAM_DATA 发布新的Stream级别流控
MAX_STREAMS 发布流量数控制
DATA_BLOCKED 通知对端Connection级别流控被触发
STREAM_DATA_BLOCKED 通知对端Stream级别流控被触发
STREAMS_BLOCKED 通知对端流量数控值被触发
NEW_CONNECTION_ID 发布新的连接ID
RETIRE_CONNECTION_ID 退役连接ID
PATH_CHALLENGE 路径验证请求
PATH_RESPONSE 路径验证响应
CONNECTION_CLOSE 连接关闭
HANDSHAKE_DONW 握手结束

1.7 安全机制

QUIC协议对网络安全的关注度非常高,该协议在设计之初即考虑了大量的安全相关特性。以下将简单介绍部分的安全特性。

1.7.1 源地址认证

为了防止QUIC服务器被用于反射放大攻击,QUIC协议要求通信的双方在连接建立之初,对对方使用的源地址进行认证,防止对方使用虚假源进行报文发送。为了防止反射放大,在完成对对端的源地址认证之前,协议规定了如下的要求:

  • 通信双方发送的Initial报文,承载其的UDP报文大小不能低于1200字节,以减小在反射放大场景下能够达到的放大倍数。
  • 在完成源地址认证前,向对方发送的报文大小,不能超过从对端收到的报文大小的三倍。

源地址认证有几种方式,可以大致分为隐式认证和显式认证两类。此外,在进行连接迁移后,同样需要进行源地址认证来防止虚假源,此时使用的机制被称为路径认证。

1. 隐式认证

QUIC协议定义了一些隐式认证场景,即当某些场景满足时,即可认为已经完成了对对端的源地址认证。具体的场景如下:

  • 如果收到了对端发送的Handshake报文,并且成功通过握手密钥对其进行了解密,则说明对端一定收到了本端发送的Initial报文,并且据此计算出了正确的握手密钥。可以认为对端通过了源地址认证。
  • 如果对端发送的报文中携带了本端发布的活动连接ID,并且该连接ID中至少包含了64位的信息熵,也可以认为对端通过了源地址认证。

2. 显式认证

显示认证主要通过向对端发布源地址认证令牌来实现。具体来说,服务器可以向客户端发布源地址认证令牌,客户端在收到该令牌后,需要在Initial报文的Token字段中携带该令牌,以完成源地址认证。

源地址认证令牌可以通过以下两种方式发布:

  • 在完成握手后,服务器可以通过NEW_TOKEN帧,向客户端发布源地址认证令牌。在之后的一段时间内,如果客户端重新发起连接,可以使用该令牌来完成源地址认证。
    • 需要注意的是,该令牌的有效时间由服务器自行决定,因此客户端建立连接时使用的令牌可能是已经失效的,这种情况下则需要重新进行源地址认证。也因为这个原因,一般建议客户端在新建连接时,总是使用服务器最近一次发布的令牌。
  • 服务器可以在收到Initial报文后,向客户端发送Retry报文,向客户端发布源地址认证令牌。客户端在收到Retry报文后,必须重新向服务器发送Initial报文,并且在报文中使用刚刚收到的令牌进行源地址认证。
    • 这种方式发布的源地址认证令牌,其只能被使用一次,且优先级高于NEW_TOKEN方式发布的令牌。即重新发送的Initial报文必须使用Retry报文中携带的令牌,不能再使用之前的其他令牌。

3. 路径认证

路径认证机制既可以用于验证对端使用的源地址是否真实,还可以用于验证双方之间的传输线路是否可靠。

路径认证机制依赖PATH_CHALLENGE帧和PATH_RESPONSE帧进行。认证的发起方通过发送一个PATH_CHALLENGE帧来发起认证,该帧中会携带一段认证信息。该帧不能被ACK帧确认,而是必须要由PATH_RESPONSE帧进行响应,且响应帧中必须将认证信息带回。当收到响应后,路径认证完成,该行为既可以说明对端的源地址真实,也可以说明双方之间的通信线路正常。

路径认证理论上可以在通信过程中,随时由任何一方根据需要发起。而在进行连接迁移后,通信双方都必须要发起路径认证。

4. Retry报文完整性标签

Retry报文中,为了保证完整性标签的正确,防止被第三方篡改,会计算并携带一个完整性标签用于对报文进行验证。

完整性标签使用基于AES_128_GCM的AEAD算法进行计算。其中,加密密钥固定为0xbe0c690b9f66575a1d766b54e368c84e,nonce固定为0x461599d35d632bf2239825bb,plaintext固定为空字符串,而关联数据使用Retry报文以及接收到的Initial报文中所携带的信息,具体结构如下:

1.7.2 首包加密

QUIC协议集成了TLS1.3协议,因此天然会对通信报文进行加密,而其中甚至包括了客户端发送的第一个Initial报文。关于QUIC协议中的TLS部分,由其补充协议RFC9001进行定义,称为QUIC-TLS。

QUIC在整个通信过程中,总共使用四组密钥:

  • Initial密钥集:用于加密Initial报文
  • Early Data密钥集:用于加密0-RTT报文
  • Handshake密钥集:用于加密Handshake报文
  • Application Data密钥集:用于加密1-RTT报文

其中,Early Data密钥集是通信双方在之前建立的连接中所使用的密钥集,Handshake密钥集由Initial报文进行协商,Application Data密钥集由Handshake报文进行协商。而Initial密钥集最为特殊,由于此时客户端与服务器还未进行任何密钥协商流程,因此无法进行任何非对称加密的密钥交换。该密钥集事实上是根据一些信息计算出来的一个对称加密密钥集,因此其加密并非无法破解。这也是为什么服务器端的传输参数不能在Server Hello中进行传输的原因,因为承载Server Hello的Initial报文虽然进行了加密,但实际上是不安全的。

Initial报文的加密密钥计算原理如上图所示。首先,通过客户端发送的第一个Initial报文中的目的连接ID字段,加上一个固定的盐值,通过基于SHA256算法的HKDF密钥派生算法,将其扩展为初始密钥。之后,再分别使用lable字段client in和server in,将其分别扩展为客户端与服务器的对应密钥。最后,再分别使用lable字段quic iv、quic key和quic hp,扩展得到加密密钥、初始向量和包头保护密钥(将在1.7.3章节中详细介绍)。最后,根据计算出的加密密钥,通过基于AES-128-GCM的AEAD算法对Initial报文的载荷部分进行加解密。具体的加密流程如下图所示:

需要注意的是,如果进行了基于Retry报文的源地址认证,客户端在重新发送Initial报文后,本次通信中所使用的Initial密钥集需要根据新发送的Initial报文重新计算。

1.7.3 包头保护

QUIC协议不仅会对报文的载荷部分进行加密,对于头部中的部分字段,也会进行一定的加密,该行为被称为包头保护。下图为长短报文头报文中进行包头保护字段的一些例子:

上图中,标记为Protected的字段即为需要被加密的字段。对这些字段的加密依赖于一种被称为包头保护掩码的数据进行。掩码是一个长度为5字节的字节序列,分别用于对报文开头的1字节(实际上,长报文头仅保护其低4bit,短报文头仅保护其5bit)和包长字段进行保护。Retry报文不进行包头保护。掩码的具体使用方式如下:

掩码的计算,通过使用密钥集计算中得到的包头保护密钥(hp)对一些报文的摘要数据(上图中的Sampled Part)进行加密而得。摘要字段长度为128bit,取包号开头向后偏移4字节的位置。

二、DDoS攻击分析

2.1 洪泛攻击

洪泛攻击是DDoS攻击中最常见的攻击方式,主要存在两种攻击目的,其一是消耗被攻击者的带宽,其二是消耗被攻击者的资源或者处理性能。目前已知的QUIC协议洪泛攻击方式有以下几种:

1. 烂包攻击

由于QUIC协议对所有报文都进行了加密,构造真实的QUIC报文,对攻击工具的实现和攻击设备的处理性能都有一定的要求。因此,一些实现比较简单的攻击工具,会使用随机或者无意义的UDP报文,攻击服务器的QUIC端口。一方面用于消耗服务器的带宽,另外也能在一定程度上消耗服务器的处理性能。

这种攻击本质上是一种UDP Flood攻击,但是由于攻击的是QUIC服务器使用的端口,因此基于传统的限速之类的防护手段,很容易出现误杀。当然,由于实现简单,此类攻击的防御思路也很简单,只需要对报文进行一定程度的解析与检查,就能够轻易的识别出异常的报文,确认其源为攻击者。

2. Initial Flood

其基本原理实际上与烂包攻击类似,但其攻击工具实现更为完整。此类攻击工具能够正确构造并加密Initial报文,并据此向目标服务器发起攻击。由于报文为真实的Initial报文,服务器需要消耗更多的性能对其进行解密、处理以及响应,因此这种攻击方式对服务器的处理性能消耗更大。此外,这类攻击方式还大量的被用于触发QUIC反射攻击。

3. 连接攻击

此类攻击中,攻击者能够完整的实现QUIC协议的握手甚至应用数据交互流程。通常情况下,攻击者会使用大量的肉鸡设备向目标发起大量的QUIC连接,进而消耗服务器的处理能力。并且,当前有研究者发现,0-RTT握手流程对服务器的处理能力消耗巨大,能够得到良好的DDoS攻击效果。另外,如果攻击者能够建立起真实的连接,还可以进一步发动应用层的攻击。

目前,此类攻击方案仅在部分资料中被提及,实际在现网中观察到的此类攻击方式还较少。

4. 防御思路

针对以上的攻击方式,目前的主要防御思路有以下几种:

  • 异常报文检查:根据协议定义的一些规则,对攻击报文进行解析与检查,找出其中无法被正常解析或者不满足协议要求的报文。该方法消耗少、对正常业务影响小,但仅能用于应对烂包攻击以及部分实现较差的InitialFlood攻击。
  • 源认证:DDoS防护的常用思路。根据协议的部分规定,对QUIC通信流程做出一些干涉,或者对源发起一些探测行为。之后,根据源对这些干涉做出的反应,来判断源是一个正常的QUIC应用程序,还是一个攻击工具。这种方式防护效果好,消耗适中,且对虚假源的攻击方式有非常好的防护效果。但这种方式依赖于攻击工具对协议实现的不完整,因此大概率对连接攻击无效,对部分实现良好的InitialFlood攻击效果也可能不及预期。
  • 行为检测:无论攻击者将攻击行为隐藏得有多好,但为了实现攻击的目的,其通信行为必然与正常的业务行为有一定的差异。通过分析源的访问行为,找到其中异常或者不符合通常操作逻辑的地方,也能识别出攻击者。
  • 特征提取:通常情况下,一次攻击的攻击者会使用一种或者有限几种工具发起攻击。这种情况下,其用于攻击的报文可能具有某些相同的特征或者指纹。通过自学习或者人工干预,能够找到这些特征或指纹,并据此识别出攻击者。

2.2 反射攻击

2.2.1 攻击分析

反射攻击,即攻击者通过预先扫描并收集网络中可利用的反射服务器,并借由这些服务器发起的一种攻击。实际攻击时,攻击者会构造一些特定的报文并向反射服务器发送,这些报文的源地址被伪造为了攻击目标的地址。这样,反射服务器在收到这些伪造的报文后,将会向攻击目标回复响应报文。大量的响应报文将会消耗目标大量的带宽资源,达到DDoS攻击的目的。

反射攻击的关键,在于反射放大倍数。该倍数指的是反射服务器向目标发送的响应报文大小,与攻击者发送的伪造报文大小之比。该倍数越高,则越是可以通过较小的攻击流量引发较大的反射流量,以达到越好的攻击效果。

如上文所述,QUIC协议在设计之初,就考虑了服务器被用于反射放大攻击的可能性,因此提出了源地址认证机制,并且做出了一系列限制。根据协议要求,QUIC服务器的反射放大倍数不会超过三倍,这会导致其作为反射源的价值大大降低。然而,根据目前收集到的信息,有研究团队对全网的QUIC服务器进行了扫描,发现有大量的服务器都能够实现超过三倍,甚至超过三十倍的放大倍数。具体可以根据放大倍数的不同,分为以下几种情况:

  • 三倍以内的放大倍数:这是最常见的情况,此类服务器严格实现了协议的要求,虽然并非不能用于反射攻击,但是价值相对较小。
  • 三倍到三十倍的放大倍数:从与这类服务器的通信报文中发现,此类服务器虽然也满足源地址认证中响应报文不能超过收到报文大小三倍的要求,但是由于QUIC协议有重传机制,在无法收到后续响应的情况下,会多次重传响应报文。这会导致实际的反射放大倍数急剧增大,最多观察到三十倍的放大倍数。
  • 超过三十倍的放大倍数:扫描过程中发现,部分服务器的IP地址在收到Initial请求后,同时响应了多个Initial报文,且内容各不相同。据此猜测,这些IP地址因为某些原因,将Initial请求广播给了多个服务器同时进行处理。这种处理方式,再加上重传机制,因此出现了极大的反射倍数。

2.2.2 防御思路

反射攻击的防护思路与洪泛攻击具有一定的差异。由于对端是一个真实的QUIC服务器,而真实的攻击者已经无法被感知到,因此类似源认证之类的机制对反射攻击都是无效的。反射攻击的常用防御思路如下:

  • 端口封禁与限速:反射攻击最常用且效果最好的防御方式,如果确定某些防护目标不会出现对外的QUIC业务访问,可以直接禁止此类响应报文。但是该方案使用场景受限,如果防御目标存在此类正常业务,则会出现严重的误杀。
  • 连接监测:QUIC是一种基于连接的协议,因此在当前场景下,必须由防护目标主动发起连接,才有可能收到正常的Initial响应。通过监测防护目标是否有主动发起连接,则可以判断出响应报文是否为正常的业务报文。然而,这种方案对防御部署有要求,在很多部署方案中,防护设备并不能观测到防护目标发起的连接行为,导致该方案无法实现。
  • 行为检测:反射攻击中的报文行为与正常的业务有明显的区别。具体为,反射攻击的报文为大量的Initial报文或者Retry报文(可能性较小,此类服务器无法实现反射放大效果)。因此,通过检测源地址的通信行为,能够识别出其是否为一个反射源。

版权声明
本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author