# Routing Wiki (/docs) 欢迎来到 Routing Wiki,这是一个围绕 Bird / BGP 的中文知识库。 # 五、之后 (/docs/beginner/after) 配置到这里,你应该已经对 BGP 有了初步的理解,并成功搭建起一套基础的 BGP + IGP 网络体系。恭喜你,迈出了至关重要的第一步! 但网络的世界远比我们目前所接触的要复杂和精彩得多。真正的网络工程,往往不仅仅停留在“跑通”这一层。要实现稳定、高效、可扩展的网络系统,还需要你不断深入学习更多协议的机制、理解更复杂的拓扑结构,以及掌握各种实战中遇到的问题排查与优化方法。 如果你对网络充满热情,以下是一些推荐的后续探索方向: * **进阶 BGP 配置**:如 Route Reflector、Confederation、BGP 策略路由(policy routing)、路由聚合与过滤(aggregation & filtering)、MED 和 Local Preference 的运用等。 * **MPLS 与 EVPN**:如果你希望进一步理解大型运营商或数据中心网络架构,可以了解 MPLS 技术及其与 BGP 的结合,尤其是 EVPN-VXLAN 的应用。 * **路由协议混合部署**:掌握 OSPF、IS-IS、Babel 等 IGP 的不同特点,并了解如何与 BGP 协同工作。 * **网络自动化**:使用 Ansible、NetBox、gNMI、BGP-LS 等工具和协议,实现网络配置和状态的自动化管理。 * **大型网络模拟与测试**:尝试使用如 GNS3、EVE-NG、Containerlab 等平台,搭建更大规模、更复杂的实验环境。 你可以通过以下方式持续学习和进阶: * 关注 [BIRD](https://bird.network.cz/)、[FRRouting](https://frrouting.org/)、[OpenBGPD](https://www.openbgpd.org/) 等主流 BGP 实现的官方文档。 * 观看 B 站上的网络相关课程,或者大学的计算机网络课程。 * 订阅相关社区或技术博客,如 NANOG、RIPE Labs,获取最新的网络技术动态。 * 实战出真知,自己动手多搭几套拓扑,亲自观察协议行为和日志输出,会比死记硬背更有效。 网络是一项充满细节、逻辑与工程性的艺术。每一条路由、每一个前缀背后,都是全球互联协作的奇迹。愿你在这条探索之路上不断成长,成为一名真正掌控网络的工程师。 # 一、开始之前 (/docs/beginner/before) 当你点开这篇文章时,你可能对 BGP 只有一个模糊的印象,但没关系,我们先把概念重新说一遍~~虽然很多时候你未必真用得上~~。 # 什么是 BGP [#什么是-bgp] 以下文段来自[维基百科](https://zh.wikipedia.org/wiki/%E8%BE%B9%E7%95%8C%E7%BD%91%E5%85%B3%E5%8D%8F%E8%AE%AE) > **边界网关协议**(英语:Border Gateway Protocol,缩写:BGP)是[互联网](https://zh.wikipedia.org/wiki/互联网)上一个核心的去中心化自治[路由协议](https://zh.wikipedia.org/wiki/路由协议)。它通过维护 IP[路由表](https://zh.wikipedia.org/wiki/路由表)或"前缀"表来实现[自治系统](https://zh.wikipedia.org/wiki/自治系统)(AS)之间的可达性,属于矢量路由协议。BGP 不使用传统的[内部网关协议](https://zh.wikipedia.org/wiki/内部网关协议)(IGP)的指标,而使用基于路径、网络策略或规则集来决定路由。因此,它更适合被称为矢量性协议,而不是路由协议。 简而言之,BGP 是在自治系统之间交换路由信息的最主要协议(虽然思科的 EIGRP 技术上也支持跨自治系统路由,但在真实世界几乎没有应用)。BGP 的核心作用是在自治系统之间传递路由信息,并通过路径属性和策略来选择并传播“最优路径”。 # 路由 [#路由] 如果你看到这里,说明你对“路由”应该已经有了基础认识。简单来说, **路由 (Route)** 作为名词,指的是“某个 IP 段要从哪条线路、走哪个网关出去”;作为动词,就是路由器在收到数据包后,帮它找出应该走哪条路的过程。路由表,顾名思义,就是记录了若干条路由信息的表格。 在路由表里,除了目的网络段和下一跳,还会包含失效时间、度量值、路由来源等附属信息。例如,下面是一条在 Linux 中查看到的去往 `10.2.5.0/24` 的路由: ```shell 10.2.5.0/24 via 10.254.0.34 dev wg0 proto bird src 10.2.3.1 metric 32 ``` 来拆解一下: * `10.2.5.0/24` 目标 IP 段 * `via 10.254.0.34` 下一跳的 IP * `dev wg0` 目标接口 * `proto bird` 路由来源的协议,此处为 bird 指的是从 bird 写入 * `src 10.2.3.1` 当这跳转发的来源是本机的时候,采用的源地址 IP * `metric 32` 路由度量值,存在多个对同一 IP 段的路由时用来决定选择哪条路由来使用,metric 越小路由优先级越高 {/* prettier-ignore */} 在 Linux 内: * 如果同时有 `dev` 和 `via`,系统会根据 ARP 表把目标 MAC 改写成下一跳的 MAC(若 ARP 表中没有,则会通过 `dev` 指定的接口发起 ARP 查询)。 * 如果只有 `dev` 没有 `via`,则直接用 ARP 表查找目标地址的 MAC(同样若无记录则发起 ARP),然后通过指定接口转发。 * 如果只有 `via` 没有`dev`,会递归查询直至找到包含 `dev` 的下一跳,然后按上面规则转发。 但你可能不知道的是,在路由器内部,路由表其实分成两张:在大多数商业路由器里,路由表分为 **RIB(Routing Information Base,路由信息库)** 和 **FIB(Forwarding Information Base,转发表)**。RIB 汇聚来自各种协议和配置的路由信息,经过选路后,只有最优路由会被安装到 FIB,供硬件用于实际数据转发。下面这张图就展示了 RIB 到 FIB 的过程。本教程中,Bird 就负责管理 RIB,而 Linux 内核就是我们的转发表 FIB。 # ASN、前缀、IRR 与 RPKI [#asn前缀irr-与-rpki] 本教程不包含如何申请 ASN 与 IP 的具体流程,请自行搜索 LIR 相关的申请教程。 你打算购买自己的 ASN 和 IP ,开始自己的 BGP 生涯。但在你搜索的时候,你对它们的关系陷入了疑惑。这两个有什么从属关系吗?如果没有从属关系,那 IP 广播又为什么会用到 ASN 呢?同时你也看到了一个新词:前缀。前缀又是什么呢? * **ASN(Autonomous System Number)** 即自治系统编号,用来标识一个自治系统(AS, Autonomous System)。一个自治系统通常代表一家企业、机构或某项网络业务,比如 Google(AS15169)、Cloudflare(AS13335)、Lumen(AS3356)。同一个公司在不同地区也可能使用多个 ASN(例如 Misaka Network Inc)。ASN 由五大 RIR(区域互联网注册机构,如 APNIC、ARIN、RIPE NCC)分配,NIR(国家互联网注册机构,如 CNNIC)或 LIR(地方互联网注册机构)可以代理申请。NIR、RIR 通常不接受个人业务,所以我们自己用 ASN 一般都是找 LIR 申请。 * **前缀(Prefix)**,也叫 IP 段,是指一段连续的 IP 地址。组织通常以块为单位申请和广播 IP。在 IPv4 中,/24 是最小的可单独广播单位;在 IPv6 中则是 /48。 前缀和 ASN 在申请时彼此独立。一个组织可以拥有多个前缀和多个 ASN。但在广播时,每个前缀都必须由一个起源 ASN(Origin AS)对外发布,未经授权广播他人 IP 就是“盗播”。那么 IP 的授权是如何确认的呢?这就涉及 IRR 和 RPKI。 钱虽然好用,但你家的钱只有你能花——IP 也是一样,只有 IP 的主人才能授权给别的 ASN 用。未经许可的 ASN 广播 IP,就成盗播了。那 IP 是怎么授权给别人用的呢?这就要提到 IRR 和 RPKI 了。 * **IRR(Internet Routing Registry)** 是一个分布式数据库系统,用于登记和查询路由信息如路由对象(route object),而路由对象上就记录了哪个前缀可以用于哪个 ASN 。常见的 IRR 可在 [https://irr.net/registry/](https://irr.net/registry/) 查询。除了五大 RIR 自建的 IRR 外,还有像 RADB、ALTDB、LEVEL3 等企业或第三方运营的 IRR。目前 IRR 仍是主流的归属管理方式,但因为分散、验证较弱,存在滥用风险。下面是 `1.1.1.0/24` 的 IRR 记录,采用 RPSL 语言,里面标识了起源 AS。 ```yaml route: 1.1.1.0/24 origin: AS13335 descr: APNIC Research and Development 6 Cordelia St mnt-by: MAINT-APNICRANDNET last-modified: 2023-04-26T02:42:44Z source: APNIC ``` * **RPKI(Resource Public Key Infrastructure)** 是一种基于公钥基础设施的路由验证机制,通过数字签名和证书链验证 IP 和 ASN 的对应关系,防止路由被劫持或冒用。RPKI 是对 IRR 的补充甚至替代,但普及仍在进行中。 目前 RPKI 的推进虽然有所成效,但还十分缓慢,所以一般(五大 RIR 的)IRR 才是判断 IP 是否能够被某个 ASN 广播的金标准。但新的 IP 段授权的时候,一般会同时加上 IRR 和 RPKI。 # BGP Session 、 BYOIP 与 IP Transit [#bgp-session--byoip-与-ip-transit] 申请完了 ASN 和 IP,你打算去买一个能够使用你的 IP 的服务。但当你找寻相关的服务时,你看到了诸如"BGP Session", "BYOIP" 和 "IP Transit" 之类的名词,让你眼花缭乱。这些名词究竟指的是什么? * **BGP Session** 指的是与服务商建立 Border Gateway Protocol(边界网关协议)会话的服务,常见于 IDC(数据中心)环境中。通过建立 BGP 会话,你可以将自己的 IP 地址广播到互联网,并通常能够接收到完整的 BGP 路由表(即“全表”)。这种服务多见于各类虚拟服务器,大多数情况下,BGP Session 所产生的流量会与你自身网络的出入流量合并计算。 通常而言,我们需要找的是带有 BGP Session 的 VPS。 * **BYOIP** 指的是“自带 IP 地址”的服务,常见于云计算平台(如 AWS、GCP)和部分 SaaS 提供商(如 Cloudflare)。这项服务允许你将自己的 IP 地址段(通常是 RIR 分配的)映射到云平台上使用。 与 BGP Session 和 IP Transit 不同的是,许多 BYOIP 服务**并不直接与用户建立 BGP 会话**。虽然服务商依然会使用 BGP 将你的 IP 广播到互联网,但你作为用户通常只需在控制面中提交 IP 段,后台由平台负责路由发布。因此,从你的角度看,它更像是使用一个“静态 IP”,而不是动态的路由对等关系。 * **IP Transit** 是最传统意义上的“接入互联网”服务,主要提供商是 ISP 或上游运营商。购买 IP Transit 意味着你可以使用对方的网络作为通往整个互联网的“高速公路”。 IP Transit 通常包含与对方建立 BGP Session 的能力,以便你用自己的 AS(自治系统号)广播自有的 IP 地址段。这使得你能将自己的网络纳入全球互联网的路由体系中。它的计费单位一般是 Mbps/月,采用 95 分位计费(95th percentile billing):每 5 分钟记录一次带宽使用量,去掉全月中最高的 5% 样本后,剩下的最大值作为该月计费带宽。 需要注意的是,**IP Transit 提供的是“通向互联网的路径”本身**,而不是像 DIA(Dedicated Internet Access)那样只提供几个静态 IP 给你使用,通常不带 BGP 功能。DIA 常面向企业客户,IP Transit 则更偏向网络服务商、内容分发商等具备一定网络运营能力的客户。 # BIRD [#bird] 有了带 BGP Session 的 VPS 后,你需要一个路由软件来建立 BGP 连接。以前这需要昂贵的硬件路由器,如今只需在 Linux 上安装路由守护进程即可。常见的有 `FRR`、`OpenBGPD`,而本教程将使用 BIRD。 ## 什么是 BIRD [#什么是-bird] 以下文段来自 [BIRD 中文文档](https://bird.xmsl.dev/docs/user-guide/1-1-introduction.html) > BIRD 全称 `BIRD Internet Routing Daemon`,旨在开发一个功能完备的动态 IP 路由守护进程 (Routing Daemon),主要针对 Linux、FreeBSD 等其他类 UNIX (UNIX-like) 系统开发适配,完整源代码采用 [GNU 通用公共许可证](https://zh.wikipedia.org/wiki/GNU通用公共许可证) ([GNU General Public License](https://en.wikipedia.org/wiki/GNU_General_Public_License)) 协议发布,它负责在运行 IP 协议的互联网上的路由器之间传递路由信息。 > > BIRD 此前是 [布拉格查拉斯大学 (Charles University in Prague)](https://en.wikipedia.org/wiki/Charles_University) 数学和物理学院的一个研究项目。 从 2009 年开始由捷克域名注册机构 - [CZ.NIC Labs](https://labs.nic.cz/) 接手项目并全权负责开发和维护。 > > 路由器一般只与相邻的路由器交换路由信息,这样就可以快速发现网络中的拓扑结构,以找到到达目标路由器的最佳(以某种度量为基础)路径,并不断地更新路由表,以便在网络拓扑发生变化(如链路故障、新增链路)时,能够及时地找到最新的最佳路径。 > > 在 BIRD 出现之前,这些路由器之间的路由信息交换是通过价格昂贵的专用硬件完成的,而 BIRD 的出现,使得这些路由器可以通过普通的计算机(通常是运行 Linux 或类 UNIX 的操作系统)来实现。 ## BIRD 的版本变迁 [#bird-的版本变迁] Bird 目前有三个大版本:v1, v2, v3 ### v1 [#v1] Bird v1 是最早的 Bird,由两个守护进程分别支持 IPv4 与 IPv6 的路由,最晚版本是 1.6.8,于 2019 年 9 月 11 日发布,现已不推荐使用。 ### v2 [#v2] Bird v2 是目前 Bird 的主线版本。与 v1 相比,v2 仅使用一个守护进程来运行 v4 与 v6 的路由。在本教程中,我们将使用 Bird v2 作为路由守护进程。 ### v3 [#v3] Bird v3 是 Bird 的下一代版本,该版本增加了多线程的支持,仍在完善中。 ## 如何安装 BIRD [#如何安装-bird] 请参阅 [BIRD 中文文档:如何安装 BIRD](https://bird.xmsl.dev/docs/user-guide/1-2-installing.html) ## Bird 的基本语法 [#bird-的基本语法] 请参阅 [BIRD 中文文档:BIRD 配置](https://bird.xmsl.dev/docs/user-guide/3-1-introduction.html) 路由基础概念的讲解到此就告一段落。如果你还想了解更多,欢迎你前往 [Cloudflare 的网络层知识](https://www.cloudflare.com/zh-cn/learning/network-layer/what-is-the-network-layer/) 进行阅览。下一章,我们将带你使用 Bird 与服务提供商建立 BGP 会话,完成你的第一次广播。 # 二、拉起一个 BGP 会话 (/docs/beginner/bring-up-a-bgp-session) 经过一番申请,你终于拿到了属于自己的 IP 和 ASN,并买好了支持 BGP Session 的 VPS,也在上面安装好了 BIRD。然而,当服务商把 IP 和 ASN 丢给你后,你可能一脸茫然:我该怎么配置 BIRD,才能让它正确广播、只发我自己的路由? 没关系,在这篇文章,你将会学到如何把你的 IP 段广播出去,完成你人生中的第一次广播! 考虑到大多数 BGP Player 当前主要持有 IPv6 段,本章示例将以 IPv6 为主,IPv4 的配置方法大同小异。 假设你的 ASN 和 IP 信息如下: * **ASN**:AS114514 * **机器上的 IP**:`fc00::2/64` * **待广播 IP 段**:`2001:db8::/48` 在本章中,我们使用的系统为 Debian 12,BIRD 版本为 2.17.1。 # 配置 BIRD [#配置-bird] BIRD 安装好后,`/etc/bird/bird.conf` 默认会带有一份 200 多行的示例配置文件,里面展示了 BIRD 的多种用法。你可以先备份一份以备研究,但在这里我们先将其清空,从零开始编写。执行以下命令即可: ```shell echo > /etc/bird/bird.conf ``` 这样就得到了一个空白的配置文件。 **注意:请不要随意修改 `envvars` 文件**。它对 **BIRD** 服务正常运行至关重要。如果误改,可按以下内容恢复: ```shell BIRD_RUN_USER=bird BIRD_RUN_GROUP=bird #BIRD_ARGS= ``` ## 常量定义 [#常量定义] 在配置文件的最开头,我们先把各类常量定义一下: ```bird2 {2, 3, 4} log syslog all; router id 10.0.0.1; # 定义 Router ID define ASN = 114514; # 定义本地使用的 ASN define OWN_IPv6 = [2001:db8::/48]; # 定义将要对外宣布的 IPv6 段 ``` 这里做了三件事: * 用 `log syslog all;` 把所有日志写到系统日志,方便排错。 * 用 `router id` 指定一个全局 Router ID。Router ID 必须为一段全球唯一的 32 位无符号整数 (uint32\_t),不过为了便于标识,通常使用机器上的公网 IPv4 单播地址。如果你的机器上有网卡绑定 IPv4 地址的话,也可以删掉这行,BIRD 会找一个网卡的 IPv4 地址作为 Router ID,不过最好还是自己指定。 * 用 `define` 定义变量,后面写配置时重复用 `ASN` 和 `OWN_IPv6` 会更简洁。 ## 协议块 [#协议块] 接下来是核心的基础 `protocol` 配置,用来告诉 BIRD 如何和系统打交道。 ```bird2 protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol static static_v6 { ipv6; route 2001:db8::/48 reject; #在 static 中添加你需要广播的路由 }; ``` 这里包含三个部分: * `protocol device`:让 BIRD 能读取本机的网卡信息。 * `protocol kernel`:把 BIRD 学到的路由写回 Linux 内核路由表(FIB),否则操作系统无法真正使用这些路由。 * `protocol static`:在 BIRD 内声明你要对外宣布的静态前缀。 `static` 协议用于在 BIRD 内定义要广播的路由,示例如下(引用自 [Soha 的新手教程](https://github.com/moesoha/bird-bgp-kickstart/blob/master/main.md#协议-protocol)): ```bird2 protocol static { ipv6; # 启用 ipv6 channel,否则不会收集 IPv6 路由 route 2001:db8:100a::/48 reject; # 定义一条路由 2001:db8:100a::/48 为 reject/unreachable route 2001:db8:100b::/48 via "eth0"; # 定义一条路由 2001:db8:100b::/48 的下一跳为 eth0,也就是说会在eth0内发邻居请求报文NS(Neighbor Solicitation)寻找IP所在的机器 route 2001:db8:100c::/48 via 2001:db8:eeee::1; # 定义一条路由 2001:db8:100c::/48 的下一跳为 2001:db8:eeee::1 } ``` 一般来说,如果只是为了对外宣布一个段,通常会用 `reject` 作为下一跳(见第 15 行),这是因为数据包到达后,实际的转发由 Linux 的路由表负责,而你的 Linux 路由表通常会包含更细化的路由(例如 dummy0 上绑定的 `/128`)。如果找不到更具体的匹配,说明这个段暂时未使用,可直接返回 `unreachable`。除非有特殊需求,否则没必要给整个 `/48` 设置一个实际的下一跳。 ## 过滤器 [#过滤器] 之所以在讲完基本配置之后就要讲过滤器,是因为不论你是持证专家(CCIE/JNCIE/HCIE 等)还是刚入门的 BGP Player,**管好自己发出的路由都是最基本的责任,也是对他人最起码的尊重**。由于过滤器配置疏忽而引发的路由泄漏、服务中断乃至全球事故比比皆是,去 Cloudflare Blog 随便搜就能找到一大堆案例。好消息是,对于新手来说,并不需要很复杂的过滤器:只要保证只发自己的路由、收对方的全部路由即可。 以下是最简单的示例: ```bird2 {3} filter export_filter_v6 { if net ~ OWN_IPv6 then accept; # 如果前缀属于 OWN_IPv6,则放行 reject; # 否则拒绝 }; filter import_filter_v6 { accept; # 照单全收 }; ``` 建议在编写过滤器时,始终**显式地定义 accept 的条件,并在末尾用 reject 兜底**,像第 3 行一样,切勿偷懒。 更多过滤器的用法可以查看[第三章 过滤器设计](/beginner/connect-with-others/filters/)、 [Soha 的教程](https://github.com/moesoha/bird-bgp-kickstart/blob/master/main.md#3-过滤过滤) 或 [BIRD 中文文档](https://bird.xmsl.dev/docs/user-guide/5-1-introduction.html)。要记住: **对于路由过滤,无论再怎么小心都不为过。你少写的每一个字符,都可能造成几百万美元的损失和几个小时的全球停机。** # 在 BIRD 中配置 BGP [#在-bird-中配置-bgp] 完成了基本配置和过滤器之后,就可以开始配置 BGP 会话了。一般来说,你会从服务商那里拿到对方的 IP、ASN,有时还会包含密码(例如 Vultr 常见)。 根据对端 IP 是否和你在同一个网段,BGP 会话大致可以分为「多跳」和「同段」两种;其中 `fe80` 链路本地地址是同段中的一种特殊情况。 ## 多跳会话 [#多跳会话] 尽管多跳会话很复杂,但是它是大部分 BGP Session 采用的模式,所以我们放在最前面讲。 假设你从服务商获得的信息是: > **ASN**:AS64512 > > **IP**:`fd00::1` 此时你会发现,对方 IP 并不在你的本地子网里,但可以正常 `ping` 通,这种情况通常就需要配置 **多跳(multihop)** BGP。 示例配置: ```bird2 {1-3, 7-8, 10} protocol bgp upstream { local fc00::2 as ASN; neighbor fd00::1 as 64512; multihop 2; # password OURPASSWORD; 密码,可选 ipv6 { import filter import_filter_v6; export filter export_filter_v6; }; graceful restart; }; ``` 让我们来解读一下这段配置: ```bird2 startLineNumber=1 protocol bgp upstream { ``` 定义了一个 BGP 协议实例,`upstream` 是该协议实例的名字,可自定义。 ```bird2 startLineNumber=2 local fc00::2 as ASN; neighbor fd00::1 as 64512; ``` 指定了本地用于会话的 IP 和 ASN,以及对端(服务商)的 IP 和 ASN。其中 `ASN` 是[前面 `define` 过的常量](/beginner/bring-up-a-bgp-session/#常量定义)。 ```bird2 startLineNumber=4 multihop 2; ``` 在网络里,\*\*“跳”(hop)\*\*指的是数据包从一台设备传到下一台网络设备(如路由器)的中转次数。如果你和对方 IP 在同一个网段,数据包可以直接送达,不需要中间路由器转发,这种情况就是“一跳可达”;如果不在同一个网段,就需要经过一个或多个路由器中转,这时就叫做“多跳会话”。 `multihop 2;` 表示这是一个多跳会话,2 就是 BGP 报文的 Hop Limit(最大跳数)。你可以用 mtr 等工具先测量到对端的实际跳数,然后设置一个相符或稍大的值即可,太大没有必要,也可能带来安全风险。 ```bird2 startLineNumber=7 import filter import_filter_v6; export filter export_filter_v6; ``` 指定了该会话的 IPv6 通道使用的过滤器,均为前面定义好的过滤器。 ```bird2 startLineNumber=10 graceful restart; ``` 启用 **Graceful Restart**,可以在重启 BIRD 或重新加载配置时最大程度减少会话中断时间。 同时,尽管多跳 BGP 便于商家管理,但 BGP 收到的路由可能会覆盖原本的路由,从而导致无法前往原本的 BGP 对端,造成断联,所以我们必须在 BIRD 里手动指定对端 BGP 地址的路由,具体方法如下: 1. 用 `ip -6 route get <对端IP>` 获取原本的路由,如下所示: ```shell "via fc00::1" root@debian:~# ip route get fd00::1 fd00::1 from :: via fc00::1 dev ens3 src fc00::2 metric 1024 pref medium root@debian:~# ``` 其中我们看到 `via fc00::1` 就表示着原本的下一跳路由。 2. 在 BIRD 的 static 里面加入这条路有,如下所示: ```diff lang="bird2" /(fd00::1/128|fc00::1)/ protocol static static_v6 { ipv6; route 2001:db8::/48 reject; + route fd00::1/128 via fc00::1; }; ``` 其中 `fd00::1/128` 是我们的对端地址,`fc00::1` 是我们在上一步获取的下一跳地址。 ## 同段会话 [#同段会话] 假设你从服务商获得的信息是: > **ASN**:AS64512 > > **IP**:`fc00::1` 你会发现对方分配给你的 IP 和你的 IP 在同一个子网,则属于同段配置,示例如下: ```bird2 {4} protocol bgp upstream { local fc00::2 as ASN; neighbor fc00::1 as 64512; direct; ipv6 { import filter import_filter_v6; export filter export_filter_v6; }; graceful restart; }; ``` 其中第四行的 ```bird2 startLineNumber=4 direct; ``` 表示本地 IP 和对端 IP 一跳可达。 ## 链路本地地址 ( link-local ) [#链路本地地址--link-local-] 在某些场景(例如 DN42 社区网络),对方可能不会给你公网或内网地址,而是直接给一个 `fe80` 开头的地址。这类 **链路本地地址(Link-local address)** 只在同一个网段/广播域有效,无法跨路由传播。 也就是说,该地址只对与之直连的网卡有效,若需要使用 `fe80` 地址建立会话,就必须 **显式指定本地使用的网卡**。 假设上游分配的地址为 `fe80::1`,且你的设备网卡为 `eth0`,示例配置如下: ```bird2 {%eth0} protocol bgp upstream { local fe80::2 as ASN; # 一般对方给你fe80地址时,你也需要用fe80地址 neighbor fe80::1%eth0 as 64512; direct; ipv6 { import filter import_filter_v6; export filter export_filter_v6; }; graceful restart; }; ``` 其中 `fe80::1%eth0` 就是指定使用 `eth0` 网卡的链路本地地址格式。 ## 启动并验证 [#启动并验证] 将上述示例根据你的实际情况 **三选一** 填入 `bird.conf`,然后执行 `birdc c`,若未出现任何报错,说明配置无误。稍等片刻后,运行 `birdc s p`,若输出中 `State` 显示为 `Established`,则表明 BGP 会话已成功建立,示例如下: ```shell "Established" root@debian:~# birdc s p BIRD 2.17.1 ready. Name Proto Table State Since Info device1 Device --- up 2025-06-27 kernel1 Kernel master6 up 2025-06-27 static_v6 Static master6 up 2025-06-27 upstream BGP --- up 12:39:32.775 Established root@debian:~# ``` 若一切正常,你的前缀就已经对外宣布出去。大约 24 小时后,全球其他网络应能访问到你的前缀。恭喜你,完成了人生第一次广播!但独乐乐不如众乐乐,下一张,我们将教你如何与其他人对等连接(peer)。 # 附录:如何在 VPS 上使用自己的 IP 上网 [#附录如何在-vps-上使用自己的-ip-上网] 广播 IP 段的最终目的是让你的机器实际使用这些地址。那么怎么用?最简单的做法,就是在本地创建一个虚拟网卡(dummy 网卡),并把你需要广播的 IP 前缀里的某个地址绑定到这个网卡上;然后,再在 BIRD 的过滤器中将所有的源地址都改为这个地址。 ## 创建虚拟网卡 [#创建虚拟网卡] 在 `/etc/network/interfaces` 中添加如下配置(这里以前面示例的前缀为例): ```interfaces # /etc/network/interfaces iface dummy0 inet6 static address 2001:db8::1/128 pre-up ip link add dummy0 type dummy post-down ip link del dummy0 type dummy ``` 保存后,执行 `ifup dummy0` : ```shell root@debian:~# ifup dummy0 Waiting for DAD... Done root@debian:~# ``` 如果如上所示,`dummy0` 启动时没有任何错误,那么虚拟网卡已经成功创建。 使用以下命令手动创建虚拟网卡和分配地址(这里以前面示例的前缀为例): ```shell # 创建 dummy0 虚拟网卡 ip link add dummy0 type dummy # 启动网卡 ip link set dummy0 up # 分配 IPv6 地址 ip addr add 2001:db8::1/128 dev dummy0 ``` 要让配置在重启后保持,可以将上述命令添加到 `/etc/rc.local` 或创建 systemd 服务。 要删除虚拟网卡,使用: ```shell ip link del dummy0 ``` 这里我们创建了一个名为 `dummy0` 的虚拟网卡,并为其分配了一个地址 `2001:db8::1/128`。注意这里使用的是 `/128` 而不是 `/64` 或 `/48`,因为我们只需要绑定这个段内的**一个**地址来宣布路由,机器本身并不需要在该网卡上与其他设备通信,也就不需要整个子网。 ## 改变收到路由的源地址 [#改变收到路由的源地址] 在你的 BIRD 配置文件中找到 `protocol kernel` ,将它改为如下内容: ```bird2 startLineNumber=7 {4} protocol kernel { ipv6 { export filter { krt_prefsrc = 2001:db8::1; # 指定使用的源地址,如dummy0接口绑定的地址 accept; }; }; }; ``` 其中 `krt_prefsrc` 就是当包是从你的机器开始发出(比如你机器上进行的 `curl`)时,你使用的源地址,这里我们设置为 `2001:db8::1`。 **请务必确保这个地址已在某个接口(如 dummy0)上绑定**,否则你会在日志里收获大量的 `RTNETLINK answers: Invalid argument` 报错。 # 全部配置 [#全部配置] 以多跳为例: ```bird2 log syslog all; router id 10.0.0.1; # 定义 Router ID define ASN = 114514; # 定义本地使用的 ASN define OWN_IPv6 = [2001:db8::/48]; # 定义将要对外宣布的 IPv6 段 protocol device { }; protocol kernel { ipv6 { export filter { krt_prefsrc = 2001:db8::1; # 指定使用的源地址,如dummy0接口绑定的地址 accept; }; }; }; protocol static static_v6 { ipv6; route 2001:db8::/48 reject; #在 static 中添加你需要广播的路由 }; filter export_filter_v6 { if net ~ OWN_IPv6 then accept; # 如果前缀属于 OWN_IPv6,则放行 reject; # 否则拒绝 }; filter import_filter_v6 { accept; # 照单全收 }; protocol bgp upstream { local fc00::2 as ASN; neighbor fd00::1 as 64512; multihop 2; # password OURPASSWORD; 密码,可选 ipv6 { import filter import_filter_v6; export filter export_filter_v6; }; graceful restart; }; ``` # 前言 (/docs/beginner) 也许,是一次为了在 Minecraft 开服时的折腾;也许,是一次想亲手体验计算机网络的学习;也许,只是一次为了配置科学上网时的搜索——无论起点是什么,你知道了 BGP 这个东西,并被“拥有属于自己的网络”这个概念所吸引。于是,你萌生了弄一个自己的 ASN 和一段自己的 IP 的念头,想要搭建一个真正属于自己的网络。 可当你开始搜索时,会发现,中文互联网(甚至整个互联网)关于 BGP 的信息参差不齐、分散零散,很多教程甚至已经遗失,只能在 Internet Archive 中窥见当年。这也是我决定写下这份教程的原因:希望为中文世界留下一个完整、系统的 BGP 新手入门教程,带你一步步把“属于自己的网络”从想法变成现实。 在这份教程里,我们会从最基础的 单节点与上游建立 BGP 会话 开始,逐步过渡到与其他节点互联,最终搭建一个具备初步规模的 BGP 自治系统(AS)。为了方便入门,我们默认使用的路由工具是 BIRD。 你需要: * 一个模拟器(如 [`pnetlab`](https://pnetlab.com/pages/main) ) 或 * 一个 ASN * 一段 IP * 一台有 BGP Session 的 VPS(或一个可用的 BGP Tunnel) 以及 * 较熟练的 Linux 使用能力 * 自行搜索、解决部分问题的能力 * 以及,一颗永远保持好奇、乐于探索的心 另外,我建议你了解一下[提问指南](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md),这会对你探索和解决问题有非常大的帮助。 我们默认使用的系统为 Debian 12。 # 附录:如何在真实世界互联 (/docs/beginner/real-world-interconnection) 看完前面的文章后,你可能会有这样的疑惑:上面的什么 IGP、iBGP 都需要节点之间直接连通,但我的设备都部署在公网上,该如何建立连接呢?这个时候,我们就要用到隧道了。 为了方便各位读者,速查表如下: | 隧道方案 | 承载协议 | 包内容 | IPv4 / IPv6 承载 MTU(以 1500 计) | 主要优点 | 主要缺点 | | --------- | ---- | ---- | ---------------------------- | -------------------------- | ---------------------------------------------- | | Wireguard | UDP | IP | 1440(-60) / 1420(-80) | 带加密
效率高
带路由 | 老设备/商业网络设备不支持 | | SIT | IPv4 | IPv6 | 1480(-20) / 一般不支持 | 包头小 | 不带加密
同 IP 间不能起多个隧道
一般只支持 IPv4 作为承载协议 | | GRE | IP | IP | 1476(-24) / 1456(-44) | 通用 | 一般不带加密;同 IP 间不能起多个隧道
包头略大 | | L2GRE | IP | 以太网帧 | 1476(-24) / 1456(-44) | 能传输二层以太网帧 | 多了一个以太网的头,不需要的时候包头大
同 IP 间不能起多个隧道 | | VXLAN | UDP | 以太网帧 | 1456(-44) / 1436(-64) | 能传输二层以太网帧
同 IP 可开多个隧道 | 多了一个以太网的头,不需要的时候包头大 | # 什么是隧道 [#什么是隧道] 网络隧道如同物理世界中穿山而过的隧道一样,在网络中的两个节点之间搭建了一条虚拟的"直连通道"。即使数据需要经过多台路由器转发,隧道两端的设备也能像直接相连一样进行通信,这样就能够部署 IGP、BGP 等路由协议了。 如果不严谨的分类一下,隧道可以分类成这几种: * L2 隧道:传输的内容为二层包,如以太网等。典型代表有 L2TP、VXLAN、L2GRE。这种隧道可以看成直接用一条网线把两端连了起来,因此可以跑一些比较低级的协议,比如 MPLS、ISIS 等。 * L3 隧道:传输的内容为三层包,如 IPv4、IPv6 等。典型代表有 GRE、SIT、IPIP,以及我们即将讲解的 Wireguard。这种隧道可以看作把两端接了一个路由器,因此可以跑 IP,但是没法用一些比较低级的协议,比如 ARP、MPLS 等。 有一个特例,MPLS可以运行在GRE上,详情在 [RFC4023](https://www.rfc-editor.org/rfc/rfc4023.html "RFC 4023") 。 我们平常会用到的隧道主要有 Wireguard、VXLAN、SIT、GRE 和 L2GRE。 # Wireguard [#wireguard] ## 介绍 [#介绍] > WireGuard ® 是一个极其简单但快速且现代的 VPN,采用最先进的加密技术。它旨在比 IPsec 更快、更简单、更精简且更实用,同时避免带来巨大的麻烦。它的性能预计远超 OpenVPN。WireGuard 被设计为通用 VPN,适用于嵌入式接口和超级计算机,适合多种不同场景。最初发布于 Linux 内核,现在已实现跨平台(Windows、macOS、BSD、iOS、Android)并广泛部署。它目前正处于积极开发阶段,但已经可以被视为业内最安全、最易用、最简洁的 VPN 解决方案。 简而言之,Wireguard 是一种很新的基于 UDP 的 L3 隧道,而且自带加密,基本可以视作 IPsec 的上位平替,目前已被包括广泛使用,连 Tailscale 也是基于 Wireguard 构建,而它也是我们这篇文章的重头戏。 ## 配置 [#配置] Wireguard 有一个很有特色的地方,那就是它自带了一定的路由功能,因此我们既可以用它来作为两个 BGP 节点之间的连接,也可以用它来连接节点和客户端,从而把广播的 IP 分配给客户端。下面将讲解两种不同的配置方法。 Wireguard 的包头大小为 40 字节,因此如果使用 IPv4 则内层还剩下 1440 字节,使用 IPv6 则内层还剩下 1420 字节。 ### 节点之间 [#节点之间] 假设网络拓扑如下: #### 安装 [#安装] 安装请参照 [https://www.wireguard.com/install/](https://www.wireguard.com/install/) ,注意当系统比较老的时候,需要使用 dkms 安装方法,例如 [Centos 7 的安装教程](https://www.wireguard.com/install/#centos-8-module-plus-module-kmod-module-dkms-tools)。Debian bullseye 及之前的版本需要使用 backport 镜像源,网址在 [https://backports.debian.org/Instructions/](https://backports.debian.org/Instructions/) 。 #### 生成密钥对 [#生成密钥对] 在 `/etc/wireguard` 目录下运行 ```bash wg genkey | tee privatekey | wg pubkey > publickey ``` 就会生成一对公钥和私钥,分别保存在 `/etc/wireguard/publickey` 和 `/etc/wireguard/privatekey` 下。 #### 编写配置文件 [#编写配置文件] 在 `/etc/wireguard` 目录下创建配置文件 `<对端名称>.conf`(建议使用字母、数字、连字符或下划线命名,如 `node-a.conf`): ```ini frame="code" title="node-b.conf" [Interface] PrivateKey = ListenPort = 51821 Address = 10.0.0.1/30 Table = off [Peer] PublicKey = Endpoint = 2.2.2.2:51821 AllowedIPs = ::/0, 0.0.0.0/0 PersistentKeepalive = 120 ``` ```ini frame="code" title="node-a.conf" [Interface] PrivateKey = ListenPort = 51821 Address = 10.0.0.2/30 Table = off [Peer] PublicKey = Endpoint = 1.1.1.1:51821 AllowedIPs = ::/0, 0.0.0.0/0 PersistentKeepalive = 120 ``` 可以看到 Wireguard 配置文件由 `[Interface]` 和 `[Peer]` 两部分组成。一个配置文件可以有多个`[Peer]`,其中 `AllowedIPs` 互不重叠(因为要拿来路由)。 注意 `AllowedIPs` 是隧道里面传输的 IP 包的地址,不只是接口地址。 另外,Endpoint 可以两边都写,也可以只写一边(例如只有一边有公网 IP),但不能两边都不写! 这里我们写了一行 ```ini startLineNumber=6 Table = off ``` 意思是让 Wireguard 不要根据 AllowedIPs 往系统路由里写路由表,此时我们只把 Wireguard 当成一个隧道来用,由我们自己控制路由 还有 ```ini PersistentKeepalive = 120 ``` 意思是让 Wireguard 每 120 秒发送一个 KeepAlive 包,这在 NAT 的时候比较有用。 #### 启动隧道与持久化 [#启动隧道与持久化] 运行 ```bash wg-quick up <文件名,如 node-a> ``` 如果没有报错,那么隧道就成功启动了。我们可以输入 `wg show <文件名>` 来查看情况。 如果你的系统使用 systemd,那么你可以使用 ```bash systemctl enable wg-quick@<文件名> ``` 来让你的隧道在开机的时候自动启动。 ### 节点和客户端 [#节点和客户端] 上面的节点互联,我们将 Wireguard 用成了一个单独的隧道,让它不加选择地通过所有流量。而对于连接节点和客户端则刚好相反,我们正好需要使用 Wireguard 写路由表的特性来路由我们的流量,假设情景如下: 配置示例如下: ```ini frame="code" title="clients.conf" [Interface] PrivateKey = ListenPort = 51821 Address = 10.0.0.1/24 [Peer] PublicKey = AllowedIPs = 10.0.0.2/32 PersistentKeepalive = 120 [Peer] PublicKey = AllowedIPs = 10.0.0.3/32 PersistentKeepalive = 120 [Peer] PublicKey = AllowedIPs = 10.0.0.4/32 PersistentKeepalive = 120 ``` ```ini frame="code" title="node-a.conf" [Interface] PrivateKey = Address = 10.0.0.2/24 [Peer] PublicKey = Endpoint = 1.1.1.1:51821 AllowedIPs = ::/0, 0.0.0.0/0 PersistentKeepalive = 120 ``` ```ini frame="code" title="node-a.conf" [Interface] PrivateKey = Address = 10.0.0.3/24 [Peer] PublicKey = Endpoint = 1.1.1.1:51821 AllowedIPs = ::/0, 0.0.0.0/0 PersistentKeepalive = 120 ``` ```ini frame="code" title="node-a.conf" [Interface] PrivateKey = Address = 10.0.0.4/24 [Peer] PublicKey = Endpoint = 1.1.1.1:51821 AllowedIPs = ::/0, 0.0.0.0/0 PersistentKeepalive = 120 ``` 可以看到在这里我们把`Table = off`给去掉了,以让 Wireguard 自动把路由写进路由表。Client 方面,由于我们去掉了`ListenPort`,它就会自动选择端口与 Node A 连接。而 Node A 方面,通过只给 Client 留它们对应的 IP 作为 AllowedIPs,既让 Wireguard 知道如何路由对应的数据包,也限制了 Client 让它们不能盗用 IP。 {/* prettier-ignore */} * 当 Wireguard 上的 IP 为 /24 而 AllowedIPs 为该 /24 下的 IP 时,Wireguard 不会添加 /32 路由,此时如果要在 BIRD 内读取路由,需要使用 protocol direct 或者写一个 static reject 。 * 当 Wireguard 上的 IP 为 /32 时,Wireguard 会添加路由,此时 BIRD 内只需写一条 static reject 即可。 当然,自己手写客户端配置还是比较麻烦的事情,我们可以使用 [wg-gen-web](https://github.com/vx3r/wg-gen-web) 来管理客户端。网上教程不少,如 [https://icloudnative.io/posts/configure-wireguard-using-wg-gen-web/](https://icloudnative.io/posts/configure-wireguard-using-wg-gen-web/) ,此处不再赘述。或者也可以使用 [https://www.wireguardconfig.com/](https://www.wireguardconfig.com/) 这种工具来一键生成。 # SIT [#sit] SIT 隧道,也被称作 6in4,是用来在 IPv4 网络上传输 IPv6 的一种隧道(看名字也知道了)。它是一种不带安全加密和路由的 L3 隧道。SIT 只需要一层 IPv4 头,故其在 1500 下的 MTU 为 1480。其配置方法如下: ```bash ip link add name sit1 type sit local <本地IP> remote <对方IP> ip link set sit1 up ip addr add <内部v6地址CIDR> dev sit1 ``` 如果想持久化可以将其写入 `/etc/rc.local` ,并配置系统启用它。 # GRE [#gre] GRE(**G**eneric **R**outing **E**ncapsulation,通用路由封装,[RFC2784](https://www.rfc-editor.org/rfc/rfc2784.html "RFC 2784")),是一种常用的 L3 隧道。跟 SIT 的区别是 GRE 有一个 GRE 头,这使得它可以传输任何 L3 包而不仅限于 IP。GRE 的包头大小为 4 字节,故其在 IPv4 下的 MTU 为 1476,在 IPv6 下的 MTU 为 1456。GRE 本身不带加密,但可以和 IPsec 组成加密隧道,具体方法请自行搜索。 配置方法如下: ```bash ip link add name gre1 type gre local <本地IP> remote <对方IP> ttl 255 ip link set gre1 up ip addr add <内部地址CIDR> dev gre1 ``` ```bash ip link add name gre1 type ip6gre local <本地IPv6> remote <对方IPv6> ttl 255 ip link set gre1 up ip addr add <内部地址CIDR> dev gre1 ``` 注意上面有一个 `ttl 255`,如果不写的话默认 TTL 会复制包内的 TTL,所以这里直接写成 255。 如果外层地址是 IPv4 就用`gre`,是 IPv6 就用`ip6gre`。如果需要在同一对地址间起多个 GRE 隧道,则需要使用 GENEVE([RFC8926](https://www.rfc-editor.org/rfc/rfc8926.html "RFC 8926"))。 ## L2GRE [#l2gre] L2GRE(又称 EoGRE、GRETAP,[RFC1701](https://www.rfc-editor.org/rfc/rfc1701.html "RFC 1701")),是一种更底层的 GRE。它跟 GRE 的区别是,GRE 内层不是一个 IP 包,而是一个以太网帧,此时它就成为了一个 L2 隧道,GRE 头内的协议号为 6558(Transparent Ethernet Bridging)。MTU 跟普通 GRE 相同,但此时因为携带了一个多的 L2 头,内层包的可用空间会变得更小。 配置方法如下(就是把 gre 改成 gretap): ```bash ip link add name gre1 type gretap local <本地IP> remote <对方IP> ip link set gre1 up ip addr add <内部地址CIDR> dev gre1 ``` ```bash ip link add name gre1 type ip6gretap local <本地IPv6> remote <对方IPv6> ip link set gre1 up ip addr add <内部地址CIDR> dev gre1 ``` L2GRE 能传输以太网帧这一特点使得它可以被加入一个桥中。 # VXLAN [#vxlan] VXLAN(Virtual eXtensible Local Area Network,[RFC7348](https://www.rfc-editor.org/rfc/rfc7348.html "RFC 7348"))是一种 L2 隧道。准确来说,是一种网络虚拟化技术。它跟 L2GRE 的一个显著区别是,L2GRE 是 Ethernet over GRE over IP,VXLAN 是 Ethernet over VXLAN over UDP over IP,多了一层 UDP。此外,VXLAN 还支持同一对 IP 上跑多个隧道,使用 VNI(VXLAN Network Identifier)进行区分。搭配上 EVPN,VXLAN 还有更多的玩法,不过这就是后话了(TODO)。 Wireguard 的包头大小为 16 字节,因此如果使用 IPv4 则内层还剩下 1456 字节,使用 IPv6 则内层还剩下 1436 字节。 配置方法如下: ```bash ip link add name vxlan1 type vxlan local <本地IP> remote <对方IP> dstport 4789 id ip link set vxlan1 up ip addr add <内部地址CIDR> dev vxlan1 ``` 相比于别的协议,VXLAN 多了一个`dstport`(默认为 4789)和一个`id`,也就是上文所说的 VNI。但配置方法大同小异。 *** 建立起隧道之后,节点之间的距离就从跨越多跳缩短为了一跳可达,此时就可以使用我们前面讲过的方法搭建网络了。 # 如何排错 (/docs/beginner/troubleshooting) # Akvorado (/docs/misc/akvorado) # 简介 (/docs/misc) 这里会放一些跟网络相关的文章,包括常用调试工具、一些监控设备等。 # 常用工具 (/docs/misc/tools) # 概念解析 (/docs/beginner/connect-with-others/concept) 在我们开始学习之前,先来了解一些多人连接中需要的概念。 # 上、下游与对等 [#上下游与对等] 在网络连接中,作为一个自治系统(AS),其他人与你的关系主要有三种: * **上游(Upstream)**:例如你服务商就是你的上游。上游通常会向你提供 BGP 全表,即整个互联网的路由,让你能顺利接入全球互联网。而你只需要向上游广播你自己及下游的路由。一般来说,上游需要付费购买(如 IP Transit),但也有出于学术、爱好等目的提供免费服务的(如 [HE](https://he.net) 的 IPv6 Transit,用于推广 IPv6 覆盖~~以及在 IPv6 时代争夺 T1 地位~~)。 * **下游(Downstream)**:相反,你对你的客户就是他们的上游。你的下游向你广播他们及他们下游的路由,而你需要向他们提供全表,让他们也能接入互联网。 * **对等(Peer)**:如 Google 与 Microsoft 互为 Peer。Peer 之间直接互联主要为了降低延迟(如同在 IX 交换)并节省费用,双方通常互换自己的路由及下游路由而不走上游。Peer 关系多见于互联网交换中心(IX),如 SFMIX、DE-CIX、EIE(Equinix Internet Exchange),国内也有前海 IX、杭州 IX 等新兴力量。 Peer 有两层含义,一种是名词,或者说是在商业层面上的**对等**,具体见上;一种是在协议层面上的**对等**,为动词,也就是建立 BGP 连接,但通常建立的都是 Peer 关系。 # Tier 1,2,3 [#tier-123] > 您们好。 > 我们公司实际上是一级运营商(Tier 1 – AS 174),在全球包括美洲、欧洲、亚洲超过 199 个市场(40 多个国家)都设有节点。在多个国家都有接点数据中心。 > > ...... 不知道你有没有收到过,这是 Cogent 的一封推广邮件。这里面出现了一个名词:一级运营商。那什么是一级运营商呢? 以下内容抄自[维基百科](https://zh.wikipedia.org/wiki/Tier_1%E7%BD%91%E7%BB%9C) > **Tier 1 网络**(Tier 1 network)是一种仅通过免费[对等互联](https://zh.wikipedia.org/wiki/Peering)(settlement-free interconnection、settlement-free peering)即可到达[互联网](https://zh.wikipedia.org/wiki/互联网)所有其他网络的[网际协议](https://zh.wikipedia.org/wiki/网际协议)网络。 > > ...... > > 最合理的定义是,Tier 1 网络供应商永远不会支付转发费用,因为所有 Tier 1 网络供应商的集合都向各地所有较低级别的网络供应商出售转发服务,而且因为: > > * 所有 Tier 1 网络供应商都与全球其他所有 Tier 1 网络供应商对等互联, > * 对等协议允许访问所有转发客户,这意味着: > * Tier 1 网络包含所有连接到全球互联网的主机。 简单来说,Tier 1 就是网络的最上层,它不需要买 IP Transit 就能到达别人,它跟别人都是**免费**peer,如 NTT、PCCWG、Lumen。而比 Tier 1 低一点的是区域 Tier 1(Regional Tier 1),例如中国电信和 Singtel,它们在本地像 Tier 1 一样免费 peer,但在非本地区域需要购买 IP Transit 或 Paid Peer。 而比 Tier 1 低一点的,是 Tier 2,也就是跟一些人免费 peer 了,但跟一些人还是需要买 IP Transit 或付费 Peer(Paid Peer),比如前面提到的 HE、Cogent。严格来讲,区域 Tier 1 也是 Tier 2。Tier 2 的定义其实比较大,通常只要你参加了一些 IX,你就可以算作是 Tier 2 了。 比 Tier 2 更低的,是 Tier 3,这种网络跟其他网络只能通过付费途径到达,具体来说就是一些 IDC、公司或 Player 了。 Tier 2 网络会与一些人免费 peer,但还需向其他网络购买 IP Transit 或 Paid Peer,典型如 HE、Cogent。若你加入了 IX,你一般也算是 Tier 2。 Tier 3 是最下层,需付费通过上游或 Peer 转发流量,典型是 IDC、小公司等。 # AS-SET [#as-set] 前文我们说过,验证一个 IP 是否授权一个 AS 广播靠的是路由对象。那如何验证一个 AS 是否包括一个 AS 作为它的下游呢?那就要靠 AS-SET 了。 顾名思义,AS-SET 是 AS 的 SET(集合)。当你要求某个 IP Transit 供应商使用这个 AS-SET 进行过滤,就意味着只有这个 AS-SET 下的 ASN 的 IP 才可以通过这个 IP Transit 广播(通过筛选路由对象),有效地防止了路由泄露。AS-SET 也是被记录在 IRR 里,因此前文提到的 IRR 都可以创建 AS-SET。如果你要带下游,你就要把你的下游的 ASN 放进你的 AS-SET,才能让你的上游确认 ta 是你的下游。 一个 AS-SET 的经典示例: ```yaml as-set: AS13335:AS-CLOUDFLARE descr: Cloudflare, Inc. 101 Townsend Street, San Francisco California 94107, US + 1-650-319-8930 members: AS13335,AS13335:AS-CUSTOMERS,AS132892,AS133877,AS202623,AS209242,AS394536 remarks: --------------- Cloudflare announces its ASNs via many upstream ASNs All Cloudflare abuse reporting can be done via https://www.cloudflare.com/abuse --------------- admin-c: ADMIN2521-ARIN tech-c: ADMIN2521-ARIN tech-c: CLOUD146-ARIN mnt-by: MNT-CLOUD14 created: 2023-04-05T08:58:54Z last-modified: 2025-01-16T19:57:46Z source: ARIN ``` 验证谁是我下游的问题解决了,但怎么验证谁能当我上游呢?目前只能靠信用来解决,这也导致了通过伪造起源 AS 进行的盗播事件时有发生。目前,一项叫 ASPA(Autonomous System Relationship Authorization)的技术正在起草其 RFC,OpenBGPD 也已经提供了实现。我们可以期待未来该技术能够普及,互联网将更加干净,安全。 # 过滤器设计 (/docs/beginner/connect-with-others/filters) 正如我们前面所言, 上下游和 peer 所需要和发的路由是不一样的:上游发全表,收我们自己和我们下游;peer 发他们自己和他们下游,收我们自己和我们下游;下游发他们自己和下游,收全表。所以,接这些的关键,就是设计良好的过滤器,使得他们发来的路由,都能按照正确的方式进行处理。 ## BGP Community [#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 才真正是为我们网络用来实现功能的。 [RFC8195](https://www.rfc-editor.org/rfc/rfc8195.html "RFC 8195") Use of BGP Large Communities 记录着一些Large Community的用法,有兴趣可以去翻一下。 ## Community 设计 [#community-设计] 假设我们的 ASN 是 AS114514,下面我们来进行一些简单的 Community 设计(全部使用 Large Community): | Community | 意思 | | -------------- | ---------------- | | (114514, 1, 1) | 该路由来自上游 | | (114514, 1, 2) | 该路由来自 Peer | | (114514, 1, 3) | 该路由来自自身(静态/OSPF) | | (114514, 1, 4) | 该路由来自下游 | 这里只是简单的设计了一些信息类 Community,操作类 Community 我们暂不涉及,详情可以参考 [小狼的教程](https://littlewolf.moe/bgp/100/)(TODO)。 ## 他山之石 [#他山之石] 在设计自己的过滤器之前,我们可以先看看别人的,比如[HE 的](https://routing.he.net/algorithm.html),翻译如下: > 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](https://www.rfc-editor.org/rfc/rfc1918.html "RFC 1918"),文档前缀等)。 > > 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](https://www.iana.org/assignments/as-numbers/as-numbers.xhtml) > > 3.8 拒绝使用未分配的 32 位 AS 号(介于 4200000000 和 4294967294 之间)的 AS 路径。根据 [RFC6996](https://www.rfc-editor.org/rfc/rfc6996.html "RFC 6996"),这些号码保留供私有使用。 > > 3.9 拒绝使用 AS 23456 的 AS 路径。支持 32 位 AS 号的 BGP 节点的 AS 路径中不应出现 AS 23456。 > > 3.10 拒绝使用 AS 0 的 AS 路径。根据 [RFC 7606](https://www.rfc-editor.org/rfc/rfc7606.html "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 就明确地采用了“显式接受”的策略。 接下来,让我们尝试实现一下自己的策略。 这里并未给出包含所有函数的自洽的完整实现,具体完整实现在[Lab示例](#Lab示例)处! ## 上游 [#上游] ### 导入 [#导入] 我们的策略是: 1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀 2. 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合) 3. 接受剩余的前缀(毕竟本来也是要发你全表的)并且将 local preference 设为 100 其中 local preference 是路由的本地优先级,越高越优先,100 为默认值。 用 BIRD 过滤器的实现如下: ```bird2 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()` 是我们用到的工具函数,具体可以去[全部配置](/beginner/connect-with-others/lab/#%E5%85%A8%E9%83%A8%E9%85%8D%E7%BD%AE)处查看。 ### 导出 [#导出] 我们的策略是: 1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀 2. 允许所有自身前缀和下游前缀 3. 拒绝所有其他前缀 BIRD 实现如下: ```bird2 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 [#peer] ### 导入 [#导入-1] Peer 的导入策略是: 1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀 2. 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合) 3. 接受有有效 IRR 的前缀并且将 local preference 设为 200(比上游高,因为通常走 Peer 是免费的) 4. 拒绝所有其他前缀 ```bird2 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)`,我们待会再讲。 ### 导出 [#导出-1] Peer 的导出策略跟上游的导出策略一样,也是: 1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀 2. 允许所有自身前缀和下游前缀 3. 拒绝所有其他前缀 BIRD 实现如下: ```bird2 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; } ``` ## 下游 [#下游] ### 导入 [#导入-2] 下游的导入和 Peer 的导入类似,策略是: 1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀 2. 拒绝所有 RPKI 状态为 INVALID 的前缀(即有 RPKI 且 RPKI 授权的 ASN 和当前广播的 ASN 不符合) 3. 允许有有效 IRR 的前缀并且将 local preference 设为 400(最高,毕竟走下游是他付费) 4. 拒绝所有其他前缀 BIRD 实现如下: ```bird2 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; } ``` ### 导出 [#导出-2] 导出就很简单了: 1. 拒绝所有不合法的 ASN、前缀和太长(超过/24 和/48)的前缀 2. 允许所有带标记的前缀 3. 拒绝所有其他前缀(防止内网前缀、IX 前缀等漏出去) BIRD 实现如下: ```bird2 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`关键字,可以快捷的调用函数。 示例如下: ```bird2 {25-26} "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` 关键字,快捷地调用了函数作为我们的过滤器,从而使代码更简洁。 {/* prettier-ignore */} `where`关键字后面实际上接的是一个表达式,比如你可以写: ```bird2 export where source ~ [RTS_DEVICE, RTS_STATIC, RTS_RIP]; ``` 来导出所有来自设备、静态路由或 RIP 的路由。 ## IRR [#irr] 刚才我们的过滤器里面出现了 `inet_irr_check(s_name)`,这个是我们用于检验 IRR 的工具函数,它大概长这样: ```bird2 function inet_irr_check(string as_set) -> bool { if net.type = NET_IP4 then { # IPv4检查 if as_set = "" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,24}, <前缀IP段>/<前缀长度>{<前缀长度>,24} ...] then return true;else return false;}; if as_set = "" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,24} ...] then return true;else return false;}; if as_set = "" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,24} ...] then return true;else return false;}; } else if net.type = NET_IP6 then { # IPv6检查 if as_set = "" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,48} ...] then return true;else return false;}; if as_set = "" then {if net ~ [<前缀IP段>/<前缀长度>{<前缀长度>,48} ...] then return true;else return false;}; if 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 用你喜欢的语言生成一个制作这个的脚本,此处省略~~留作课后习题~~。 BIRD 的`include`是文本拼合,所以如果报错可能会在一个不存在的行数(2.17 无此问题)。也请注意不要出现循环引用问题。 # 前言 (/docs/beginner/connect-with-others) 慢慢地,你会遇到一些志同道合的人。他们有的说想和你建立对等(peer)关系,有的说要做你的下游,有的甚至愿意当你的上游。这时,你也许会疑惑:peer、下游、上游到底指什么?我该如何与他们建立连接、互相收发路由? 别担心,本文将帮助你理解 BGP 中这些核心概念,并教你如何与上下游和同行正确连接。 # Lab 示例 (/docs/beginner/connect-with-others/lab) 让我们建立一个 Lab,同时包含三种角色: # 全部配置 [#全部配置] 这里我们将一些常量和定义分到了 `utils.conf`,将函数整理到了 `functions.conf`并按照对应机器的 ASN 进行更改,省略 `irr.conf` ```bird2 {22,52} roa4 table rpki4; roa6 table rpki6; protocol rpki rpki_cloudflare { roa4 { table rpki4; }; roa6 { table rpki6; }; remote 172.65.0.2 port 8282; retry keep 5; refresh keep 30; expire 600; transport tcp; } define BOGON_ASNS = [ 0, # RFC 7607 23456, # RFC 4893 AS_TRANS 64496..64511, # RFC 5398 and documentation/example ASNs # 64512..65534, # RFC 6996 Private ASNs 这里我们注释掉,但你在公网上不行! 65535, # RFC 7300 Last 16 bit ASN 65536..65551, # RFC 5398 and documentation/example ASNs 65552..131071, # RFC IANA reserved ASNs 4200000000..4294967294, # RFC 6996 Private ASNs 4294967295 # RFC 7300 Last 32 bit ASN ]; define BOGON_PREFIXES_V4 = [ 0.0.0.0/8+, # RFC 1122 'this' network 10.0.0.0/8+, # RFC 1918 private space 100.64.0.0/10+, # RFC 6598 Carrier grade nat space 127.0.0.0/8+, # RFC 1122 localhost 169.254.0.0/16+, # RFC 3927 link local 172.16.0.0/12+, # RFC 1918 private space 192.0.2.0/24+, # RFC 5737 TEST-NET-1 192.88.99.0/24{25,32}, # RFC 7526 deprecated 6to4 relay anycast. If you wish to allow this, change `24+` to `24{25,32}`(no more specific) 192.168.0.0/16+, # RFC 1918 private space 198.18.0.0/15+, # RFC 2544 benchmarking 198.51.100.0/24+, # RFC 5737 TEST-NET-2 203.0.113.0/24+, # RFC 5737 TEST-NET-3 224.0.0.0/4+ # multicast ]; define BOGON_PREFIXES_V6 = [ ::/8+, # RFC 4291 IPv4-compatible, loopback, et al 0064:ff9b::/96+, # RFC 6052 IPv4/IPv6 Translation 0064:ff9b:1::/48+, # RFC 8215 Local-Use IPv4/IPv6 Translation 0100::/64+, # RFC 6666 Discard-Only 2001::/32{33,128}, # RFC 4380 Teredo, no more specific 2001:2::/48+, # RFC 5180 BMWG 2001:10::/28+, # RFC 4843 ORCHID # 2001:db8::/32+, # RFC 3849 documentation 这里我们注释掉,但你在公网上不行! 2002::/16{17,128}, # RFC 7526 deprecated 6to4 relay anycast. If you wish to allow this, change `16+` to `16{17,128}`(no more specific) 3ffe::/16+, 5f00::/8+, # RFC 3701 old 6bone fc00::/7+, # RFC 4193 unique local unicast fe80::/10+, # RFC 4291 link local unicast fec0::/10+, # RFC 3879 old site local unicast ff00::/8+ # RFC 4291 multicast ]; ``` ```bird2 include "utils.conf"; function net_len_too_long() -> bool { case net.type { NET_IP4: return net.len > 24; # IPv4 CIDR 大于 /24 为太长 NET_IP6: return net.len > 48; # IPv6 CIDR 大于 /48 为太长 else: print "net_len_too_long: unexpected net.type ", net.type, " ", net; return false; } } function is_not_valid_prefix() -> bool { case net.type { NET_IP4: return net ~ BOGON_PREFIXES_V4; NET_IP6: return net ~ BOGON_PREFIXES_V6; else: print "is_not_valid_prefix: unexpected net.type ", net.type, " ", net; return false; } } function is_not_valid_asn() -> bool { if bgp_path ~ BOGON_ASNS then return true; return false; } function rpki_check() -> bool { if ( net.type = NET_IP4 && roa_check(rpki4, net, bgp_path.last) = ROA_INVALID ) then { return false; } else if ( net.type = NET_IP6 && roa_check(rpki6, net, bgp_path.last) = ROA_INVALID ) then { return false; } else { return true; } } include "irr.conf"; 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; } 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; } 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; } 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; } 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; } ``` ```bird2 include "functions.conf"; log syslog all; router id 1.1.1.1; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol static static_v6 { ipv6; route 2001:db8:1::/48 reject {bgp_large_community.add((65001,1,3));}; }; protocol bgp upstream { local fd00::1:1 as 65001; neighbor fd00::1:2 as 65002; direct; ipv6 { import where import_filter_downstream("AS65001"); export where export_filter_downstream(); }; graceful restart; }; ``` ```bird2 include "functions.conf"; log syslog all; router id 1.1.1.2; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol static static_v6 { ipv6; route 2001:db8:2::/48 reject {bgp_large_community.add((65002,1,3));}; }; protocol bgp upstream { local fd00::1:2 as 65002; neighbor fd00::1:1 as 65001; direct; ipv6 { import where import_filter_upstream(); export where export_filter_upstream(); }; graceful restart; }; protocol bgp peer { local fd00::2:1 as 65002; neighbor fd00::2:2 as 65003; direct; ipv6 { import where import_filter_peer("AS65003"); export where export_filter_peer(); }; graceful restart; }; ``` ```bird2 include "functions.conf"; log syslog all; router id 1.1.1.3; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol static static_v6 { ipv6; route 2001:db8:3::/48 reject {bgp_large_community.add((65003,1,3));}; }; protocol bgp peer { local fd00::2:2 as 65003; neighbor fd00::2:1 as 65002; direct; ipv6 { import where import_filter_peer("AS65002"); export where export_filter_peer(); }; graceful restart; }; ``` # 结果 [#结果] ```shell root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8:1::/48 unreachable [static_v6 13:08:09.860] * (200) 2001:db8:2::/48 unicast [upstream 13:09:27.800] * (100) [AS65002i] via fd00::1:2 on ens3 root@debian:~# ``` ```shell root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8:1::/48 unicast [upstream 13:09:27.861] * (100) [AS65001i] via fd00::1:1 on ens3 2001:db8:2::/48 unreachable [static_v6 13:04:01.157] * (200) 2001:db8:3::/48 unicast [peer 00:46:07.340] * (100) [AS65003i] via fd00::2:2 on ens4 root@debian:~# ``` ```shell root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8:2::/48 unicast [peer 00:47:21.551] * (100) [AS65002i] via fd00::2:1 on ens3 2001:db8:3::/48 unreachable [static_v6 00:44:32.293] * (200) root@debian:~# ``` 可以看到路由成功地被传递,并且没有泄露。 # 附录:Role (/docs/beginner/connect-with-others/role) 正常来讲,要做到如上的区分,我们需要用 Community 来标记哪些路由是上游的,哪些是客户的,哪些是 peer 的,再用一堆过滤器去筛选它,就像我们上面用了。但这太麻烦了,还容易出错!于是,有人提出了 [RFC9234](https://www.rfc-editor.org/rfc/rfc9234.html "RFC 9234"),用内置的属性来进行区分,而这项 RFC 也在 BIRD 内得到了支持。在下面,我们写一版使用这种方式进行过滤的配置。 前述不变,这里只放 BGP 配置: ```bird2 {5} protocol bgp upstream { local fd00::1:1 as 65001; neighbor fd00::1:2 as 65002; direct; local role provider; ipv6 { import all; export all; }; graceful restart; }; ``` ```bird2 {5.16} protocol bgp bgp_upstream { local fd00::1:2 as 65002; neighbor fd00::1:1 as 65001; direct; local role customer; ipv6 { import all; export all; }; graceful restart; }; protocol bgp bgp_peer { local fd00::2:1 as 65002; neighbor fd00::2:2 as 65003; direct; local role peer; ipv6 { import all; export all; }; graceful restart; }; ``` ```bird2 {5} protocol bgp bgp_peer { local fd00::2:2 as 65003; neighbor fd00::2:1 as 65002; direct; local role peer; ipv6 { import all; export all; }; graceful restart; }; ``` 可以看到,我们在这里没有用任何我们之前定义的函数,但是我们一样能够实现不漏表,就像上面那样。为什么呢? 让我们随便抽一条路由来看看: ```bird2 {9} 2001:db8:3::/48 unicast [bgp_peer 01:25:55.700] * (100) [AS65003i] via fd00::2:2 on ens4 Type: BGP univ BGP.origin: IGP BGP.as_path: 65003 BGP.next_hop: fd00::2:2 fe80::526e:69ff:fe00:300 BGP.local_pref: 100 BGP.large_community: (65003, 1, 3) BGP.otc: 65003 ``` 可以看到,第九行有一个 `BGP.otc` 这个就是 [RFC9234](https://www.rfc-editor.org/rfc/rfc9234.html "RFC 9234") 中新增的路由属性:只发给客户(Only to Customer),带上这个属性的路由将会只发给自己的客户,而不是上游或者 peer。 让我们摘取一段(经过翻译的)RFC 原文,位置在[这里](https://www.rfc-editor.org/rfc/rfc9234.html#name-bgp-only-to-customer-otc-at): > 好的,我帮你把这段关于 **BGP Only to Customer (OTC) 属性** 的 RFC 段落翻译成简体中文,并保持术语和逻辑准确: > > *** > > **5. BGP 仅向客户(OTC)属性** > > OTC 属性是 UPDATE 报文中的一个可选可传递(Optional Transitive)的路径属性,属性类型代码(Attribute Type Code)为 35,长度为 4 个八位字节。该属性的目的是强制执行一条路由一旦被发送给客户(Customer)、对等体(Peer)或路由服务器客户端(RS-Client,见 3.1 节定义),后续只能再次发送给客户。属性值是一个由下述过程确定的自治系统号(ASN)。 > > **接收端的处理过程:** > > * 如果从客户或 RS-Client 收到带有 OTC 属性的路由,则视为路由泄漏(Route Leak),**必须**视为不合法(不可用)(见第 3 节)。 > * 如果从对等体(即具有对等角色的远程 AS)收到带有 OTC 属性的路由,且属性值不等于该远程(即对等体)AS 的号码,则视为路由泄漏,**必须**视为不合法。 > * 如果从上游(Provider)、对等体或路由服务器(RS)收到路由,且未携带 OTC 属性,则**必须**在该路由上添加 OTC 属性,值为远程 AS 的号码。 > > **发送端的处理过程:** > > * 如果要将路由发送给客户、对等体或 RS-Client(当发送者是 RS 时),且该路由未携带 OTC 属性,则在发送时**必须**添加 OTC 属性,值为本地 AS 的号码。 > * 如果路由已经包含 OTC 属性,则**不得**将其传播给上游(Provider)、对等体或 RS。 > > 上述过程既实现了本地 AS 的泄漏防护,也支持多跳下游的泄漏检测与缓解。在本地 AS 层面,OTC 属性的存在表明该路由是从对等体、上游或 RS 学到的,只能向客户发布。同一个 OTC 属性也为后续的 AS 检测路由泄漏提供了依据。例如,如果一个 AS 在向对等体发送路由时设置了 OTC 属性,而该路由后续被某个合规的 AS 从客户处收到,则根据 OTC 属性可检测到该路由已被泄漏。 > > OTC 属性可以在远程 AS 出口处设置,也可以在本地 AS 入口处设置——即如果远程 AS 不符合本规范,则本地 AS 在属性缺失时需要补加 OTC 属性。在这两种场景下,OTC 值是相同的。这种机制提高了方案的鲁棒性,并对早期采用者有利。 > > 如果 OTC 属性的长度不是 4,则视为格式错误(malformed)。对于携带格式错误 OTC 属性的 UPDATE 报文,**应**按照 [RFC7606](https://www.rfc-editor.org/rfc/rfc7606.html "RFC 7606") 的“视同撤销(treat-as-withdraw)”方法处理。 > > 本文件指定的 BGP 角色协商和基于 OTC 属性的过程,**不推荐**在自治系统联盟(AS Confederation,见 [RFC5065](https://www.rfc-editor.org/rfc/rfc5065.html "RFC 5065"))内部的自治系统之间使用。如果从 AS Confederation 出口处添加了 OTC 属性,其值**必须**等于该 AS Confederation 的标识符(Identifier)。此外,从 AS Confederation 出口处发送的 UPDATE 报文**不得**包含值为除 AS Confederation Identifier 之外的任何成员 AS 号的 OTC 属性。 > > 本文件中定义的过程,可在使用私有 ASN 的场景下(如数据中心网络 [RFC7938](https://www.rfc-editor.org/rfc/rfc7938.html "RFC 7938") 或存根客户)使用,但相关细节不在本文件范围内。从面向互联网的 ASN 出口时,OTC 属性**不得**包含除该 ASN 之外的其他值。 > > 一旦设置了 OTC 属性,**必须**保持不变(这同样适用于 AS Confederation)。 > > 上述入站和出站过程仅适用于地址族 AFI 1(IPv4)和 AFI 2(IPv6),且 SAFI 均为 1(单播)。默认情况下**不得**用于其他地址族,运营者**不得**有能力修改本节定义的过程。 可能有的人会问,既然有了 role,那我们是不是可以不用上文的函数了呢?当然不行,因为你还需要函数去做一些除了来源之外的检查,比如 IRR、地址长度等等。而且,我们也需要函数提供一个平台来让我们添加 community,从而更好地管理我们的路由。所以,你可以把 role 和函数融合在一起来增强你的过滤的可靠性,但不要只依靠 role。 # IBGP:联通边界 (/docs/beginner/multi-location/ibgp) 假设你有三台 VPS,其中两台已建立 BGP 会话,如下图所示。你希望在没有 BGP 会话的那台 VPS 上提供服务,并通过两台已有会话的 VPS 对外发布该服务的路由。你当然希望用户能从距离最近的节点访问服务,也就是说,需要将两份 BGP 全表合并后综合判断路由,但同时对外 AS 又不变。这种需求可以通过一种机制来实现:iBGP。 # iBGP [#ibgp] iBGP(Internal BGP 或 Interior BGP)是指在同一个自治系统(AS)内部建立的 BGP 会话。它与 eBGP(External BGP)相比,有以下三点主要区别: 1. **管理距离不同**。在 BIRD 中,eBGP 的管理距离为 20,OSPF 是 110,而 iBGP 是 200。这意味着当一条路由同时从 eBGP 和 iBGP 学到时,路由器会优先选择 eBGP 的路径,从而有效避免路由环路。而且对于大多数场景来说,越早将流量引出本地网络越好。 2. **下一跳处理不同**。eBGP 默认会将下一跳地址修改为本机,而 iBGP 默认保留原有的下一跳地址。因此,我们通常会在将 eBGP 学到的路由传给 iBGP 时手动将下一跳设置为本地地址,以避免在 IGP 中传播不必要的外部路由(这也是 IX 要求使用不广播的公网 IP 地址的原因之一)。 3. **环路防护机制不同**。eBGP 通过 AS Path 来检测和防止环路,而 iBGP 由于自治系统号相同,不会在 AS Path 中增加自己的 ASN(否则你就会看到一条路由中同一个 ASN 重复十几次)。因此,iBGP 采用\*\*水平分割(split horizon)\*\*机制:**一般情况下,不会将从一个 iBGP 对等体学到的路由转发给另一个 iBGP 对等体**。 # 配置方式 [#配置方式] 由于 iBGP 的水平分割机制,无法像 eBGP 那样“一配即通”。为了确保所有节点能接收到完整路由信息,需要将所有 iBGP 节点以特定方式互联。常见方式有: * Full Mesh(全连接) * Route Reflector(路由反射器) * Confederation(联盟) 其中 Confederation 由于现网案例少、问题多,我们不在这里讲述(TODO)。 ## Full Mesh(全连接) [#full-mesh全连接] 理解了 iBGP 的防环机制,我们最直接的做法就是:把它们全部连起来!这种方式称为 Full Mesh,适用于节点数量较少的情况。以下是一个简单配置示例: ```bird2 protocol bgp bgp_ipeer { local fe80::a as 114514; neighbor fe80::c%e2 as 114514; direct; ipv6 { import all; export all; }; graceful restart; } ``` 由于 iBGP 没有上下游角色限制,因此可以放心地使用 `import all` 和 `export all`。 ## Route Reflector(路由反射器) [#route-reflector路由反射器] Full Mesh 在少量的时候配起来是很简单的,但当节点数量增多时,Full Mesh 会面临连接数量爆炸的问题。有的工程师就想:既然我的目的是防环,那我可以让一个专门的中心节点来管理路由,只让它来发不就好了。秉承着这种想法,Route Reflector(路由反射器,简称 RR)诞生了。Route Reflector 是一台经过特殊配置的路由器,按照如下规则转发路由: 1. 从非 RR Client 学到的路由,可转发给所有 RR Client; 2. 从 RR Client 学到的路由,可转发给所有非 RR Client 和其他 RR Client(不含原始发起者); 3. 从 eBGP 对等体学到的路由,可转发给所有 RR Client 和非 Client。 为了防止环路,RR 在转发路由时会添加 `Originator ID` 和 `Cluster List`: * `Originator ID`:标记原始路由发起者(即 RR Client)的 Router ID,当原始路由该项为空时写发起者的 Router ID; * `Cluster List`:RR 会在其中添加自己的 Router ID,若路由再次经过自身,则拒收,防止回环。 依靠这两个属性,RR 成功实现了跟其他节点的防环。从整体来看,一个 RR 及其所有的 RR Client 就像花瓣一样组成了一个集群,可以把它“看作”是一个大的 iBGP 节点,并继续向上组网,形成分级 RR 结构。 如果两个 RR 管理的是不同集群,Cluster ID 应不同;若互为备份、共同服务于同一集群,则 Cluster ID 可以一致。 示例配置如下: ```bird2 {9} protocol bgp bgp_ipeer_c { local fe80::a as 114514; neighbor fe80::c%e2 as 114514; direct; ipv6 { import all; export all; }; rr client; graceful restart; } ``` ```bird2 protocol bgp bgp_ipeer_rr { local fe80::c as 114514; neighbor fe80::a%e0 as 114514; direct; ipv6 { import all; export all; }; graceful restart; } ``` 可以看到,RR 端需要在对 Client 的连接中添加 `rr client;`,而 Client 无需额外配置。 # 模板 [#模板] 如果说 eBGP 配置是“千人千面”,那么 iBGP 的配置可以说是“高度统一”。尤其在配置多个 RR Client 时,很多参数都是重复的。这种场景下,**模板机制**(template)就显得非常有用。 让我们举个例子: ```bird2 template bgp ibgp { direct; ipv6 { import all; export all; }; rr client; graceful restart; } protocol bgp node_b from ibgp { local fe80::a as 114514; neighbor fe80::b%e1 as 114514; } protocol bgp node_c from ibgp { local fe80::a as 114514; neighbor fe80::c%e2 as 114514; } ``` ```bird2 protocol bgp node_b from ibgp { local fe80::a as 114514; neighbor fe80::b%e1 as 114514; direct; ipv6 { import all; export all; }; rr client; graceful restart; } protocol bgp node_c from ibgp { local fe80::a as 114514; neighbor fe80::c%e2 as 114514; direct; ipv6 { import all; export all; }; rr client; graceful restart; } ``` 可以看到,使用模板显著减少了重复代码,配置更清晰,也更易维护。 # IGP:打通内网 (/docs/beginner/multi-location/igp) 让我们先来看一个情景: 问题是:从 A 到 C 的最小开销路径是哪一条? 你很可能会毫不犹豫地回答:经由 B 再到 C!这看起来毫无难度。 但如果我们面对的是下面这种情况呢? 现在计算起来还容易吗?更进一步,如果 cost 是动态变化的呢?你难道要 24 小时守着配置器随时手动调整吗? 聪明的工程师显然不会允许自己日夜值班,于是他们发明了 IGP。 # 概念介绍 [#概念介绍] ## IGP [#igp] 以下引自[维基百科](https://zh.wikipedia.org/wiki/%E5%86%85%E9%83%A8%E7%BD%91%E5%85%B3%E5%8D%8F%E8%AE%AE): > **内部网关协议**(英语:Interior Gateway Protocol,缩写为 IGP)是指在一个[自治系统](https://zh.wikipedia.org/wiki/自治系统)(AS)内部所使用的一种[路由协议](https://zh.wikipedia.org/wiki/路由协议)。 > > 与此相对,[外部网关协议](https://zh.wikipedia.org/wiki/外部网关协议)用来在自治系统之间确定网络可达性、并通过内部网关协议来解析某个自治系统内部的路由。 通俗来说,BGP 决定了自治系统之间如何通信,而 IGP 决定了自治系统内部的路径选择。 严格说来,IGP 的对立面是 EGP,但目前 EGP 几乎只有 BGP 在广泛使用,因此我们做此简化。 IGP 可分为三种: **IGP(Interior Gateway Protocol,内部网关协议)可大致分为三类:** 1. **距离向量路由协议(Distance Vector Protocol)** 这类协议的特点是每个路由器只把自己知道的路由信息和距离(通常以跳数计)告诉相邻的路由器,路由器之间彼此交换信息,靠不断更新来找到最短路径。典型代表是 **RIP(Routing Information Protocol)**。RIP 配置简单,但扩展性差,收敛速度慢,适用于小型、简单网络。相比之下,**Babel** 是近年来较为流行的现代化距离向量协议,发布于 2021 年,虽然本质上也属于距离向量算法,但它在度量方式、环路避免和链路检测上做了很多改进,支持同时处理 IPv4 和 IPv6,并且非常适合动态变化的网络环境,比如无线 Mesh、流动性高的 VPS 组网等。 2. \*\*链路状态路由协议(Link State Protocol)\*\*这类协议要求每台路由器收集整个网络的拓扑信息,然后使用如 Dijkstra 算法计算最短路径,特点是收敛快、可扩展性强,适合中大型网络。典型代表是 **OSPF(Open Shortest Path First)** 和 **IS-IS(Intermediate System to Intermediate System)**。OSPF 是目前最常见的开源 IGP,分区域设计、支持认证和多路径,适用于复杂场景。ISIS 则常用在运营商的内网中,因为其扩展性高,适用于 MPLS 等需要携带更多信息的内网需求。 3. **混合路由协议(Hybrid Protocol)** 顾名思义,这类协议融合了前两种的优点,比如同时使用邻居交换和拓扑更新机制。典型代表是 **EIGRP(Enhanced Interior Gateway Routing Protocol)**,是思科私有协议。EIGRP 结合了距离向量的简单和链路状态的快速收敛,但因专有性质较少使用。 RIP 太过时,EIGRP 太封闭,ISIS 需要 L2 连接,因此我们的选项仅有 OSPF 和 Babel。但考虑到 Babel 的时间较短,应用不够广泛,本节将只讲 OSPF,Babel 的讲解将在后面扩展中提到。(TODO) ## OSPF [#ospf] > OSPF(Open Shortest Path First,开放式最短路径优先)是是大中型网络上使用较为广泛的[IGP](https://zh.wikipedia.org/wiki/内部网关协议)协议。OSPF 是对[链路状态路由协议](https://zh.wikipedia.org/w/index.php?title=链路状态路由协议\&action=edit\&redlink=1)的一种实现,运作于[自治系统](https://zh.wikipedia.org/wiki/自治系统)内部。OSPF 分为 OSPFv2 和 OSPFv3 两个版本:OSPFv2 定义于 [RFC 2328](https://www.rfc-editor.org/rfc/rfc2328.html "RFC 2328")(1998),支持[IPv4](https://zh.wikipedia.org/wiki/IPv4)网络;而 OSPFv3 定义于 [RFC 5340](https://www.rfc-editor.org/rfc/rfc5340.html "RFC 5340")(2008),支持[IPv6](https://zh.wikipedia.org/wiki/IPv6)网络。 > > OSPF 提出了“区域(Area)”的概念,一个网络可以由单一区域或者多个区域组成。其中,一个特别的区域被称为骨干区域(Backbone Area),该区域是整个 OSPF 网络的核心区域,并且所有其他的区域都与之直接连接。所有的内部路由都通过骨干区域传递到其他非骨干区域。所有的区域都必须直接连接到骨干区域,如果不能建立直接连接,那么可以通过[虚链路](https://zh.wikipedia.org/w/index.php?title=虚链路\&action=edit\&redlink=1)(virtual link)和骨干区域建立[虚拟连接](https://zh.wikipedia.org/w/index.php?title=虚拟连接\&action=edit\&redlink=1)。 > > 它使用链路状态数据库(LSDB)用来保存当前[网络拓扑](https://zh.wikipedia.org/wiki/网络拓扑)结构,[路由器](https://zh.wikipedia.org/wiki/路由器)上属于同一区域的链路状态数据库是相同的(属于多个区域的路由器会为每个区域维护一份链路状态数据库)。 > > 同一个[广播域](https://zh.wikipedia.org/wiki/广播域)(Broadcast Domain)的[路由器](https://zh.wikipedia.org/wiki/路由器)或者一个[点对点](https://zh.wikipedia.org/wiki/点对点)(Point To Point)连接的两端的路由器,在发现彼此的时候,建立邻接(Adjacencies)\[[注 1\]](https://zh.wikipedia.org/wiki/开放式最短路径优先#cite_note-1)。多路访问网络以及非广播多路访问网络的路由器会选举指定路由器(Designated Router, DR)和备份指定路由器(Backup Designated Router, BDR),DR 和 BDR 作为网络的中心负责路由器之间的信息交换从而降低了网络中的信息流量。OSPF 协议同时使用[单播](https://zh.wikipedia.org/wiki/單播)(Unicast)和[群播](https://zh.wikipedia.org/wiki/組播)(Multicast)来发送[Hello 包](https://zh.wikipedia.org/w/index.php?title=Hello包\&action=edit\&redlink=1)和链路状态更新(Link State Updates),使用的群播[地址](https://zh.wikipedia.org/wiki/IP地址)为 224.0.0.5 和 224.0.0.6。与[RIP](https://zh.wikipedia.org/wiki/路由信息协议)和[BGP](https://zh.wikipedia.org/wiki/BGP)不同的是,OSPF 协议不使用 TCP 或者 UDP 协议而是直接承载在 IP 协议之上,[IP 协议号](https://zh.wikipedia.org/wiki/IP协议号列表)为 89。 在小型网络中,我们可以只使用一个骨干区域,不需要了解 DR、BDR、ASBR 等复杂机制。OSPF 分为 v2、v3 两个版本, 其中 v2 仅支持 IPv4,v3 支持 IPv4 和 IPv6,由于我们是 IPv6 内网,所以直接使用 OSPFv3 就可以。因为 IPv6 自带了 link-local 地址,所以 OSPFv3 不需要配置 IP 就可以使用,更加方便。 虽然 OSPFv3 可基于 link-local 地址自动运行,但接口必须先启用才能生成地址! 需要注意的是,在大部分路由系统中,OSPFv3 都需要分两个实例来分别跑 IPv4、IPv6 并独立计算 LSDB,因此实际操作中经常是 IPv6 起一个 OSPFv3,IPv4 起一个 OSPFv2(现在你应该知道为什么运营商不喜欢配双栈了)。OSPFv2 将在之后讲解(TODO),但通常而言,只需要你把`v3`改成`v2`就好了。 # 配置 [#配置] 以上图为例。 ## 单个配置 [#单个配置] 以 Node A 举例: ```bird2 protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; } interface "e1" { cost 30; type ptp; } interface "dummy0" { stub; } }; } ``` 其中: ```bird2 startLineNumber=1 "v3" protocol ospf v3 ospfv3 { ``` 中的 `v3` 是版本号,省略掉或者改成 `v2` 就是 OSPFv2 ```bird2 startLineNumber=3 "0" area 0 { ``` 中的 `0` 是区域,可以用点分十进制 32 位数字(像 Router ID 一样)或者直接用数字,0 区域是骨干区域,在一个 OSPF 网络中所有区域必须与 0 区域有连接。 ```bird2 startLineNumber=4 interface "e0" { ``` 中的 `e0` 是加入 OSPF 网络的接口。OSPF 是链路状态协议,交换的东西是链路状态,因此加入的是接口而不是路由,路由是依托于接口而存在的。 ```bird2 startLineNumber=5 cost 10; ``` 这个`cost`是这个接口到对面所用的开销,在 BIRD 中默认是 10。 商业路由器的计算公式是: $$ \text{Cost} = \frac{\text{参考带宽}}{\text{接口带宽}} $$ 其中参考带宽为 100Mbps,Cost 小于 1 时取 1。 而我们通常会用到的就是将延迟转化为整数(例如 x100),因为延迟通常比带宽更重要。你也可以根据自己的需要微调 cost。 ```bird2 startLineNumber=6 type ptp; ``` 这里的类型有 `broadcast/bcast`、`pointopoint/ptp`、`nonbroadcast/nbma`和`pointomultipoint/ptmp`几种,介绍让我们摘录一下 BIRD 的原文: > * `type broadcast|bcast` > BIRD 会自动检测已连接网络的类型,但有时手动强制使用不同类型会更方便。在广播网络(如以太网)中,泛洪和 Hello 消息通过多播发送(对所有邻居发送单个数据包)。会选举一个指定路由器,该路由器负责同步链路状态数据库并生成网络 LSA。此网络类型不能用于物理 NBMA 网络和无编号网络(没有适当 IP 前缀的网络)。 > > * `type pointopoint|ptp` > 点对点网络仅连接两个路由器。不会进行选举,也不会生成网络 LSA,这使得建立过程更简单、更快速。此网络类型不仅适用于物理点对点接口(如 PPP 或隧道),也适用于作为点对点链路使用的广播网络。此网络类型不能用于物理 NBMA 网络。 > > * `type nonbroadcast|nbma` > 在 NBMA 网络中,由于缺乏多播功能,数据包会分别发送给每个邻居。与广播网络类似,会选举一个指定路由器,该路由器在 LSA 的传播中起核心作用。此网络类型不能用于无编号网络。 > > * `type pointomultipoint|ptmp` > 这是另一种设计用于处理 NBMA 网络的网络类型。在这种情况下,NBMA 网络被视为一组点对点(PtP)链路。如果 NBMA 网络上的每对路由器之间没有直接通信,或者 NBMA 网络被用作(可能是无编号的)点对点链路,这种方式非常有用。 简述而言,就是前两者会通过`ff02::5`地址发送广播包来发现邻居,后二者需要手动配置邻居。如果你在一个实体网络内,可以用第一个;通常我们用 Wireguard 或者点对点链路(插线)会用第二个。 1. 在 Wireguard 上使用的时候,最好使用 `ptp` 或 `ptmp`。 2. 在 BIRD 与 Mikrotik 通过 Wireguard 进行配对的时候,请尽量使用 `ptp`,有传言说这两个软件发送的 `ptmp` 包不相同。 细心的你会发现,我们在这里直接 `ipv6;` 而不是配置 `import` 和 `export`,这是因为我们并不需要用它来传递外部路由,只需要传递本地地址——而这已经够我们接下来起 iBGP 的需求。如果你需要用 OSPF 传输比如静态路由,那你还需要把`import`和`export`配好。 或许你会问:既然我们没有导入路由,那我们怎么传递在`dummy`接口上的地址呢?答案很简单:既然它是个接口,那我们就把它加进 OSPF 就好了。不过,既然`dummy`接口是个虚拟接口,并没有实际的连接,那我们就要配置 BIRD 不在这个接口发送 OSPF 包,方法是把它配置成`stub`接口,在别的路由器里也可能是把它配置成`passive`,如下所示: ```bird2 {2} startLineNumber=12 interface "dummy0" { stub; # 这里将其配置成 stub 接口 } ``` 你可能会问:那为什么我不把它当成外部路由导入,而是把它当成接口加入呢?一方面,内部路由比外部路由优先级更高;另外一方面,这样能减少 Type5 LSA,提高 OSPF 效率(Type 5 LSA 会穿过汇总边界,对于性能低的 OSPF 设备不利)。 ## 全部配置 [#全部配置] 让我们把三个机器配满,来看看效果: ```bird2 log syslog all; router id 10.0.0.1; # 定义 Router ID protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol ospf v3 ospfv3 { ipv6; area 0 { # 我们只用 area0,设备不多没必要分 area interface "e0" { cost 10; type ptp; }; interface "e1" { cost 30; type ptp; }; interface "dummy0" { stub; }; }; } ``` ```bird2 log syslog all; router id 10.0.0.2; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; }; interface "e1" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } ``` ```bird2 log syslog all; router id 10.0.0.3; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; }; interface "e1" { cost 30; type ptp; }; interface "dummy0" { stub; }; }; } ``` ## 验证 [#验证] 我们可以通过`birdc show ospf state`和`birdc show route`来验证是否成功搭建。 state: ```shell root@debian:~# birdc show ospf state BIRD 2.17.1 ready. area 0.0.0.0 router 10.0.0.1 distance 0 router 10.0.0.2 metric 10 router 10.0.0.3 metric 30 stubnet 2001:db8::1/128 metric 0 router 10.0.0.2 distance 10 router 10.0.0.1 metric 10 router 10.0.0.3 metric 10 stubnet 2001:db8::2/128 metric 0 router 10.0.0.3 distance 20 router 10.0.0.2 metric 10 router 10.0.0.1 metric 30 stubnet 2001:db8::3/128 metric 0 root@debian:~# ``` route: ```shell root@debian:~# birdc show route BIRD 2.17.1 ready. Table master6: 2001:db8::2/128 unicast [ospfv3 2025-07-11] * I (150/10) [10.0.0.2] via fe80::5220:deff:fe00:100 on ens3 2001:db8::3/128 unicast [ospfv3 2025-07-11] * I (150/20) [10.0.0.3] via fe80::5220:deff:fe00:100 on ens3 2001:db8::1/128 unicast [ospfv3 2025-07-11] * I (150/0) [10.0.0.1] dev dummy0 root@debian:~# ``` state: ```shell root@debian:~# birdc show ospf state BIRD 2.17.1 ready. area 0.0.0.0 router 10.0.0.1 distance 0 router 10.0.0.2 metric 10 router 10.0.0.3 metric 30 stubnet 2001:db8::1/128 metric 0 router 10.0.0.2 distance 10 router 10.0.0.1 metric 10 router 10.0.0.3 metric 10 stubnet 2001:db8::2/128 metric 0 router 10.0.0.3 distance 20 router 10.0.0.2 metric 10 router 10.0.0.1 metric 30 stubnet 2001:db8::3/128 metric 0 root@debian:~# ``` route: ```shell root@debian:~# birdc show route BIRD 2.17.1 ready. Table master6: 2001:db8::2/128 unicast [ospfv3 2025-07-11] * I (150/0) [10.0.0.2] dev dummy0 2001:db8::3/128 unicast [ospfv3 2025-07-11] * I (150/10) [10.0.0.3] via fe80::5288:acff:fe00:200 on ens4 2001:db8::1/128 unicast [ospfv3 2025-07-11] * I (150/10) [10.0.0.1] via fe80::5215:b5ff:fe00:300 on ens3 root@debian:~# ``` state: ```shell root@debian:~# birdc show ospf state BIRD 2.17.1 ready. area 0.0.0.0 router 10.0.0.1 distance 0 router 10.0.0.2 metric 10 router 10.0.0.3 metric 30 stubnet 2001:db8::1/128 metric 0 router 10.0.0.2 distance 10 router 10.0.0.1 metric 10 router 10.0.0.3 metric 10 stubnet 2001:db8::2/128 metric 0 router 10.0.0.3 distance 20 router 10.0.0.2 metric 10 router 10.0.0.1 metric 30 stubnet 2001:db8::3/128 metric 0 root@debian:~# ``` route: ```shell root@debian:~# birdc show route BIRD 2.17.1 ready. Table master6: 2001:db8::2/128 unicast [ospfv3 2025-07-11] * I (150/10) [10.0.0.2] via fe80::5220:deff:fe00:101 on ens3 2001:db8::3/128 unicast [ospfv3 2025-07-11] * I (150/0) [10.0.0.3] dev dummy0 2001:db8::1/128 unicast [ospfv3 2025-07-11] * I (150/20) [10.0.0.1] via fe80::5220:deff:fe00:101 on ens3 root@debian:~# ``` 可以看到,我们成功通过 OSPF 传递了路由。并且你可以发现,在完全同步的 OSPF 区域内部,所有机器拿到的 area 信息是完全一样的。 # 附录:OSPF 的常见问题 [#附录ospf-的常见问题] OSPF 是一个有很多要求的协议,这里梳理出一些常见问题给大家: 1. Wireguard 上建议使用 `ptp` 类型,尤其对端为 Mikrotik 时,使用 `ptmp` 可能存在兼容性问题。 2. 七个一样 * MTU 必须一样 * 区域类型必须一样 * 区域号必须一样 * 链路类型必须一样 * 网段必须一样 * hello/dead 时间必须一样 * 认证必须一样 # 前言 (/docs/beginner/multi-location) 在经过一段时间的发展后,你可能已经拥有了多个分布在不同大洲的 VPS。这些 VPS 有些建立了 BGP 会话,有些则没有,但你希望它们都能使用你申请到的 IP 地址段。这就需要将没有 BGP 会话的 VPS 接入到已经具备 BGP 会话的节点,通过内部网络获取和传递路由和地址那怎么把这些 VPS 连接起来呢?。 同时,你可能会惊讶地发现,很多时候,用户的访问流量先进入距离他们最近的 VPS,再通过隧道转发到实际的服务节点,反而比直接访问服务节点本身更低延迟、更稳定。那么,如何才能既在不同地点通过 BGP 广播 IP,又能通过内网把这些节点高效连接起来呢?也许你会觉得可以一个一个单独广播再用隧道连接,但有没有更优雅、更可控的方法? 在本章中,我们将讲解如何使用 IGP(内部网关协议)来让 VPS 之间互联互通,以及如何利用 iBGP(内部 BGP)在拥有 BGP 的 VPS 之间同步和传递路由信息,从而构建一个稳定高效的全球分布式网络。 # Lab 示例 (/docs/beginner/multi-location/lab) 现在让我们构建一个 Lab,综合运用前面学习过的 IGP 与 iBGP 协议。拓扑结构如下图所示: 细心的你可能已经注意到,此处我们采用的是回环地址(Loopback IP)。这是因为在部署 iBGP 时,有一个被广泛认可的最佳实践:在存在 IGP 的网络环境中,建议使用回环地址作为 iBGP 的建立地址。这样一来,即便节点间的物理链路发生变化,IGP 也能及时收敛并更新路径,实现协议层次的合理分离。 当然,若两个节点之间是点对点直连(PtP),也可以直接使用接口地址建立 iBGP 会话,这在简单拓扑中也非常常见。 本实验采用 RR(Route Reflector)配置方式,指定 Node A 作为 RR 节点。`functions.conf` 等基础配置沿用我们[在前一章节中的内容](/beginner/connect-with-others/lab),此处不再赘述。 # 配置 [#配置] ## IGP [#igp] ```bird2 protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e1" { cost 10; type ptp; }; interface "e2" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } ``` ```bird2 protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } ``` ```bird2 protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } ``` ## eBGP [#ebgp] ```bird2 protocol bgp upstream { local fe80::a as 114514; neighbor fe80::aa%e0 as 65001; direct; ipv6 { import where import_filter_upstream(); export where export_filter_upstream(); }; graceful restart; }; ``` ```bird2 protocol bgp upstream { local fe80::c as 114514; neighbor fe80::cc%e0 as 65001; direct; ipv6 { import where import_filter_upstream(); export where export_filter_upstream(); }; graceful restart; }; ``` ## iBGP [#ibgp] ```bird2 template bgp ibgp { local 2001:db8::a as 114514; multihop 2; ipv6 { import all; export all; }; rr client; graceful restart; } protocol bgp node_b from ibgp { neighbor 2001:db8::b as 114514; } protocol bgp node_c from ibgp { neighbor 2001:db8::c as 114514; } ``` ```bird2 protocol bgp node_a { local 2001:db8::b as 114514; neighbor 2001:db8::a as 114514; multihop 2; ipv6 { import all; export all; }; graceful restart; } ``` ```bird2 protocol bgp node_a { local 2001:db8::c as 114514; neighbor 2001:db8::a as 114514; multihop 2; ipv6 { import all; export all; }; graceful restart; } ``` ## 全部配置 [#全部配置] ```bird2 include "functions.conf"; log syslog all; router id 1.1.1.1; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e1" { cost 10; type ptp; }; interface "e2" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } protocol bgp upstream { local fe80::a as 114514; neighbor fe80::aa%e0 as 65001; direct; ipv6 { import where import_filter_upstream(); export where export_filter_upstream(); }; graceful restart; }; template bgp ibgp { local 2001:db8::a as 114514; multihop 2; ipv6 { import all; export all; }; rr client; graceful restart; } protocol bgp node_b from ibgp { neighbor 2001:db8::b as 114514; } protocol bgp node_c from ibgp { neighbor 2001:db8::c as 114514; } ``` ```bird2 log syslog all; router id 1.1.1.2; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } protocol static { ipv6; route 2001:db8:100::/48 reject {bgp_large_community.add((114514,1,3));}; } protocol bgp node_a { local 2001:db8::b as 114514; neighbor 2001:db8::a as 114514; multihop 2; ipv6 { import all; export all; }; graceful restart; } ``` ```bird2 log syslog all; router id 1.1.1.3; protocol device { }; protocol kernel { ipv6 { export all; }; }; protocol ospf v3 ospfv3 { ipv6; area 0 { interface "e0" { cost 10; type ptp; }; interface "dummy0" { stub; }; }; } protocol bgp upstream { local fe80::c as 114514; neighbor fe80::cc%e0 as 65002; direct; ipv6 { import where import_filter_upstream(); export where export_filter_upstream(); }; graceful restart; }; protocol bgp node_a { local 2001:db8::c as 114514; neighbor 2001:db8::a as 114514; multihop 2; ipv6 { import all; export all; }; graceful restart; } ``` ## 验证 [#验证] ```bash root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8:aa::/48 unreachable [static1 04:33:32.394] * (200) 2001:db8:100::/48 unicast [bgp1 04:44:21.386] * (100) [AS114514i] via fe80::a on ens3 root@debian:~# ``` ```bash root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8::b/128 unicast [ospfv3 04:44:17.618] * I (150/10) [1.1.1.2] via fe80::525b:9dff:fe00:100 on ens4 unicast [node_b 04:44:21.087 from 2001:db8::b] (100/10) [i] via fe80::525b:9dff:fe00:100 on ens4 unicast [node_c 04:48:37.945 from 2001:db8::c] (100/10) [i] via fe80::52dd:ddff:fe00:200 on ens5 2001:db8:aa::/48 unicast [upstream 04:38:54.271] * (100) [AS65001i] via fe80::aa on ens3 2001:db8:cc::/48 unicast [node_c 04:50:54.638 from 2001:db8::c] * (100/10) [AS65002i] via fe80::52dd:ddff:fe00:200 on ens5 2001:db8::c/128 unicast [ospfv3 04:48:36.618] * I (150/10) [1.1.1.3] via fe80::52dd:ddff:fe00:200 on ens5 unicast [node_b 04:48:37.031 from 2001:db8::b] (100/10) [i] via fe80::525b:9dff:fe00:100 on ens4 unicast [node_c 04:48:37.945 from 2001:db8::c] (100/10) [i] via fe80::52dd:ddff:fe00:200 on ens5 2001:db8::a/128 unicast [ospfv3 04:41:48.619] * I (150/0) [1.1.1.1] dev dummy0 unicast [node_b 04:44:21.087 from 2001:db8::b] (100/10) [i] via fe80::525b:9dff:fe00:100 on ens4 unicast [node_c 04:48:37.945 from 2001:db8::c] (100/10) [i] via fe80::52dd:ddff:fe00:200 on ens5 2001:db8:100::/48 unicast [node_b 04:44:21.087 from 2001:db8::b] * (100/10) [i] via fe80::525b:9dff:fe00:100 on ens4 root@debian:~# ``` ```bash root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8::b/128 unicast [ospfv3 04:43:10.297] * I (150/0) [1.1.1.2] dev dummy0 unicast [node_a 04:44:21.515 from 2001:db8::a] (100/10) [i] via fe80::52fe:b6ff:fe00:301 on ens3 2001:db8:aa::/48 unicast [node_a 04:44:21.515 from 2001:db8::a] * (100/10) [AS65001i] via fe80::52fe:b6ff:fe00:301 on ens3 2001:db8:cc::/48 unicast [node_a 04:50:55.068 from 2001:db8::a] * (100/20) [AS65002i] via fe80::52fe:b6ff:fe00:301 on ens3 2001:db8::c/128 unicast [ospfv3 04:48:37.459] * I (150/20) [1.1.1.3] via fe80::52fe:b6ff:fe00:301 on ens3 unicast [node_a 04:48:37.048 from 2001:db8::a] (100/10) [i] via fe80::52fe:b6ff:fe00:301 on ens3 2001:db8::a/128 unicast [ospfv3 04:44:17.459] * I (150/10) [1.1.1.1] via fe80::52fe:b6ff:fe00:301 on ens3 unicast [node_a 04:44:21.515 from 2001:db8::a] (100/10) [i] via fe80::52fe:b6ff:fe00:301 on ens3 2001:db8:100::/48 unreachable [static1 04:43:10.196] * (200) root@debian:~# ``` ```bash root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8::b/128 unicast [ospfv3 04:48:38.195] * I (150/20) [1.1.1.2] via fe80::52fe:b6ff:fe00:302 on ens3 unicast [node_a 04:48:38.547 from 2001:db8::a] (100/10) [i] via fe80::52fe:b6ff:fe00:302 on ens3 2001:db8:aa::/48 unicast [node_a 04:48:38.547 from 2001:db8::a] * (100/10) [AS65001i] via fe80::52fe:b6ff:fe00:302 on ens3 2001:db8:cc::/48 unicast [upstream 04:50:55.240] * (100) [AS65002i] via fe80::cc on ens4 2001:db8::c/128 unicast [ospfv3 04:48:23.195] * I (150/0) [1.1.1.3] dev dummy0 unicast [node_a 04:48:38.547 from 2001:db8::a] (100/10) [i] via fe80::52fe:b6ff:fe00:302 on ens3 2001:db8::a/128 unicast [ospfv3 04:48:38.195] * I (150/10) [1.1.1.1] via fe80::52fe:b6ff:fe00:302 on ens3 unicast [node_a 04:48:38.547 from 2001:db8::a] (100/10) [i] via fe80::52fe:b6ff:fe00:302 on ens3 2001:db8:100::/48 unicast [node_a 04:48:38.547 from 2001:db8::a] * (100/20) [i] via fe80::52fe:b6ff:fe00:302 on ens3 root@debian:~# ``` ```bash root@debian:~# birdc s r BIRD 2.17.1 ready. Table master6: 2001:db8:cc::/48 unreachable [static1 04:49:59.628] * (200) 2001:db8:100::/48 unicast [bgp1 04:50:55.368] * (100) [AS114514i] via fe80::c on ens3 root@debian:~# ``` 可以看到, Node B 上广播的路由已经成功地被发送到了 Upstream A 和 C,并且能够按照最优路径前往 Upstream A 和 C 的路由。 # 后续 [#后续] 你可能会好奇:如果网络规模更大,两个有 BGP 会话的节点之间并非直连,而是需要经过多个中间节点进行转发,这种情况下该如何处理? 在当前场景下,可以通过设置 RR,使中间节点同样获得 BGP 路由表,从而具备中转能力。但如果中间节点的内存资源较为紧张,无法完整承载 BGP 全表,又该怎么办? 这时候,我们就需要引入一种名为 **MPLS(多协议标签交换)** 的技术。MPLS 能够有效解决这一问题,不过它属于进阶内容,因此不会在本系列入门教程中展开介绍(TODO)。如果你对此感兴趣,可以自行查阅资料~~或者等待我将高级教程“咕咕咕”出来~~ 。