外观
渲染产物参考
agent 把 DesiredState 渲染成一组文件落在 <state-dir>/nodes/<node_id>/rendered/,再 bind 挂载给各容器。本文按目录逐个说明产出什么、被谁消费、由 schema 的哪一部分驱动。
渲染器在 packages/dn42_templates/,写盘计划在 packages/dn42_runtime/。逐字节的 golden 样本在 examples/rendered-hkg1/,改模板会触发回归比对。
本文的示例取自生产节点 hkg1(AS4242420028,10 节点 fleet),密钥类内容已脱敏。
目录布局
rendered/
├── bird/ → bird-router 容器 /etc/bird
│ ├── bird.conf 主配置:常量、静态聚合、过滤器函数、kernel、模板
│ ├── rpki.conf RPKI protocol(连 underlay 上的 stayrtr)
│ ├── community_filters.conf DN42 large community 编解码与策略
│ ├── custom_filters.conf import / export 过滤器主体
│ ├── ospf.conf OSPF 实例
│ ├── ospf_interfaces.conf OSPF 接口与 cost
│ ├── ibgp.conf iBGP 会话(由 internal_topology 合成)
│ ├── dn42_peers.conf eBGP 会话(由 bgp_sessions 合成)
│ ├── anycast_services.conf direct protocol,承载任播地址
│ └── local_flapfeed.conf ⚠ 旁路进程自管,不属于渲染集
├── wireguard/ → wg-gateway 容器 /etc/wireguard(只读挂载)
│ └── <iface>.conf 每接口一份
├── scripts/ → wg-gateway 与 bird-router 容器 /opt/dn42/scripts(只读挂载)
│ ├── wg/apply-<iface>.sh 单接口应用
│ ├── wg/apply-all-wg.sh 全量重放
│ ├── wg/start-wg-gateway.sh 容器入口
│ └── bird/{apply,start}-*.sh BIRD 重载与容器入口
├── coredns/ → dns 容器 /etc/coredns(订阅 DNS 组时才有)
│ ├── Corefile
│ └── zones/db.<zone>
├── resolv/ → debug-shell 容器 /etc/resolv.conf(只读)
└── runtime/
└── bird-run/ → bird-router 容器 /run/bird(可写;宿主 agent 由此直连 bird.ctl)渲染产物不携带
generation,跨代逐字节稳定。这是最小扰动设计的前提:没有实质变化时 file plan 全是 noop。
bird/
bird.conf
由 NodeSpec 与 Bird2ConfigSpec 驱动的主配置。关键派生:
| 渲染出的内容 | 源字段 |
|---|---|
define OWNAS | node.asn |
define OWNIP / OWNIPv6 | node.loopback_ipv4 / loopback_ipv6 |
define DN42_REGION | bird.region,回落 node.region |
hostname | node.node_id |
static_routes4/6 的 reject route | node.ipv4_prefixes / ipv6_prefixes |
is_self_net() / is_self_net_v6() | 同上,加 + 匹配 more-specific |
is_self_net_aggregate() | 同上,只匹配聚合前缀本身 |
router id | OWNIP |
protocol kernel 的 krt_prefsrc | OWNIP / OWNIPv6 |
自有前缀以 reject route 进入 BIRD——这样 BGP 能宣告聚合前缀,而内核不会把聚合内不存在的目的地转发出去。
is_valid_network() 是 DN42 可接受前缀的白名单(含 ChaosVPN、Freifunk、NeoNetwork 段),不随节点变化,属于协议级常量。
末尾是固定的 include 链:
include "/etc/bird/ospf.conf";
include "/etc/bird/ibgp.conf";
include "/etc/bird/dn42_peers.conf";
include "/etc/bird/local*.conf"; ← 旁路挂载点
include "/etc/bird/anycast_services.conf";local*.conf 这一行是 flapfeed 的挂载点。 它让旁路进程往同一目录丢配置而不必改任何渲染文件——代价是 file plan 必须对该 glob 豁免 prune,否则每轮 reconcile 都会把它当孤儿删掉。见 flap 观测。
dn42_peers.conf
每条 BgpSessionSpec 渲染一个 protocol bgp <name> from dnpeers:
# 会话类型:MP-BGP
protocol bgp furry_4242420088_hkg1_v6_v4 from dnpeers {
neighbor fe80::88 as 4242420088;
interface "as4242420088";
passive off;
bfd yes; ← BfdSpec.enabled
ipv4 {
import where dn42_import_filter(0,23,34);
export where dn42_export_filter(0,23,34);
import limit 8500 action block; ← import_limit / _action
import table;
extended next hop on; ← extended_next_hop
};
ipv6 { …同上,无 extended next hop… };
}- 协议名即会话名,遵循
<slug>_<asn>_<node>_<af>的命名规范。 - 过滤器的三个参数依次是
link_latency/link_bandwidth/link_crypto,驱动 DN42 链路质量社区;0表示未配置。语义见 路由策略。 import table恒开:birdc show route ... all与过滤前 RPKI 分布采集都依赖它。- MP-BGP 形态下
extended next hop on只出现在 ipv4 通道——那正是「用 v6 会话承载 v4 路由」的开关。
ibgp.conf
由 internal_topology 合成,不来自 bgp_sessions。full-mesh 下每个对端一个 protocol bgp ibgp_<node>:
protocol bgp ibgp_sha2 {
local as 4242420028;
neighbor fdc1:e451:2020:9500::3 as 4242420028; ← 对端 hosts[].ownip6
source address fdc1:e451:2020:9500::1; ← 本节点 ownip6
advertise hostname on;
ipv4 {
next hop address 172.20.202.1; ← 本节点 ownip
next hop self ebgp;
import table;
};
ipv6 { next hop address fdc1:e451:2020:9500::1; … };
}ibgp_transport 决定 neighbor 与 source address 用 v6 还是 v4 loopback。生产 fleet 十个节点当前全部是 ipv6——ipv4 回落是给 underlay 无 IPv6 的节点准备的能力,尚未启用。
ospf_interfaces.conf 与 anycast_services.conf
OSPF 接口由 igp_adjacencies 渲染,每条带 cost 与 type ptp。
任播服务是一条 direct protocol:
protocol direct direct_anycast {
ipv4;
ipv6;
interface "dns-anycast";
}接口名 dns-anycast 是保留名,由 _normalize_dns_anycast 从 dns.bind_addresses 派生——这条链是单一真相源的范例:改一处地址,接口、内核路由、BGP 宣告全部跟随。
rpki.conf
连 underlay 上的 stayrtr:remote "10.254.42.3" port 8282,取自 RpkiSpec.listen_host / listen_port。用 retry keep 保守策略,避免缓存短暂不可用时立即清空 ROA 表。
wireguard/
每个 WG 接口一份 .conf,形态极简:
ini
[Interface]
PrivateKey = <节点密钥>
ListenPort = 51829 ← 端口池 external 段内
MTU = 1420
[Peer]
PublicKey = eIv+hTDFg6Nb69FY68LWnfI0bMS5ui7VWUyKZK4DCy0=
Endpoint = hkg1.exploro.one:32641
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25[Interface] 里没有 Address。 接口地址由 apply 脚本用 ip addr 配,不走 wg-quick 的地址管理——这样地址变更不必重建接口。
PersistentKeepalive 恒为 25 秒。
⚠️ 这些文件含明文私钥,且模式为
0644、目录链0755。 同一节点全部接口共用同一把节点密钥。节点上任何本地用户都能读到它,见安全模型。
scripts/
apply-<iface>.sh
单接口应用脚本,被 convergence plan 的 WG_SYNC_INTERFACE 调用。骨架:
flock串行化(/run/dn42-control/wg-apply.lock)——避免 reconcile 收敛与 L3 自愈并发时ip link add的 check-then-create 竞态。- 把持久 conf 复制到
/run/dn42-control/wireguard/并chmod 600。 ip link add ... type wireguard(已存在则忽略),随后ip link show兜底确认。wg-quick strip去掉 wg-quick 专有指令 →wg syncconf增量同步。- 先
ip link set ... up,再配地址——顺序是刻意的:地址命令万一失败(改名时旧接口未拆、点对点地址撞用),接口至少已经 UP,不会留下「建了却 DOWN」的半成品。
脚本里还有一段读
/run/dn42-control/secrets/node.key替换PrivateKey的分支,那是密钥收权前的兑现逻辑。现在是死代码:该文件不存在(实测),渲染 conf 里已是字面私钥。脚本内的注释「持久产物全程不见明文」也随之失效。
其它脚本
| 脚本 | 作用 |
|---|---|
wg/apply-all-wg.sh | 全量重放所有接口;容器重建后与 L3 自愈补齐时调用 |
wg/apply-dn42-lo.sh | dummy loopback 接口(承载节点身份地址) |
wg/apply-dns-anycast.sh | dns-anycast dummy 接口(承载任播地址) |
wg/start-wg-gateway.sh | wg-gateway 容器入口 |
bird/apply-bird.sh | birdc configure 热重载,对应 convergence plan 的 BIRD_RELOAD |
bird/start-bird-router.sh | bird-router 容器入口 |
coredns/
只在节点订阅了 DNS 组时才渲染。
Corefile 每个 zone 一个 server 块,全部 bind 到 dns.bind_addresses:
natlan.dn42:53 {
bind 172.20.202.57 fdc1:e451:2020:56::53
file /etc/coredns/zones/db.natlan.dn42
cache 300
reload
}外加一个 .:53 根块只做 errors 与 reload,不做递归——节点只跑权威 DNS。
zone 文件由 DnsZoneSpec 加其记录渲染,SOA 各字段来自 zone 配置(未配置时取 schema 默认)。反向 zone 的文件名把 / 替换成 _(RFC 2317 委派形如 0/26.202.20.172.in-addr.arpa)。
resolv/ 与 runtime/
resolv/resolv.conf 只读挂给 debug-shell,指向 DN42 递归解析器 172.20.0.53——节点自身只跑权威 DNS,逐跳反解不能指向本机。见主动拨测。
runtime/bird-run/ 是唯一可写的挂载点,容器内是 /run/bird,里面是 BIRD 的控制 socket bird.ctl。宿主上的 agent 由此直连 BIRD 采集路由,不再走 docker exec birdc。该挂载由 _normalize_bird_control_socket 无条件注入,是 bird-router 的一等不变量。
与 file plan 的关系
planner 对渲染集算 create / update / delete / noop(SHA-256 对比),并开启 prune——不在渲染集里的文件视为孤儿删除,这样隧道删除后不留残留 conf。
唯一豁免是 bird/local*.conf:它与渲染集共目录,但归旁路进程自行读写。
计划三层与执行顺序见 Node Agent 内部。