Skip to content

过滤器设计

正如我们前面所言, 上下游和 peer 所需要和发的路由是不一样的:上游发全表,收我们自己和我们下游;peer 发他们自己和他们下游,收我们自己和我们下游;下游发他们自己和下游,收全表。所以,接这些的关键,就是设计良好的过滤器,使得他们发来的路由,都能按照正确的方式进行处理。

想象一下你的快递,如果上面不贴那个面单,也不让你写写画画,是不是分拣、运送就会难得多?同样的,对于一堆路由,如果没有办法让我们给它们“贴面单”,那我们要处理也会困难得多。所以,工程师们为路由添加了 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 才真正是为我们网络用来实现功能的。

假设我们的 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 路由过滤算法 这是针对具有显式过滤的客户和对等体的路由过滤算法:

  1. 尝试为该网络找到一个 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。

  2. 收集与该 ASN 所有 BGP 会话接收的路由。这包括接受和过滤的路由详情。

  3. 对每条路由执行以下拒绝测试:

    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. 对每条路由执行以下接受测试:

    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 句柄匹配,则接受该前缀。

  5. 拒绝所有未被明确接受的前缀

我们可以看到,HE 就明确地采用了“显式接受”的策略。

接下来,让我们尝试实现一下自己的策略。

我们的策略是:

  1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
  2. 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合)
  3. 接受剩余的前缀(毕竟本来也是要发你全表的)并且将 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() 是我们用到的工具函数,具体可以去全部配置处查看。

我们的策略是:

  1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
  2. 允许所有自身前缀和下游前缀
  3. 拒绝所有其他前缀

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 的导入策略是:

  1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
  2. 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合)
  3. 接受有有效 IRR 的前缀并且将 local preference 设为 200(比上游高,因为通常走 Peer 是免费的)
  4. 拒绝所有其他前缀
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 的导出策略跟上游的导出策略一样,也是:

  1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
  2. 允许所有自身前缀和下游前缀
  3. 拒绝所有其他前缀

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 的导入类似,策略是:

  1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
  2. 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合)
  3. 允许有有效 IRR 的前缀并且将 local preference 设为 400(最高,毕竟走下游是他付费)
  4. 拒绝所有其他前缀

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;
}

导出就很简单了:

  1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀
  2. 允许所有带标记的前缀
  3. 拒绝所有其他前缀(防止内网前缀、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;
};

为了使用它,你需要:

  1. 使用比如bgpq4等软件获取 ASN/ASSET 的 IP 前缀列表。
  2. 用工具(如 jinja2)将前缀拼合进如上函数,并保存到如 irr.conf 中。
  3. 使用 include irr.conf;将其导入到函数中。
  4. 如果是在运行时变动,则重载 BIRD。

你可以让 AI 用你喜欢的语言生成一个制作这个的脚本,此处省略留作课后习题