WG上的Babel动态路由title
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,一切可以优雅而高效地处理,不用手动创建和定义每个接口网络。

    网络重构的全部更改

    要做的是在每个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 字节)3

    上文的 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节点锚定本机的地址,防止路由出现多条到本地地址的路径(不同device)。虽然说不管也没事,因为通常都是按字典序选择一条,export也只会导出一条。

    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 的参数可以参考4进行优化。

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

    > 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 呢?

    "
    • 它管得太多,比如magicdns,炸的时候影响也更大
    • 控制平面不够去中心化
    • 没办法让wg流量走隧道了
    • 我所suffering的nat全都是 Independent mapping, Port depended filtering 所以没有打洞成功的可能,辜负了它的feature
    • 我的小内网没有ACL的需求
    • 不好和bird集成以便未来的路由拓展
    • go

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

    network-topology-v1

    generated by nix-topology, definition

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

    2. Run Babeld over Wireguard 2

    3. Wireguard performance tuning

    4. babel rtt options