RPKI 路由起源验证实战:防 BGP 劫持

RPKI 路由起源验证是当前对抗 BGP 前缀劫持最成熟、也最容易落地的手段。它解决的问题很具体:当有人向互联网宣告一段并不属于他的 IP 前缀时,接收方路由器能否自动识别并拒绝。没有这套机制时,路由器只能相信相邻 AS 说的一切;有了 RPKI 起源验证,接收方可以拿本地缓存的加密签名记录做比对,把明显伪造的起源直接丢弃。本文从 ROA 签发讲到路由器侧的 ROV 过滤,给出可以直接部署的验证器与路由配置。

适用场景

第一类是需要宣告自有 IP 前缀的企业与服务商,包括自建机房、多线接入、以及持有独立 ASN 的云上部署。只要你的前缀出现在全球路由表里,就存在被劫持的可能,也就需要先把自己的 ROA 签出来,这是所有防护的前提。第二类是需要给下游客户做过滤的 ISP 与 IDC,通过对上游路由做 ROV,可以把明显错误的前缀挡在门外,减少路由表污染。第三类是安全团队在做供应链与第三方依赖评估时,需要判断某个合作方的网段是否具备基本的路由安全能力。

需要注意的是,RPKI 起源验证只解决「起源是否可信」,不解决「路径是否可信」。攻击者如果伪造的起源恰好是合法的,比如在合法 AS 内部做前缀子集宣告,起源验证依然会判为 VALID。要覆盖路径层面需要 ASPA 与 BGPsec,这两者目前仍处于标准推进与试点阶段,本文不展开。

前置条件

  • 持有可宣告的公网 IP 前缀,且该前缀已在对应 RIR(APNIC、ARIN、RIPE 等)登记。
  • 拥有 RIR 门户账号,并具备签发 ROA 的权限(通常需要 Maintainer 或 Admin 角色)。
  • 路由器支持 RTR 协议,主流厂商与开源实现(BIRD、FRR、Cisco IOS-XE、Juniper Junos)均已支持。
  • 一台可运行验证器的 Linux 主机,建议与路由器同机房以降低 RTR 会话延迟。
  • 上线前必须有变更窗口,ROV 开启属于可能影响全量路由的操作,不做灰度会出事故。

原理说明

整条链路有三个角色。第一个是 RIR,它掌握前缀的分配关系,可以据此签发 ROA。第二个是验证器(Validator),它定期从五个 RIR 的仓库同步签名对象,逐条验签后汇总成一张「前缀—起源 AS—最大长度」的清单,也就是 VRP 集合。第三个是路由器,它通过 RTR 协议从验证器拉取 VRP,用于对收到的 BGP 宣告做检查。

ROA 里最关键的两个字段是 origin AS 和 maxLength。origin AS 说明这段前缀允许由哪个 AS 起源宣告;maxLength 说明允许宣告的最长掩码。举例来说,一条 203.0.113.0/24、origin AS 65000、maxLength 24 的 ROA,含义是「这段 /24 只能由 AS65000 宣告,且不允许再往下切成更细的子网」。如果有人宣告 203.0.113.0/25,即使 origin AS 正确,也会因为超过 maxLength 被判为 INVALID。

路由器侧对每条路由有三种判定结果。VALID 表示找到了匹配的 ROA 且完全符合;INVALID 表示找到了覆盖该前缀的 ROA,但 origin AS 或掩码长度不符合;NOTFOUND 表示没有任何 ROA 覆盖该前缀。三种结果对应的策略由运营方决定,业界常见做法是拒绝 INVALID、接受 VALID、对 NOTFOUND 保持接受,因为大量前缀尚未签发 ROA,全量拒绝会造成大范围不可达。

RTR 协议的作用是把 VRP 增量同步到路由器。它走 TCP 323 端口,支持增量更新与序列号校验,路由器断线重连后会从上次的序列号继续拉取,不需要每次全量传输。这个设计让 RPKI 的开销可以做到很小,一个完整的全球 VRP 集合在千万级条目规模下,内存占用通常在几十到几百 MB 之间,取决于实现。

操作步骤

第一步:签发 ROA

先在 RIR 门户里为每个需要宣告的前缀创建 ROA。这一步的重点不是操作流程,而是参数填对,因为 ROA 写错的后果比不写更严重:一旦 maxLength 或 origin AS 与实际宣告不符,自己的合法路由在全网都会被判为 INVALID,等于自我断网。

  • origin AS:填实际发起 BGP 宣告的 ASN,不是上游的 ASN,也不是注册联系人所属公司的 ASN。
  • maxLength:按实际宣告的最长掩码填写。如果确实需要拆分子网宣告,maxLength 要放到对应的位数,否则子网宣告全部 INVALID。
  • 有效期:不要图省事设成很长。建议一年,并在日历上设置到期前 60 天提醒,ROA 过期后 VRP 消失,前缀会退化为 NOTFOUND,在严格策略下等同于失去保护。

签发完成后不要立刻全面开启 ROV,先等 24 到 48 小时让全球验证器完成同步。同步有延迟是正常现象,急于验证会把「还没同步」误判成「配置错误」。

第二步:部署 Routinator 验证器

Routinator 是 NLnet Labs 维护的开源验证器,用 Rust 编写,部署成本低。首次运行需要接受 ARIN 的使用协议,否则 ARIN 仓库的数据不会被拉取,导致大量北美前缀判定异常。

# 安装(以官方发布包为例,版本号按实际情况替换)
curl -L -o routinator.tar.gz \
  https://github.com/NLnetLabs/routinator/releases/latest/download/routinator-linux-x86_64.tar.gz
tar xzf routinator.tar.gz
install -m 0755 routinator /usr/local/bin/routinator

# 初始化:接受 ARIN RPA 并准备仓库目录
routinator init --accept-arin-rpa

# 启动验证器:RTR 走 3323,HTTP 状态接口走 8323
routinator server --rtr 0.0.0.0:3323 --http 127.0.0.1:8323

生产环境建议用 systemd 托管,并限制资源。验证器启动后会持续同步与验签,CPU 与内存占用不高但需要稳定运行,放在受限的 unit 里既安全也便于观测。

cat > /etc/systemd/system/routinator.service <<'EOF'
[Unit]
Description=Routinator RPKI Validator
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=routinator
ExecStart=/usr/local/bin/routinator server \
  --rtr 0.0.0.0:3323 \
  --http 127.0.0.1:8323 \
  --log-level info
Restart=always
RestartSec=10
MemoryMax=2G
TasksMax=128
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/routinator

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now routinator

第三步:路由器接入 RTR 并做 ROV

以 BIRD 2 为例,配置分三段:定义 ROA 表、建立 rpki 协议从 RTR 拉取 VRP、在 BGP 导入策略里用 roa_check 判定。BIRD 的 roa_check 返回三个枚举值,正好对应前面说的三种判定。

# /etc/bird/bird.conf
roa4 table r4;
roa6 table r6;

protocol rpki roa_v4 {
  roa4 { table r4; };
  remote "127.0.0.1" port 3323;
  retry keep 5;
  refresh keep 900;
  expire keep 172800;
}

protocol rpki roa_v6 {
  roa6 { table r6; };
  remote "127.0.0.1" port 3323;
  retry keep 5;
  refresh keep 900;
  expire keep 172800;
}

# 导入过滤:只拒绝 INVALID,放行 VALID 与 NOTFOUND
filter rpki_rov_v4 {
  if (roa_check(r4, net, bgp_path.last) = ROA_INVALID) then {
    print "RPKI INVALID: ", net, " origin ", bgp_path.last;
    reject;
  }
  accept;
}

protocol bgp upstream_a {
  local as 65000;
  neighbor 192.0.2.1 as 65001;
  ipv4 {
    import filter rpki_rov_v4;
    export where source = RTS_STATIC;
  };
}

这段配置里三个参数值得单独说明。refresh keep 900 是重新拉取 VRP 的间隔,15 分钟足够跟上前缀变更,设得过大在 ROA 撤销时反应会慢。expire keep 172800 是会话断开后本地缓存的有效期,48 小时的设计意图是「验证器短时故障不影响转发」,但如果这个值设得过大,验证器已经停止工作而你仍在用过期数据,风险反而更高。print 语句建议在生产环境先保留一段时间,它能把被拒绝的路由写进日志,是灰度期发现误封的主要依据。

如果使用 FRR,配置思路一致,只是语法不同,核心是把 RTR 缓存接进来再用 route-map 匹配判定结果。

# FRR 7.4 及以上参考配置
configure terminal
rpki
 rpki cache 127.0.0.1 3323 preference 1
 exit
!
route-map RPKI-ROV deny 10
 match rpki invalid
route-map RPKI-ROV permit 20
 match rpki valid
route-map RPKI-ROV permit 30
!
router bgp 65000
 address-family ipv4 unicast
  neighbor 192.0.2.1 route-map RPKI-ROV in
 exit-address-family

Cisco 与 Juniper 的对应能力分别是 bgp rpki server 与 rpki validation 相关配置,逻辑同为「拉 VRP、按 valid/invalid/notfound 做策略」,具体命令随平台版本差异较大,投产前需对照本平台版本的命令手册确认。

第四步:灰度上线

ROV 上线最容易出事的地方不是配置写错,而是顺序错了。推荐按下面的顺序推进,每一步都留观察窗口,把「拒绝」换成「记录」先跑一轮再收紧。

# 阶段一:只记录不拒绝(把 reject 换成 accept)
filter rpki_rov_v4 {
  if (roa_check(r4, net, bgp_path.last) = ROA_INVALID) then {
    print "RPKI INVALID: ", net, " origin ", bgp_path.last;
  }
  accept;
}

# 阶段二:观察日志至少 7 天,统计 INVALID 来源 AS
grep "RPKI INVALID" /var/log/bird/bird.log | awk '{print $NF}' | sort | uniq -c | sort -rn

# 阶段三:确认自家前缀与关键合作方均不在 INVALID 列表后,改回 reject 并重载
birdc configure

配置验证

验证分三个层次:验证器是否同步完整、路由器是否拿到 VRP、判定结果是否符合预期。

# 1) 验证器状态:确认已同步的仓库数与 VRP 条数
curl -s http://127.0.0.1:8323/api/v1/status | head -40
routinator vrps --format csv | wc -l

# 2) 路由器侧:RTR 会话是否建立、ROA 表是否有条目
birdc show protocols all roa_v4
birdc show route table r4 count

# 3) 逐条核验具体前缀的判定结果
birdc show route table r4 203.0.113.0/24 all

最关键的一步是验证自家前缀。把每个对外宣告的前缀拿到验证器里比对,确认判定为 VALID 而不是 NOTFOUND 或 INVALID。NOTFOUND 说明 ROA 没签或者还没同步,INVALID 说明 ROA 参数填错了,这两种情况都必须在上线 ROV 之前解决,否则等于自己把自己的路由封掉。

# 检查自有前缀的 ROA 覆盖情况
routinator vrps --format csv | grep -E "^203\.0\.113\.0/24"
# 期望输出形如:AS65000,203.0.113.0/24,24

# 若无输出,说明该前缀没有 ROA,需回到 RIR 门户补签

上线后建议加一条例行监控:统计每小时被拒绝的路由条数,突增即告警。这个指标的突增通常意味着两种事,要么上游出现了大规模前缀劫持,要么某个大客户的 ROA 被误配导致其路由被判为 INVALID,两种情况都需要人介入。

# 简易监控脚本:统计近一小时 INVALID 拒绝数,超过阈值即告警
#!/bin/bash
THRESHOLD=50
COUNT=$(journalctl --since "1 hour ago" -u bird \
        | grep -c "RPKI INVALID")
if [ "$COUNT" -gt "$THRESHOLD" ]; then
  echo "RPKI ALERT: ${COUNT} invalid routes rejected in 1h" | \
    mail -s "RPKI ROV 异常" noc@example.com
fi

常见问题

Q1:开启 ROV 之后,自家部分子网突然不可达,怎么排查?

大概率是 ROA 的 maxLength 填小了。举例来说,注册的是 203.0.113.0/24 但实际对外宣告了 203.0.113.0/25 和 203.0.113.128/25,如果 ROA 的 maxLength 只写到 24,这两个 /25 会被全网判为 INVALID 并丢弃。处理办法是回到 RIR 门户把 maxLength 调整为实际使用的最长掩码,或者干脆调整宣告方式只发 /24。修改后需要等验证器重新同步,通常 15 分钟内生效,但全球传播需要更长时间,期间应保持 ROV 处于只记录模式。另一个可能是 origin AS 填成了上游的 ASN,这类错误在所有对端表现一致,特征很明显。

Q2:为什么我的前缀 ROA 明明签了,查出来还是 NOTFOUND?

按顺序排查四点。第一,同步延迟:ROA 签发后需要 RIR 发布到仓库、验证器再拉取,正常 1 到 24 小时,刚签完立刻查必然是 NOTFOUND。第二,ARIN RPA 未接受:Routinator 首次运行必须带 –accept-arin-rpa,否则 ARIN 仓库整个不拉,北美前缀全部缺失。第三,前缀写错:ROA 里的前缀必须与注册记录完全一致,包括掩码长度,写成 /23 覆盖 /24 不会被匹配到。第四,验证器缓存异常:routinator vrps 直接在本地查而不是查路由表,如果本地有而对端没有,问题在 RTR 会话而不是 ROA 本身。

Q3:NOTFOUND 的路由要不要一起拒绝?

现阶段不建议全量拒绝。全球仍有相当比例的前缀没有签发 ROA,全拒绝意味着大量正常站点不可达,而且这些缺失往往出现在你无法控制的对端。业界的普遍做法是只拒绝 INVALID,对 NOTFOUND 保持接受,同时把 NOTFOUND 的比例作为一项长期观测指标,比例下降说明生态在改善。如果场景允许,可以对特定来源做更严格的策略,比如对已知应签发 ROA 的大型合作方,把 NOTFOUND 也视为异常并人工跟进,但这必须逐条确认,不能做成全局默认。

总结

RPKI 路由起源验证的价值在于把「信任邻接 AS」变成「验证密码学凭据」,代价是需要自己先把 ROA 签对。落地路径可以概括为四步:签 ROA、部署验证器、路由器接入 RTR 做 ROV、按灰度顺序收紧策略。整个过程里最容易出错的不是技术配置,而是参数填错与上线顺序颠倒,所以务必要有「先只记录、再统计、后拒绝」的过渡阶段,并且把被拒绝路由的条数纳入例行监控。做完这一套,前缀劫持的门槛会显著提高,同时你也具备了在被劫持时快速定位的证据链。