Skip to content

Control Server(TS 面)内部

本文讲主控制面 apps/control-server-ts 的内部结构:一个部署在 Cloudflare Workers 上的 TypeScript 实现,2026-07 起承接全部 fleet 节点流量,唯一 HTTP 入口 https://api.natlan.io。它与 Python 温备面(apps/control-server,见 control-server.md)实现同一套 HTTP 契约、读写同一个 PostgreSQL;本文只讲 TS 面特有的运行时形态与实现差异,materialize / 健康推导 / token 等双面共通语义control-server.md,不在此重复。

为什么是单个 Worker

控制面的关键写路径是多语句原子事务(materialize 物化、WireGuard key 校验 + 对端传播),拆成多个服务会让事务跨 RPC 边界、无法保原子,因此整个控制面落成一个 Workerapps/control-server-ts/README.md:3-6src/db.ts:6-8)。Workers 没有常驻进程与内存态:连接池由 Hyperdrive 在边缘维护,每请求即取即用;进程内单例(Python 面的 EventBus、ProbeHub)改由 Durable Object 承载。

运行时形态

绑定用途取代 Python 面的什么
HYPERDRIVE边缘连接池接 PostgreSQL(GCP 主机 dn42-ts-pg 容器)database_url 直连 + SQLAlchemy 池
CACHE_KVdesired-state(generation 键)、注册表名称、liveness、pflap 热态、routing 预计算等缓存Redis
RELEASES_R2agent wheel 发布件(自更新分发源)本地发布目录
DOORBELL / PROBE_HUBDurable Object:门铃 WS、拨测作业中枢进程内 EventBus / ProbeHub
triggers.cronsregistry 同步、flap 告警评估、routing 预计算lifespan 背景循环

配置面(secrets / vars / 绑定 id)见 ../reference/configuration.md;部署与回滚操作见 ../guides/deployment.md

入口与路由树

入口 src/index.ts:Hono app 挂 CORS(:25-32)与每请求 DB 中间件(:34,Hyperdrive → pg.Client → drizzle,请求结束 waitUntil 归还连接,src/db.ts:18-27),/healthz:36,仅返回 {status:'ok', runtime:'workers-ts'},不探库),统一错误处理(:54-61HTTPException{detail}DesiredStateReferenceError → 422)。路由挂载(:40-51):

前缀文件鉴权内容
/api/v1/agentsrc/routes/agent.tsagent token注册、desired-state、上报族、自更新 wheel、WG key escrow
/api/v1/adminsrc/routes/admin/*admin(静态 token 或会话)节点/接口/BGP/Peering/DNS 组/token/审批/provision/健康/路由/审计/agent 发布/flap 处置/流量
/api/v1/authsrc/routes/auth.ts无(自身即登录)账号登录、改密
/api/v1/uisrc/routes/ui.ts + uiFlaps.ts / uiNodes.ts / uiRegistry.tsadmin只读 BFF:dashboard、fleet/节点观测、flap 全家桶、registry、拨测
/api/v1/publicsrc/routes/public.ts公开地图 GET /map(见 ../reference/public-map-api.md

两条裸 WebSocket 通道不走 Hono,在 fetch 顶层分流(index.ts:188-193):

  • /api/v1/agent/ws/{node_id}:门铃。Worker 入口完成 token 鉴权并校验 principal.nodeId == URL nodeId 后转交该节点的 Doorbell DO(handleAgentWsindex.ts:66-104)。
  • /api/v1/agent/probe-stream/{probe_id}:拨测输出回传,校验 probe 归属后转 ProbeHub(index.ts:109-138)。

admin 子路由的中间件顺序是 audit → requireAdminsrc/routes/admin/index.ts:27-28)——审计中间件在鉴权之前,失败尝试也进审计日志。

鉴权模型

src/auth.ts

  • requireAdmin:23-48):未配置 ADMIN_TOKEN 时一律 403(fail-closed)。接受两种凭据、无差别放行:静态 admin token(定时安全比较)或账号会话令牌(resolveSession,argon2id 账号体系见 security.md);principal.identity 记录来源供审计区分。
  • requireAgent:51-64):Bearer → agent_tokens 表解析(未撤销、未过期)。
  • 通道隔离:门铃 WS 只允许连自己节点的 DO;拨测回传只允许归属 agent 连对应 probe。

Durable Objects

Doorbell(每节点一实例)

src/doorbell.ts,取代 Python 面的进程内 EventBus。采用 Hibernation WebSocket(acceptWebSocket:22)——DO 在无事件时休眠不计费,连接保持。职责:

  • 握手即发 hello(带当前 generation,供 agent 追赶,:25);
  • publish / ringDesiredState / ringSnapshotRequest / ringProbeRequest:31-58):admin 写路径事务提交后经 RPC 摇铃;
  • webSocketMessage:67-83):处理 agent 上行心跳——记录 liveness(KV)、比对版本、偏低即推 agent_update_available

事件语义与 Python 面一致:WS 只传门铃不传业务数据(probe_request 例外),agent 收铃后走 HTTP 拉全量。

ProbeHub(单例)

src/probeHub.tsidFromName('hub')),取代 Python 面 core/probe_hub.py。纯核心逻辑抽在 src/probeJobs.tsProbeJobs,可独立单测),DO 只做 RPC 转发与 WS/SSE 封装;作业状态放内存不落 storage(拨测是短命作业,DO 重启丢弃可接受,:1-13)。下发链:POST /ui/probes → ProbeHub 建作业 → 经 Doorbell 发 probe_request → agent 在 debug-shell 容器执行并经 probe-stream WS 回传 → UI 经 GET /ui/probes/{id}/stream(SSE,src/routes/ui.ts:888-923)实时读取。

cron 背景任务

handleScheduledsrc/index.ts:148-185),配置在 wrangler.jsonc:66-68):

计划任务
0 */6 * * *DN42 registry 同步(registrySync:Forgejo tarball + sha 门控,失败保旧数据只记日志,见 ../guides/registry-mirror.md
* * * * *flap 告警边沿评估(evaluateFlapAlertsOnce);dashboard routing 块预计算写 KV(ROUTING_PRECOMPUTE_KEY,TTL 180s,src/services/routing.ts:16-19

数据层

  • src/schema.ts:全部表的 drizzle 定义(与 Python 面 app/db/models/ 同构镜像;表清单与语义见 ../reference/database.md)。索引列 + spec JSON 双层的约定同 Python 面。
  • 迁移:drizzle/0000_init.sql0003_route_prefix_hashes.sqldrizzle/meta/_journal.json)。drizzle-kit 在 Worker 外部直连源 PG 应用,与 Worker 部署解耦。
  • 大表写路径:/agent/routing-table 摄入用 withFreshDbsrc/db.ts:36-47)放进 waitUntil 后台连接执行——先回响应给 agent,解耦 agent 读超时与 DB 事务(src/routes/agent.ts:314-328)。
  • 路由明细差分写:agent 上报带 per-prefix 规范化哈希,与 node_route_prefix_hashes 基准比对后只写变化行(跨面 FNV 哈希逐字节一致)。
  • prefix-flap 热态出 SQL:打分条目 + 路径证据存 KV 单键 blob(pflap:<node>src/services/prefixFlaps.ts),SQL 只留 meta 与速率存档;与 Python 面的 Redis 同名键逐字段一致。

关键服务速查(src/services/

模块职责
materializer.tsDesiredState 组装 + 写世代 + validateReferences(行锁 FOR UPDATE OF nodes:65
desiredState.tsgeneration 键 KV 缓存读取(:20-40
nodeStatus.ts健康五态派生;listAllStatusCompact(dashboard 预提取小列)
routing.ts / routingAggregate.ts整表/差分写、聚合查询、fleet overview、预计算
flaps.ts / prefixFlaps.ts / flapLocalize.ts / flapAlerts.ts / flapConfig.tsflap P0/P1/P2 + V2 根因定位(原理见 flap-detection.md
registry.tsForgejo sha 门控 + tar.gz 流式解析 + ASN→名称 KV 缓存
agentReleases.ts / agentLiveness.tsR2 wheel 发布与目标版本;心跳 → KV liveness
traffic.ts / metrics.ts / observability.ts流量存档、自观测指标、UI BFF 计算
adminAccounts.ts / loginThrottle.ts / turnstile.ts账号登录三件套
wireguardKeys.ts / enrollment.ts / pendingRegistrations.ts / tokens.ts凭证与注册
internalTopology.ts / geo.tsiBGP/OSPF 视图;站点地理注册表

与共享包的关系:平行再实现

TS 面不消费任何 packages/dn42_*。校验、命名、内容寻址哈希、IP 规范化在 TS 侧平行再实现:src/validators.ts(对齐 dn42_common 校验)、src/schemas.ts(zod 对齐 Pydantic)、src/crypto.ts(Web Crypto)、src/ip.tsnormalizePrefix)、src/services/naming.ts维护纪律:改 Python 包的校验/哈希规则必须同步 TS 对应实现,一致性由 apps/control-server-ts/parity/ 对拍 harness(parity_py.py / parity_ts.mts)护航。注入器(CoreDNS、dns-anycast、WG 端口发布等 normalize 钩子)不在 TS 面复刻——agent 拉取 DesiredState 后 model_validate 会幂等重跑(src/services/materializer.ts:9-15)。

与 Python 面的刻意差异

差异说明
秒级缓存KV 最小 TTL 60s,Redis 式秒级缓存略去;dashboard 改用 Cache API 3s TTL(src/routes/ui.ts:383-386
登录限速落 KV,最终一致(跨 PoP 可能略多放行几次,可接受)
/healthz不探库(Workers 无常驻连接可探);Python 面保留 DB 探针行为
OpenAPI/docs / /openapi.json(Hono 未挂载);契约文档见 ../reference/api.md
前缀半衰期600s 是 agent 耦合不变量,刻意不经 vars 开放(src/env.ts:27
TS 独有端点GET /admin/nodes/{id}/traffic(单节点流量时间线)

观测与排错

  • Workers Logs / observability 配置在 wrangler.jsonc:9-23,当前 enabled:false,按需开启)。
  • 控制面重部署或发 agent release 会让全部 agent WS 同时断连重连——管理端看到「全 agent 掉线」是预期现象而非事故。
  • 曾出现 exceededCpu 503:已购 Workers Paid 解除(计费口径见 ../reference/cloudflare-billing.md)。