QUIC 做为新一代的互联网传输协议,IETF QUIC 工作组在设计协议标准时除了关注优化性能,安全性也是需要重点考虑的。这篇文章介绍了 QUIC 的地址验证(Address Validation)。

地址验证主要是用于确保端点(endpoint)不能被用于流量放大攻击(traffic amplification attack)。攻击者如果伪造数据包的源地址为受害者的地址,发送大量的数据包给服务端,如果服务端没有进行地址验证,直接响应大量数据包给源地址(受害者),就会被攻击者利用、进行流量放大攻击。

QUIC 针对放大攻击的主要防御措施是验证端点(endpoint)是否能够在其声明的传输地址接收数据包。地址验证在连接建立(connection establishment)期间和连接迁移(connection migration)期间进行。

  • 连接建立时,为了验证客户端的地址是否是攻击者伪造的,服务端会生成一个令牌(token)并通过重试包(Retry packet)响应给客户端。客户端需要在后续的初始包(Initial packet)带上这个令牌,以便服务端进行地址验证。
  • 服务端可以在当前连接中通过 NEW_TOKEN 帧预先发布令牌,以便客户端在后续的新连接使用,这是 QUIC 实现 0-RTT 很重要的一个功能。
  • 当我们的网络路径变化时(比如从蜂窝网络切换到 WIFI),QUIC 提供了连接迁移(connection migration)的功能来避免连接中断。QUIC 通过路径验证(Path Validation)验证网络新地址的可达性(reachability),防止在连接迁移中的地址是攻击者伪造的。

下面我们将详细了解 QUIC 是如何进行地址验证(Address Validation)的:

连接建立时的地址验证

连接的建立隐式地为两端(both endpoints)提供了地址验证。特别是接收到一个用握手(Handshake)的密钥保护的数据包,意味着对端(peer)成功处理了初始数据包(Initial packet)。一旦服务端成功处理了一个客户端发送的握手(Handshake)数据包,那么它可以认为客户端的地址已经被验证为有效。

如果对端(peer)使用的连接ID(connection id)是端点(endpoint)所选择的,并且连接ID包含至少64位的熵,那么端点(endpoint)可以考虑对端(peer)的地址已经被验证了。对于客户端,第一个初始数据包(Initial packet)中的目标连接ID(Destination Connection ID)字段可以用来验证服务端地址。

服务端可能希望在开始加密握手之前验证客户端地址的有效性,QUIC 在初始数据包(Initial packet)中使用令牌(token)进行地址验证。这个令牌(token)可以在连接建立期间通过重试数据包(Retry packet)或在以前的连接中使用 NEW_TOKEN 帧传给客户端。只有令牌验证通过后才能进行后续的握手(handshake)。

注意事项

  • 在验证客户端的地址之前,服务端发送的字节数不能超过它接收到的字节数的三倍,这避免攻击者在地址验证之前进行放大攻击(amplification attack)。
  • 客户端必须确保包含初始数据包(Initial packets)的 UDP 数据报(datagrams)的 payload 至少为 1200 字节,如果少于 1200 字节则可以添加 PADDING 帧填充。
  • 客户端丢失来自服务端的初始(Initial)或握手(Handshake)数据包,可能导致死锁(deadlock)。为了防止这种死锁,客户端必须在探测超时(PTO,probe timeout)时发送数据包。如果客户端没有握手(Handshake)密钥,它应该发送一个包含 初始(Initial)包的 UDP 数据报(至少 1200 字节)。如果客户端有握手(Handshake)密钥, 则它应该发送一个握手(Handshake)数据包。

使用重试数据包(Retry Packets)验证地址

在接收到客户端的初始数据包(Initial packet)后,服务端可以通过发送包含令牌(token)的重试数据包(Retry packet)来请求地址验证。客户端在接收到这个重试数据包(Retry packet)的令牌(token)之后,必须在该连接中后续所有初始数据包(Initial packet)中附上该令牌(token)。

下图显示重试数据包(Retry packet)的使用:

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, "..."]

服务端在收到带有令牌的初始数据包时(Initial packet),它不能重新发送另外一个重试数据包(Retry packet)。服务端只能选择中止连接,或者允许该包被继续处理。

令牌(token)是随机生成的,攻击者不可能为自己的地址生成一个有效的令牌。因此它可以用于向服务端证明客户端接收到了令牌,地址是有效性的。

注意事项

  • 服务端还可以使用重试数据包(Retry packet)来延迟连接建立的状态和处理开销。这要求服务端提供一个不同的连接 ID 以及传输参数 original_destination_connection_id,强制服务端证明自己或与自己交互的实体接收到了客户端的原始初始数据包(Initial packet)。提供一个不同的连接ID还可以让服务端控制后续数据包的路由方式。这可用于将连接定向到其他服务端实例。
  • 如果服务端接收到包含无效令牌(token)的客户端初始数据包,则知道客户端没有通过地址验证。服务端可以丢弃数据包、关闭连接,并返回 INVALID_TOKEN 错误。

后续的连接使用 NEW_TOKEN 帧的令牌(token)

服务端可以在一次连接中向客户端提供地址验证令牌(token),该令牌可用于后续的连接。这对于 0-RTT 尤其重要,后续的新连接可以直接使用该令牌进行地址验证,而无需额外的 1-RTT。

服务端使用 NEW_TOKEN 帧向客户端提供地址验证令牌,该令牌可用于验证后续的连接。在后续的连接中,客户端在初始数据包(Initial packets)中包含该令牌,以提供地址验证。

重试数据包(Retry packet)中提供的令牌只能立即使用,不能用于后续连接的地址验证。而 NEW_TOKEN 帧生成的令牌可以在一个时间范围内使用,这个令牌应该有一个过期时间,可以是显式的过期时间,也可以是可用于动态计算过期时间的时间戳(timestamp)。服务端可以存储过期时间,也可以在令牌中以加密的形式包含它。

在 NEW_TOKEN 帧中接收到的令牌适用于任何具有权威性的服务端的连接(例如,证书中包含服务器名称)。当连接到一个服务端时,客户端为其保留了一个适用的和未使用的令牌,它应该在其初始包(Initial packets)的 Token 字段中包括该令牌。包含令牌可以允许服务器验证客户端地址,而无需额外的 RTT。客户端不得向正在连接的服务端发送不适用的令牌,除非客户端知道发出令牌的服务端和客户端连接的服务端正在共同管理令牌。

在无状态(stateless)设计中,服务端可以使用加密的和经过身份验证的令牌将信息传递给客户端,服务端稍后可以恢复这些信息并用它来验证客户端地址。令牌没有集成到加密握手(cryptographic handshake)中,因此它们没有经过身份验证。比如,客户端可能重复使用令牌。为了避免相关攻击利用这个特性,服务端可以限制令牌的仅包含验证客户端地址所需的信息。

注意事项

  • 客户端必须在它发送的所有初始数据包(Initial packets)中包含该令牌,除非重试数据包(Retry packet)将令牌替换为新的令牌。
  • 客户端不能在后续的新连接中使用重试(Retry)提供的令牌,因为重试包(Retry packet)的令牌只能在当前连接中立即使用。服务端可以丢弃任何不携带预期令牌的初始数据包。
  • NEW_TOKEN 帧颁发的令牌不能包含泄露连接(connection)的信息。例如,它不能包括以前的连接ID或地址信息,除非这些值是加密的。
  • 服务端必须确保它发送的每一个 NEW_TOKEN 帧在所有客户端中都是唯一的,除非是重新发送用来修复先前丢失的 NEW_TOKEN 帧。
  • 令牌允许服务端将创建令牌的连接和使用该令牌的连接建立关联。客户端如果不想继续使用服务端的令牌,可以丢弃从 NEW_TOKEN 帧获取的令牌。从重试包(Retry packet)中获取的令牌必须在当前连接尝试(connection attempt)立即使用,而不应在后续的连接尝试(connection attempts)中使用。
  • 客户端不应该在不同的连接尝试(connection attempts)中重复使用 NEW_TOKEN 令牌。
  • 客户端可能在一个连接上接收多个令牌。服务端可以发送额外的令牌来启用多个连接尝试(connection attempts)的地址验证,或者替换旧的已失效令牌。对于客户端来说,这种模糊性意味着发送最近未使用的令牌最有可能是有效的。虽然保存和使用旧令牌没有负面影响,但是客户端可以认为旧令牌对服务端进行地址验证不太有用。
  • 当服务端接收到带有地址验证令牌的初始包(Initial packet)时,它必须尝试验证令牌,除非它已经完成地址验证。如果令牌无效,则服务端应该把客户端还没有经过地址验证的一样继续操作,包括可能发送重试包(Retry packet)。如果验证成功,服务器应该允许握手继续进行。
  • 服务端应该对 NEW_TOKEN 帧和重试数据包(Retry packets)的令牌使用不同的编码(encode),并且更严格地验证后者。

令牌完整性(Token integrity)

令牌一定要很难被猜测到,在令牌中包含足够大的随机值即可。服务端需要记住它发送给客户端的值,以便于后续的地址验证。

令牌必须被完整性保护(integrity protection)覆盖,以防客户端修改或伪造。如果没有完整性保护,恶意客户端可以生成或猜测服务端可以接受的令牌。

令牌不需要一个定义良好(well-defined)的格式,因为生成令牌的服务端也会使用它。在重试数据包(Retry packets)中发送的令牌应包含允许服务端验证客户端数据包中的源 IP 地址和端口是否保持不变的信息。

在 NEW_TOKEN 帧中发送的令牌必须包含允许服务器验证从令牌发出时起客户端 IP 地址没有更改的信息。服务端可以使用 NEW_TOKEN 中的令牌来决定不发送重试包(Retry packet,),即使客户端地址已更改。如果客户端 IP 地址已更改,服务器必须遵守防放大(anti-amplification)限制。请注意,在存在 NAT 的情况下,此要求可能不足以保护共享 NAT 的其他主机免受放大攻击(amplification attack)。

攻击者可以重放令牌(replay tokens)从而使服务端做为 DDoS 攻击的放大器(amplifiers)。为了防止此类攻击,服务端必须确保防止或限制令牌的重放(replay)。服务端应确保在重试数据包(Retry packets)中发送的令牌仅在短时间内被接受。在 NEW_TOKEN 帧中提供的令牌需要更长的有效期,但不应在短时间内多次接受,鼓励服务端只允许令牌使用一次。如果可能的话,令牌可以包含有关客户端的附加信息,以进一步缩小适用性或重用。

路径验证(Path Validation)

路径验证被用于双端(both peers),当连接迁移(connection migration)时验证地址更改后的可达性(reachability)。在路径验证中,端点(endpoints)测试本地地址和对端(peer)地址之间的可达性,其中地址是 IP 地址和端口的二元组。

路径验证用于确保从迁移方接收的数据包不携带伪造的源地址。任何端点(endpoint)都可以随时使用路径验证。例如,端点可能会检查对端(peer)在静默期之后是否仍然是使用原来的地址。

路径有效性并不是被设计为一种 NAT 的穿透机制,尽管路径有效性对于建立支持穿透的 NAT 绑定来说是可能是高效的,但预期是对端(peer)在没有先发送数据包的情况下可以收到数据包。高效的 NAT 穿透需要额外的同步机制,而这种机制在这里并没有提供。

端点在路径验证时,可以把 PATH_CHALLENGE 和 PATH_RESPONSE 帧和其他帧组合发送。特别是端点可以在 PATH_CHALLENGE 帧中添加 PADDING 帧以达到 1200 字节,也可以将 PATH_RESPONSE 帧与它自己的 PATH_CHALLENGE 帧组合发送。

端点对从新的本地地址发送的探测使用新的连接ID。当探测新的路径的时候,端点希望确保对端(peer)有一个没有使用的连接 ID 来发送响应。如果对端(peer)的active_connection_id_limit 许可,端点可以在同一个包中发送 NEW_CONNECTION_ID 和 PATH_CHALLENGE 帧,这可以确保对端(peer)有一个未使用的连接 ID 用于发送响应。

启动路径验证(Initiating Path Validation)

启动路径验证,端点(endpoint)将发送一个 PATH_CHALLENGE 帧,其中必须包含一个不可预测的 payload,以便它可以将对端(peer)的响应与对应的 PATH_CHALLENGE 相关联。

端点可以发送多个 PATH_CHALLENGE 帧以防止数据包丢失。但是不应该在同一个数据包(packet)中发送多个 PATH_CHALLENGE 帧,而是要分别在不同的数据包(packet)中发送。

端点不应以高于初始数据包(Initial packet) 的频率发送 PATH_CHALLENGE 帧,这样可以确保连接迁移(connection migration)不会比在新路径上建立新连接有更多的负载(more load)。

端点(endpoint)必须将包含 PATH_CHALLENGE 帧的数据报(datagrams)至少扩展到 1200 字节。

路径验证响应(Path Validation Responses)

在接收到 PATH_CHALLENGE 帧时,端点(endpoint)必须通过 PATH_RESPONSE 帧回显 PATH_CHALLENGE 帧中的数据来。除非受到拥塞控制(congestion control)的限制,否则端点不得延迟传输包含 PATH_RESPONSE 帧的数据包。

PATH_RESPONSE 帧必须在接收到 PATH_CHALLENGE 的网络路径上发送。这可以确保只有在当路径的两个方向上都起作用时,路径验证才会成功。启动路径验证的端点不得强制执行此要求,因为这会导致对连接迁移的攻击。

端点必须将包含 PATH_RESPONSE 帧的数据报(datagrams)至少扩展到 1200 字节。

一个端点不能发送多个 PATH_RESPONSE 帧来响应一个 PATH_CHALLENGE 帧。对端(peer)根据需要可以发送更多的 PATH_CHALLENGE 帧,以引发额外的 PATH_RESPONSE 帧。

成功的地址验证(Successful Path Validation)

当接收到一个 PATH_RESPONSE 帧时,并且它包含先前 PATH_CHALLENGE 帧中发送的数据,则路径验证成功。

收到包含 PATH_CHALLENGE 帧的数据包的确认(ACK) 不能证明路径验证的有效,因为 ACK 可能被恶意对端(peer)欺骗。

失败的路径验证(Failed Path Validation)

路径验证只有在尝试验证的一方主动放弃时才会失败。

端点应该基于计时器(timer)来主动放弃路径验证。设置计时器(timer)时,要注意新路径的 RTT 可能比原始路径长。建议使用当前探测超时时间(PTO,Probe Timeout)或者新路径 PTO 中较大值的三倍。

这个超时允许多个 PTOs 在路径验证失败之前过期,因此单个 PATH_CHALLENGE 或 PATH_RESPONSE 帧的丢失不会导致路径验证失败。

注意,端点可以在新的路径接收包含其他帧的包,但为了成功验证路径有效,包含正确数据的 PATH_RESPONSE 帧是必需的。

当端点(endpoint)放弃路径验证时,即确认路径不可用。这并不一定意味着连接(connection)失败,端点可以根据需要继续通过其他路径发送数据包。如果没有可用的路径,端点可以选择等待直到有可用的新路径,或者关闭连接。

还有其他原因会导致放弃路径验证。比如当一个连接开始迁移到新的路径时,一个旧路径的验证正在处理。


QUIC 的全称是 Quick UDP Internet Connections protocol,由 Google 设计提出,目前由 IETF 工作组推动进展,其设计的目标是替代 TCP 成为 HTTP/3 的数据传输层协议。熹乐科技在物联网(IoT)和边缘计算(Edge Computing)场景也一直在打造底层基于 QUIC 通讯协议的边缘计算微服务框架YoMo,长时间关注 QUIC 协议的发展,本系列文章总结了学习 QUIC 协议时的知识点。

在线社区:discord/quic

维护者:YoMo

05-03 22:31