技术说明
这份文档描述当前代码里的几条核心机制。消息字段、帧格式和握手时序的完整表见 线路协议。
运维端和设备都向外连接 exchange
家用和企业分支的设备通常在 NAT 后面,exchange 无法主动拨进去。因此两边都向 exchange 发起 WebSocket:
- 设备连
wss://<主机>/ws/device(明文调试时是ws://),在首帧里带上设备令牌注册 - 运维端连
wss://<主机>/ws/operator,握手使用运维令牌
exchange 自己只做鉴权、路由和留痕。监听地址由 --listen 或 REMOTE_LISTEN 决定,TLS 由前面的反向代理终止。
两套会话编号
运维端和设备各自维护会话编号,两边的数字互不占用。运维端发出请求时就可以带上自己的编号,不必先等服务端分配再发后续帧。设备侧编号由这台设备当前这条连接单独分配,从 1 起递增,两个运维端即使都使用本地编号 1,设备上也会落到不同的编号。
编号 0 不分配给任何会话,留给连接级消息。运维侧编号为 0,或这条运维连接已经占用了该编号时,登记直接失败,不会写入路由表。
翻译表在 crates/exchange/src/hub/session_table.rs。hub.rs 把翻译结果挂到具体连接上:设备侧是「设备编号 → 运维编号」,运维侧是「运维编号 →(设备连接, 设备编号)」。
转发时只改写会话编号
PTY 字节和命令的 stdout/stderr 走二进制帧。帧头 9 字节:1 字节流类型、4 字节 session_id、4 字节载荷长度,后面是裸载荷。定义在 crates/proto/src/frame.rs。
exchange 转发这种帧时只替换 session_id:
- 运维端发往设备:
crates/exchange/src/ws/operator.rs用DataFrame::with_session_id换成设备侧编号,再把编码后的字节放进设备的发送队列 - 设备发往运维端:
crates/exchange/src/ws/device.rs的handle_data把设备侧编号换回运维端自己的编号。源码注释写的就是这一处改写
载荷原样通过。路由不根据 PTY 按键或命令输出的内容做判断;查不到会话时帧被丢弃。控制消息是 JSON 文本帧,打开和结束会话时同样换成对端那一套编号,业务字段保持原样。
设备重连会顶替旧连接
同一设备再次连上时,新连接写入在线表并顶替旧连接,旧连接交给调用方关闭。不能靠拒绝新连接来处理「重复连接」:半开的旧连接可能还挂在表里,拒绝会让设备一直等到旧连接超时。
每条设备连接有一个 epoch(UUID),在 Hub::register_device 里生成。断开清理走 Hub::unregister_device,只有表里当前记录的 epoch 与这次清理带来的 epoch 一致时才摘掉设备并标为离线。旧连接的清理若晚于新连接注册,epoch 对不上,函数返回空列表,新连接继续留在表里。
发送队列有界,写满就断开这条连接
每条 WebSocket 对应一条容量 512 的队列,实现在 crates/exchange/src/hub/outbound.rs 的 channel。路由路径上持有连接表的锁,所以投递只用 Sender::try_send,不在锁里等待对端。
队列已满时 try_send 返回 SendError::Lagging,并置位关闭信号。Close 控制消息不进队列,只置位同一信号,因此队列堵死时仍然能要求连接退出。设备侧与运维侧的接入循环都在 tokio::select! 里等待 sender.closed()(crates/exchange/src/ws/device.rs、crates/exchange/src/ws/operator.rs)。信号一到,转发循环结束,随后按 epoch 注销这台设备或清理这个运维端的会话。对端已经把接收端丢掉时返回 SendError::Closed,与「来不及消费」区分开。
令牌是前缀加密文,密文只存 argon2
明文形如 <prefix>.<secret>。crates/exchange/src/auth.rs 的 split_token 按第一个 . 拆开,空前缀或空密文都视为格式错误。
签发时前缀是 8 字节随机数的十六进制,密文是 32 字节随机数的 base64url。数据库只保存两列:
token_prefix:明文,带唯一索引,校验时按它做一次查找token_hash:密文的 argon2id PHC 字符串,由hash_secret生成
设备和运维账号都按这个格式存。校验时用前缀取出哈希,再用 verify_secret 比对密文。明文只在签发当时返回给调用方,库里还原不出来。
argon2 参数是 8 MiB 内存、2 次迭代、并行度 1,低于库的默认档位。密文是 256 位随机值,慢哈希挡不住额外的穷举,却会把设备同时重连变成握手延迟。这组参数只适用于这种随机令牌。