Juicity 的设计理念为简约且能将 quic 发挥得当。
协议规范中出现的关键词“必须”、“应当”、“不应”应按照 RFC2119 中的描述进行解释。
- “必须”:对应 RFC2119 中的 “MUST”,意味着该定义是规范的绝对要求。
- “应当”、“不应”:对应 RFC2119 中的 “SHOULD”、“SHOULD NOT”,意味着在特定情况下忽略该定义可能存在正当理由,但在选择不同的方式之前必须理解并仔细权衡其全部含义。
协议规范中出现的 connection 一般指的是 quic connection。在 Juicity 中,TCP connection 承载在 quic connection 的 stream 上,因此:
quic connection : quic stream : TCP connection
= 1 : N : N
Juicity 使用的传输层是 quic,quic 保证了信息安全、多路复用、可靠性以及高带宽。
Juicity 要求 quic 应当至少支持 bbr 拥塞控制算法;要求 tls 的版本必须 1.3 以上, alpn 必须是 h3。
需要注意的是,一般情况下,quic 对 maxOpenIncomingStreams 存在限制,客户端必须维护对端 quic connection 可用 stream 的动态数量,在可用数量不足时建立新的 connection 处理该 stream 的打开请求。当客户端不具有这样的能力时,例如 quic 底层库未暴露该接口时,客户端必须在同一个 quic connection 中打开累计 30 个 streams 后新建一个 quic connection 处理后续 stream 的打开。一般地,quic 底层库暴露的数量是准确的,从上层通过 close 和 open 计数来维护该数量是不准确的,实现不应通过这种方式维护该数量。
服务端的 maxOpenIncomingStreams 参数必须大于等于 30,客户端无此要求。
Juicity 通过 UUID 和 password 对用户进行鉴权。一个 quic connection 可以承载多个 quic streams,对于每个 quic connection,认证只需要一次即可。
与 Tuic 一样,Juicity 的认证发生在建立 quic connection 时,客户端打开一个 unidirectional_stream 发送认证请求到服务端。
enum bit<8> CmdType {
Authentication = 0;
};
header auth_h {
bit<128> uuid;
bit<256> token;
}
header_union command_body {
auth_h auth;
};
header command_t {
bit<8> version;
CmdType cmd_type;
};
其中 version 为 0,token 使用如下方式产生:
ExportKeyingMaterial returns length bytes of exported key material in a new slice as defined in RFC 5705.
token = quicConnState.TLS.ExportKeyingMaterial(uuid, password, length=32)
认证请求没有应答。在客户端,代理请求无须也无法等待认证流程的结束,代理请求和认证请求可同时发送。在服务端,可同时接收认证请求和代理请求,但只有在认证成功后才开始处理代理请求,认证失败时则关闭整个 quic connection。
根据不同类型的四层协议,代理请求的行为略有不同,但代理头的格式是共享的。所有控制字段均为大端。
代理头的具体格式如下:
enum bit<8> Network {
TCP = 1,
UDP = 3
};
enum bit<8> AddrType {
IPV4 = 0,
IPV6 = 1,
DOMAIN = 2
};
header domain_address_t {
bit<8> len;
varbit<2048> domain;
};
// address_t can be one of ipv4, ipv6 and domain.
header_union address_t {
bit<32> ipv4;
bit<128> ipv6;
domain_address_t domain;
};
header proxy_t {
Network network;
AddrType addr_type;
address_t address;
bit<16> port;
};
对于每个 TCP 连接(<sip, sport, tcp, dest, dport>)的代理请求,客户端打开一个 stream,发送代理头和荷载,其中 Network 为 TCP。一个承载 TCP 的 stream 只需要发送一次 proxy header。
Juicity 不解决长度混淆问题,因此代理头可单独发送,也可与数据字段一起发送。当具体实现选择与数据字段一起发送时,为了防止一些游戏场景的服务端推送出现问题,即服务端发送首包的场景,在客户端没有发送数据超过一定时间后,必须单独发送代理头,推荐值为 100ms 到 300ms。
Juicity 的 UDP 数据报基于 quic stream 传输,类似于 UDP over TCP。为了实现更好的 full-cone NAT,每一个源地址三元组(<sip, sport, UDP>)的数据报应当在同一个 stream 中传输,源地址三元组没有对应的 stream 时打开一个 stream。
由于每个 UDP 数据报均可指定不同的目的地址,因此对于每个 UDP 数据报的代理请求均要发送代理头和荷载,其中 Network 为 UDP。与 TCP 不同的是,荷载前需要给出 2 字节的荷载长度,如下:
[proxy header][len][payload]
在客户端维护源地址三元组与 stream 的映射,并通过 NAT timeout 控制映射和 stream 的生命周期。具体地,在没有收到和发送任何数据包超过 timeout 后,stream 可被关闭,并删除映射。
在服务端,一个 stream 对应一个 outbound UDP 端点,在 stream 关闭后删除 UDP 端点映射,服务端也可(MAY)建立 nat timeout 机制,在 timeout 后关闭 stream。在这种情况下,服务端的 nat timeout 应当大于建议值 3 分钟。
Juicity 的 UDP 支持 dial domain,服务端实现需要为每个承载 UDP 的 stream 建立一个域名到 ip 的映射,以便于在读出代理头的时候并其转为 IP 时保持映射稳定。
Juicity 是基于 Tuic 的改进,主要改进 Tuic 的 UDP 所存在的一些问题。
- 当 Tuic 的 udp_relay_mode 使用 native 时,在丢包线路中的应用层重试将变得严重,例如 DNS 的重试通常会发生在几秒后,较为影响体验。
- 当 Tuic 的 udp_relay_mode 使用 quic 时,每一个 UDP 数据报均使用单独的 unidirectional_stream 传输,消耗不必要的资源。
Juicity 使用 UDP over Stream 解决这上述问题,并在规范中给出更多实现建议和约束,以避免其他可能出现的问题。