过滤器设计
正如我们前面所言, 上下游和 peer 所需要和发的路由是不一样的:上游发全表,收我们自己和我们下游;peer 发他们自己和他们下游,收我们自己和我们下游;下游发他们自己和下游,收全表。所以,接这些的关键,就是设计良好的过滤器,使得他们发来的路由,都能按照正确的方式进行处理。
BGP Community
Section titled “BGP Community”想象一下你的快递,如果上面不贴那个面单,也不让你写写画画,是不是分拣、运送就会难得多?同样的,对于一堆路由,如果没有办法让我们给它们“贴面单”,那我们要处理也会困难得多。所以,工程师们为路由添加了 BGP Community 属性,让我们能够便捷地区分路由,从而知道更多的信息或执行不同的策略。
BGP Community 分三类:
- 普通 Community 长 4 个字节,前两个字节为 ASN,后 2 个字节为标识符,例如
(7720, 1)
。这种 Community 是最普遍的 Community,但是由于它硬性要求 ASN 为两个字节,所以我们使用不了。 - Extended Community (扩展社区)长 8 个字节,为一个八字节的值,前二字节为类型,后六字节可以为一个 2 字节 ASN+一个 4 字节的值,也可以为一个 4 字节的 ASN 或 IP+一个 2 字节的值,在 BIRD 内一般表示为
(type,administrator,value)
。Extended Community 一般用于 MPLS VPN 内,我们基本不会用到。 - BGP Large Community(大型社区)长 12 个字节,前四字节为 ASN,后八个字节分别为数据 1 和数据 2,各 4 个字节,在 BIRD 内一般表示为
(4byte ASN,4byte value,4byte value)
,。它被开发的主要原因是因为 4 字节 ASN 不能用于普通的 BGP 社区,也因此它会成为我们接下来最主要使用的 BGP Community,毕竟我们没有别的可选。
对于我们而言,最主要用到的是普通 Community 和 Large Community。普通 Community 更多是用来操纵我们的路由,Large Community 才真正是为我们网络用来实现功能的。
Community 设计
Section titled “Community 设计”假设我们的 ASN 是 AS114514,下面我们来进行一些简单的 Community 设计(全部使用 Large Community):
Community | 意思 |
---|---|
(114514, 1, 1) | 该路由来自上游 |
(114514, 1, 2) | 该路由来自 Peer |
(114514, 1, 3) | 该路由来自自身(静态/OSPF) |
(114514, 1, 4) | 该路由来自下游 |
这里只是简单的设计了一些信息类 Community,操作类 Community 我们暂不涉及,详情可以参考 小狼的教程(TODO)。
在设计自己的过滤器之前,我们可以先看看别人的,比如HE 的,翻译如下:
Hurricane Electric 路由过滤算法 这是针对具有显式过滤的客户和对等体的路由过滤算法:
尝试为该网络找到一个 as-set。
1.1 在 peeringdb 中,针对该 ASN,检查是否有 IRR as-set 名称。 通过检索验证 as-set 名称。如果存在,则使用它。
1.2 在 IRR 中,查询该 ASN 的 aut-num。如果存在,检查该 ASN 的 aut-num,看看是否能从其 IRR 策略中提取一个 as-set,方法是查找导出(export)或多协议导出(mp-export)到 AS6939、ANY 或 AS-ANY。 优先顺序如下:使用第一个匹配项,先检查“export”,再检查“mp-export”,并且先检查“export: to AS6939”,再检查“export: to ANY”或“export: to AS-ANY”。 通过检索验证 as-set 名称。如果存在,则使用它。
1.3 检查 Hurricane Electric 的 NOC 维护的各种内部列表,这些列表将 ASN 映射到我们发现或被告知的 as-set 名称。 通过检索验证 as-set 名称。如果存在,则使用它。
1.4 如果前面的步骤未找到 as-set 名称,则使用 ASN。
收集与该 ASN 所有 BGP 会话接收的路由。这包括接受和过滤的路由详情。
对每条路由执行以下拒绝测试:
3.1 拒绝默认路由 0.0.0.0/0 和 ::/0。
3.2 拒绝使用 BGP AS_SET 表示法的 AS 路径(即 {1} 或 {1 2} 等)。参见 draft-ietf-idr-deprecate-as-set-confed-set。
3.3 拒绝前缀长度小于最小值或大于最大值的路由。IPv4 的范围是 8 到 24,IPv6 的范围是 16 到 48。
3.4 拒绝 bogons(RFC1918,文档前缀等)。
3.5 拒绝所有 Hurricane Electric 连接的 IX 的 IX 前缀。
3.6 拒绝长度超过 50 跳的 AS 路径。过度的 BGP AS 路径预置是一种自我造成的漏洞。
3.7 拒绝使用未分配的 32 位 ASN(介于 1000000 和 4199999999 之间)的 AS 路径。https://www.iana.org/assignments/as-numbers/as-numbers.xhtml
3.8 拒绝使用未分配的 32 位 AS 号(介于 4200000000 和 4294967294 之间)的 AS 路径。根据 RFC6996,这些号码保留供私有使用。
3.9 拒绝使用 AS 23456 的 AS 路径。支持 32 位 AS 号的 BGP 节点的 AS 路径中不应出现 AS 23456。
3.10 拒绝使用 AS 0 的 AS 路径。根据 RFC 7606,“BGP 节点不得发起或传播 AS 号为零的路由”。
3.11 拒绝在路径中任何存在客户 ASPA 记录且提供者 ASN 未被列为提供者的跳点,未通过 ASPA(自治系统提供者授权)检查的路由。
3.12 拒绝通过路由服务器学习到的路由,并且该路由包含一个跳点,该跳点的 ASN 已指定“绝不通过路由服务器”(peeringdb 标志)。
3.13 拒绝基于源 AS 和前缀的 RPKI 状态为 INVALID_ASN 或 INVALID_LENGTH 的路由。
对每条路由执行以下接受测试:
4.1 如果源是邻居 AS,则接受基于源 AS 和前缀的 RPKI 状态为 VALID 的路由。
4.2 如果前缀是一个已宣布的下游路由,且是因 RPKI 或 RIR 句柄匹配而被接受的起源前缀的子网,则接受该前缀。
4.3 如果前缀和对等 AS 的 RIR 句柄匹配,则接受该前缀。
4.4 如果该前缀与此对等体的 IRR 策略允许的前缀完全匹配,则接受该前缀。
4.5 如果路径中的第一个 AS 与对等体匹配,路径长度为两跳,且起源 AS 包含在对等 AS 的扩展 AS 集合中,并且 RPKI 状态为 VALID 或起源 AS 和前缀存在 RIR 句柄匹配,则接受该前缀。
拒绝所有未被明确接受的前缀
我们可以看到,HE 就明确地采用了“显式接受”的策略。
接下来,让我们尝试实现一下自己的策略。
我们的策略是:
- 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
- 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合)
- 接受剩余的前缀(毕竟本来也是要发你全表的)并且将 local preference 设为 100
其中 local preference 是路由的本地优先级,越高越优先,100 为默认值。
用 BIRD 过滤器的实现如下:
function import_filter_upstream() -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " invalid prefix, reject"; return false; } if !rpki_check() then return false; bgp_large_community.add((114514,1,1)); # 添加区分 Community bgp_local_pref = 100; # 设置路由优先级为比较靠后 return true;}
if_not_valid_asn()
和 is_not_valid_prefix()
是我们用到的工具函数,具体可以去全部配置处查看。
我们的策略是:
- 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
- 允许所有自身前缀和下游前缀
- 拒绝所有其他前缀
BIRD 实现如下:
function export_filter_upstream() -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " export invalid prefix, reject"; return false; } if bgp_large_community ~ [(114514, 1, 3), (114514, 1, 4)] then return true; return false;}
Peer 的导入策略是:
- 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
- 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合)
- 接受有有效 IRR 的前缀并且将 local preference 设为 200(比上游高,因为通常走 Peer 是免费的)
- 拒绝所有其他前缀
function import_filter_peer(string s_name) -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " invalid prefix, reject"; return false; } if !rpki_check() then return false; if inet_irr_check(s_name) then { bgp_large_community.add((114514,1,2)); bgp_local_pref = 200; return true; } return false;}
这里出现了个 inet_irr_check(s_name)
,我们待会再讲。
Peer 的导出策略跟上游的导出策略一样,也是:
- 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
- 允许所有自身前缀和下游前缀
- 拒绝所有其他前缀
BIRD 实现如下:
function export_filter_peer() -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " export invalid prefix, reject"; return false; } if bgp_large_community ~ [(114514, 1, 3), (114514, 1, 4)] then return true; return false;}
下游的导入和 Peer 的导入类似,策略是:
- 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
- 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合)
- 允许有有效 IRR 的前缀并且将 local preference 设为 400(最高,毕竟走下游是他付费)
- 拒绝所有其他前缀
BIRD 实现如下:
function import_filter_downstream(string s_name) -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " invalid prefix, reject"; return false; } if !rpki_check() then return false; if inet_irr_check(s_name) then { bgp_large_community.add((114514,1,4)); bgp_local_pref = 400; return true; } return false;}
导出就很简单了:
- 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
- 允许所有带标记的前缀
- 拒绝所有其他前缀(防止内网前缀、IX 前缀等漏出去)
BIRD 实现如下:
function export_filter_downstream() -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " export invalid prefix, reject"; return false; } if bgp_large_community ~ [(114514, 1, 1), (114514, 1, 2), (114514, 1, 3), (114514, 1, 4)] then return true; return false;}
细心的你可能注意到了,我们上面写的都是 function,不是 filter,这是因为 function 可以有函数,方便我们根据参数进行判断。那是不是我们需要写一个 filter 将其包起来呢?也不是,BIRD 为我们提供了where
关键字,可以快捷的调用函数。
示例如下:
function import_filter_upstream() -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " invalid prefix, reject"; return false; } if !rpki_check() then return false; bgp_large_community.add((114514,1,1)); # 添加区分 Community bgp_local_pref = 100; # 设置路由优先级为比较靠后 return true;}function export_filter_upstream() -> bool { if net_len_too_long() || is_not_valid_asn() || is_not_valid_prefix() then { print net, " export invalid prefix, reject"; return false; } if bgp_large_community ~ [(114514, 1, 3), (114514, 1, 4)] then return true; return false;}# 这里以上一章的 protocol 作为示例protocol bgp upstream { local fc00::2 as ASN; neighbor fd00::1 as 64512; multihop 2; ipv6 { import where import_filter_upstream(); export where import_filter_upstream(); }; graceful restart;};
这里我们使用 where
关键字,快捷地调用了函数作为我们的过滤器,从而使代码更简洁。
刚才我们的过滤器里面出现了 inet_irr_check(s_name)
,这个是我们用于检验 IRR 的工具函数,它大概长这样:
function inet_irr_check(string as_set) -> bool { if net.type = NET_IP4 then { # IPv4检查 if as_set = "<AS-SET>" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,24}, <前缀IP段>/<前缀长度>{<前缀长度>,24} ...] then return true;else return false;}; if as_set = "<AS-SET>" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,24} ...] then return true;else return false;}; if as_set = "<AS-SET>" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,24} ...] then return true;else return false;}; } else if net.type = NET_IP6 then { # IPv6检查 if as_set = "<AS-SET>" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,48} ...] then return true;else return false;}; if as_set = "<AS-SET>" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,48} ...] then return true;else return false;}; if as_set = "<AS-SET>" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,48} ...] then return true;else return false;}; } else return false;};
为了使用它,你需要:
- 使用比如
bgpq4
等软件获取 ASN/ASSET 的 IP 前缀列表。 - 用工具(如 jinja2)将前缀拼合进如上函数,并保存到如
irr.conf
中。 - 使用
include irr.conf;
将其导入到函数中。 - 如果是在运行时变动,则重载 BIRD。
你可以让 AI 用你喜欢的语言生成一个制作这个的脚本,此处省略留作课后习题。