Skip to content

Control Server 内部

控制面实现在 apps/control-server(FastAPI),生产入口 https://api.natlan.io/control/v1/*

内部结构

内部组件职责代码
materialize()读规范化表,合成完整 DesiredState,校验后写入新 generationapp/services/materializer.py
provision_node_from_state()接受完整 DesiredState,整节点幂等落库并 materializeapp/db/provision.py
DesiredStateStorenode_id 读最新 generation;bump() 触发重新 materializeapp/services/desired_state.py
TokenStoreAgent 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-outapp/core/probe_hub.py

materialize

materialize(node_id) 是控制面的核心,把数据库里的规范化事实合成为一份完整、经校验的 DesiredState。完整流程、派生注入与世代裁剪见 materialize 写入路径

要点:

  1. 锁节点行FOR UPDATE OF nodes)串行化并发管理写。限定只锁 nodes 主表是必须的——Node.dns_grouplazy="joined" 的可空关系,get() 带外连接,PostgreSQL 不允许 FOR UPDATE 锁外连接的可空侧。
  2. 递增 generation,加载子表。
  3. 三类单一真相源派生注入:节点 WireGuard 私钥(无条件覆盖每个 WG 接口)、节点 link-local(注入外部 eBGP WG 接口地址)、内部对端公钥(从对端节点档案现取现填)。
  4. 再校验一遍 schema,拒绝任何漂移;管理路由把失败转成 422
  5. 写 generation 并裁剪保留窗口外的旧代。

materialize 不触发事件广播——由调用方在事务提交后决定是否发门铃,避免「事件先发、事务后回滚」让 agent 拉到旧数据。


Peering 聚合根

PeeringWgInterfaceBgpSession 之上的聚合根:一条逻辑互联聚合其下的接口与会话,可关联远端节点(remote_node_id)。

组合读、全量 PUT 与 backfill 归并端点见 管理面 API;表结构见 节点与网络;操作步骤见 建立互联

索引列(name / kind / remote_asn / enabled)由 apply_spec() 从校验过的 spec JSON 单源投影,避免双写漂移。


健康推导(五态)

NodeStatusStore 在每次 record_snapshot / record_report / record_apply 之后重算健康。_derive_health() 按顺序判定:

  1. report 与 apply 都没有 → unknown
  2. report 或 apply 为 faileddegraded
  3. drift_count > 0degraded
  4. report 或 apply 为 degradeddegraded
  5. desired_generation != observed_generation(都已知且不同)→ stale
  6. 否则 → ok

时间阈值覆盖在读取时叠加,不写库:

  • 静默超过 down_after_seconds(默认 3600)→ 覆盖为 downunknown 除外)。
  • 否则 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 直接 403pending 返回 PENDING_APPROVAL不消费 enrollment token,agent 重试;未知节点记为 pending;已落库且有 generation 才签发 token。

管理端另有账号登录子系统(argon2id 密码、Cap 人机验证、会话令牌、登录限速),落 admin_usersadmin_sessions 两表;管理鉴权无差别接受静态 admin token 或账号会话令牌。完整安全模型见 安全模型


WebSocket 通知

门铃由进程内 EventBus 承载,实现在 app/api/v1/agent_ws.py

  1. 握手Authorization: BearerTokenStore.resolve();校验 principal.node_id == URL node_id(不符关闭码 4403,无效 4401)。
  2. 连接即发 hello,带当前 generation 供 agent 追赶。
  3. 事件泵:reader 检测断连,writer 从 EventBus 队列取门铃。
  4. 发布:管理写 + materialize + 事务提交后 bus.publish(node_id, event)
  5. 队列上限 64,满则丢——agent 的兜底周期 reconcile 补偿。

事件类型与字段见 节点面 API


拨测与日志的过境链路

ProbeHub 是拨测与日志查看共用的内存作业机:浏览器 SSE ← 控制面 fan-out ← agent 临时 WS 回传 ← 节点侧执行。作业与缓冲都在内存,完成后 TTL 回收,不落库

这是唯一两个「门铃携带业务数据」的事件类型。它们仍不构成远程执行接口:ProbeSpectool 是三选一枚举,target 过 IP 白名单;LogSpec 只能读日志。见 安全模型


启动与配置

app/main.pycreate_app() 装配 FastAPI、lifespan(初始化 DB、KV 与缓存、装配各 service、按需引导管理员账号、可选启动 flap 告警与分区维护循环)、/healthz 分级探针,以及审计管理写的中间件。

后端栈是 PostgreSQL + Valkey + Redis,三者按角色分工(完整模型见数据层参考):

连接串实例角色不可用时
DN42_CONTROL_DATABASE_URLPostgreSQL事实源、观测与时序存档/healthz 503
DN42_CONTROL_KV_URLValkey db 1有 TTL 的持久小状态(唯一副本)/healthz 503;各调用点回落 SQL / 进程内存
DN42_CONTROL_REDIS_URLRedis db 1可丢弃的派生缓存/healthz 200 但标 degraded,全程 no-op 回落 DB

⚠️ 两个 KV 实例的差别是角色不是协议:把唯一副本(如 flap 热态)写进 Redis 会被 LRU 淘汰、容器重建即丢。装配处的注入由静态检查守住(tests/test_storage_discipline.py)。

配置统一从 control-server.toml 加载,环境变量覆盖。

schema 初始化db_auto_migrate 分流:Truealembic upgrade head(fail-fast),False(默认,也是现役生产的取值)走 create_all——只建缺失的表、不应用 ALTER。差异、接管步骤与已知偏差见 迁移

生产启动绝不写入任何节点:空库由导入或 provision 流程填充。测试需要预置节点时在 conftest 里显式 seed,不经生产路径。

全部配置项见 Control Server 配置