外观
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-state | Agent Bearer | 拉取本节点完整 DesiredState;无则 404 |
GET | /recovery-public-key | Agent Bearer | 取离线托管恢复公钥(PEM)与指纹 |
POST | /wireguard-keys | Agent Bearer | 登记节点 WG 公钥与托管密文,一致性校验 + 对端传播 |
POST | /runtime-snapshot | Agent Bearer | 上报运行时快照(容器 / 接口 / WG / BGP 观测) |
POST | /routing-table | Agent Bearer | 上报 BIRD 路由全表观测 |
POST | /prefix-flaps | Agent Bearer | 上报前缀级 flap 打分快照(按节点整表替换) |
POST | /wireguard-traffic | Agent Bearer | 上报 30s 轻量 WG 流量采样 |
POST | /wireguard-reresolve | Agent Bearer | 上报 WG endpoint 重解析结果(自愈观测) |
POST | /reconciliation-report | Agent Bearer | 上报一次 reconcile 报告 |
POST | /apply-result | Agent Bearer | 上报一次 apply 结果 |
GET | /update/manifest | Agent Bearer | 自更新:拉取全局目标版本的发布清单;无目标或无清单 204 |
GET | /update/wheels/{version}/{filename} | Agent Bearer | 自更新:下载某版本下的单个 wheel;不存在 404 |
POST /agent/register
请求 AgentRegistrationRequest:{ enrollment_token, requested_node_id, inventory },inventory 为 HostInventory(hostname / 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_token为null,message说明原因。此时不消费 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)。version与filename过严格白名单(防路径穿越),不存在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_request 与 log_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 中途断开时服务端补发一条
done(error: "agent disconnected"),避免订阅的浏览器 SSE 无限等待。
log-stream 的 log 消息为 { log_id, seq, entries[] },条目为 { ts, level, logger, message }。snapshot 模式分批发完即收尾,tail 模式实时推送直到 duration_seconds 到期。