From 80006a67394dec63fdcdeb56b79034e284555674 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:13:52 +0000 Subject: [PATCH] feat: implement HA router multi-wan, vrrp, and conntrack modules This commit implements the requirements of GROUP 2 for the High Availability (HA) NixOS router plan (`0001-ha-router-setup-plan.md`): 1. **VRRP (`vrrp/default.nix`)**: Configures `Keepalived` VRRP instances for the local LAN/VLANs. It defines `notifyMaster` and `notifyBackup` scripts to handle dynamic WAN MAC spoofing and interface administrative state control on failover. 2. **Multi-WAN (`multi-wan/default.nix`)**: Implements a `multi-wan-healthcheck` systemd service that monitors external connectivity via ICMP. It dynamically adjusts the default route metric to shift traffic away from broken uplinks while preserving the gateway discovery capabilities for failback. 3. **Conntrack (`conntrack/default.nix`)**: Configures `conntrackd` FTFW (Failover/Fault-Tolerant Firewall) state synchronization securely over a dedicated `vlan40` interface, using multicast. 4. **Base Refactor (`masthead/default.nix`)**: Defines necessary `cfg` options for the MAC, VRRP Virtual IPs, and health check IPs. It properly provisions "real" dedicated IPs for all interfaces based on the specific node's `role` (primary/backup) to avoid IP conflicts and allow VRRP daemons to communicate. Co-authored-by: ProjectInitiative <6314611+ProjectInitiative@users.noreply.github.com> --- .../hosts/masthead/conntrack/default.nix | 71 ++++++++++++++++ modules/nixos/hosts/masthead/default.nix | 72 ++++++++++++++++ .../hosts/masthead/multi-wan/default.nix | 53 ++++++++++++ modules/nixos/hosts/masthead/vrrp/default.nix | 83 +++++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 modules/nixos/hosts/masthead/conntrack/default.nix create mode 100644 modules/nixos/hosts/masthead/multi-wan/default.nix create mode 100644 modules/nixos/hosts/masthead/vrrp/default.nix diff --git a/modules/nixos/hosts/masthead/conntrack/default.nix b/modules/nixos/hosts/masthead/conntrack/default.nix new file mode 100644 index 0000000..594f045 --- /dev/null +++ b/modules/nixos/hosts/masthead/conntrack/default.nix @@ -0,0 +1,71 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: + +with lib; +let + cfg = config.${namespace}.hosts.masthead; +in +{ + config = mkIf cfg.enable { + environment.etc."conntrackd/conntrackd.conf".text = '' + Sync { + Mode FTFW { + ResendQueueSize 131072 + CommitTimeout 180 + PurgeTimeout 5 + ACKWindowSize 300 + DisableExternalCache Off + } + + Multicast { + IPv4_address 225.0.0.50 + IPv4_interface vlan40 + Port 3780 + Group 3780 + } + } + + General { + HashSize 32768 + HashLimit 131072 + Syslog on + LockFile /var/lock/conntrack.lock + UNIX { + Path /var/run/conntrackd.ctl + Backlog 20 + } + NetlinkBufferSize 2097152 + NetlinkBufferSizeMaxGrowth 8388608 + Filter From Userspace { + Protocol Accept { + TCP + SCTP + DCCP + } + Address Ignore { + IPv4_address 127.0.0.1 + } + } + } + ''; + + systemd.services.conntrackd = { + description = "Connection tracking state synchronization daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ + pkgs.conntrack-tools + pkgs.iproute2 + ]; + serviceConfig = { + ExecStart = "${pkgs.conntrack-tools}/bin/conntrackd -C /etc/conntrackd/conntrackd.conf"; + Restart = "always"; + }; + }; + }; +} diff --git a/modules/nixos/hosts/masthead/default.nix b/modules/nixos/hosts/masthead/default.nix index e84b42a..4a3c18b 100644 --- a/modules/nixos/hosts/masthead/default.nix +++ b/modules/nixos/hosts/masthead/default.nix @@ -13,12 +13,26 @@ let cfg = config.${namespace}.hosts.masthead; in { + imports = [ + ./vrrp + ./multi-wan + ./conntrack + ]; + options.${namespace}.hosts.masthead = with types; { enable = mkBoolOpt false "Whether or not to enable the masthead router base config."; role = mkOpt (types.enum [ "primary" "backup" ]) "primary" "The role of the masthead router."; + wanMac = mkOpt types.str "00:00:00:00:00:00" "MAC address for WAN spoofing."; + wanVip = mkOpt types.str "203.0.113.100" "Virtual IP for WAN interface."; + lanVip = mkOpt types.str "172.16.1.1" "Virtual IP for LAN interface."; + vlan10Vip = mkOpt types.str "192.168.10.1" "Virtual IP for VLAN 10."; + vlan21Vip = mkOpt types.str "192.168.21.1" "Virtual IP for VLAN 21."; + vlan22Vip = mkOpt types.str "192.168.22.1" "Virtual IP for VLAN 22."; + vlan30Vip = mkOpt types.str "192.168.30.1" "Virtual IP for VLAN 30."; + healthCheckIp = mkOpt types.str "8.8.8.8" "IP address for Multi-WAN health check."; }; config = mkIf cfg.enable { @@ -29,6 +43,60 @@ in }; }; + networking.interfaces.lan0 = { + ipv4.addresses = [ + { + address = if cfg.role == "primary" then "172.16.1.2" else "172.16.1.3"; + prefixLength = 24; + } + ]; + }; + + networking.interfaces.vlan10 = { + ipv4.addresses = [ + { + address = if cfg.role == "primary" then "192.168.10.2" else "192.168.10.3"; + prefixLength = 24; + } + ]; + }; + + networking.interfaces.vlan21 = { + ipv4.addresses = [ + { + address = if cfg.role == "primary" then "192.168.21.2" else "192.168.21.3"; + prefixLength = 24; + } + ]; + }; + + networking.interfaces.vlan22 = { + ipv4.addresses = [ + { + address = if cfg.role == "primary" then "192.168.22.2" else "192.168.22.3"; + prefixLength = 24; + } + ]; + }; + + networking.interfaces.vlan30 = { + ipv4.addresses = [ + { + address = if cfg.role == "primary" then "192.168.30.2" else "192.168.30.3"; + prefixLength = 24; + } + ]; + }; + + networking.interfaces.vlan40 = { + ipv4.addresses = [ + { + address = if cfg.role == "primary" then "169.254.255.1" else "169.254.255.2"; + prefixLength = 24; + } + ]; + }; + networking.vlans = { vlan1 = { id = 1; @@ -50,6 +118,10 @@ in id = 30; interface = "lan0"; }; + vlan40 = { + id = 40; + interface = "lan0"; + }; }; # Configure DNS resolvers diff --git a/modules/nixos/hosts/masthead/multi-wan/default.nix b/modules/nixos/hosts/masthead/multi-wan/default.nix new file mode 100644 index 0000000..45556f1 --- /dev/null +++ b/modules/nixos/hosts/masthead/multi-wan/default.nix @@ -0,0 +1,53 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: + +with lib; +let + cfg = config.${namespace}.hosts.masthead; +in +{ + config = mkIf cfg.enable { + systemd.services.multi-wan-healthcheck = { + description = "Multi-WAN Health Check and Failover Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + path = [ + pkgs.iputils + pkgs.iproute2 + pkgs.gawk + ]; + + script = '' + set -euo pipefail + + while true; do + # Dynamically resolve gateway via wan0 + GATEWAY=$(ip -4 route show default dev wan0 2>/dev/null | awk '{print $3}' | head -n 1 || true) + + if [ -n "$GATEWAY" ]; then + if ping -I wan0 -c 1 -W 2 "${cfg.healthCheckIp}" > /dev/null 2>&1; then + # Ping succeeded. Ensure default route exists with appropriate metric + ip route replace default via "$GATEWAY" dev wan0 metric 10 + else + # Ping failed. Set metric to 1000 so it acts as disabled but gateway remains discoverable + ip route replace default via "$GATEWAY" dev wan0 metric 1000 || true + fi + fi + + sleep 10 + done + ''; + + serviceConfig = { + Restart = "always"; + RestartSec = "5s"; + }; + }; + }; +} diff --git a/modules/nixos/hosts/masthead/vrrp/default.nix b/modules/nixos/hosts/masthead/vrrp/default.nix new file mode 100644 index 0000000..d04b187 --- /dev/null +++ b/modules/nixos/hosts/masthead/vrrp/default.nix @@ -0,0 +1,83 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: + +with lib; +let + cfg = config.${namespace}.hosts.masthead; + priority = if cfg.role == "primary" then 255 else 100; +in +{ + config = mkIf cfg.enable { + networking.vrrp.enable = true; + + networking.vrrp.vrrpInstances = { + lan0 = { + virtualRouterId = 20; + priority = priority; + interface = "lan0"; + virtualIPs = [ + { + address = cfg.lanVip; + prefixLength = 24; + } + ]; + notifyMaster = "${pkgs.writeShellScript "notify-master-lan0" '' + ${pkgs.iproute2}/bin/ip link set dev wan0 address ${cfg.wanMac} + ${pkgs.iproute2}/bin/ip link set dev wan0 up + ''}"; + notifyBackup = "${pkgs.writeShellScript "notify-backup-lan0" '' + ${pkgs.iproute2}/bin/ip link set dev wan0 down + ''}"; + }; + vlan10 = { + virtualRouterId = 30; + priority = priority; + interface = "vlan10"; + virtualIPs = [ + { + address = cfg.vlan10Vip; + prefixLength = 24; + } + ]; + }; + vlan21 = { + virtualRouterId = 40; + priority = priority; + interface = "vlan21"; + virtualIPs = [ + { + address = cfg.vlan21Vip; + prefixLength = 24; + } + ]; + }; + vlan22 = { + virtualRouterId = 50; + priority = priority; + interface = "vlan22"; + virtualIPs = [ + { + address = cfg.vlan22Vip; + prefixLength = 24; + } + ]; + }; + vlan30 = { + virtualRouterId = 60; + priority = priority; + interface = "vlan30"; + virtualIPs = [ + { + address = cfg.vlan30Vip; + prefixLength = 24; + } + ]; + }; + }; + }; +}