Bubu Blog: ~/article $ ls
构建一个异地 Mesh 网络

前言

首先第一个问题是“这个项目是什么?”。答:异地组网,即把多个地方的局域网连接起来形成一个更大的局域网。

异地组网本质就是 VPN,属于 Site-to-site 类型。本质上就是每个节点都能向对方推送路由,且对方接受路由。

WireGuard、OpenVPN 等都支持这种模式。有做个异地组网的朋友或许还用过如 Tinc、Nebula、ZeroTier、Tailscale 以及 Netmaker 等 P2P/Mesh 产品,后三种相比 WireGuard、OpenVPN 上更为易用。

那么本文的组网与这些有什么区别?

综上,本文旨在建立一个 Mesh 网络,且能够根据用户需求去定义和设置路由权重,且能做到链路冗余

本文设计的局限性:

架构

WireGuard 作为最底层,使用 UDP 连接,意味可以在绝大部分网络上运行。同时 WireGuard 也提供加密。

由于 WireGuard 是 L3 网络,不支持广播,而使用的动态路由协议需要广播工作,所以需要在 WireGuard 之上再建立一层支持 L2 的网络。这里我选择了 VXLAN。

本文的重点是动态路由协议,这里选择的动态路由协议是 Babel。

WireGuard 和 VXLAN 接口都需要分配 IP,这里用的是 IPv4 地址。为方便管理,设定如下规则:

假设节点 A 的 WireGuard 地址是 10.100.11.4,那么其 VXLAN 接口的地址是 10.200.11.4

WireGuard

WireGuard 是基于 UDP 的 VPN 协议。基于 UDP 意味绝大部分网络(TCP/IP)都能够运行。当然由于 UDP 以及协议设定(固定协议号),在一些 ISP 或防火墙环境下,直连 WireGuard 的效果不佳甚至直接被阻断。下文“改进和展望”中有提到如何绕过防火墙。

WireGuard 自身具有加密功能,相比 GRE+IPSEC 方案,配置上简介了许多,且性能损耗很低。

配置

本方案中 WireGuard 只需要启用一个 Interface。配置上,每个 Peer 的 AllowedIPs 只包括 Peer 本身的/32地址。

由于节点众多,如果每个机器都手动配置,那复杂度堪比 Tinc/Nebula。为了避免过于繁琐,我用了 wg-meshconf 来管理所有节点的信息,它还可以直接生成每个节点的配置文件,接着只要使用 scp 发送到节点上即可。

当节点多了,scp 还是有一些繁琐:假设已经有了10台机器,添加一台机器意味着要执行 11 次 scp 命令,如果用 wg-quick 意味这还需要重新启动 interface。

这里我用 hono 写了一个简单的后端,每个节点根据自己的 hostname 请求自己的配置。每个节点分发一个 bash 脚本,脚本流程如下:

  1. 使用 curl 请求自己的配置
  2. 使用 jq 解析响应
  3. iproute2 以及 wg 来配置 interface 以及 peer

把这个 bash 脚本放到 crontab 定时执行,如果有新的节点加入,那么脚本执行后就会自动更新。

但以上方案还是略微繁琐,特别对于讨厌脚本/Crontab 的朋友。好事是由于 WireGuard 的“优秀设计”,目前有挺多基于 WireGuard 的 Mesh 方案,我用过的一个是 wiresmith,这一软件通过与 Consul(其 KV 数据库)通信,上报自己的配置和获取 Peer 的配置,从而更新 Peer 的配置,这种方式类似 ZeroTier/Tailscale 等,通过一个后端来管理所有节点。

弃用原因:

  1. 个人不想使用 Consul,因为需要搭建服务器以及配置证书,而使用 Cloudflare Worker 对于我来说比较方便;
  2. 后续的 VXLAN 配置仍然需要使用 iproute2 来配置,干脆把 WireGuard 以及 VXLAN 的配置命令写到一个脚本上(当然你也可以选择魔改 wiresmith)

题外话:目前大多数的在 WireGuard 上使用动态路由协议的文章,都是设计成每两个节点之间一个 interface,这是由于 AllowedIPs 的限制(WireGuard 在同一个 interface 下,如果有多个 Peer 的 AllowedIPs 都包含了相同的路由,那么在整个运行期间,会随机选取一个并一直使用作为出口,参考)。

DDNS

由于我这边许多节点都是动态 IP,还需要考虑一个 DDNS 的问题。

这点我主要参考了 OpenWRT 的 wireguard_watchdog,其流程大致如下:

  1. 获取 WireGuard Interface 配置
  2. 使用 wg show <iface> latest-handshakes 获取握手信息
  3. 筛选出超时的节点,用 wg set <iface> peer <public_key> endpoint <endpoint> 重新配置

VXLAN

VXLAN 诞生是试图解决与大型云计算部署相关的可扩展性问题。其封装是将 OSI 第2层以太网帧封装在第4层 UDP 数据报内(WireGuard 是将 OSI 第3层的 IP Packet 封装在 UDP 数据报内)。封装以太网帧意味能够支持广播,满足了大部分动态路由协议对网络的要求。而基于 UDP 则保证了性能,且 VXLAN 没有加密、混淆功能,减少了开销。

配置

本文创建的 VXLAN 接口是组播类型的。

使用 ip link add <VXLAN_iface> type VXLAN id <VXLAN_id> dev <wg_iface> dstport 0 创建 VXLAN 接口。

再使用 ip addr 命令给 VXLAN 接口分配 ip 地址。

接着添加转发表。

bridge fdb append to 00:00:00:00:00:00 dst <vtep_ip> dev <iface>

这里,每个 WireGuard Peer 都要运行上述命令,可以写一个脚本使用 wg 获取 Peer,然后在一个循环中执行上述命令。

假设节点 A 通过 VXLAN 与节点 B 通信(单播),此时会向所有的 Peer 发送 ARP 请求报文,其他节点收到后,会记录源 MAC 地址和 IP 地址信息到 FDB 表中,即 ARP 学习。对于节点 B,除了学习外,还需要响应,此时节点 A 会根据响应学习,并记录到 FDB 表中。

动态路由协议 babel

babel 是一个比较新的动态路由协议,在 2021 年,IETF Homenet 才制作其标准版本。 相对于 ospf、rip 等。 设计之初就考虑了 Mesh 网络,

开头提到的,链路冗余以及自定义路由权重(Cost/Weight)都由 Babel 提供。

本文使用 Bird2 运行 Babel,WireGuard 的所有节点都运行 Bird。

Bird 配置(/etc/bird.conf)如下:

log syslog all;
debug protocols all;
debug channels all;

router id 10.200.X.Y;

protocol direct {
   ipv4;
   interface "enp2s0";  # 假设 enp2s0 接入的一个本地局域网(192.168.1.1/24)
}

protocol device {
}

protocol kernel {
  ipv4 {
    export all;
  };
}

protocol babel {
    ipv4 {
        export all;
    };


    interface "<VXLAN_iface>" {
       hello interval 1s;
    };
}

上述会把 192.168.1.1/24 通过 Babel 广播出去。

MASQUERADE/IP FORWARD

每个节点需要开启 IP Forward(IP 转发)以及做 MASQUERADE。

这里我用的是 Firewalld,新建或使用已有的 Zone,比如我使用的是名为 Trusted 的 Zone,第一步先把网络接口加入进入

firewall-cmd --permanent --zone=trusted --add-interface=<wg_iface>
firewall-cmd --permanent --zone=trusted --add-interface=<vxlan_iface>
firewall-cmd --permanent --zone=trusted --add-interface=<other_iface> # 其他接口,比如其他 Tunnel 或者局域网的 eth0 等
firewall-cmd --permanent --zone=trusted --add-forward
firewall-cmd --permanent --zone=trusted --add-masquerade
firewall-cmd --reload

其中 permanent 参数是持久化的意思,如果带有这个参数,执行命令的时候并不会生效,而是在 reload 之后才会生效。如果不带这个参数会立刻生效,但 firewalld 重启后就会丢失规则。

扩展

动态修改路由 Cost

目前所有的 Cost 都是由公网带宽乘以一个常数系数得到。网络路由的原则是保留最大带宽,则需要宽带越大,Cost 越小(节点自动选择 Cost 最小的路由)。

跟路由协议一样,静态的 Cost 在某些场景上并不那么好用,比如:

动态修改路由 Cost 也方便切换到以延时为指标的 Cost 或者更高级的 Cost 指标(例如 EIGRP 的五要素)。这类指标或要素与时间有较大关系。

UDP/WireGuard 限制

在部分 ISP 或者防火墙的环境下,直接使用 WireGuard 效果并不好。此时可以考虑两种方式,

Mesh 拓扑

Mesh 是网络拓扑中的一种,但近几年由于 Wifi 技术上也经常采用 Mesh 组网这词,导致许多朋友认为这个是 Wifi 组网。实则不是,但一样的是,网络设计上都用了 Mesh 的拓扑。

Mesh 拓扑的网络,每个节点之间都是尽可能互相连接的,这意味着,从一个节点到另外一个节点,有很多线路可以走,可以直连、或经过别的节点进行中转。

Mesh Topology

如上图,Node 1 想和 Node 5 进行通信,有若干种线路可选:

Bird 还是 FRR?

FRR 的可配置项比 Bird 要少

有一次测试我采用了 OSPF,但无奈 FRR 的可配置项太少了,诸如邻居、包类型都无法修改。但这些 Bird 都是能够控制的。

FRR 的优点之一可能在于支持的协议稍微多一点,如 EIGRP。

OSPF/EIGRP

由于 OSPF 需要选举出 DR/BDR,设计上就已经和 Mesh 网络有些背道相驰了。

但用 OSPF 也是能够做到 Mesh 网络拓扑,但是要区分多个 Area。这样一来,配置变得也非常麻烦。

EIGRP 没有选举之说,但不会考虑可达性,假如当节点 A(还接了一个 192.168.1.0/24 的网段)到节点 B 链路不通,但都可以和节点 C 通信。节点 B 那边收到的 192.168.1.0/24 的路由,其下一条是节点 A 而不是节点 C。

Babel 会使用 Hello 包以及 IHU(I Heard You)来判断节点是否可达,假设在上面的场景中,A 和 B 由于不能互相通信,那么在节点 A 上对于目的 192.168.1.0/24 的下一条会设置为节点 C。

扩展阅读