3. 通过 HTTP 隧道传输 UDP (Tunneling UDP over HTTP)
为允许协商通过 HTTP 承载 UDP 的隧道, 本文档定义了 "connect-udp" HTTP upgrade token. 生成的 UDP 隧道使用 Capsule Protocol (见 [HTTP-DGRAM] Section 3.2), 并使用 Section 5 中定义格式的 HTTP Datagram.
为发起与单个 HTTP 流关联的 UDP 隧道, 客户端会发出包含 "connect-udp" upgrade token 的请求. 客户端通过 URI Template 的 "target_host" 和 "target_port" 变量向 UDP 代理指出隧道目标; 见 Section 2.
"target_host" 支持使用 DNS 名称、IPv6 字面量和 IPv4 字面量. 注意, 不支持 IPv6 作用域寻址区域标识符 (scoped addressing zone identifier). 使用 [URI] 中的 IPv6address、IPv4address、reg-name 和 port 术语时, "target_host" 和 "target_port" 变量必须 (MUST) 使用 [ABNF] 表示法, 遵循 Figure 2 中的格式. 此外:
-
"target_host" 和 "target_port" 变量都禁止 (MUST NOT) 为空.
-
如果 "target_host" 包含 IPv6 字面量, 冒号 (":") 必须 (MUST) 进行百分号编码. 例如, 如果目标主机为 "2001:db8::42", 则它在 URI 中会被编码为 "2001%3Adb8%3A%3A42".
-
"target_port" 必须 (MUST) 表示 1 到 65535 之间的整数, 包含端点值.
target_host = IPv6address / IPv4address / reg-name target_port = port
Figure 2: URI Template 变量格式
发送 UDP 代理请求时, 客户端应当 (SHALL) 执行 URI Template 展开, 以确定其请求的路径和查询部分.
如果请求成功, UDP 代理承诺在隧道关闭之前, 将收到的 HTTP Datagram 转换为 UDP 分组, 反之亦然.
根据 Capsule Protocol 的定义 (见 [HTTP-DGRAM] Section 3.2), UDP 代理请求不携带任何消息内容. 同样, 成功的 UDP 代理响应也不携带任何消息内容.
3.1. UDP 代理处理 (UDP Proxy Handling)
收到 UDP 代理请求时:
-
如果接收方被配置为使用另一个 HTTP 代理, 它将充当中介, 把请求转发给另一个 HTTP 服务器. 注意, 如果此类中介使用的 HTTP 版本不同于接收请求时使用的版本, 它们可能需要重新编码请求, 因为请求编码会随版本不同而不同 (见下文).
-
否则, 接收方将充当 UDP 代理. 它从根据请求头部重构的 URI 中提取 "target_host" 和 "target_port" 变量, 解码其百分号编码, 并通过直接打开到所请求目标的 UDP 套接字来建立隧道.
与 TCP 不同, UDP 是无连接的. 打开 UDP 套接字的 UDP 代理无法知道目标是否可达. 因此, 它需要在不等待来自目标的分组的情况下响应请求. 但是, 如果 "target_host" 是 DNS 名称, UDP 代理在回复 HTTP 请求之前必须 (MUST) 执行 DNS 解析. 如果此过程中发生错误, UDP 代理必须 (MUST) 拒绝请求, 并应当 (SHOULD) 使用合适的 Proxy-Status 头字段 [PROXY-STATUS] 发送详细信息. 例如, 如果 DNS 解析返回错误, 代理可以使用 [PROXY-STATUS] Section 2.3.2 中的 dns_error Proxy Error Type.
如果操作系统支持, UDP 代理可以使用已连接的 UDP 套接字, 这允许 UDP 代理依赖内核只向它发送匹配正确 5-tuple 的 UDP 分组. 如果 UDP 代理使用未连接的套接字, 它必须 (MUST) 验证收到分组的 IP 源地址和 UDP 源端口, 以确保它们与客户端请求匹配. 不匹配的分组必须 (MUST) 由 UDP 代理丢弃.
套接字生命周期绑定到请求流. 当请求流打开时, UDP 代理必须 (MUST) 保持套接字打开. 如果 UDP 代理收到操作系统通知, 得知其套接字不再可用, 它必须 (MUST) 关闭请求流. 例如, 收到 ICMP Destination Unreachable 消息时可能发生这种情况; 见 [ICMP6] Section 3.1. UDP 代理可以 (MAY) 因一段时间不活动而选择关闭套接字, 但关闭套接字时必须 (MUST) 关闭请求流. 对于在不活动一段时间后关闭套接字的 UDP 代理, 不应 (SHOULD NOT) 使用短于两分钟的时长; 见 [BEHAVE] Section 4.3.
成功响应 (定义见 Section 3.3 和 3.5) 表示 UDP 代理已经打开到所请求目标的套接字, 并愿意代理 UDP 负载. 成功响应之外的任何响应都表示请求失败; 因此, 客户端必须 (MUST) 中止该请求.
UDP 代理在把 HTTP Datagram 转发到 UDP 套接字时, 禁止 (MUST NOT) 在 IP 层引入分片; 过大的数据报会被静默丢弃. 在 IPv4 中, 如果可能, 必须 (MUST) 设置 Don't Fragment (DF) bit, 以防止路径上发生分片. 未来扩展可以 (MAY) 移除这些要求.
UDP 代理实现者阅读 [UDP-USAGE] 中的指导会有所帮助.
3.2. HTTP/1.1 请求 (HTTP/1.1 Request)
使用 HTTP/1.1 [HTTP/1.1] 时, UDP 代理请求将满足以下要求:
-
方法应当 (SHALL) 为 "GET".
-
请求应当 (SHALL) 包含单个 Host 头字段, 其中包含 UDP 代理的 origin.
-
请求应当 (SHALL) 包含值为 "Upgrade" 的 Connection 头字段 (注意, 根据 [HTTP] Section 7.6.1, 此要求不区分大小写).
-
请求应当 (SHALL) 包含值为 "connect-udp" 的 Upgrade 头字段.
不符合这些限制的 UDP 代理请求格式错误. 这种格式错误请求的接收方必须 (MUST) 以错误响应, 并应当 (SHOULD) 使用 400 (Bad Request) 状态码.
例如, 如果客户端配置了 URI Template "https://example.org/.well-known/masque/udp/\{target_host\}/\{target_port\}/", 并希望打开到目标 192.0.2.6:443 的 UDP 代理隧道, 它可以发送以下请求:
GET https://example.org/.well-known/masque/udp/192.0.2.6/443/ HTTP/1.1 Host: example.org Connection: Upgrade Upgrade: connect-udp Capsule-Protocol: ?1
Figure 3: HTTP/1.1 请求示例
在 HTTP/1.1 中, 本协议使用 GET 方法来模拟 WebSocket Protocol [WEBSOCKET] 的设计.
3.3. HTTP/1.1 响应 (HTTP/1.1 Response)
UDP 代理应当 (SHALL) 通过符合以下要求的回复来表示成功响应:
-
响应中的 HTTP 状态码应当 (SHALL) 为 101 (Switching Protocols).
-
响应应当 (SHALL) 包含值为 "Upgrade" 的 Connection 头字段 (注意, 根据 [HTTP] Section 7.6.1, 此要求不区分大小写).
-
响应应当 (SHALL) 包含单个值为 "connect-udp" 的 Upgrade 头字段.
-
响应应当 (SHALL) 满足启动 Capsule Protocol 的 HTTP 响应要求; 见 [HTTP-DGRAM] Section 3.2.
如果不满足其中任何要求, 客户端必须 (MUST) 将此次代理尝试视为失败, 并中止连接.
例如, UDP 代理可以这样响应:
HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: connect-udp Capsule-Protocol: ?1
Figure 4: HTTP/1.1 响应示例
3.4. HTTP/2 和 HTTP/3 请求 (HTTP/2 and HTTP/3 Requests)
使用 HTTP/2 [HTTP/2] 或 HTTP/3 [HTTP/3] 时, UDP 代理请求使用 HTTP Extended CONNECT. 这要求服务器按 [EXT-CONNECT2] 和 [EXT-CONNECT3] 的规定发送 HTTP Setting, 并要求请求使用符合以下要求的 HTTP 伪头字段:
-
:method 伪头字段应当 (SHALL) 为 "CONNECT".
-
:protocol 伪头字段应当 (SHALL) 为 "connect-udp".
-
:authority 伪头字段应当 (SHALL) 包含 UDP 代理的 authority.
-
:path 和 :scheme 伪头字段禁止 (SHALL NOT) 为空. 它们的值应当 (SHALL) 包含 URI Template 展开过程完成后的 scheme 和 path.
不符合这些限制的 UDP 代理请求格式错误 (见 [HTTP/2] Section 8.1.1 和 [HTTP/3] Section 4.1.2).
例如, 如果客户端配置了 URI Template "https://example.org/.well-known/masque/udp/\{target_host\}/\{target_port\}/", 并希望打开到目标 192.0.2.6:443 的 UDP 代理隧道, 它可以发送以下请求:
HEADERS :method = CONNECT :protocol = connect-udp :scheme = https :path = /.well-known/masque/udp/192.0.2.6/443/ :authority = example.org capsule-protocol = ?1
Figure 5: HTTP/2 请求示例
3.5. HTTP/2 和 HTTP/3 响应 (HTTP/2 and HTTP/3 Responses)
UDP 代理应当 (SHALL) 通过符合以下要求的回复来表示成功响应:
-
响应中的 HTTP 状态码应当 (SHALL) 位于 2xx (Successful) 范围内.
-
响应应当 (SHALL) 满足启动 Capsule Protocol 的 HTTP 响应要求; 见 [HTTP-DGRAM] Section 3.2.
如果不满足其中任何要求, 客户端必须 (MUST) 将此次代理尝试视为失败, 并中止请求.
例如, UDP 代理可以这样响应:
HEADERS :status = 200 capsule-protocol = ?1
Figure 6: HTTP/2 响应示例