Full Mesh with wireguard & babeltitle
2025-02-05date
Redundant network based on wireguard and babel
description

之前尝试过使用静态的Wireguard路由定义来实现故障转移之类的东西,当然预期是用caddy内建的fallback来实现的,可不能指望路由真的自动迁移。 但是我折腾傻了也没搞明白这个wg到底怎么处理,在群友的建议下直接改造成了动态路由的 Overlay Network,Still based on wireguard.

关于相关的替代方案

由于 Wireguard 它不支持 Multicast 而且我们使用 babel 进行路由, 所以不能采用 Wiregurad 本身基于目的地址的端口复用1, 于是就只能每个 Wireguard Interface 单个 Peer,那么如果要构建类似 full mesh 的网络就需要每个 node 上存在 nodes - 1 个interface 2。 庆幸的是我参与组网的机器全部是nixos,一切可以优雅而高效地处理,不用手动创建和定义每个接口网络3

网络重构的全部更改

预期得到一个在 WireGuard tunnel 上搭建的 overlay network。具备以下特性:

  • full mesh
  • 动态路由
  • 故障转移
  • 基于边界的防御模型
  • 极高的拓展能力

要做的是在每个node上创建 n - 1 个具有唯一 peer 的 wireguard interface,然后再给它们配置地址使其互相一一连接,再把 babel 跑起来; 但是这些个地址有点讲究,babel要求 IPv6 link local address才能运行, 而 Wireguard 设备本身是不具备ll addr的,那我们就需要手动配置一下。

回顾nixos上配置Wireguard,其实基本符合 sd-networkd 的原有配置格式。分为 netdevsnetworks

{
  systemd.network = {
   netdevs.wg2 = {
      netdevConfig = {
        Kind = "wireguard";
        Name = "wg2";
        MTUBytes = "1380";
      };
      wireguardConfig = {
        PrivateKeyFile = config.vaultix.secrets.wgab.path;
        ListenPort = 51821;
        RouteTable = false;
      };
      wireguardPeers = [
        {
          PublicKey = "49xNnrpNKHAvYCDikO3XhiK94sUaSQ4leoCnTOQjWno=";
          AllowedIPs = [ "::/0" "0.0.0.0/0"];
          RouteTable = false;
        }
      ];
    };

    networks."10-wg2" = {
      # match the netdevs.wg2.netdevConfig.Name
      matchConfig.Name = "wg2";
      addresses = [
        {
          # address, identical across different WireGuard interfaces on the same machine.
          Address = "fdcc::2/128";
          Peer = "fdcc::1/128"; # the address of peer
        }
        # also ipv4 addr
        {
          # link local address, identical across different WireGuard interfaces on the same machine,
          Address = "fe80::216:3eff:fe15:ec52/64";
          Peer = "fe80::baba:baba:1/64"; # link local address of peer
          Scope = "link";
        }
      ];
      networkConfig = {
        DHCP = false;
      };
    };
  };
}

Scope = "link" 的 address 就是 link local addr 了,这是必需的地址,其他的可以配一两个IPv4/v6地址,我这里只配了v6。

Notice

注意以上配置中的两个 RouteTable=false,关闭wg的自动路由配置非常重要

Tips

WireGuard 接口上的 MTU 应比其隧道数据包所经过的以太网接口的 MTU 小 60 字节(使用 IPv4 传输隧道数据包时;使用 IPv6 时小 80 字节)。隧道 TCP 数据包的 MSS 应再小 40 字节(当数据包本身使用 IPv4 时;使用 IPv6 时为 60 字节)4

上文的 link local address 可以用以下nushell脚本生成,或者使用参考2的python脚本。

$"fe80::(random binary 8 | encode hex --lower | split chars | window 4 --stride 4 | each {str join} | str join ':')"

刚刚提到过每台机器上都需要创建 nodes - 1 的interfaces, 也就是需要不停重复添加 netdevs 和 networks 配置,是个人都得累死,那么引入 nixos module 自动处理吧。

不过在这之前我们还有个问题。

唯一端口分配

因为每个node与另一node连接都需要占用双方一个端口,为统一管理,我需要预设所有node一对一连接所使用的端口唯一(AB连接时使用同一端口号),并且nodes数量变动不改变已有端口配对。 为解决这个问题我给每个机器递增唯一ID, 然后使用康托尔配对将两个机器的ID即两个自然数映射到唯一的自然数, 作为index,之后再加上一个 base value 就是特定两台机器的全局唯一端口号。

因为我的机器数量比较少(小于10)所以原始的 Cantor Pairing 就够用:

𝐶(𝑖,𝑗)=(𝑖+𝑗)(𝑖+𝑗+1)2+𝑗

从我的网络结构算出来的大概长这样:

nix-repl> :p (lib.conn {})
{
  abhoth = {
    azasos = 51850;
    eihort = 51825;
    hastur = 51814;
    kaambl = 51819;
    yidhra = 51832;
  };
  azasos = {
    abhoth = 51850;
    eihort = 51833;
    hastur = 51820;
    kaambl = 51826;
    yidhra = 51841;
  };
  eihort = {
    abhoth = 51825;
    azasos = 51833;
    hastur = 51805;
    kaambl = 51808;
    yidhra = 51818;
  };
  #...
}

Nix实现

接着完成最后一块拼图,即wg接口和地址配置的自动化。

生成

Nix实现, 由于我的配置还涉及反审查和NAT下的机器的自动处理,可能会稍微复杂一点(填了半天真值表)。

回顾我们已经具备的信息,

  • peer 两端的hostname
  • peer两端端口
  • 两端公钥/私钥
  • 两端内网 ULA
  • 两端内网 Link Local Addr
  • peer的Endpoint

然后生成所有对端的iface和地址配置,见如上实现。我推荐将地址信息和唯一id什么的统一管理,方便删改。

使用dummy节点锚定本机的地址 (据说(包括但不限于) babel 必须有一条 direct loopback,才能正常配置内核路由/向外导出路由,大概是起到类似 ospf stub 的作用5)

protocol direct {
  ipv6;
  interface "anchor-*"; 
};

networkd中新增一个dummy接口,然后分配address为当前主机的组网内网ip。更改

这里是迁移之后的bird配置,用以替代babeld。

log syslog all;
debug protocols all;
timeformat protocol iso long;

# machine uniq
router id 10.0.0.2;
define HORTUS_OWNIP = fdcc::2;

protocol device {
  scan time 20;
};

protocol direct {
  ipv6;
  interface "anchor-*";
};

define HORTUS_FIELD = [ fdcc::/64+ ];

function in_hortus() {
  return net ~ HORTUS_FIELD;
};

filter to_hortus {
  if in_hortus() && (source = RTS_BABEL || source = RTS_DEVICE) then accept;
  reject;
};

filter to_kernel {
  case source {
    RTS_STATIC: reject;
    RTS_BABEL: {
      krt_prefsrc = HORTUS_OWNIP;
      krt_metric = 128;
      accept;
    }
    RTS_DEVICE: {
      krt_metric = 64;
      accept;
    }
    else: reject;
  }
};

protocol kernel {
  scan time 20;
  metric 0;
  ipv6 {
    preference 100;
    import none;
    export filter to_kernel;
  };
};

protocol babel {
  interface "wg-*" {
    type tunnel;
    hello interval 1s;
    update interval 2s;
    rtt cost 192;
    rtt max 300ms;
    rtt decay 60;
    check link no;
    extended next hop yes;
  };
  ipv6 {
    import where in_hortus();
    export filter to_hortus;
  };
};

interface 的参数可以参考6进行优化。

在我的六台机器全联通状态下,预期的路由是:

> sudo birdc show babel route
BIRD 3.0.1 ready.
babel1:
Prefix                   Nexthop                   Interface Metric F Seqno Expires
fdcc::6/128              fe80::216:3eff:fe0f:37d8  wg-hastur    199       4  41.909
fdcc::6/128              fe80::216:3eff:fe4f:e85f  wg-eihort    196       4  42.489
fdcc::6/128              fe80::216:3eff:fe15:ec52  wg-abhoth    195       4  55.647
fdcc::6/128              fe80::216:3eff:fe6c:5a57  wg-azasos     96 *     4  47.541
fdcc::6/128              fe80::216:3eff:fe41:49c6  wg-yidhra    200       4  54.505
# 手动分隔
fdcc::3/128              fe80::216:3eff:fe0f:37d8  wg-hastur    199 +   185  41.909
fdcc::3/128              fe80::216:3eff:fe4f:e85f  wg-eihort     99 *   185  42.489
fdcc::3/128              fe80::216:3eff:fe15:ec52  wg-abhoth    192 +   185  55.647
fdcc::3/128              fe80::216:3eff:fe6c:5a57  wg-azasos    193     185  47.541
fdcc::3/128              fe80::216:3eff:fe41:49c6  wg-yidhra    202     185  54.505

fdcc::1/128              fe80::216:3eff:fe0f:37d8  wg-hastur    103 *   203  46.935
fdcc::1/128              fe80::216:3eff:fe4f:e85f  wg-eihort    195 +   203  47.036
fdcc::1/128              fe80::216:3eff:fe15:ec52  wg-abhoth    288     203  55.647
fdcc::1/128              fe80::216:3eff:fe6c:5a57  wg-azasos    194 +   203  47.541
fdcc::1/128              fe80::216:3eff:fe41:49c6  wg-yidhra    200 +   203  54.505

fdcc::4/128              fe80::216:3eff:fe0f:37d8  wg-hastur    199     110  41.909
fdcc::4/128              fe80::216:3eff:fe4f:e85f  wg-eihort    195     110  42.489
fdcc::4/128              fe80::216:3eff:fe15:ec52  wg-abhoth    192     110  55.647
fdcc::4/128              fe80::216:3eff:fe6c:5a57  wg-azasos    193     110  47.541
fdcc::4/128              fe80::216:3eff:fe41:49c6  wg-yidhra    104 *   110  54.505

fdcc::5/128              fe80::216:3eff:fe0f:37d8  wg-hastur    199     463  41.909
fdcc::5/128              fe80::216:3eff:fe4f:e85f  wg-eihort    195     464  45.468
fdcc::5/128              fe80::216:3eff:fe15:ec52  wg-abhoth     96 *   464  55.647
fdcc::5/128              fe80::216:3eff:fe6c:5a57  wg-azasos    192     464  47.541
fdcc::5/128              fe80::216:3eff:fe41:49c6  wg-yidhra    200     464  54.505

F为星号代表已选择的路由。如果有哪个peer断开了,首先metric会上涨,到达阈值后自动切换到metric较小的路由(via other interface/peer)。

bird的调试比babeld舒适太多,如果设备性能不是特别寒酸建议还是跑bird。

"

为什么我依然用原装的内核wireguard而不是基于IPSec的 ranet 或者同时支持 IPSec 和 Wireguard 的 rait 呢?

"

因为

  • 没有合规性要求
  • 没有非常高的安全要求

所以不想用比较古老而且性能稍微弱点的IPSec;

因为不喜欢go和它不够专一所以不想选择rait(主要是它们两个文档对于不熟悉IPSec的人来说太稀缺了)。

"

为什么不用基于WireGuard协议的有 ACL 和 udp打洞 等更多功能的 tailscale / zerotier 呢?

"
  • ts 管得太多,比如magicdns,炸的时候影响也更大
  • ts 控制平面不够去中心化
  • ts 两点之间直接连接断开后并不能自动寻路用临近节点中转,而是必须用DERP,我不太喜欢
  • 没办法让wg流量走隧道了
  • 我所 suffering 的 nat 全都是 Independent mapping, Port depended filtering 所以没有打洞成功的可能,辜负了ts的feature
  • 我的小内网没有ACL的需求
  • 不好和 bird 集成以便未来的路由拓展
  • zerotier 乱发arp
  • go

其它类似的现成组网工具大多同此理。

network-topology-v1

generated by nix-topology, definition

  1. 在 WireGuard 构建的 Overlay Network 上跑 babel 路由协议

  2. Run Babeld over Wireguard 2

  3. 非NixOS的话也可以使用配置生成,见 https://github.com/vx3r/wg-gen-web

  4. Wireguard performance tuning

  5. https://dn42.dev/howto/Bird2#getting-routes-installed-to-the-kernel-automatically

  6. babel rtt options

All Systems Operational
©2018-2025 Secirian | CC BY-SA 4.0