大概 5 年前就听说过 DN42,不过因为没有相对稳定的 VPS 和某些心智负担一直没有尝试,最近周末心血来潮就尝试了一下,简单记录一下流程。
DN42 ASN
在 DN42/registry 开 pull request 申请,注册账号之后最好添加 ssh 和 pgp public keys,pipeline ci 在合并前会验证 commit signature,Kioubit oauth 认证时也会用到。
PR 里需要提交以下 object:
mntner— 维护者对象,auth 用 ssh/pgpaut-num— ASN 定义,填 ASN、AS 名称、mntner、admin-c / tech-cinetnum— IPv4 段申请,填 netname、cidr、mntner、countryinet6num— IPv6 段申请,同上route/route6— 宣告前缀,origin 填 ASN,mntner 填 mntnerperson— 联系人对象,admin-c / tech-c 引用
可以参考 registry 里已有的提交,照葫芦画瓢就行。
DN42 DNS
比较通用的做法是用 IATA 机场代码 来命名 DNS,这样换 IDC 服务商时也无需向外部 peer 重定向 DNS 名称,我用了 dnscontrol 来管理 DNS,最好为公网 v4 和 v6 单独加 endpoint,双栈 DNS 记录可能会在 peer 时遇到线路问题。
写两个 helper func,ENDPOINT_RECORDS 为公网 DNS 记录,DN42_ENDPOINT_RECORDS 为 DN42 网络 DNS 记录。
function ENDPOINT_RECORDS(host, names, modifiers) {
var extraModifiers = modifiers || [];
DISALLOW_CF_PROXY_ON("ENDPOINT_RECORDS", extraModifiers);
var records = HOST_RECORDS(host, names, [CF_PROXY_OFF].concat(extraModifiers));
names.forEach(function (name) {
if (host.ipv4) {
records.push(A.apply(null, ["v4." + name, host.ipv4].concat([CF_PROXY_OFF], extraModifiers)));
}
if (host.ipv6) {
records.push(
AAAA.apply(null, ["v6." + name, host.ipv6].concat([CF_PROXY_OFF], extraModifiers)),
);
}
});
return records;
}
function DN42_HOST_RECORDS(host, names, modifiers) {
var extraModifiers = modifiers || [];
DISALLOW_CF_PROXY_ON("DN42_HOST_RECORDS", extraModifiers);
var dn42Names = names.map(function (name) {
return name + ".dn42";
});
return HOST_RECORDS(host, dn42Names, [CF_PROXY_OFF].concat(extraModifiers));
}
// DN42
DN42_ENDPOINT_RECORDS(DN42_HOSTS.ams0, ["ams-0"]),
ENDPOINT_RECORDS(HOSTS.ams0, ["ams-0"]),
预期得到的 DNS 记录长这样:
- Endpoint: ams-0.diffumist.me
- IPv4 Endpoint: v4.ams-0.diffumist.me
- IPv6 Endpoint: v6.ams-0.diffumist.me
- DN42 Endpoint: ams-0.dn42.diffumist.me
DN42 mesh
由于我用 NixOS,用到了 @prince213 的 nix-dn42 modules 来初始化 bird BGP 和 babel IGP。内部 mesh 基于 WireGuard 全互联 + babel,不使用 OSPF(babel 更适合隧道 mesh)。
nix-dn42 的最小启动配置:
# flake.nix
inputs.nix-dn42.url = "git+https://git.sr.ht/~prince213/nix-dn42";
inputs.dn42-registry = {
url = "git+https://git.lantian.pub/backup/dn42-registry.git";
flake = false;
};
# 节点配置
networking.dn42 = {
enable = true;
asn = 4242420642;
ipv4 = {
address = "172.22.64.65"; # 本节点 IPv4
pool = "172.22.64.64/27"; # IPv4 段
};
ipv6 = {
address = "fd22:1056:95a4:1::1";
pool = "fd22:1056:95a4::/48";
};
roa = {
enable = true;
registry = inputs.dn42-registry; # ROA 数据源
};
babel = {
enable = true;
interfaces = {
wg-ams0-sjc0 = { openFirewall = true; };
# ... 其他 mesh link
};
};
};
services.bird = {
enable = true;
autoReload = true;
};
ROA(Route Origin Authorization)用来验证收到的路由前缀是否由注册的 ASN 宣告,防止路由劫持。nix-dn42 会从 dn42-registry checkout 生成 ROA 表,bird import filter 里会自动拒绝 ROA 不合规的路由。
把 nix-dn42 和 dn42-registry 加到 flake inputs,填好 ASN / IP / ROA / babel,bird 就会自动拉起。剩下的工作就是搭好节点间的 WireGuard tunnel,让 babel 在 interface 上跑起来。
DN42 peer
nix-dn42 没有管理外部 peer 的 options,所以写了一个 dn42-peer nixos module 来管理各机器上的 eBGP peers。
写完 nixos module 之后管理外部 peer 就很方便了:
my.services.dn42-peers.example-peer = {
asn = 4242429999;
listenPort = 29999;
endpoint = "peer.example.com";
peerPort = 20642;
publicKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=";
peerLinkLocal = "fe80::9999";
mtu = 1420;
};
每个 peer 会自动生成:一个 WireGuard interface + systemd-networkd 配置 + bird BGP protocol(link-local + extended-next-hop)。
端口通常约定:2 + 对方 ASN 后 4 位:
- 我方 listen 端口 =
2 + 对方 ASN 后四位(对方 dial 我们) - 对端 listen 端口 =
2 + 我方 ASN 后四位(我们 dial 对方,即 20642)
peer 时需要和对方确认的内容有:
- ASN
- peer 哪个地区
- endpoint(对端公网 IP 或域名)和 peerPort(对端 WG listen 端口)
- link-local 地址(BGP session 现在通常支持 link-local IPv6 + extended next hop)
- WireGuard public key
- MTU(默认 1420)
模块默认复用 dn42_wg_private_key(和 mesh 同一把 key);如果对端要求独立 key,可以通过 privateKeyFile 覆盖。
Other Problem
之后实际配置中遇到了 peer 后 v4 路由黑洞的问题,而且 all.rp_filter 和 default.rp_filter 均无法缓解,调查发现 systemd 带了段 50-default.conf,发行版打包可能会直接打进去。
# Source route verification
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.*.rp_filter = 2
-net.ipv4.conf.all.rp_filter
这会导致所有新创建的 wg peer interface rp_filter=2,要求「内核主路由表认为源地址可达」,但 dn42 数据面路由经常会变化,不对称路由下最好直接设置为:
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.*.rp_filter = 0