QUIC的连接标识(以下图中简称为连接ID或者CID)是端点用来将QUIC报文关联到具体的QUIC连接的关键字段,也是中间件的一致性路由的重要指示,所以报文中的连接标识是明文。
每个端点独立地选择自己使用的连接标识,在连接建立期间将自己选择的连接标识通过长首部报文通知给对端,以便对端发送报文时将其填入目的连接标识字段。因此,发送报文时使用对端提供的连接标识(即接收到报文的源连接标识)作为目的连接标识。
因为QUIC连接需要通过长首部报文确定两端的初始连接标识,所以长首部报文中既有目的连接标识又有源连接标识;而发送短首部报文时已经完成了连接的建立,也确定了两端使用的连接标识,因此只携带目的连接标识。连接标识的使用如图2-16所示,其中客户端选择使用的连接标识是c1,服务器选择的连接标识是s1。
图2-16 QUIC两端的连接标识
注意 QUICv1中,规定连接标识的长度是0~20字节间的整数字节长度;其他版本暂不明确,但受限于连接标识长度字段的长度是1字节,也就是说长度的最大值是255字节,因此,理论上的连接标识最大长度是2040位。
每个连接可以使用多个连接标识,可以发布的最大连接标识的数量由对端通过传输参数active_connection_id_limit指定。每个连接标识都有对应的序列号,用于发布和撤销的管理。发布连接标识有以下三种方式。
●建立连接时,通过初始报文的源连接标识发布,初始连接标识的序列号没有显式指定,固定为0。
●服务器通过传输参数preferred_address发布,其中发布的连接标识的序列号没有显式指定,固定为1。
●使用NEW_CONNECTION_ID帧发布,在帧中指定连接标识的序列号。
连接标识的序列号必须每次增加1,这样在撤销时可以指定要撤销连接标识的最大序列号进行批量处理,方便管理。客户端发送的首个初始报文中,目的连接标识和重试报文的源连接标识只临时用来加密和验证,因此并不在需要管理的连接标识之列,也就没有序列号。
在常规的QUIC使用中,通常只有连接迁移的情况下消耗连接标识,单次迁移到新路径消耗两端各一个连接标识。在QUICv1中,只有客户端可以发起连接迁移。当客户端发起连接迁移时,需要确定服务器发布了可用的连接标识,即服务器之前使用NEW_CONNECTION_ID帧发给客户端的连接标识。客户端也需要保证自己有额外的可用连接标识,且已经通过NEW_CONNECTION_ID帧发布给服务器,这样服务器才可以使用客户端新的连接标识填入新路径上的QUIC短首部报文中的目的连接标识。这个消耗连接标识的过程也可能发生在连接迁移之前,客户端使用探测报文验证新路径时。如果服务器已经没有可用连接标识,客户端不能够发起连接迁移。连接迁移的具体过程见3.6节。
理论上,端点可以在任何时刻切换到新的连接标识,但除了客户端有意的连接迁移,这样的做法并没有太大的意义。除非某个端点更改了连接标识的编码方案,需要平滑地切换到新的连接标识,或者某个端点有着管理连接标识的需要,导致某些连接标识会在一定时间内过期。但这样的情况一般重新建立QUIC连接就可以了,没有必要处理罕见的更换连接标识逻辑。所以我们对于更换连接标识的讨论重点放在连接迁移的场景。
一个端点发布连接标识后,必须能够接收处理发往该连接标识的报文。如果存在多个路径,每个路径都要有对应的连接标识,可能还需要维护每个路径的状态。所以,连接标识的数量并非越多越好,应该根据路径使用情况取适当的值。
使用零长度连接标识的终端不能发布连接标识,只能一直使用零长度连接标识。如果选择零长度连接标识,对端发起连接迁移时就会因为没有新的连接标识而失败。
连接标识的撤销有以下两种方式。
●通知对端撤销自己之前发布连接标识,这可以通过NEW_CONNECTION_ID帧中的Retire Prior To字段指定连接标识的序列号方式进行,如图2-17中NEW_CONNECTION_ID(连接ID=y,序列号=101,Retire Prior To=100)。通过这种方式停用连接标识,需要对端回复RETIRE_CONNECTION_ID帧确认。
●通知对端自己将不再使用对端发布的连接标识,这是通过RETIRE_CONNECTION_ID帧中指定连接标识序列号方式指定,如图2-17中RETIRE_CONNECTION_ID(序列号=100)。
这个过程使用了两个帧:NEW_CONNECTION_ID帧的作用为发布新的连接标识,并可能通知对端停用之前自己发布的连接标识;RETIRE_CONNECTION_ID帧的作用为通知对端不再使用它发布的连接标识,并可能请求新的连接标识。下面通过两个具体的例子说明这个过程。
图2-17展示了发布者主动停用自己发布的连接标识过程。在之前的连接过程中,服务器发布了一个值为x的连接标识,序列号为100;之后服务器想停用这个连接标识(x),于是发布新的值为y的连接标识,对应序列号101,同时通知客户端停用序列号小于等于100的连接标识;客户端收到后需要回复序列号为100的RETIRE_CONNECTION_ID帧,表明自己已经停用序列号小于等于100的连接标识,在本例中就是停用了连接标识x。
图2-17 停用自己发布的连接标识
图2-18则展示了停用对端发布的连接标识的过程。在之前的连接过程中,服务器发布了一个连接标识x,序列号为100;之后客户端想要停用这个连接标识,于是使用序列号为100的RETIRE_CONNECTION_ID帧通知服务器自己将停用服务器发布的序列号小于等于100的连接标识(本例中对应连接标识x),同时请求服务器发布新的连接标识;服务器收到后发布新的连接标识y,序列号为101。
图2-18 停用对端发布的连接标识
当连接标识为零时,使用UDP四元组关联QUIC连接。使用零长度连接标识的端点不能发布新的连接标识,也不能使用无状态重置,服务器选择零长度连接标识时还不能够提供首选地址。
当服务器选择使用零长度连接标识时,客户端不能简单地更改源地址或源端口号发起连接迁移。一方面,服务器缺少了连接标识信息,也就没有办法将不同的UDP四元组关联到相同的QUIC连接上;另一方面,中间件如负载均衡器会错误地认为迁移后的报文属于新连接,因此路由到新的服务器。因此,这种情况下也不能容忍NAT重绑定。在这种情况下,服务器可以使用disable_active_migration传输参数阻止客户端连接迁移,但是还是无法阻止NAT重绑定,所以服务器很少使用零长度连接标识。
这也不是说服务器使用了零长度连接标识,客户端就完全没有办法迁移,实际上还可以通过应用层的方案。比如服务器为每个客户端提供不一样的目的IP或目的端口号,然后使用目的IP和目的端口号来关联具体的QUIC连接。但是,负载均衡器仍然可能按照UDP四元组错误的路由,所以负载均衡器也需要知晓具体方案,使用目的IP和目的端口号路由。但这会给观察者提供可用信息,使其可以关联到客户端的网络活动。在HTTP中,可以通过HTTP替代服务来实现,如图2-19所示。
图2-19 零长度连接标识中的HTTP替代服务
相对来说,客户端选择零长度的连接标识影响较小,如果客户端不想使用QUIC的连接迁移功能,也不想在一个UDP端口上复用多个QUIC连接,就可以选择零长度的连接标识。
上文已经介绍了连接标识发布的几种方式:连接建立期间协商、通过首选地址发布、通过NEW_CONNECTION_ID帧发布。后两种都是连接建立完成后才能使用,发布也是通过协商的密钥进行的,所以没有必要验证。本节主要介绍连接建立期间,连接标识协商与验证的过程。
客户端首次向服务器发送初始报文时,选择自己使用的连接标识,这个标识作为初始报文的源连接标识,在连接建立期间不能改变;由于还不知道服务器选择的连接标识,所以使用一个随机数填入目的连接标识字段,这个目的连接标识也用于保护客户端和服务器发送的初始报文。如果客户端需要发送0-RTT报文,那么在收到服务器报文之前,也使用初始报文的目的连接标识。客户端选择的连接标识需要包含在传输参数initial_source_connection_id中,传输参数也在初始报文中发送给服务器。
服务器收到客户端的初始报文后,可能会发送重试报文验证客户端地址。该重试报文需要设置目的连接标识字段为客户端选择的连接标识,源连接标识字段设置为自己选择的临时连接标识。
客户端如果收到了重试报文,则将初始报文的目的连接标识字段设置为重试报文的源连接标识,添加重试令牌、更改保护密钥为新的目的连接标识,重新发送初始报文。
服务器收到客户端的首个初始报文,或者重试报文触发重发的初始报文后,选择自己的连接标识,并将自己的连接标识填入发送的初始报文的源连接标识字段中,以及之后发送的握手报文等相同字段中。为了验证连接标识,服务器将自己选择的连接标识包含在传输参数initial_source_connection_id中,并将收到的客户端初始报文中的目的连接标识填入传输参数original_destination_connection_id中,如果存在重试报文,还需要将重试报文的源连接标识填入传输参数retry_source_connection_id中。
客户端在收到第一个有效初始报文时,改变自己使用的目的连接标识,之后发送报文中的目的连接标识字段都设置为该初始报文中的源连接标识。
在两端都握手结束后,初始连接的连接标识就协商和验证完成了,之后可以使用短首部报文只包含目的连接标识通信了。握手完成保证了双方都验证了对端的TLS Finished消息,认定报文没有遭到篡改,也就验证了QUIC的传输参数,说明关键的几个连接标识也没有被篡改过。虽然服务器可以在第一次回应就发送1-RTT报文,但是此时还没确定握手是否遭到篡改。
典型的例子如图2-20所示,客户端首先选择了自己的连接标识c1,作为初始报文的源连接标识(图中SCID=c1),选择随机数x作为初始报文的目的连接标识(图中DCID=x),并将自己的连接标识填入传输参数中(图中initial_source_connection_id=c1)。服务器收到客户端的初始报文后,选择了s1作为自己的连接标识,作为初始报文中的源连接标识字段(图中SCID=s1),将客户端选择的连接标识作为初始报文中的目的连接标识字段(图中DCID=c1)。在服务器发送的握手报文中,需要设置传输参数initial_source_connection_id为s1,传输参数original_destination_connection_id为x。协商和验证完成后就只需要发送1-RTT报文,其中只有目的连接标识,客户端发送的报文只包含服务器选择的s1,服务器发送的报文值包含客户端选择的c1。
如果服务器发送了重试报文,如图2-21所示,客户端第一个初始报文同图2-20。服务器为重试报文选择了源连接标识s1(图中SCID=s1),目的连接标识字段设置为客户端选择的连接标识c1(图中DCID=c1)。客户端收到重试报文后更改初始报文的目的连接标识字段为s1(图中SCID=c1,DCID=s1)。服务器收到该报文后,重新选择一个连接标识s2,作为初始报文的源连接标识(图中SCID=s2,DCID=c1),同时设置传输参数retry_source_connection_id为s1,传输参数initial_source_connection_id为s2,传输参数original_destination_connection_id为x。客户端收到服务器的初始报文后,更改目的连接标识为s2,之后的报文使用s2发送报文。
图2-20 连接标识协商和验证
图2-21 重试情况下的连接标识协商和验证