Agent 裸传输协议:preface 与帧流
Agent 裸传输协议:preface 与帧流
本文说明 Agent 通过 tls://、tcp:// 与 quic:// 连接 Server 时,在帧协议之前发生的认证步骤。帧格式本身与 WebSocket 路径完全相同,见 websocket.md。
为什么需要 preface
WebSocket 在 HTTP 升级请求的 Authorization: Bearer 头里携带令牌,因此认证发生在任何帧之前。裸 TCP 与 QUIC 没有 HTTP 升级,所以在第一个帧之前插入一个固定格式的 preface。
preface 由一个只读取固定长度字段的小解析器处理,在令牌被接受之前,不会把任何字节交给帧解码器,因此未认证的字节不会触达 1 MiB 的帧负载上限。
布局
magic "TMAG" 4 字节
version u8 = 1 1 字节,preface 版本,与帧版本独立
flags u8 = 0 1 字节,保留;非零即拒绝
tokLen u16 (1..2048) 2 字节,大端
token tokLen 字节 与 WebSocket Bearer 相同的令牌
任何 magic、version、flags 或长度不符合上述约束的 preface 都会被拒绝。长度在读取令牌主体之前校验,服务端最多为一个未认证连接缓冲 2048 字节。
服务端应答
认证结果以两个字节返回,之后才开始帧交换:
status u8 0 ok | 1 unauthorized | 2 bad_preface | 3 unsupported_version | 4 busy
0 u8 保留,必须为 0
0:令牌有效。Agent 随后发送AgentHello,并按原有流程完成能力协商与元数据确认。1、3:Agent 把它视为拒绝(rejection),退避时间按拒绝处理,与 WebSocket 的 401 相同。2、4:普通失败,按普通重连退避处理。- 失败后服务端立即关闭连接。应答和错误信息都不包含令牌字节。
超时(默认 3 秒)未完成 preface,服务端返回 bad_preface 并关闭。
认证之后
令牌通过后,服务端把同一个 agentConnectionAuthentication 交给与 WebSocket 共用的核心逻辑(serveAgentConnection)。从此处开始,AgentHello、AgentMetadataAck、心跳、流控与帧语义与 WebSocket 路径完全一致。
心跳与空闲:Agent 每 30 秒发送一次 Ping,连续三个周期无响应即断开。服务端在收到 AgentHello 之前使用 1 秒的读取预算,之后使用 90 秒的每帧空闲预算。
QUIC 的流模型
QUIC 连接上只使用一个双向流。preface 与整个帧协议都在这个流上进行,因此:
- 帧格式、流复用、窗口与流控不需要任何改动。
- 服务端的
MaxIncomingStreams设为 1。额外的流会被 QUIC 层拒绝,因此一个对端无法通过多开流放大自己的未认证预算。 - 关闭该流即关闭整个连接,因为协议不使用其他流。
- ALPN 必须是
tunnelmesh-agent/1。使用其他 ALPN 的客户端在 TLS 握手阶段就会失败,永远到不了 preface。
安全边界
- 令牌只能在 TLS 或 QUIC 保护之下发送。
tcp://是明文,仅允许回环地址或显式的allow_insecure_transport。 - 服务端在认证之前执行三项限制:未认证连接总数、每地址的未认证连接数、以及每地址的认证失败次数。
- 令牌在服务端使用后会被清零,日志与错误信息不会输出 preface 内容。