Files
forward_server/MAIN_SERVER_IMPLEMENTATION_PLAN.md
T
2026-10-22 17:07:08 +08:00

18 KiB
Raw Blame History

Main 转发通道接入方案

本文对应当前 forward_server 实现的接入契约。本文只描述 Main 的后续修改,不能视为 Main、Node 或 Web 已完成接入。各项目在自己的目录维护协议类型和校验,不跨目录导入生产协议源码。

1. 已实现的转发服务接口

启动接口已升级为配置对象,不再支持旧的位置参数:

const relay = await start_forward_server({
    forward_id: 0,
    key: config.forward_secret_key,
    http_server,
    path: '/api/forward_server',
    namespace: '/',
    allowed_origins: config.forward_allowed_origins,
    allow_no_origin: true,
    control: authenticated_control,
    logger: console_print,
});

forward_id 为非负安全整数,内置服务为 0。node_id/bot_id/user_id 为正安全整数。密钥至少 16 字符,不允许已知默认值兜底。允许的 Origin 必须是准确的 HTTP(S) Origin,不能填写 * 或包含路径。Node 可以不发送 Origin,但仍必须完成 token 和会话鉴权。

借用 http_server 时不能再指定 host/port;自建服务使用 host/port。返回的 handle 包含 io/http_server/store/forward_id/path/namespace/owns_http_server、bind_control(transport) 和幂等的 stop()。Main 保留 handle,在进程退出时显式停止;转发服务的关闭不会关闭借用的主站 HTTP server。

关闭默认最多等待 5 秒,可用 limits.close_timeout_ms 调整;超时强制释放自有 TCP 连接并返回失败,仍复位实例状态。借用宿主的 TCP 连接不归 Forward 强制销毁。

重复相同配置启动返回同一实例;配置变化必须先停止再启动。控制重连通过 bind_control() 重新安装处理器,不能通过重复启动来隐式换控制连接。旧的 disposer 不影响新控制绑定。

控制 transport 只要求 on/off/emit,支持 Main 内置 EventEmitter、Main 服务端 Socket 及 Node 客户端 Socket。数据 Socket 无法调用控制命令。

2. 控制通道 wire 契约

所有时间均为 Unix 毫秒,UUID 使用 crypto.randomUUID()。node_session_id 是当前已认证 Node 连接标识,允许非空、最大 256 UTF-8 字节字符串,不要求 UUID。Main 当前内部保存的 socket.id 可以作为该值,但必须在 Node 注册成功 ACK 中显式下发。

type ForwardAck<T> =
    | {status: 'success'; data: T}
    | {status: 'error'; data: {
        code: ForwardErrorCode;
        message: string;
        retryable: boolean;
    }};

interface SessionScope {
    forward_id: number;
    forward_session_id: string;
    node_session_id: string;
    node_id: number;
    bot_id: number;
}

输入 DTO 为精确字段白名单,不能附加数据库实体、内部配置或其他字段。

事件 请求 成功 data
forward_open_session SessionScope & {lease_expires_at:number} 实际 scope 与 lease_expires_at
forward_authorize_grant {forward_session_id,grant_id,user_id,permissions,expires_at} 同结构的实际授权信息
forward_revoke_session {forward_session_id,reason} {forward_session_id,revoked:true}
forward_revoke_grant {forward_session_id,grant_id,reason} {forward_session_id,grant_id,revoked:true}

同一 session 的 scope 不可改变,同一 Node/Bot 不能同时登记不同 session;先撤销旧会话,再建立新会话。同一 grant 的用户和权限不可变,权限改变时撤销后分配新 UUID。续期使用原身份和权限,延长时间。撤销消息幂等,reason 为非空、最大 256 UTF-8 字节字符串。

Forward 在本进程中保留有界撤销记录,期限为最大 token 寿命加时钟偏差,防止迟到的 open/authorize 复活已撤销身份。未过期撤销记录计入 session/grant 容量,达到限制返回 RATE_LIMITED,不提前逐出撤销记录。Main 收到容量限制时应退避,不能紧循环分配新 UUID。

默认 session 租约上限为当前时间后 5 分钟;grant 到期不得超过 session。Main 应统一计算截止时间,给网络传输保留余量,不能在远端处理时已经过期。撤销的 UUID 不得重新使用。

forward_session_status 为 Forward 向 Main 的单向通知,字段为:

interface ForwardSessionStatus extends SessionScope {
    node_ready: boolean;
    reason: string;
    state: 'open' | 'ready' | 'unavailable' | 'closed';
    reported_at: number;
}

Main 只接受当前转发控制连接的报告,再核对本地 session 的完整 scope。closed 立即使本地会话不可复用,unavailable 表示转发 Node 未就绪,不覆盖 Bot 游戏状态。通知没有 ACK;申请成功仍须依靠有截止时间的命令 ACK,不能依赖通知到达时序。

Socket.IO 控制请求使用带超时的单 Socket ACK。EventEmitter 适配器使用自己的定时器和一次性回调,不能调用 .timeout() 或 .disconnect()。禁止将旧的数字 ACK 解析器用于本协议。

3. Token 签发契约

Main 在本项目的 token 工具中实现签发,格式保持 AES-256-GCM:base64url(IV[12] + tag[16] + ciphertext),密钥为共享 secret 的 SHA-256。使用随机 IV,JSON 使用 UTF-8,不输出密钥或 token 到日志。

interface RelayClaims extends SessionScope {
    v: 1;
    role: 'node' | 'web';
    user_id?: number;
    grant_id?: string;
    permissions: RelayMethod[];
    issued_at: number;
    join_before: number;
    expires_at: number;
    jti: string;
}

Node token 不带 user_id/grant_id;Web token 必须同时带这两个字段。jti/forward_session_id/grant_id 为 UUID。权限不重复,只能取 bot.snapshot/bot.logs/bot.subscribe/bot.unsubscribe/bot.chat。Web 权限不得超出 Main 已登记的 grant,Node 权限应覆盖它需要执行的 RPC。

事件读取权限为:log 需要 bot.subscribe 与 bot.logs;status_update/message/bot_error 需要 bot.subscribe 与 bot.snapshot。取消订阅需要 bot.unsubscribe。首版完整交互可授予上述五个方法,纯只读页面不能包含 bot.chat。订阅默认空,重连后 Web 必须重新设置。

默认 issued_at < join_before <= expires_at,token 寿命不超过 5 分钟,入房或 reauth 窗口不超过 60 秒,时钟偏差 5 秒。凭证文本最大 8192 字节。建议到期前 60 秒续期,重连超过入房窗口时需获取新 token,不能因为旧 token 尚未到 expires_at 就直接重入。

Main 与 Forward 使用相同合法/非法测试样例和无生产密钥的固定测试向量。不能只用各自加解密往返测试判断跨服务兼容。

固定向量位于 tests/token.test.ts 的首个测试,密钥 relay-test-key-not-a-production-secret、IV 十六进制 000102030405060708090a0b、验证时间 1800000000000,包含完整预期 token 文本和 claims。该固定 IV 仅用于测试;生产签发必须使用随机 IV。

4. Main 文件修改安排

文件或模块 后续工作
index.ts 使用新 options,内置 ID 为 0,保留 handle,显式 stop;删除默认密钥兜底
runtime/forward_server/socket.ts 健康注册表、当前控制连接 generation、disposer、控制请求适配器
runtime/forward_server/event_handler/ 接收并校验 forward_session_status,只接受当前连接
router/node/main_socket.ts Node 注册成功 ACK 下发 node_session_id
runtime/node/socket.ts 提供当前 Node 会话查询、替换/移除撤销 hook
service/forward/ 或现有等效目录 session/grant 存储、申请合并、授权、续期、撤销、定时回收
router/node/action.ts 修正原缺少 / 的路由,接入申请/续期/释放服务
types/、utils/ 本地协议类型、严格 ACK 校验、token 签发、HTTP 错误映射
database/forward/init.ts 与配置 endpoint 字段和安全回填、内置服务解析规则
已有删除/迁移/退出/权限入口 调用 grant/session 撤销,不复制 Bot 生命周期

main_server/forward_server、node_server/forward_server 均为 ../forward_server 符号链接。Main 不再修改另一份 relay 副本,Node 入口调用点也必须同步升级新启动参数;本次没有替 Main/Node 修改这些入口。

5. Main 内存模型

Session 以 (forward_id,node_id,bot_id,node_session_id) 查找,分配独立 forward_session_id,记录:当前 relay 注册 generation、Node Socket 身份、Bot config_revision/资源分配、租约、建立 Promise、等待者及 ready/closed 状态。

Grant 以随机 grant_id 查找,记录:session、user_id、HTTP sessionID、权限、期限和撤销状态。不同标签页独立 grant;同一 grant 在 Forward 只保留一个当前 Web Socket。

多个 HTTP 申请合并 session 建立任务,各自保持截止时间和取消状态。一个 HTTP 超时不能关闭其他等待者或有效 grant 仍使用的通道。没有成功返回凭证的申请不保留永久 grant,迟到 Node ACK 不产生 HTTP 成功。

转发服务重启会丢失授权。Main 必须先重新登记 session/grant,再下发新 token 让 Node/Web 恢复;Node 会话或资源分配变化则建立新 UUID,不能恢复旧 scope。Main 自身重启不假装恢复旧内存授权。

6. 初次申请流程

接口:POST /api/user/node/forward_bot,body 精确为 {bot_id}。

  1. 验证登录会话、活跃账号、正安全整数 Bot ID。
  2. 使用 query_bot_info(user_id,bot_id) 确认归属,保存 Node/team/config_revision。
  3. 复用资源授权逻辑确认团队、Node 与 Forward 权限。现有 authorize_bot_resources(user,bot.node_id,0,bot.team_id) 可作为提取入口,proxy_id=0 跳过与转发无关的代理检查。核对团队 allocation 和成员限额。
  4. 获取当前可命令 Node Socket 及 node_session_id,不能仅查看缓存 ONLINE。
  5. 获取当前健康 Forward 控制连接及部署 endpoint,捕获注册 generation。
  6. 查找或合并 session 建立,控制请求 forward_open_session 成功后签发 Node token。
  7. 通过当前 Node Socket 发送原计划的 forward_bot 命令,等待完整 ready ACK。
  8. 再次核对当前 Node、Forward 控制连接、Bot 归属、资源分配及配置版本;变化则拒绝并清理本申请。
  9. 创建独立 grant,经 forward_authorize_grant ACK 成功后签发 Web token。
  10. 显式投影公开 DTO 返回,不返回实体、密钥、Node token 或 node_url。

不检查 Bot 游戏状态、Minecraft 版本、启动队列或代理可用性,不调用 prepare_bot_resources/start_bot_context,不把申请写入离线停止队列。

公开 DTO 保持原计划的 {bot_id,node_id,forward_id,forward_session_id,grant_id,url,path,namespace,token,expires_at}。url 使用 public_url;token 只能是 Web 角色。

7. Node 接入前置条件

Main 的 forward_bot 成功 ACK 必须表示 Node 已完成:连接 Forward、join_room ACK、安装 RPC 与上下文观察、forward_node_ready ACK。新增数据事件:

// Node -> Forward
forward_node_ready({forward_session_id}, callback);
// callback: ForwardAck<{node_ready:true}>

join_room/reauth 只收 {token},成功 data 为完整 SessionScope 加 role/node_ready/expires_at。Node 入房后 initially node_ready=false,需要显式确认。

Forward 向 Node 定向发送的 relay_request 在原请求上增加 correlation_id/forward_session_id/deadline_at。Node 通过同一个 Socket.IO ACK 返回 ForwardAck;chat 成功 data 为 {accepted:true},snapshot/logs 按 forward_server/types.ts 的 DTO 本地同步。subscribe/unsubscribe 由 Forward 本地处理。

无 BotContext 的 snapshot/status 必须为 offline/context_exists:false/context_generation:null;存在上下文时 generation 为 UUID。日志按 seq 递增、ID 唯一且不得超过读取/事件水位,响应数量不得超过请求 limit。所有 DTO 使用准确字段,serializer 不附带原始 Bot、配置或堆栈。

Node/Web 接收 forward_session_closed {forward_session_id?,reason,code} 后按原因停止旧授权重试。未入房连接被关闭时可能没有 session ID。relay_status.reason === 'resync_required' 要触发快照/日志重新同步。

8. 续期与释放

POST /api/user/node/forward_bot/renew,body {bot_id,forward_session_id,grant_id}。验证账号、HTTP 会话、Bot 归属、grant 所有权和当前分配;先续 session,再对 Node ensure/reauth 成功,登记延长的 grant,最后签发 Web 新 token。建议返回相同 ForwardAccess 结构,前端调用 reauth。Node 会话/转发分配变化返回 SESSION_STALE,重新初次申请。

POST /api/user/node/forward_bot/release,body 相同,严格验证所有权,撤销本 grant 并调用 forward_revoke_grant,幂等返回成功。其他标签页 grant 不受影响。通过本地有界终态记录支持重复 release,不能让未知 grant 成为可操作他人授权的入口。

远端撤销失败时本地立即标记不可续期,记录有界重试直到当前健康控制通道恢复或租约到期。不能把“本地已撤销”描述成“远端已断开”。Forward 的 session/grant/token 到期和空闲回收提供兜底。

9. 真实撤销入口

入口 接入方式
service/bot/delete.ts 事务提交后 撤销该 Bot 的全部 session
service/bot/update.ts 提交后 Node/team/资源分配变化时撤销旧 session;普通不影响授权的配置不无条件踢线
router/user/user_info.ts logout 按当前 HTTP sessionID 撤销对应 grants,不能只按 user_id 踢掉全部设备
runtime/node/socket.ts 替换或移除当前连接 按 expected node_session_id 撤销旧 session,旧 disconnect 不影响新会话
service/user/team.ts 成员移除/团队删除 提交后重新验证并撤销失效 grant/session
授权巡检 独立巡检 relay grants 的登录会话、活跃账号和资源权限,覆盖没有 Main Web Socket 的用户

现有 service/user/socket_session.ts 的巡检只遍历 Main 用户 Socket,不能直接认为它覆盖 relay-only 浏览器。当前未确认存在账号停用和 Node/Forward 重新分配的 HTTP 入口;后续真实添加时接同一个撤销服务,不创建从未调用的 hook 充当完成。

10. HTTP 错误映射

沿用现有 res.json({code:number,data}),不整体改变 HTTP envelope。建议增加未占用的转发错误码并在 Main/Web 同步冻结:

情况 建议 code
成功 0
未登录 498
参数非法 484
Bot 不存在或不属于用户 485
数据库错误 480
权限不足 499
Node 离线 501
超时 488
内部错误 486
Relay 不可用 510(新增建议)
SESSION_STALE 511(新增建议)
TOKEN_EXPIRED 512(新增建议)
CANCELLED 513(新增建议)
RATE_LIMITED 487,结合原有 spam 语义确认

错误 data 建议投影为 {error_code,message,retryable},前端不能解析任意服务内部错误。上述新增数字是 Main 的后续方案,尚未修改其码表。NO_CONTEXT、BOT_NOT_ONLINE、RESYNC_REQUIRED 是业务 RPC 语义,不能因 Bot 无上下文而阻止申请。

总预算沿用 Node 建立 8 秒、Main 等待 10 秒、HTTP 12 秒,所有阶段共享绝对 deadline,不能逐阶段叠加无限等待。Node 副作用操作 ACK 超时不可自动重发。

11. 注册表与部署配置

注册表存储 origin、transport、generation、node_session_id、健康状态和 disposer。当前连接重复注册幂等,初始化完成才入健康表并 ACK。旧断开只清理自己拥有的记录。origin=1 借用 Node 控制 Socket,删除 Forward 登记不得断开整个 Node;origin=0 的 EventEmitter 不调用 disconnect。

Forward endpoint 模型增加 node_url/public_url/path/namespace/allowed_origins/config_revision,监听 host/port 与公布地址分开。当前数据库启用 synchronize 且未发现迁移目录,新增字段应先可空、回填、校验,再收紧,不能直接添加无默认的非空字段影响现有行。

内置 ID 0 当前只有 runtime 登记并不足够,资源授权仍会查 Forward 实体;需显式 seed/验证内置配置或统一 builtin resolver,并保留权限检查。HTTPS 页面使用 HTTPS 公开入口,代理同时支持 polling 与 WebSocket。

12. 实施与验收顺序

  1. 冻结本地 DTO、错误映射及 endpoint 配置;实现 token 测试向量。
  2. 修改 Main 启动和健康注册表,Node 注册 ACK 暴露当前会话;接控制 ACK 适配器。
  3. 实现 session/grant service,先用模拟 Node 验证并发、deadline、撤销和旧 ACK。
  4. 接申请、续期、释放路由以及真实撤销入口;接 Node 新协议后执行真实 relay 联调。
  5. 接 Web 并验证多标签页、无上下文、stop/start、凭证续期、服务重启及故障恢复。

验收至少覆盖:全部 Bot 状态和无上下文可申请;foreign Bot/无权限/离线 Node 拒绝;等待期间迁移或替换会话不返回旧凭证;并发标签页共享上行而 grant 独立;release/logout 只影响对应授权;公开响应无密钥/Node token/内部地址;转发重启先登记授权再恢复;Main/Node/relay 分别断线不互相覆盖状态。

当前 relay 的本地测试通过不能代替以上 Main/Node/Web 端到端验收。上线需同批升级协议,不允许新旧客户端混用。

13. 当前交付验证

在 Node.js 22.23.2 下执行 pnpm run typecheck 通过,pnpm test 共 38 项全部通过。测试包含真实 Socket.IO polling/WebSocket、模拟 Node RPC、异步 adapter、凭证与撤销、限流背压、借用宿主关闭/重启及自有 HTTP 关闭超时;未连接生产数据库、Minecraft 或实际 Main/Node/Web 全流程。

首版会话与授权保存在单进程内存。Socket.IO 固定为 4.8.3,RPC 取消及缓冲检测使用该版本的内部 acks/writeBuffer 结构;升级依赖时必须重新验证取消与背压测试。借用宿主模式在同一 host/path 保留一个无活动会话的转接监听器,停止时返回 503,重启复用,避免破坏其他 Socket.IO 服务的 HTTP 监听链。