Skip to content

Control Server —— 节点面 API

源码:apps/control-server/app/api/v1/agent_http.py(HTTP)与 agent_ws.py(WebSocket)。前缀 /control/v1/agent

/register 外全部需要 Agent Bearer,且请求体里的 node_id 必须等于 token 绑定的节点,否则 403


HTTP 端点

方法路径鉴权说明
POST/register体内 enrollment token注册换取 agent token
GET/desired-stateAgent Bearer拉取本节点完整 DesiredState;无则 404
GET/recovery-public-keyAgent Bearer取离线托管恢复公钥(PEM)与指纹
POST/wireguard-keysAgent Bearer登记节点 WG 公钥与托管密文,一致性校验 + 对端传播
POST/runtime-snapshotAgent Bearer上报运行时快照(容器 / 接口 / WG / BGP 观测)
POST/routing-tableAgent Bearer上报 BIRD 路由全表观测
POST/prefix-flapsAgent Bearer上报前缀级 flap 打分快照(按节点整表替换)
POST/wireguard-trafficAgent Bearer上报 30s 轻量 WG 流量采样
POST/wireguard-reresolveAgent Bearer上报 WG endpoint 重解析结果(自愈观测)
POST/reconciliation-reportAgent Bearer上报一次 reconcile 报告
POST/apply-resultAgent Bearer上报一次 apply 结果
GET/update/manifestAgent Bearer自更新:拉取全局目标版本的发布清单;无目标或无清单 204
GET/update/wheels/{version}/{filename}Agent Bearer自更新:下载某版本下的单个 wheel;不存在 404

POST /agent/register

请求 AgentRegistrationRequest{ enrollment_token, requested_node_id, inventory }inventoryHostInventoryhostname / os / arch / has_systemd / capabilities 等)。

enrollment token 无效或绑定节点不符 401;节点已被管理员 reject 403即使已 provision 也照拒)。

BootstrapStatus 枚举里还有第三个值 rejected,但注册端点不会返回它——被否决的节点直接以 403 结束。

响应 AgentRegistrationResponse 有两种状态:

  • accepted:节点已 provision 且审批放行 → node_id / agent_id / agent_token / desired_state_generation 同时非空。
  • pending-approval(注意是连字符,不是下划线):节点未 provision、审批 pending、或已建但尚无第一代 → agent_tokennullmessage 说明原因。此时不消费 enrollment token,agent 会重试。
json
// POST /control/v1/agent/register
{
  "enrollment_token": "ent_xxxx.oneTimeSecret",
  "requested_node_id": "edge1",
  "inventory": {
    "hostname": "edge1.example",
    "os": "linux",
    "arch": "x86_64",
    "has_systemd": true,
    "capabilities": []
  }
}
json
// 200 OK
{
  "status": "accepted",
  "node_id": "edge1",
  "agent_id": "agent-edge1",
  "agent_token": "agt_ab12cd.s3cr3tVALUEonlyShownOnce",
  "desired_state_generation": 3,
  "message": null
}

接入全流程见 节点接入


POST /agent/wireguard-keys

请求 WireGuardKeyReport{ node_id, public_key, private_key_escrow? }

agent 上报的公钥由它从 DesiredState 里的节点密钥推导得来——agent 不生成密钥private_key_escrow 只在控制面配了离线恢复公钥时非空。

公钥与已记录的不符 → 409 且事务回滚(节点绝不用偏离的密钥拉隧道);未知节点 404

响应 WireGuardKeyReportResult{ node_id, accepted, status, detail?, propagated_to[] }

status含义
stored首次登记。同事务把「对端是本节点」的内部 peering 重新物化,propagated_to 列出受影响节点
matched一致,放行;托管密文若非空则刷新
rejected不一致,端点回 409

GET /agent/recovery-public-key 返回 RecoveryPublicKeyResponse{ configured, public_key_pem?, fingerprint? }生产当前未配置恢复公钥,该端点恒返回 configured=false,agent 据此跳过封装。

密钥单一事实源见 fleet 供给收权,托管与恢复见 密钥托管与恢复


POST /agent/runtime-snapshot

请求 RuntimeSnapshot{ node_id, generation?, captured_at, containers[], interfaces[], wireguard_interfaces[], bgp_protocols[], … }

json
// 200 OK
{ "accepted": true, "node_id": "edge1", "generation": 3, "containers": 4, "interfaces": 6 }

回执先行:健康态主写(node_status upsert + 事件)同步完成后即回响应;三路衍生存档在响应之后作为后台任务执行——

  • per-interface 流量存档(per-peer 累计字节按接口差分,是按对端 AS 划分流量的数据层);
  • 自观测存档(self_metrics 的 cpu / rss / total_failures 差分,是 /ui/nodes/metrics 的数据层);
  • 会话级 flap 比对。

同步执行这三路会拖过 agent 的读超时、造成对账假失败,因此必须放在回执之后。单轮存档失败只记日志,下一份快照自然补齐;对旧版 agent 的快照均为 no-op。


其余上报端点

响应均为确认型对象:

端点响应
/routing-table{ accepted, node_id, observation, routes }
/prefix-flaps{ accepted, node_id, entries, feed_state }
/wireguard-traffic{ accepted, node_id, rx_bytes, tx_bytes, peer_count }
/wireguard-reresolve{ accepted, node_id, checked, reresolved }
/reconciliation-report{ accepted, node_id, status, drift_items }
/apply-result{ accepted, node_id, generation, status }

/prefix-flaps 请求 PrefixFlapReport:agent 侧按 (prefix, 归因对端 AS) 算好的指数衰减分 Top-N,每条含 peer_asn(AS_PATH 第一跳)与 origin_asn(最后一跳 = 前缀起源);feed_state 是 flapfeed BGP 采集会话的状态。

/wireguard-traffic 上报的是全 peer 累计收发字节之和,用于画比完整快照更细粒度的吞吐曲线。


自更新分发

wheel 发布与全局目标版本由管理面的 Agent 发布 管理。agent 侧流程:经 WS 心跳上报 agent_version → 版本低于全局目标时被推送一次 agent_update_available(携带目标版本与 wheel 清单)→ 经本组端点下载安装。

  • GET /agent/update/manifest:返回全局目标版本的 AgentReleaseManifest{ version, wheels: [{ filename, sha256, size }] })。无目标版本或目标版本无 manifest → 204,agent 据此判定「无更新」。端点只发清单、不做版本判定,agent 自行用本地版本比对,需要才下载。
  • GET /agent/update/wheels/{version}/{filename}:下发单个 wheel(application/octet-stream)。versionfilename 过严格白名单(防路径穿越),不存在 404。agent 下载后按 manifest 的 sha256 逐字节核对,再由独立的 systemd 更新器整组安装。

健康门与回滚机制见 Agent 滚动升级


WebSocket 端点

端点说明
WS /control/v1/agent/ws/{node_id}节点私有事件通道(常驻)
WS /control/v1/agent/probe-stream/{probe_id}拨测输出回传临时通道(按作业建连,done 后收尾)
WS /control/v1/agent/log-stream/{log_id}日志条目回传临时通道(与 probe-stream 同构)

握手与鉴权

复用 HTTP 的 Authorization: Bearer <agent token> 头,不在 query 里传 token

  • token 解析失败 → 以关闭码 4401 拒绝(未握手 accept)。
  • token 绑定的 node_id 与 URL 路径里的 {node_id} 不一致 → 关闭码 4403

数据契约

连接成功后服务端立即下发一条 hello,之后只下发门铃事件——agent 收到后回到 HTTP 拉取真实业务数据。agent 上行只认 heartbeat(报活并上报 agent_version / applied_generation / apply_status,写入 liveness 注册表;版本低于全局目标时触发一次 agent_update_available 推送);其余上行消息一律忽略,兼顾感知断连与旧版 agent 兼容。

事件类型(apps/control-server/app/schemas/events.py):

type何时下发字段
hello握手成功后立即{ type, node_id, generation? }
desired_state_updated控制面发布新世代{ type, generation(≥1), reason? }
snapshot_request要求 agent 主动上报快照{ type, reason? }
agent_update_available心跳版本低于全局目标(同一目标只推一次){ type, version, wheels[], reason? }
probe_request管理面发起拨测{ type, probe_id, spec }
log_request管理面发起日志查看{ type, log_id, spec }

probe_requestlog_request 是仅有的两个携带业务数据的事件(旁路形态,agent 立即执行并经临时通道回传)。

事件队列上限 64,满则丢弃——agent 的兜底周期 reconcile 会补偿。

json
// 连接成功后第一条
{ "type": "hello", "node_id": "edge1", "generation": 3 }
json
// 配置变更门铃
{ "type": "desired_state_updated", "generation": 4, "reason": "interface updated" }

probe-stream / log-stream

agent 收到 probe_request / log_request 后按作业 id 建立的临时回传通道:

  • 鉴权同样复用 Authorization: Bearer <agent token> 头,解析失败以 4401 拒绝。
  • 归属校验:作业必须存在(未过期)且归属 token 绑定的节点,否则 4403——防止某节点往别人的作业里灌输出。
  • 消息:probe-stream 只接受上行 output / done,log-stream 只接受 log / done,其余忽略;服务端强制把消息里的作业 id 覆盖为路径值(防伪造)。收到 done 即收尾关闭。
  • 断连兜底:agent 中途断开时服务端补发一条 doneerror: "agent disconnected"),避免订阅的浏览器 SSE 无限等待。

log-stream 的 log 消息为 { log_id, seq, entries[] },条目为 { ts, level, logger, message }。snapshot 模式分批发完即收尾,tail 模式实时推送直到 duration_seconds 到期。