外观
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 边界、无法保原子,因此整个控制面落成一个 Worker(apps/control-server-ts/README.md:3-6、src/db.ts:6-8)。Workers 没有常驻进程与内存态:连接池由 Hyperdrive 在边缘维护,每请求即取即用;进程内单例(Python 面的 EventBus、ProbeHub)改由 Durable Object 承载。
运行时形态
| 绑定 | 用途 | 取代 Python 面的什么 |
|---|---|---|
HYPERDRIVE | 边缘连接池接 PostgreSQL(GCP 主机 dn42-ts-pg 容器) | database_url 直连 + SQLAlchemy 池 |
CACHE_KV | desired-state(generation 键)、注册表名称、liveness、pflap 热态、routing 预计算等缓存 | Redis |
RELEASES_R2 | agent wheel 发布件(自更新分发源) | 本地发布目录 |
DOORBELL / PROBE_HUB | Durable Object:门铃 WS、拨测作业中枢 | 进程内 EventBus / ProbeHub |
triggers.crons | registry 同步、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-61,HTTPException → {detail}、DesiredStateReferenceError → 422)。路由挂载(:40-51):
| 前缀 | 文件 | 鉴权 | 内容 |
|---|---|---|---|
/api/v1/agent | src/routes/agent.ts | agent token | 注册、desired-state、上报族、自更新 wheel、WG key escrow |
/api/v1/admin | src/routes/admin/* | admin(静态 token 或会话) | 节点/接口/BGP/Peering/DNS 组/token/审批/provision/健康/路由/审计/agent 发布/flap 处置/流量 |
/api/v1/auth | src/routes/auth.ts | 无(自身即登录) | 账号登录、改密 |
/api/v1/ui | src/routes/ui.ts + uiFlaps.ts / uiNodes.ts / uiRegistry.ts | admin | 只读 BFF:dashboard、fleet/节点观测、flap 全家桶、registry、拨测 |
/api/v1/public | src/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(handleAgentWs,index.ts:66-104)。/api/v1/agent/probe-stream/{probe_id}:拨测输出回传,校验 probe 归属后转 ProbeHub(index.ts:109-138)。
admin 子路由的中间件顺序是 audit → requireAdmin(src/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.ts(idFromName('hub')),取代 Python 面 core/probe_hub.py。纯核心逻辑抽在 src/probeJobs.ts(ProbeJobs,可独立单测),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 背景任务
handleScheduled(src/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.sql…0003_route_prefix_hashes.sql(drizzle/meta/_journal.json)。drizzle-kit 在 Worker 外部直连源 PG 应用,与 Worker 部署解耦。 - 大表写路径:
/agent/routing-table摄入用withFreshDb(src/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.ts | DesiredState 组装 + 写世代 + validateReferences(行锁 FOR UPDATE OF nodes,:65) |
desiredState.ts | generation 键 KV 缓存读取(:20-40) |
nodeStatus.ts | 健康五态派生;listAllStatusCompact(dashboard 预提取小列) |
routing.ts / routingAggregate.ts | 整表/差分写、聚合查询、fleet overview、预计算 |
flaps.ts / prefixFlaps.ts / flapLocalize.ts / flapAlerts.ts / flapConfig.ts | flap P0/P1/P2 + V2 根因定位(原理见 flap-detection.md) |
registry.ts | Forgejo sha 门控 + tar.gz 流式解析 + ASN→名称 KV 缓存 |
agentReleases.ts / agentLiveness.ts | R2 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.ts | iBGP/OSPF 视图;站点地理注册表 |
与共享包的关系:平行再实现
TS 面不消费任何 packages/dn42_*。校验、命名、内容寻址哈希、IP 规范化在 TS 侧平行再实现:src/validators.ts(对齐 dn42_common 校验)、src/schemas.ts(zod 对齐 Pydantic)、src/crypto.ts(Web Crypto)、src/ip.ts(normalizePrefix)、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 掉线」是预期现象而非事故。
- 曾出现
exceededCpu503:已购 Workers Paid 解除(计费口径见 ../reference/cloudflare-billing.md)。