Skip to content

DesiredState 字段参考

DesiredState 是「单节点应运行什么」的完整声明式输入,schema_version v1;本文是其字段单一事实源。

源码位于 packages/dn42_schemas/dn42_schemas/desired_state.py(顶层 + 校验 + normalize 钩子)、network.pyrouting.pydns.pyruntime.pyenums.py,示例见 testing.py

相关文档:DB 事实如何变成本对象见 ../reference/database.md;materializer / 控制面如何组装并下发见 ../internals/control-server.md;地址的源/派生/副本分类见 ../reference/addressing-model.md


StrictModel 不变量

本包所有 schema 对象继承 StrictModelpackages/dn42_schemas/dn42_schemas/base.py:16):

  • extra="forbid":拒绝任何未知字段——控制面与 Agent 之间任何隐式协议漂移都会在解析期直接报错。
  • frozen=True:模型不可变。唯一受控豁口是 DesiredState.validate_references 里 normalize 钩子用 object.__setattr__ 回写 runtime / interfaces / bird;其它任何地方都按不可变对象使用。
  • canonical_json():稳定可复算的 JSON(排序键、紧凑分隔符、ensure_ascii=False),使控制面与 Agent 在不同进程对同一对象算出一致字节序列;用于内容寻址。
  • canonical_sha256():上述 canonical JSON 的 SHA-256 十六进制摘要,用于哈希门控 / 内容寻址。

DesiredState(顶层)

源码 packages/dn42_schemas/dn42_schemas/desired_state.py:50

字段类型必填说明
schema_versionLiteral["v1"]否(默认 "v1"schema 版本号;当前只接受 v1
generationintge=1期望状态世代号,通常由控制面递增。
nodeNodeSpec节点身份、ASN、前缀、loopback、link-local。
runtimeRouterRuntimeSpecruntime 部署:underlay、Dockerfile、RPKI、服务列表、WG 端口范围。
birdBird2ConfigSpec否(默认空配置)BIRD2 模板高层配置。
interfaceslist[InterfaceSpec]否(默认 []节点应创建的接口(dummy loopback、WireGuard 等)。
bgp_sessionslist[BgpSessionSpec]否(默认 []节点应建立的 BGP 会话。
dnsDnsSpec | None否(默认 None本地 DNS 服务配置;None 表示不生成 DNS。
templatesTemplateSetSpec否(默认值)渲染时选用的模板集版本。

validate_referencesmodel_validator(mode="after"))在解析后跑跨字段校验并执行 normalize 钩子,见下文「校验规则」与「Normalize 钩子」。


NodeSpec

源码 packages/dn42_schemas/dn42_schemas/network.py:23

字段类型必填说明
node_idstr节点唯一标识;通常也是模板层 / runtime 层稳定节点名。
sitestr站点 / 机房标识。
regionDn42OriginRegionCommunity否(默认 ASIA_EASTDN42 区域枚举(来自 dn42_common)。
asnintge=1节点 ASN。
router_idstrBIRD router id,通常是稳定 IPv4;走 validate_ip_address
ipv4_prefixeslist[str]否(默认 []自有可宣告 IPv4 前缀;逐项走 validate_dn42_ipv4_network
ipv6_prefixeslist[str]否(默认 []自有可宣告 IPv6 前缀;逐项走 validate_dn42_ipv6_network
loopback_ipv4str | None节点 loopback IPv4;设置则必须 ∈ ipv4_prefixes 之一,且走 validate_ip_address
loopback_ipv6str | None节点 loopback IPv6;设置则必须 ∈ ipv6_prefixes 之一。
link_localstr | None节点级 IPv6 link-local(fe80::/10,不带 %zone);走 validate_ipv6_link_local_address单一真相源:见下。

link_local 单一真相源:一节点一个本端 LLA,所有外部 eBGP WireGuard 接口复用(WG 建邻 + eBGP-over-link-local 源)。materializer 把 <link_local>/64 派生到这些接口的 addresses,存量接口侧不再各存一份(配套 backfill 剥离副本);渲染器再与各接口 fe80 peer_route 配成 peer 形式。不含内部互联:iBGP/OSPF 的内部 WG 接口用各自 LL,不取本字段。地址源/派生/副本分类见 ../reference/addressing-model.md


InterfaceSpec

源码 packages/dn42_schemas/dn42_schemas/network.py:132

字段类型必填说明
namestr接口名;校验 ≤ 15 字符(Linux 限制)。
kindInterfaceKind接口类型,枚举 dummy / wireguard / underlay
mtuint | Nonege=576, le=9000否(默认 1420接口 MTU;None 交由 runtime / 脚本默认。
addresseslist[str]否(默认 []配置到接口的地址;逐项走 validate_ip_interface(须带前缀,如 /32)。
peer_routeslist[str]否(默认 []对端直连 / 宿主路由;逐项走 validate_ip_network
listen_portint | Nonege=1, le=65535WireGuard 监听端口。
private_key_refstr | NoneWireGuard 私钥;kind=wireguard 时必填。两种形态secret:// 前缀 = 托管引用(agent 本地生成 node.key 兑现,私钥不经控制面,见 secret-recovery);否则视为 X25519 私钥明文(随 desired-state 下发)。同一节点勿混用两形态,否则钥分叉(见 postmortem)。
wireguard_peerWireGuardPeerSpec | NoneWireGuard 对端;kind=wireguard 时必填,非 wireguard 时必须为 None

模型级校验(validate_wireguard_fields):wireguard 接口必须同时有 private_key_refwireguard_peer;非 wireguard 接口不得设 wireguard_peer


WireGuardPeerSpec

源码 packages/dn42_schemas/dn42_schemas/network.py:95

字段类型必填说明
public_keystr对端公钥;走 validate_wireguard_key
preshared_key_refstr | None预共享密钥引用;解析方式由运行环境决定。
endpointstr | None对端端点(host:port);走 validate_wireguard_endpoint
allowed_ipslist[str]否(默认 []peer allowed IP;逐项走 validate_ip_network
persistent_keepalive_secondsint | Nonege=1持久保活间隔(秒)。

RouterRuntimeSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:312

字段类型必填说明
project_namestr | Noneruntime 项目名;None 时由节点名推导。校验须匹配 [a-z0-9][a-z0-9_-]*
underlayUnderlayNetworkSpecunderlay 网络定义。
rpkiRpkiSpec否(默认值)RPKI cache 参数。
router_dockerfileRouterDockerfileSpec否(默认值)路由器镜像构建模板参数。
wireguard_port_rangeWireGuardPortRangeSpec | NoneWG UDP 端口范围;设置后驱动端口发布注入(见 normalize 钩子)。
serviceslist[RuntimeServiceSpec]runtime 服务列表。

模型级校验 validate_services 见「校验规则」。


RuntimeServiceSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:159

字段类型必填说明
namestr服务名;同一 deployment 内唯一。
roleServiceRole服务语义角色(见枚举)。
imagestr | None直接使用的镜像名;与 build 互斥。
buildBuildSpec | None构建参数;与 image 互斥。
ipv4_addressstr | Noneunderlay 中显式 IPv4;走 ip_address。用 network_mode 时不得设置。
commandlist[str]否(默认 []启动命令;wg-gateway / bird-router 角色必填。
environmentdict[str, str]否(默认 {}环境变量。
portslist[PortPublishSpec]否(默认 []发布端口;用 network_mode 时不得发布端口。
network_modestr | None网络模式,如 service:<name>service: 目标须存在。
cap_addlist[str]否(默认 []额外 Linux capability(未设时按角色默认回落)。
deviceslist[str]否(默认 []透传设备。
sysctlsdict[str, str]否(默认 {}创建时应用的 sysctl(未设时按角色默认回落)。
volumeslist[VolumeMount]否(默认 []挂载卷;部分角色有必需挂载目标。
depends_onlist[str]否(默认 []启动依赖;目标须存在。
healthcheckHealthCheckSpec | None健康检查(未设时部分角色有默认)。
enabledbool否(默认 True是否参与最终渲染。

模型级校验:imagebuild 不可同设。角色相关必需项(_validate_role_requirements):wg-gateway/bird-router 必须有 command;不共享 netns 的 dns 角色须发布 53/udp + 53/tcp;wg-gateway 必须挂 /opt/dn42/scripts + /etc/wireguardbird-router 必须挂 /opt/dn42/scripts + /etc/bird


VolumeMount

源码 packages/dn42_schemas/dn42_schemas/runtime.py:35

字段类型必填说明
sourcestr宿主路径 / 命名卷 / 相对来源。
targetstr容器内挂载目标。
readonlybool否(默认 True是否只读。

HealthCheckSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:49

字段类型必填说明
testlist[str]健康检查命令数组。
interval_secondsintge=1否(默认 15检查间隔。
timeout_secondsintge=1否(默认 5单次超时。
retriesintge=1否(默认 5连续失败次数阈值。
start_period_secondsintge=0否(默认 10启动宽限期。

BuildSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:67。Dockerfile 内容由 runtime.router_dockerfile 在 agent 内生成并经 Docker Engine API 内存构建——无构建上下文目录、无落盘 Dockerfile。

字段类型必填说明
targetstr | None多阶段构建 target 名。
argsdict[str, str]否(默认 {}build args。

PortPublishSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:83

字段类型必填说明
container_portintge=1, le=65535容器端口。
container_port_endint | Nonege=1, le=65535容器端口范围末端;须 ≥ container_port
host_portint | Nonege=1, le=65535宿主端口;None 仅声明容器端口。
host_port_endint | Nonege=1, le=65535宿主端口范围末端;须先有 host_port
protocolstr否(默认 "tcp"协议,限 tcp / udp / sctp(归一化为小写)。
host_ipstr | None绑定宿主地址;走 ip_address

模型级校验 validate_ranges:仅给 host_port + container_port_end 时自动补算 host_port_end;宿主与容器端口范围大小须一致;host_port_end 须 ≤ 65535 且 ≥ host_port


WireGuardPortRangeSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:253

字段类型必填说明
startintge=1, le=65535容器侧端口范围起。
endintge=1, le=65535容器侧端口范围止;须 ≥ start
host_startint | Nonege=1, le=65535宿主侧起始端口;推导出的宿主末端不得 > 65535。
host_ipstr | None绑定宿主地址;走 ip_address

派生属性:effective_host_start(= host_start or start)、effective_host_endcontains(port)


UnderlayNetworkSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:213

字段类型必填说明
namestr否(默认 "router_underlay"underlay 网络名。
subnetstrunderlay IPv4 子网 CIDR;走 ip_network(strict=False)
gatewaystrunderlay IPv4 网关。
ipv6_subnetstr | Noneunderlay IPv6 ULA 子网;设置后启用 IPv6(NAT66 出网)。
ipv6_gatewaystr | Noneunderlay IPv6 网关;设 ipv6_subnet 时必填,走 ip_address

模型级校验:ipv6_subnet 设了就必须有 ipv6_gateway


RpkiSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:290

字段类型必填说明
enabledbool否(默认 True是否启用 RPKI cache。
cache_urlstr否(默认 dn42 burble ROA JSON)上游 RPKI JSON 源。
listen_hoststr否(默认 "10.254.42.3"underlay 监听地址;走 ip_address
listen_portintge=1, le=65535否(默认 8282监听端口。

RouterDockerfileSpec

源码 packages/dn42_schemas/dn42_schemas/runtime.py:147

字段类型必填说明
base_imagestr否(默认 "debian:13-slim"路由器镜像基础镜像。
debian_mirrorstr否(默认 "deb.debian.org"Debian 包源镜像。

Bird2ConfigSpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:231

字段类型必填说明
regionDn42OriginRegionCommunity | None节点 DN42 区域;None 回退到节点级。
internal_topologyInternalTopologySpec | NoneAS 内部拓扑;存在时驱动 OSPF / iBGP 生成。
large_communitiesBgpLargeCommunitySpec否(默认值)large community 编码 / 策略参数。
dn42_ratelimitintge=1否(默认 15BIRD 默认限速参数。
import_limitintge=0否(默认 8500每 eBGP 对端 per-channel 默认导入前缀上限;0 = 不限制。
import_limit_actionImportLimitAction否(默认 "block"超限动作,见下。
disable_ebgpbool否(默认 False是否整体禁用 eBGP 邻居生成。
export_ownnetsbool否(默认 True是否默认对外宣告自有前缀。
dummy_interfacesdict[str, DummyInterfaceSpec]否(默认 {}BIRD 引用的 dummy 接口映射。
stub_ifnameslist[str]否(默认 []按 stub 处理的接口名。
stub_ifnames_appendlist[str]否(默认 []追加到默认 stub 集的接口名。
static_routes4list[str]否(默认 []IPv4 静态路由表达式;首段走 validate_ip_network
static_routes6list[str]否(默认 []IPv6 静态路由表达式;首段走 validate_ip_network
cold_potato_medintge=0, le=4294967295否(默认 50同大区 cold-potato 偏好的 MED(越低越优):eBGP 导入时给带本节点同大区 region 社区(或无 region)的路由设此 MED,跨区域保持 100,优先就近。
route_local_preflist[RouteLocalPrefSpec]否(默认 []per-prefix local-pref 覆盖(精细路由调优,按前缀精确匹配,不波及 session 其余路由)。见下文 RouteLocalPrefSpec 与 路由调优三件套
blocked_prefixeslist[str]否(默认 []运维拒收前缀(flap 处置):eBGP 导入侧精确匹配 reject,不波及其余路由;由控制面 mitigation 端点下发/撤销(人工触发,非自动抑制)。

ImportLimitAction = Literal["block", "restart", "disable", "warn"]routing.py:22):block 丢多余前缀但保会话(防灌表/震荡首选,不 flap);restart/disable 拆/关会话;warn 只告警。


RouteLocalPrefSpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:243。对单个前缀的 local-pref 覆盖:eBGP 导入时对精确匹配该前缀的路由设 bgp_local_pref,只影响这一个前缀、不波及 session 其余路由(避开对 transit peer 整条会话抬权的 fleet 级爆炸半径)。仅本节点导入侧生效、经 iBGP 传播。

字段类型必填说明
prefixstr精确匹配的目标前缀(v4 或 v6 网络);走 validate_ip_network
local_prefintge=0, le=4294967295命中后设置的 BGP local-pref(越大越优;eBGP 默认 100)。

BgpSessionSpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:39

字段类型必填说明
namestr会话逻辑名;同节点内唯一。
remote_asnintge=1对端 ASN。
neighborstr对端地址;可纯 IP 或 IPv6%iface(带 zone)。
source_addressstr本端源地址;走 validate_ip_address
address_familyAddressFamily地址族,枚举 ipv4 / ipv6 / mp-bgp
interfacestr | None邻居所属接口名;链路本地 IPv6 邻居常需。
policystr否(默认 "dnpeers"模板策略名,如 dnpeers / internal
import_modestr否(默认 "filter"模板导入模式。
export_modestr否(默认 "filter"模板导出模式。
protocol_suffixstr否(默认 ""追加到生成 protocol 名的后缀。
extended_next_hopbool否(默认 False是否启用 extended next hop。
bfdBfdSpec | None否(默认新建 BfdSpecBFD 参数;None 表示不生成 BFD。
route_reflector_clientbool否(默认 False是否视对端为 RR client。
import_limitint | Nonege=1本会话导入上限覆盖;None 用节点级。
import_limit_actionImportLimitAction | None超限动作覆盖;None 用节点级。
local_prefint | Nonege=0, le=4294967295本会话导入路由的 local-pref 覆盖;None 用 BIRD 默认(eBGP=100)。⚠️ 整条会话生效,勿用于 transit peer(爆炸半径 = 该会话全部导入前缀);按前缀精调用 Bird2ConfigSpec.route_local_pref。见 路由调优
link_latencyint | Nonege=1, le=9本会话 WG 链路的 DN42 延迟档(1-9 对数分档);设了就在 import+export 时给路由打 (64511, 档) 延迟社区(沿路径取最差档)。仅可见性/信令,本 fleet 不据此选路。
enabledbool否(默认 True是否参与渲染。

模型级校验 validate_neighbor:拆出 zone 后地址走 validate_ip_address;若 neighbor 带 zone 且设了 interface,两者须一致;IPv6 link-local 邻居必须给 %zone 或显式 interface。方法 is_internal(own_asn)remote_asn == own_asnpolicy == "internal" 即视为内部会话。


BfdSpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:25

字段类型必填说明
enabledbool否(默认 True是否启用 BFD。
interval_msintge=50否(默认 1000控制报文间隔(毫秒)。
multiplierintge=1否(默认 5连续丢失多少报文判失活。

InternalTopologySpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:154

字段类型必填说明
routerslist[str]参与内部路由域的路由器节点名。
private_nodeslist[str]否(默认 []仅内部可见的私有节点。
hostsdict[str, BirdHostSpec]节点名 → BirdHostSpec,iBGP/OSPF 邻居信息主来源。
igp_adjacencieslist[IgpAdjacencySpec]否(默认 []显式 IGP 邻接(非全连接场景)。
full_mesh_ibgpbool否(默认 True是否在 routers 间建 full-mesh iBGP。
ospf_v2bool否(默认 True是否生成 OSPFv2。
ospf_v3bool否(默认 True是否生成 OSPFv3。

模型级校验 validate_topology_hostsroutersprivate_nodes、每条 igp_adjacencies.node 都必须在 hosts 里有 hostvars,否则报缺失。


BirdHostSpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:105

字段类型必填说明
ownipstr节点 loopback / iBGP 标识 IPv4;走 validate_ip_address
ownip6str节点 loopback / iBGP 标识 IPv6;走 validate_ip_address
ibgp_rr_upstreamslist[str]否(默认 []RR 拓扑下的上游 RR 节点名。

IgpAdjacencySpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:125

字段类型必填说明
nodestr对端节点名,须在 topology hostvars 中。
costint | Nonege=1IGP 开销;None 交模板默认。
interfacestr | None显式承载接口名。
iface_typestr否(默认 "ptp"IGP 接口类型。

DummyInterfaceSpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:141

字段类型必填说明
ifnamestr出现在模板中的接口名。
track_servicebool否(默认 FalseTrue 表示承载任播/服务地址(进 direct protocol);False 仅作 stub。

BgpLargeCommunitySpec

源码 packages/dn42_schemas/dn42_schemas/routing.py:201。各整数字段范围 0 ~ 4294967295

字段类型必填说明
origin_node_typeint否(默认 100标记来源节点 ID 的社区类型编号。
origin_region_typeint否(默认 101标记来源区域的社区类型编号。
policy_typeint否(默认 102标记策略字段的社区类型编号。
origin_node_idint | None显式来源节点 ID;未设由模板推导。
policy_local_prefint否(默认 10local-pref 语义策略值。
policy_deprepint否(默认 20de-prepend 语义策略值。
rejected_asnslist[int]否(默认 []标记为拒绝来源的 ASN;逐项须为合法 ASN。

DnsSpec

源码 packages/dn42_schemas/dn42_schemas/dns.py:107None 等价不部署 DNS;enabled=False 则保留配置但不输出 Corefile。

字段类型必填说明
enabledbool否(默认 True是否部署 DNS。
bind_addresseslist[str]DNS 任播 / 服务地址唯一真源;逐项走 validate_ip_address
cache_ttl_secondsintge=0否(默认 300缓存 TTL。
zoneslist[DnsZoneSpec]否(默认 []本地接管 zone。
forwardslist[DnsForwardSpec]否(默认 []转发规则。

bind_addresses 驱动 _normalize_dns_anycast 合成 dns-anycast 接口,见「Normalize 钩子」。


DnsZoneSpec

源码 packages/dn42_schemas/dn42_schemas/dns.py:44

字段类型必填说明
zonestrzone 名;允许单 label,允许含 /(RFC 2317 反向委派),走 validate_domain_name(require_multi_label=False, allow_slash=True)
records_refstr外部 record 集逻辑引用(如 zone://example.dn42)。
primary_nsstr | NoneSOA 主 NS FQDN;有内联 records 时必填。
admin_emailstr | NoneSOA 管理邮箱 zone 写法;有内联 records 时必填。
soa_refreshintge=0否(默认 86400SOA refresh(秒)。
soa_retryintge=0否(默认 7200SOA retry(秒)。
soa_expireintge=0否(默认 3600000SOA expire(秒)。
soa_minimumintge=0否(默认 3600SOA minimum(秒)。
default_ttlintge=0否(默认 3600zone $TTL,单条 record 未给 TTL 时用。
recordslist[DnsRecordSpec]否(默认 []内联记录;非空触发 zone 文件渲染。

模型级校验:有内联 recordsprimary_nsadmin_email 必须同时提供(构造 SOA)。


DnsRecordSpec

源码 packages/dn42_schemas/dn42_schemas/dns.py:20

字段类型必填说明
namestr记录名;相对 owner / @(apex)/ FQDN。
typestr记录类型;限 A/AAAA/CNAME/NS/PTR/TXT/MX/SRV/CAA,归一化为大写。
valuestrrdata;语义由调用方保证。
ttlint | Nonege=0单条 TTL;省略回落 default_ttl

DnsForwardSpec

源码 packages/dn42_schemas/dn42_schemas/dns.py:88

字段类型必填说明
zonestr转发 zone;允许单 label,走 validate_domain_name(require_multi_label=False)
upstreamslist[str]上游 resolver;逐项走 validate_ip_address

TemplateSetSpec

源码 packages/dn42_schemas/dn42_schemas/desired_state.py:30。决定模板层选用哪套模板目录。

字段类型必填说明
birdstr否(默认 "config-bird2/v1"BIRD 模板集版本。
wireguardstr否(默认 "config-wireguard/v1"WireGuard 模板集版本。
corednsstr | None否(默认 "config-coredns/v1"CoreDNS 模板集版本;None 表示不使用。
dockerstr否(默认 "config-docker/v1"Docker 构建产物模板集版本。
scriptsstr否(默认 "config-scripts/v1"启动 / 应用脚本模板集版本。

枚举

源码 packages/dn42_schemas/dn42_schemas/enums.py,全部继承 (str, Enum),可直接接受原始字符串输入并序列化为可读字符串。

枚举取值用途
InterfaceKinddummy / wireguard / underlayInterfaceSpec.kind
ServiceRolerouter-netns / wg-gateway / bird-router / rpki-cache / dns / debug-shellRuntimeServiceSpec.role;决定必需挂载与默认 IP 推导。
AddressFamilyipv4 / ipv6 / mp-bgpBgpSessionSpec.address_family

注:enums.py 还含 ApplyStatusBootstrapStatusAgentCapabilityRuntimeResourceStatusDriftSeverityNodeHealthObservationStatus 等,但这些属于 agent 上报 / 控制面观测协议,不出现在 DesiredState 中。


校验规则

DesiredState.validate_referencesdesired_state.py:81)按顺序执行:

  1. 接口名唯一interfaces[*].name 不得重复。
  2. BGP 会话名唯一bgp_sessions[*].name 不得重复。
  3. BGP 会话引用的接口须存在:每个非空 session.interface 必须出现在 interfaces 名集合里。
  4. 每 WG 接口单一外部远端 ASN:对 enabled 且非内部(is_internal(node.asn) 为假)的会话,按 interface 聚合 remote_asn;同一接口出现多个外部 ASN 即报错。
  5. 内部拓扑须包含本节点:若 bird.internal_topology 存在,node.node_id 必须 ∈ routers + private_nodes

随后调用嵌套模型自身的校验(解析期已生效),关键有:

  • NodeSpecloopback_ipv4 ∈ ipv4_prefixesloopback_ipv6 ∈ ipv6_prefixes;前缀走 DN42 网络校验;link_local 须为合法 fe80::/10
  • InterfaceSpec:名 ≤ 15 字符;wireguard 接口须有 private_key_ref + wireguard_peer,非 wireguard 不得有 wireguard_peer
  • BgpSessionSpec:neighbor / zone / interface 一致性;IPv6 link-local 邻居须给 %zoneinterface
  • InternalTopologySpecrouters / private_nodes / igp_adjacencies 引用的节点都须在 hosts 里。
  • RouterRuntimeSpec.validate_servicesruntime.py:349):
    • 服务名唯一;
    • enabled 服务必须覆盖三个必需角色router-netnswg-gatewaybird-router
    • depends_onnetwork_mode service:<name> 引用的服务都须存在;
    • network_mode 的服务不得同时设 ipv4_address
    • 各角色必需挂载(wg-gateway / bird-router 的脚本与配置目录;非共享 netns 的 dns 须发布 53/udp+53/tcp);
    • 服务 IPv4 须落在 underlay subnet 内(经 resolve_service_ipv4 解析后),不得等于 underlay 网关,且不得与其它服务重复;
    • 发布端口(含范围)在同一 (host_ip, host_port, protocol) 上不得冲突;用 network_mode 的服务不得发布端口。
  • UnderlayNetworkSpecipv6_subnet 设了必须有 ipv6_gateway
  • PortPublishSpec / WireGuardPortRangeSpec:端口范围一致性与上界校验。
  • DnsZoneSpec:内联 records 须有 primary_ns + admin_email

Normalize 钩子(派生/注入/剥离)

validate_references 在所有校验后按 wireguard_port_publish → bird_control_socket → dns_runtime → dns_anycast 顺序归一化,全部经 object.__setattr__ 回写到 frozen 对象。各钩子幂等——序列化后的 desired 回灌 agent 再次校验不会翻倍。

_normalize_wireguard_port_publish_runtimedesired_state.py:126

  • 收集所有 kind=wireguard 接口的 listen_port;同节点内 listen_port 必须唯一(重复报错)。
  • 若设了 runtime.wireguard_port_range,所有 listen_port 必须落在该范围内(越界报错)。
  • wireguard_port_range 合成一条托管 PortPublishSpechost_ip/effective_host_start-effective_host_endstart-endudp),注入到 router-netns 角色服务的 ports(已存在同 key 则不重复)。未设端口范围 / 无 WG 接口时原样返回。

_normalize_bird_control_socketdesired_state.py:227

  • 无条件注入 bird-router 的 /run/bird 可写挂载(source=runtime/bird-runtarget=/run/birdreadonly=False)。这是 bird-router 一等不变量:宿主 agent 经此直连 bird.ctl 采集路由。
  • 已有 /run/bird 挂载则只校验其可写(只读 → 报错)。无 bird-router 服务(极简/异常状态)则原样返回。常量见 runtime.py:16

_normalize_dns_runtimedesired_state.py:278

「分配组即启用」的服务面。据 dns 决定框架托管的 CoreDNS(dns 角色)服务:

  • dnsNoneenabled=False剥掉所有 dns 角色服务(desired 不含 → agent 拆除 CoreDNS)。
  • 启用且尚无 dns 角色服务注入 CoreDNS(name=dn42-dnsimage=coredns/coredns:1.12.1command=["-conf", "/etc/coredns/Corefile"]network_mode=service:<router-netns>、挂 coredns → /etc/coredns、依赖 router-netns + 可选 wg-gateway)。无 router-netns 服务则跳过注入。已有 dns 服务则保留(去重)。常量见 runtime.py:22

_normalize_dns_anycastdesired_state.py:351

「分配组即启用」的网络面,与上一钩子同构。dns.bind_addresses 是 DNS 服务地址唯一真源(地址源/派生分类见 ../reference/addressing-model.md):

  • 先按保留名 dns-anycastruntime.py:32剔除 interfacesbird.dummy_interfaces 中的同名残留(单源识别 ⇒ 幂等)。
  • 启用(dns 非空、enabled、有 bind_addresses派生重建:合成一条 dns-anycast dummy 接口承载这些地址(裸 IP → v4 /32、v6 /128),并登记为 track_service=TrueDummyInterfaceSpec ⇒ BIRD direct_anycast 起源对应前缀、任播地址进 BGP。多节点订阅同组拿相同地址 ⇒ anycast。
  • 未启用 / 无 bind 地址 ⇒ 只剔除不重建:地址既不挂内核也不宣告,未提供 DNS 的节点不会黑洞任播流量。

完整示例

下例摘自 packages/dn42_schemas/dn42_schemas/testing.pybuild_hkg1_example_state()(HKG1 黄金样本),为可读性按 JSON 缩略呈现并加注释。注意:DNS 任播地址写在 dns.bind_addresses,由 _normalize_dns_anycast 派生出 dns-anycast 接口与 dummy 登记——它们手写在 interfaces / bird.dummy_interfaces 里。

jsonc
{
  "schema_version": "v1",
  "generation": 1,
  "node": {
    "node_id": "edge1",
    "site": "hkg",
    "region": "asia-east",            // Dn42OriginRegionCommunity
    "asn": 4242420000,
    "router_id": "172.20.0.62",
    "ipv4_prefixes": ["172.20.0.0/26"],
    "ipv6_prefixes": ["fdce:1111:2222::/48"],
    "loopback_ipv4": "172.20.0.62",   // ∈ ipv4_prefixes
    "loopback_ipv6": "fdce:1111:2222:9500::1"
  },
  "runtime": {
    "underlay": { "subnet": "10.254.42.0/24", "gateway": "10.254.42.1" },
    "router_dockerfile": { "base_image": "debian:13-slim", "debian_mirror": "deb.debian.org" },
    "services": [
      { "name": "dn42-router-netns", "role": "router-netns",
        "build": { "target": "netns" }, "command": ["sleep", "infinity"],
        "cap_add": ["NET_ADMIN", "NET_RAW"], "devices": ["/dev/net/tun:/dev/net/tun"] },
      { "name": "dn42-wg-gateway", "role": "wg-gateway",
        "build": { "target": "wg-gateway" },
        "command": ["/opt/dn42/scripts/wg/start-wg-gateway.sh"],
        "network_mode": "service:dn42-router-netns",
        "volumes": [ { "source": "wireguard", "target": "/etc/wireguard" },
                     { "source": "scripts", "target": "/opt/dn42/scripts" } ],
        "depends_on": ["dn42-router-netns"] },
      { "name": "dn42-bird-router", "role": "bird-router",
        "build": { "target": "bird-router" },
        "command": ["/opt/dn42/scripts/bird/start-bird-router.sh"],
        "network_mode": "service:dn42-router-netns",
        "volumes": [ { "source": "bird", "target": "/etc/bird" },
                     { "source": "scripts", "target": "/opt/dn42/scripts" },
                     // /run/bird 可写挂载——也可省略,由 _normalize_bird_control_socket 注入
                     { "source": "runtime/bird-run", "target": "/run/bird", "readonly": false } ],
        "depends_on": ["dn42-router-netns", "dn42-wg-gateway", "dn42-rpki-cache"] },
      { "name": "dn42-rpki-cache", "role": "rpki-cache", "image": "rpki/stayrtr:latest",
        "command": ["-checktime=false", "-cache=https://dn42.burble.com/roa/dn42_roa_46.json"] }
    ]
  },
  "bird": {
    "region": "asia-east",
    "large_communities": { "origin_node_id": 62 },
    "internal_topology": {
      "routers": ["edge1", "edge2"],          // 必须含本节点 edge1
      "hosts": {
        "edge1": { "ownip": "172.20.0.62", "ownip6": "fdce:1111:2222:9500::1" },
        "edge2": { "ownip": "198.18.1.3",  "ownip6": "fdce:1111:2222:ff01::3" }
      },
      "igp_adjacencies": [ { "node": "edge2", "cost": 10 } ]
    }
  },
  "interfaces": [
    { "name": "dn42-lo", "kind": "dummy", "mtu": null,
      "addresses": ["172.20.0.62/32", "fdce:1111:2222:9500::1/128"] },
    { "name": "as4242420001", "kind": "wireguard",
      "private_key_ref": "secret://nodes/edge1/wireguard/as4242420001/private-key",
      "addresses": ["172.20.0.62/32", "fdce:1111:2222:9500::1/128"],
      "peer_routes": ["172.20.0.105/32", "fdce:1111:2222:dead::11/128"],
      "wireguard_peer": { "public_key": "+aFW7xRRTwOZ6w0EmrvqN4ng2QcFA0/9Wdu9GkdwJgQ=",
                          "allowed_ips": ["0.0.0.0/0", "::/0"] } }
    // ... igp-edge2 内部 WG 接口略
  ],
  "bgp_sessions": [
    { "name": "demopeer_4242420001_ex01_v4", "remote_asn": 4242420001,
      "neighbor": "172.20.0.105", "source_address": "172.20.0.62",
      "address_family": "ipv4", "interface": "as4242420001" }
    // ... _v6 会话略
  ],
  "dns": {
    "bind_addresses": ["172.20.0.20", "172.20.0.22",
                       "fdce:1111:2222::20", "fdce:1111:2222::22"],
    "zones": [ { "zone": "example.dn42", "records_ref": "zone://example.dn42" },
               { "zone": "0.20.172.in-addr.arpa", "records_ref": "zone://0.20.172" } ],
    "forwards": [ { "zone": "dn42", "upstreams": ["172.20.0.53"] } ]
  },
  "templates": { "bird": "config-bird2/v1", "wireguard": "config-wireguard/v1",
                 "coredns": "config-coredns/v1", "docker": "config-docker/v1",
                 "scripts": "config-scripts/v1" }
}

validate_references 后,因 dns.enabled 默认为真且有 bind_addresses

  • interfaces 会多出一条 dns-anycast dummy 接口(地址 172.20.0.20/32...::20/128 等);
  • bird.dummy_interfaces 会多出 dns-anycast: { track_service: true }
  • runtime.services 会多出 dn42-dns(CoreDNS)服务;
  • bird-router 的 /run/bird 可写挂载即使省略也会被补齐。