Skip to content
This repository has been archived by the owner on Nov 14, 2023. It is now read-only.

Latest commit

 

History

History
137 lines (91 loc) · 6.13 KB

spec.md

File metadata and controls

137 lines (91 loc) · 6.13 KB

Juicity 协议规范

简体中文 | English

Juicity 的设计理念为简约且能将 quic 发挥得当。

约定

协议规范中出现的关键词“必须”、“应当”、“不应”应按照 RFC2119 中的描述进行解释。

  1. “必须”:对应 RFC2119 中的 “MUST”,意味着该定义是规范的绝对要求。
  2. “应当”、“不应”:对应 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,客户端无此要求。

协议设计

Authenticate

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。

Proxy

根据不同类型的四层协议,代理请求的行为略有不同,但代理头的格式是共享的。所有控制字段均为大端。

Header

代理头的具体格式如下:

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

对于每个 TCP 连接(<sip, sport, tcp, dest, dport>)的代理请求,客户端打开一个 stream,发送代理头和荷载,其中 Network 为 TCP。一个承载 TCP 的 stream 只需要发送一次 proxy header。

Juicity 不解决长度混淆问题,因此代理头可单独发送,也可与数据字段一起发送。当具体实现选择与数据字段一起发送时,为了防止一些游戏场景的服务端推送出现问题,即服务端发送首包的场景,在客户端没有发送数据超过一定时间后,必须单独发送代理头,推荐值为 100ms 到 300ms。

UDP

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 所存在的一些问题。

  1. 当 Tuic 的 udp_relay_mode 使用 native 时,在丢包线路中的应用层重试将变得严重,例如 DNS 的重试通常会发生在几秒后,较为影响体验。
  2. 当 Tuic 的 udp_relay_mode 使用 quic 时,每一个 UDP 数据报均使用单独的 unidirectional_stream 传输,消耗不必要的资源。

Juicity 使用 UDP over Stream 解决这上述问题,并在规范中给出更多实现建议和约束,以避免其他可能出现的问题。