外观
架构
本文说明系统如何把控制平面的数据库记录变成节点上的 Docker、WireGuard、BIRD、RPKI 和 DNS runtime,以及节点状态如何回流到控制面,覆盖"组件—边界—数据流—变更闭环"的全貌。各组件内部细节见 control-server.md、node-agent.md、shared-packages.md。
组件职责
控制面有两个实现,HTTP 契约一致、读写同一个 PostgreSQL——TS 面是主控制面(2026-07 起承接全部 fleet 流量,入口 https://api.natlan.io),Python 面退为温备(入口 https://legacy.api.natlan.io):
| 组件 | 位置 | 职责 |
|---|---|---|
| Control Server(TS,主) | apps/control-server-ts | Cloudflare Workers 上的控制面:Hono 路由、drizzle + Hyperdrive 访问 PostgreSQL、Durable Objects 承担门铃/拨测通道、KV 缓存、R2 存 agent wheel、cron 背景任务。见 control-server-ts.md |
| Control Server(Python,温备) | apps/control-server | FastAPI 实现,语义与 TS 面一致(materialize / 健康 / token 的共通语义以本实现的文档为准),生产上作为回退面保留。见 control-server.md |
| Node Agent | apps/node-agent | 节点常驻守护进程:拉取 DesiredState、渲染配置、规划执行本机变更、上报结果 |
| Web UI | 独立仓库 dn42-control-webui | SvelteKit 单页管理界面,Cloudflare Git 关联 Worker 静态资产托管(console.natlan.io),账号登录后经 CORS 调用 Admin/UI API |
| Database | PostgreSQL(生产)/ 本地 SQLite(Python 面开发) | 节点、接口、BGP、DNS、token、generation 快照、节点状态、路由、flap、账号 |
dn42_schemas | packages/dn42_schemas | 跨组件传输的数据结构 |
dn42_templates | packages/dn42_templates | 把 DesiredState 渲染为配置文件和脚本 |
dn42_runtime | packages/dn42_runtime | 渲染文件类型、写盘计划、router Dockerfile 渲染 |
dn42_common | packages/dn42_common | 公共校验、命名、label、community、crypto 工具 |
系统边界
Control Server 内部包含 API 路由、token / enrollment、desired-state、node-status、routing、注册审批、审计等存储服务,以及 materialize() / provision 与门铃发布——这些不是外部服务。两面的对应实现:Python 面是 app/services/*.py 的 Store 类 + 进程内 EventBus(control-server.md);TS 面是 src/services/*.ts + Durable Object Doorbell(control-server-ts.md)。Node Agent 内部包含配置加载、身份缓存、controller client、渲染、planner、部署 backend、本机收敛、采集器和对账器——也不是外部服务。唯一的公开出口是 GET /api/v1/public/map(城市级隐私裁剪的地图数据,见 ../reference/public-map-api.md),其余接口一律凭证鉴权。
数据流
一:管理端写入变更
业务表包括 nodes、peerings、wg_interfaces、bgp_sessions、dns_groups / dns_group_zones / dns_records、agent_tokens、enrollment_tokens。generations 保存已发布给 Agent 的完整状态快照。校验失败(schema 不过)返回 422,业务表写入随事务一起回滚。详见 control-server.md。
二:Agent 接收新状态
WebSocket 只传事件门铃,不传业务数据(拨测作业 probe_request 是唯一例外)。Agent 收到门铃后通过 HTTP 拉取完整 DesiredState。主面的门铃由每节点一个的 Durable Object 承载(连接在 Worker 入口鉴权后转交,见 control-server-ts.md);Python 温备面由进程内 EventBus 承载。agent 还在这条 WS 上发心跳(携带版本号),控制面据此维护 liveness 并推送自更新事件。
三:Agent 本地部署
Docker backend 先准备镜像、再删需重建的旧容器,避免构建失败时先破坏现有部署。容器不用 docker-compose,由 Agent 直接通过 Docker Engine API 创建;容器名固定 <project>-<service>-1 后缀。详见 node-agent.md。
四:状态回流与健康视图
健康为五态,由上报状态 + 漂移计数 + generation 差推导,再叠加时间阈值覆盖(stale_after / down_after)。判定逻辑见 control-server.md。
Agent 另有一组旁路任务独立于 reconcile 闭环:路由表采集(直连 BIRD 控制 socket)、WireGuard 流量采集、endpoint 域名周期重解析、L3 自愈回路、自监控指标、flapfeed(进程内 BGP 监管会话,前缀级抖动检测)。清单与周期见 node-agent.md。
上报侧的写路径瘦身与最小扰动同源:路由明细按 per-prefix 规范化哈希与 node_route_prefix_hashes 基准差分写(无变化的前缀零写入);prefix-flap 打分热态存 KV/Redis 单键 blob 而非 SQL 行;大表摄入端点先回响应、DB 事务放后台(解耦 agent 读超时)。
新节点接入生命周期
approve 只是放行名单;真正能工作取决于 provision 是否下发了 DesiredState。完整步骤见 ../guides/node-onboarding.md。
节点 runtime
| role | 作用 |
|---|---|
router-netns | 提供共享 network namespace(其它服务 network_mode 共享它) |
wg-gateway | 应用 WireGuard 配置、创建隧道接口 |
bird-router | 运行 BIRD 2,承载 BGP、OSPF、静态路由和过滤策略 |
rpki-cache | 为 BIRD 提供 DN42 RPKI/ROA 数据 |
dns | 运行 CoreDNS(订阅 DNS 组时注入) |
debug-shell | 可选调试容器 |
internal_topology 一致性不变量:同一 AS 内多节点的 iBGP/OSPF 由
DesiredState.bird.internal_topology合成(不是bgp_sessions)。所有节点的routers+hosts必须是同一份完整集合,否则会隐蔽缺路由。设计与加节点 checklist 见 internal-routing.md(事故案例存于仓库外的运维叙事档案)。
最小扰动设计
系统刻意采用电平触发(level-based)+ 内容寻址而不是"控制面推送 delta"(边沿触发):事件丢失、乱序、agent 重启都不影响正确性,每轮 reconcile 都从最新全量状态推导出最小动作集。
| 层 | 机制 | 效果 |
|---|---|---|
| 容器 | 身份 = dn42.config_hash(服务 spec + underlay + 构建参数的哈希) | generation 递增不重建任何容器;只有容器定义本身变化才重建 |
| 渲染产物 | 不携带 generation,跨代逐字节稳定 | 无实质变化时 file plan 全 noop |
| 配置文件 | file plan(SHA-256 对比)算出精确差异 | agent 本地就知道"到底变了什么" |
| 数据面 | 定向收敛:birdc configure 热重载、按接口 WireGuard 同步/拆除 | 加一个 peer 只拉起一条隧道,其余 BGP 会话零扰动 |
| 事件 | WS 门铃 + 防抖合并 + 兜底周期 | 突发批量变更合并为一次 reconcile |
控制面在事件里附带 reason 供日志排错,但 agent 的收敛判定完全基于本地观测对比——"该收敛什么"永远以事实为准。
并发与一致性
| 场景 | 保障 |
|---|---|
| 并发 admin 写同一节点 | materialize 对节点行加 SELECT ... FOR UPDATE,generation 严格单调;UNIQUE(node_id, generation) 兜底 |
| 事件先发、事务后回滚 | materialize 不发事件;路由层在事务提交后才广播 |
| WS 事件丢失 / 队列溢出 | agent 兜底周期 reconcile 拉平 |
| 突发批量变更 | agent 防抖窗口合并;每次 reconcile 拉取的都是最新全量状态,中间代次天然被跳过 |
| agent 重复 reconcile | 幂等:file plan 全 noop、容器 plan 全 keep、收敛零动作 |
| 同节点多 agent 实例 | 不支持,部署约定每节点单实例(systemd 单元天然如此) |
生产为 PostgreSQL(真正的行级锁;主面经 Hyperdrive 访问)。SQLite 仅用于 Python 面的本地开发——TS 主面没有 SQLite 路径。
变更闭环
这条闭环保证系统不是"远程执行命令",而是"发布期望状态、节点本地收敛、回报观察结果"。Control Server 不提供远程 shell 或任意命令执行接口(见 security.md)。