外观
Control Server 内部
控制面实现在 apps/control-server(FastAPI),生产入口 https://api.natlan.io/control/v1/*。
内部结构
| 内部组件 | 职责 | 代码 |
|---|---|---|
materialize() | 读规范化表,合成完整 DesiredState,校验后写入新 generation | app/services/materializer.py |
provision_node_from_state() | 接受完整 DesiredState,整节点幂等落库并 materialize | app/db/provision.py |
DesiredStateStore | 按 node_id 读最新 generation;bump() 触发重新 materialize | app/services/desired_state.py |
TokenStore | Agent Bearer token 的签发、哈希存储、解析、过期校验、轮换、撤销 | app/services/tokens.py |
EnrollmentTokenStore | 一次性或绑定节点的注册凭证 | app/services/enrollment.py |
NodeStatusStore | 持久化上报、推导五态健康 | app/services/node_status.py |
RoutingStore | 路由快照入库(哈希门控与差分写)、聚合查询 | app/services/routing.py |
NodeKeyStore | 节点 WireGuard 密钥的单一事实源 | app/services/node_keys.py |
PortAllocator | 端口池取号,全 fleet 统一段位 | app/services/port_allocator.py |
PendingRegistrationStore | 注册审批流转 | app/services/pending_registrations.py |
AuditLogStore | 记录所有管理写操作(含失败) | app/services/audit.py |
EventBus | 进程内按 node_id 的发布订阅队列,向 WS 推门铃;队满丢弃 | app/core/events.py |
ProbeHub | 拨测与日志查看作业的内存作业机与 SSE fan-out | app/core/probe_hub.py |
materialize
materialize(node_id) 是控制面的核心,把数据库里的规范化事实合成为一份完整、经校验的 DesiredState。完整流程、派生注入与世代裁剪见 materialize 写入路径。
要点:
- 锁节点行(
FOR UPDATE OF nodes)串行化并发管理写。限定只锁nodes主表是必须的——Node.dns_group是lazy="joined"的可空关系,get()带外连接,PostgreSQL 不允许FOR UPDATE锁外连接的可空侧。 - 递增 generation,加载子表。
- 三类单一真相源派生注入:节点 WireGuard 私钥(无条件覆盖每个 WG 接口)、节点 link-local(注入外部 eBGP WG 接口地址)、内部对端公钥(从对端节点档案现取现填)。
- 再校验一遍 schema,拒绝任何漂移;管理路由把失败转成
422。 - 写 generation 并裁剪保留窗口外的旧代。
materialize 不触发事件广播——由调用方在事务提交后决定是否发门铃,避免「事件先发、事务后回滚」让 agent 拉到旧数据。
Peering 聚合根
Peering 是 WgInterface 与 BgpSession 之上的聚合根:一条逻辑互联聚合其下的接口与会话,可关联远端节点(remote_node_id)。
组合读、全量 PUT 与 backfill 归并端点见 管理面 API;表结构见 节点与网络;操作步骤见 建立互联。
索引列(name / kind / remote_asn / enabled)由 apply_spec() 从校验过的 spec JSON 单源投影,避免双写漂移。
健康推导(五态)
NodeStatusStore 在每次 record_snapshot / record_report / record_apply 之后重算健康。_derive_health() 按顺序判定:
- report 与 apply 都没有 →
unknown - report 或 apply 为
failed→degraded drift_count > 0→degraded- report 或 apply 为
degraded→degraded desired_generation != observed_generation(都已知且不同)→stale- 否则 →
ok
时间阈值覆盖在读取时叠加,不写库:
- 静默超过
down_after_seconds(默认 3600)→ 覆盖为down(unknown除外)。 - 否则
ok且静默超过stale_after_seconds(默认 900)→ 覆盖为stale。
读取时叠加意味着改阈值无需改库。阈值配置见 Control Server 配置。
node_status_events 按 (节点, 种类) 各保留最近若干条,自动修剪防膨胀。
token 与注册
- Agent token:DB 只存 sha256 哈希;格式
<id>.<secret>或固定字面量(literal_token_id取哈希派生 id);明文只在签发响应里出现一次。resolve()校验未撤销、未过期;rotate()撤旧签新。 - Enrollment token:同哈希模型。
node_id为空表示任意节点可用,非空表示仅该节点;一次性(used_at)。 - 注册审批闸门(
agent_http.py的 register):查pending_registrations状态——rejected直接403;pending返回PENDING_APPROVAL且不消费 enrollment token,agent 重试;未知节点记为 pending;已落库且有 generation 才签发 token。
管理端另有账号登录子系统(argon2id 密码、Cap 人机验证、会话令牌、登录限速),落 admin_users 与 admin_sessions 两表;管理鉴权无差别接受静态 admin token 或账号会话令牌。完整安全模型见 安全模型。
WebSocket 通知
门铃由进程内 EventBus 承载,实现在 app/api/v1/agent_ws.py:
- 握手:
Authorization: Bearer→TokenStore.resolve();校验principal.node_id == URL node_id(不符关闭码4403,无效4401)。 - 连接即发 hello,带当前 generation 供 agent 追赶。
- 事件泵:reader 检测断连,writer 从
EventBus队列取门铃。 - 发布:管理写 + materialize + 事务提交后
bus.publish(node_id, event)。 - 队列上限 64,满则丢——agent 的兜底周期 reconcile 补偿。
事件类型与字段见 节点面 API。
拨测与日志的过境链路
ProbeHub 是拨测与日志查看共用的内存作业机:浏览器 SSE ← 控制面 fan-out ← agent 临时 WS 回传 ← 节点侧执行。作业与缓冲都在内存,完成后 TTL 回收,不落库。
这是唯一两个「门铃携带业务数据」的事件类型。它们仍不构成远程执行接口:ProbeSpec 的 tool 是三选一枚举,target 过 IP 白名单;LogSpec 只能读日志。见 安全模型。
启动与配置
app/main.py 的 create_app() 装配 FastAPI、lifespan(初始化 DB、KV 与缓存、装配各 service、按需引导管理员账号、可选启动 flap 告警与分区维护循环)、/healthz 分级探针,以及审计管理写的中间件。
后端栈是 PostgreSQL + Valkey + Redis,三者按角色分工(完整模型见数据层参考):
| 连接串 | 实例 | 角色 | 不可用时 |
|---|---|---|---|
DN42_CONTROL_DATABASE_URL | PostgreSQL | 事实源、观测与时序存档 | /healthz 503 |
DN42_CONTROL_KV_URL | Valkey db 1 | 有 TTL 的持久小状态(唯一副本) | /healthz 503;各调用点回落 SQL / 进程内存 |
DN42_CONTROL_REDIS_URL | Redis db 1 | 可丢弃的派生缓存 | /healthz 200 但标 degraded,全程 no-op 回落 DB |
⚠️ 两个 KV 实例的差别是角色不是协议:把唯一副本(如 flap 热态)写进 Redis 会被 LRU 淘汰、容器重建即丢。装配处的注入由静态检查守住(tests/test_storage_discipline.py)。
配置统一从 control-server.toml 加载,环境变量覆盖。
schema 初始化按 db_auto_migrate 分流:True 走 alembic upgrade head(fail-fast),False(默认,也是现役生产的取值)走 create_all——只建缺失的表、不应用 ALTER。差异、接管步骤与已知偏差见 迁移。
生产启动绝不写入任何节点:空库由导入或 provision 流程填充。测试需要预置节点时在 conftest 里显式 seed,不经生产路径。
全部配置项见 Control Server 配置。