diff --git a/clanServices/wireguard/default.nix b/clanServices/wireguard/default.nix index 093cd8f92..c686b36a4 100644 --- a/clanServices/wireguard/default.nix +++ b/clanServices/wireguard/default.nix @@ -309,10 +309,12 @@ in List of external peer names that are not part of the clan. For ever entry here, a key pair for an external device will be generated. - Each external peer must be configured on exactly ONE controller. This key pair can then then be displayed via `clan vars get` and inserted into an external device, like a phone or laptop. - The names in this list must not collide with machine names in the clan. + Each external peer can connect to the mesh through one or more controllers. + To connect to multiple controllers, set `roles.controller.settings.externalPeers`. + + The external peer names must not collide with machine names in the clan. The machines which are part of the clan will be able to resolve the external peers via their host names, but not vice versa. External peers can still reach machines from within the clan via their IPv6 addresses. ''; @@ -340,6 +342,10 @@ in let allOtherControllers = lib.filterAttrs (name: _v: name != machine.name) roles.controller.machines; allPeers = roles.peer.machines; + # Collect all external peers from all controllers + allExternalPeers = lib.unique ( + lib.flatten (lib.mapAttrsToList (_: ctrl: ctrl.settings.externalPeers) roles.controller.machines) + ); in { imports = [ @@ -447,7 +453,7 @@ in persistentKeepalive = 25; }) allPeers) ++ - # External peers configuration + # External peers configuration - includes all external peers from all controllers (map (peer: { publicKey = ( builtins.readFile ( @@ -474,7 +480,7 @@ in # No endpoint for external peers, they initiate the connection persistentKeepalive = 25; - }) settings.externalPeers) + }) allExternalPeers) ++ # Other controllers configuration (lib.mapAttrsToList (name: value: { diff --git a/clanServices/wireguard/tests/vm/default.nix b/clanServices/wireguard/tests/vm/default.nix index 54bf69800..9325dbc14 100644 --- a/clanServices/wireguard/tests/vm/default.nix +++ b/clanServices/wireguard/tests/vm/default.nix @@ -21,6 +21,11 @@ let config.clan.directory + "/vars/per-machine/${controllerName}/wireguard-network-wg-test-one/prefix/value" ); + peerSuffix = + peerName: + builtins.readFile ( + config.clan.directory + "/vars/per-machine/${peerName}/wireguard-network-wg-test-one/suffix/value" + ); # external peer suffixes are stored via shared vars externalPeerSuffix = externalName: @@ -70,6 +75,8 @@ in roles.controller.machines."controller2".settings = { endpoint = "192.168.1.2"; + # add the same external peer to controller2 to test multi-controller connection + externalPeers = [ "external1" ]; }; roles.peer.machines = { @@ -99,15 +106,21 @@ in nodes.external1 = let controller1Prefix = controllerPrefix "controller1"; + controller2Prefix = controllerPrefix "controller2"; external1Suffix = externalPeerSuffix "external1"; in { networking.extraHosts = '' ${controller1Prefix}::1 controller1.wg-test-one + ${controller2Prefix}::1 controller2.wg-test-one ''; networking.wireguard.interfaces."wg0" = { - ips = [ "${controller1Prefix + ":" + external1Suffix}/56" ]; + # Multiple IPs, one in each controller's subnet + ips = [ + "${controller1Prefix + ":" + external1Suffix}/56" + "${controller2Prefix + ":" + external1Suffix}/56" + ]; privateKeyFile = builtins.toFile "wg-priv-key" @@ -116,7 +129,9 @@ in # echo "AGE-SECRET-KEY-1PL0M9CWRCG3PZ9DXRTTLMCVD57U6JDFE8K7DNVQ35F4JENZ6G3MQ0RQLRV" | SOPS_AGE_KEY_FILE=/dev/stdin nix run nixpkgs#sops decrypt clanServices/wireguard/tests/vm/vars/shared/wireguard-network-wg-test-one-external-peer-external1/privatekey/secret "wO8dl3JWgV5J+0D/2UDcLsxTD25IWTvd5ed6vv2Nikk="; + # Connect to both controllers peers = [ + # Controller 1 { publicKey = ( builtins.readFile ( @@ -124,14 +139,26 @@ in ) ); - # Allow each controller's /56 subnet - allowedIPs = [ - # "${controller1Prefix}::/56" - "::/0" - ]; + # Allow controller1's /56 subnet + allowedIPs = [ "${controller1Prefix}::/56" ]; endpoint = "controller1:51820"; + persistentKeepalive = 25; + } + # Controller 2 + { + publicKey = ( + builtins.readFile ( + config.clan.directory + "/vars/per-machine/controller2/wireguard-keys-wg-test-one/publickey/value" + ) + ); + + # Allow controller2's /56 subnet + allowedIPs = [ "${controller2Prefix}::/56" ]; + + endpoint = "controller2:51820"; + persistentKeepalive = 25; } ]; @@ -141,8 +168,8 @@ in testScript = '' start_all() - # Show all addresses - machines = [peer1, peer2, peer3, controller1, controller2] + # Start network on all machines including external1 + machines = [peer1, peer2, peer3, controller1, controller2, external1] for m in machines: m.systemctl("start network-online.target") @@ -154,14 +181,35 @@ in print("STARTING PING TESTS") print("="*60) - for m1 in machines: - # ping all other machines - for m2 in machines: + # Test mesh connectivity between regular clan machines + clan_machines = [peer1, peer2, peer3, controller1, controller2] + for m1 in clan_machines: + for m2 in clan_machines: if m1 != m2: print(f"\n--- Pinging from {m1.name} to {m2.name}.wg-test-one ---") m1.wait_until_succeeds(f"ping -c1 {m2.name}.wg-test-one >&2") - # ping external peer from all other peers and controllers - print(f"\n--- Pinging from {m1.name} to external1.wg-test-one ---") - m1.wait_until_succeeds("ping -c1 external1.wg-test-one >&2") + + # Test that external peer can reach both controllers (multi-controller connection) + print("\n--- Testing external1 -> controller1 (direct connection) ---") + external1.wait_until_succeeds("ping -c1 controller1.wg-test-one >&2") + + print("\n--- Testing external1 -> controller2 (direct connection) ---") + external1.wait_until_succeeds("ping -c1 controller2.wg-test-one >&2") + + # Test that all clan machines can reach the external peer + for m in clan_machines: + print(f"\n--- Pinging from {m.name} to external1.wg-test-one ---") + m.wait_until_succeeds("ping -c1 external1.wg-test-one >&2") + + # Test that external peer can reach a regular peer via controller1 + print("\n--- Testing external1 -> peer1 (via controller1) ---") + external1.wait_until_succeeds("ping -c1 ${controllerPrefix "controller1"}:${peerSuffix "peer1"} >&2") + + # Test controller failover + print("\n--- Shutting down controller1 ---") + controller1.shutdown() + print("\n--- Testing external1 -> peer1 (via controller2 after controller1 shutdown) ---") + external1.wait_until_succeeds("ping -c1 ${controllerPrefix "controller2"}:${peerSuffix "peer1"} >&2") + ''; }