传统的 BGP 路由泄漏防护高度依赖人工维护的导入、导出策略。一旦运营商误配策略,原本只该发给客户的路由可能被传播给上游或对等网络。RFC 9234 引入 BGP Roles 和 Only to Customer(OTC)属性,让路由器能够根据邻居关系识别并拒绝部分泄漏,而不必等监控系统报警后再人工处置。
针对部署情况的测量表明,这套机制已经进入真实网络,但传播链条并不总是可靠:研究观察到两个 Tier 1 网络意外删除了 OTC。这个现象提醒我们,部署 RFC 9234 不只是打开一个邻居选项,还必须验证 OTC 是否端到端保留。
BGP Roles 把商业关系变成协议输入
BGP 的传统配置知道邻居地址和 ASN,却不天然理解双方是客户、提供商还是对等网络。运营人员通常用路由策略表达这些关系,但策略错误很难从会话本身发现。
RFC 9234 定义了五种角色:
| 本端 BGP Role | 对端通常扮演的角色 | 关系含义 |
|---|---|---|
| Provider | Customer | 本端向客户提供传输能力 |
| Customer | Provider | 本端购买上游传输能力 |
| Peer | Peer | 双方进行对等互联 |
| RS | RS-Client | 本端是路由服务器 |
| RS-Client | RS | 本端连接路由服务器 |
角色通过 BGP 能力协商,使双方能够检查关系是否匹配。例如,一端声明自己是 Provider,另一端也声明 Provider,通常意味着配置与实际商业关系不一致。支持严格模式的实现可以因此拒绝建立会话,避免错误策略开始传播路由。
角色并不能替代 prefix-list、RPKI Origin Validation 或 AS_PATH 过滤。它解决的是另一个问题:把“这条会话在拓扑中的方向”交给协议,让设备有条件执行统一的防泄漏规则。
OTC 如何给路由加上“只能向客户传播”的约束
OTC 是一个可传递的 BGP 路径属性。它表达的核心约束是:这条路由已经进入只能继续向客户传播的阶段,不能再被发送给提供商或对等网络。
可以把正常传播路径理解成一个“先上坡、可横向、再下坡”的谷形模型:
Customer -> Provider -> Peer -> Customer
上坡 横向 下坡
一条从提供商或对等网络学到的路由,通常只能向客户方向继续传播。如果客户错误地把带 OTC 的路由重新通告给另一个上游或对等网络,支持 RFC 9234 的接收方就能识别这种异常,并按实现与策略拒绝该更新。
这里有两个重要边界:
- OTC 不是全局真实性证明。 它依赖参与网络正确设置 BGP Role,并按规范生成、保留和检查属性。
- 链路中途删除 OTC 会破坏后续判断。 下游路由器看到的将是一条缺少约束标记的普通路由,即使它本来已经跨过不应再次“上坡”的边界。
因此,两个 Tier 1 网络意外剥离 OTC 的发现尤其值得关注。Tier 1 位于大量路径的中间位置,属性被删除后,影响可能延伸到许多并不直接相邻的网络。这不必然说明存在恶意行为,更可能是实现版本、策略模板或未知属性处理方式造成的互操作问题,但结果同样需要排查。
可以这样实践:检测 OTC 是否在传播途中消失
实际环境可以从 BMP、实验室 BGP 邻居、路由器遥测或受控前缀探测中采集更新,再把观测结果归一化。下面这个可直接运行的 Python 脚本模拟两条路径:一条正常保留 OTC,另一条在中间网络之后丢失 OTC。
将以下内容保存为 check_otc.py,然后执行 python3 check_otc.py:
#!/usr/bin/env python3
from collections import defaultdict
# 实际使用时,可将这里替换为 BMP、路由器 JSON 输出或实验探针数据。
# 同一 probe 的 before/after 分别表示进入和离开被测网络时的观测。
observations = [
{
'probe': 'tier1-a',
'prefix': '192.0.2.0/24',
'stage': 'before',
'as_path': [64510, 64520],
'otc': 64510,
},
{
'probe': 'tier1-a',
'prefix': '192.0.2.0/24',
'stage': 'after',
'as_path': [64530, 64510, 64520],
'otc': None,
},
{
'probe': 'transit-b',
'prefix': '198.51.100.0/24',
'stage': 'before',
'as_path': [64540, 64550],
'otc': 64540,
},
{
'probe': 'transit-b',
'prefix': '198.51.100.0/24',
'stage': 'after',
'as_path': [64560, 64540, 64550],
'otc': 64540,
},
]
groups = defaultdict(dict)
for item in observations:
key = (item['probe'], item['prefix'])
groups[key][item['stage']] = item
for (probe, prefix), stages in sorted(groups.items()):
before = stages.get('before')
after = stages.get('after')
if not before or not after:
print(f'INCOMPLETE probe={probe} prefix={prefix}')
continue
if before['otc'] is not None and after['otc'] is None:
print(
f'STRIPPED probe={probe} prefix={prefix} '
f'expected_otc={before["otc"]} after_path={after["as_path"]}'
)
elif before['otc'] != after['otc']:
print(
f'CHANGED probe={probe} prefix={prefix} '
f'before={before["otc"]} after={after["otc"]}'
)
else:
print(f'PRESERVED probe={probe} prefix={prefix} otc={after["otc"]}')
预期输出为:
PRESERVED probe=transit-b prefix=198.51.100.0/24 otc=64540
STRIPPED probe=tier1-a prefix=192.0.2.0/24 expected_otc=64510 after_path=[64530, 64510, 64520]
这只是最小检测器。接入生产数据时,还需要加入以下约束,避免把观测差异误判成属性剥离:
- 确认前后观测的是同一个前缀、同一轮探测和可比较的 AS_PATH。
- 排除路由切换导致的路径变化;最好使用受控前缀和明确的时间窗口。
- 区分“设备没有生成 OTC”与“设备收到后删除 OTC”。只有同时看到进入和离开观测,才能较有把握地判断剥离发生在哪一段。
- 检查采集系统是否完整保留未知或可选可传递属性,防止数据管道本身删除 OTC。
上线时不要只检查会话是否建立
部署 RFC 9234 可以从少量明确关系的 eBGP 会话开始,而不是一次修改全部边界路由器。一个稳妥的上线清单包括:
- 盘点每个外部邻居的商业关系,并为 Provider、Customer、Peer、RS 和 RS-Client 建立单一事实来源。
- 检查路由器操作系统版本是否支持 BGP Roles、OTC 生成、OTC 保留和基于角色的拒绝逻辑。
- 先在监控或告警模式观察角色不匹配,再评估是否启用严格拒绝,避免历史配置错误造成大面积会话中断。
- 用受控测试前缀验证 OTC 的生成、传播和拒绝行为,而不是只检查配置文本。
- 对升级、路由反射、路由服务器以及跨厂商边界分别做回归测试。
- 保留现有的导入导出策略、RPKI 验证和前缀过滤;RFC 9234 是附加防线,不是替代品。
BGP Roles 让设备理解邻居关系,OTC 则把传播约束带到后续网络。二者结合后,部分路由泄漏可以在控制平面内直接被拒绝。不过,任何依赖可传递属性的安全机制都要面对同一个现实:路径中最不兼容的一跳,可能决定整条链路的保护效果。部署完成后持续测量 OTC 是否被保留,与启用功能本身同样重要。