Skip to content

Control Server 内部(共通语义 + Python 温备面)

控制面有两个实现:主控制面是 TS 面apps/control-server-ts,Cloudflare Workers,见 control-server-ts.md),Python 面(apps/control-server)为温备。两面 HTTP 契约一致、读写同一个 PostgreSQL。本文承担两件事:讲双面共通的控制面语义——materialize 合成、健康推导、token 与注册、门铃通知模型(这些语义对两面同样成立,代码引用以 Python 实现为例);以及 Python 面特有的装配与运行细节。TS 面特有的运行时形态(Durable Objects、KV/R2、cron)见 control-server-ts.md。API 清单见 ../reference/api.md,表结构见 ../reference/database.md

内部结构

内部组件职责代码
materialize()读规范化表,合成完整 DesiredState,校验后写入新 generationapp/services/materializer.py:33
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
PendingRegistrationStore注册审批流转app/services/pending_registrations.py
AuditLogStore记录所有 admin 写操作(含失败)app/services/audit.py
EventBus进程内按 node_id 的发布/订阅队列,向 WS 推门铃;队满丢弃app/core/events.py

以上组件在 TS 面均有同构对应(src/services/*.tsEventBus 对应 Durable Object Doorbell),映射表见 control-server-ts.md

materialize

materialize(node_id)app/services/materializer.py:33)是控制面的核心,把数据库里的规范化事实合成为一份完整、经校验的 DesiredState

  1. 锁节点行with_for_update={"of": Node}FOR UPDATE OF nodes)串行化并发 admin 写。限定只锁 nodes 主表是必须的:Node.dns_grouplazy="joined" 可空关系,get() 带外连接,PostgreSQL 不允许 FOR UPDATE 锁外连接可空侧(详见 迁移指南)。
  2. 递增 generationnew = (current_generation or 0) + 1
  3. 加载子表:interfaces、bgp_sessions(按 sort_order, id)、订阅的 DNS 组(Node.dns_group_id → 组 + 启用 zone + 启用 record)。
  4. 组装快照_assemble_snapshot):以 node.base_template 为底,注入 schema_versiongenerationnode payload(DB 列覆盖 base_template)。
    • 退役态lifecycle=decommissioned):清空 interfaces / bgp / dns,核心 runtime 服务保留空转——agent 收敛即拆隧道、撤会话、停宣告。
    • 活跃态:逐接口 _interface_payload,逐会话 _bgp_payload,DNS 来自订阅组。
  5. 两类单一真相源派生注入_interface_payloadmaterializer.py:214):
    • 节点 LLA:对外部 eBGP WG 接口(peering.is_internal=False),把 NodeSpec.link_local 派生为 <link_local>/64 注入 addresses(去重)。
    • 内部对端 WG 公钥:对内部互联接口,从对端 Node.wireguard_public_key 派生注入 wireguard_peer.public_key,而非存 spec 副本。
  6. 校验DesiredState.model_validate(snapshot)——拒绝任何 schema 漂移(admin 路由把它转 422)。
  7. 写 generation + 更新 nodes.current_generation
  8. 修剪旧 generation:保留最近 DEFAULT_GENERATION_RETENTION(默认 100)代。

派生注入是"把副本变派生"的两个落地范例,见 ../reference/addressing-model.md

Peering 聚合根

PeeringWgInterface + BgpSession 之上的聚合根:一条逻辑互联聚合其下的接口与会话,可关联远端节点(remote_node_id)。组合读 + 全量 PUT + backfill 归并端点的设计见 ../reference/database.md../guides/peering.md。索引列(name/kind/remote_asn/enabled)由 apply_spec() 从校验过的 spec JSON 单源投影,避免双写漂移。

健康推导(五态)

NodeStatusStoreapp/services/node_status.py)在每次 record_snapshot/report/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

时间阈值覆盖(读取时叠加,_row_to_dict):

  • 静默超过 down_after_seconds(默认 3600)→ 覆盖为 downunknown 除外)。
  • 否则 ok 且静默超过 stale_after_seconds(默认 900)→ 覆盖为 stale

阈值可配(DN42_CONTROL_HEALTH_STALE_AFTER / _DOWN_AFTER,见 ../reference/configuration.md)。读取时叠加意味着改阈值无需改库。node_status_events 按 (node, kind) 各保留最近若干条,自动修剪防膨胀。

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 密码 + Turnstile 人机校验 + 会话令牌 + 登录限速,admin_users / admin_sessions 表);admin 鉴权无差别接受静态 admin token 或账号会话令牌。完整安全模型见 security.md

WebSocket 通知

门铃语义两面一致;承载不同——主面由每节点一个的 Durable Object 承载(见 control-server-ts.md),Python 面由进程内 EventBus 承载。以下为 Python 面实现 /api/v1/agent/ws/{node_id}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. 发布:admin 写 + materialize + 事务提交后 bus.publish(node_id, event)
  5. 队列上限 64,满则丢——agent 兜底周期 reconcile 补偿。

事件类型(app/schemas/events.py):hellodesired_state_updatedsnapshot_requestagent_update_available(心跳版本低于全局目标时推送)、probe_request(拨测作业下发,例外地携带业务数据)。除 probe_request 外 WS 只传门铃,业务数据走 HTTP 拉取;完整字段见 ../reference/api.md

启动与配置(Python 温备面)

app/main.py:create_app() 装配 FastAPI、lifespan(初始化 DB + Redis 缓存、可选 seed、装配 service)、/healthz DB 探针、以及审计 admin 写的中间件。后端栈是 PostgreSQL + Redis:DB 经 DN42_CONTROL_DATABASE_URL,缓存经 DN42_CONTROL_REDIS_URLservices/cache.py,未配置 / 不可用即全程 no-op 回落 DB,缓存是旁路);配置统一从 control-server.toml 加载、env 覆盖。启动用 Base.metadata.create_all 建表;迁移状况见 ../reference/database.md../guides/upgrades-and-migrations.md。全部配置项见 ../reference/configuration.md

TS 主面无 lifespan(Workers 无常驻进程):连接池由 Hyperdrive 维护、缓存用 KV、背景任务用 cron、配置经 wrangler vars/secrets——见 control-server-ts.md