wireguard: add support for external peers
This adds support for external peers via the instance option `roles.controller.<name>.settings.externalPeers = ["external1"]`. External peers are peers which are not associated wth a machine inside the clan, for example a mobile phone or a device that cannot be managed via clan for some reason.
This commit is contained in:
@@ -106,8 +106,30 @@ let
|
||||
in
|
||||
"${peerIP} ${peerName}.${domain}"
|
||||
) roles.peer.machines;
|
||||
|
||||
# External peers
|
||||
externalPeerHosts = lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
ctrlName: _ctrlValue:
|
||||
lib.map (
|
||||
peer:
|
||||
let
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/suffix/value"
|
||||
);
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
in
|
||||
"${peerIP} ${peer}.${domain}"
|
||||
) (roles.controller.machines.${ctrlName}.settings.externalPeers)
|
||||
) roles.controller.machines
|
||||
);
|
||||
in
|
||||
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts);
|
||||
builtins.concatStringsSep "\n" (controllerHosts ++ peerHosts ++ externalPeerHosts);
|
||||
};
|
||||
|
||||
# Shared interface options
|
||||
@@ -268,12 +290,33 @@ in
|
||||
{
|
||||
imports = [ sharedInterface ];
|
||||
|
||||
options.endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "vpn.clan.lol";
|
||||
description = ''
|
||||
Endpoint where the controller can be reached
|
||||
'';
|
||||
options = {
|
||||
endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "vpn.clan.lol";
|
||||
description = ''
|
||||
Endpoint where the controller can be reached
|
||||
'';
|
||||
};
|
||||
externalPeers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"moms-phone"
|
||||
"daddies-laptop"
|
||||
];
|
||||
description = ''
|
||||
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.
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
@@ -310,23 +353,54 @@ in
|
||||
;
|
||||
})
|
||||
];
|
||||
# Network allocation generator for this controller
|
||||
clan.core.vars.generators."wireguard-network-${instanceName}" = {
|
||||
files.prefix.secret = false;
|
||||
# Network prefix allocation generator for this controller
|
||||
clan.core.vars.generators = {
|
||||
"wireguard-network-${instanceName}" = {
|
||||
files.prefix.secret = false;
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
];
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
];
|
||||
|
||||
# Invalidate on network or hostname changes
|
||||
validation.hostname = machine.name;
|
||||
# Invalidate on network or hostname changes
|
||||
validation.hostname = machine.name;
|
||||
|
||||
script = ''
|
||||
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" controller "${machine.name}"
|
||||
'';
|
||||
};
|
||||
script = ''
|
||||
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" controller "${machine.name}"
|
||||
'';
|
||||
};
|
||||
}
|
||||
# For external peers, generate: suffix, public key, private key
|
||||
// lib.genAttrs' settings.externalPeers (peer: {
|
||||
name = "wireguard-network-${instanceName}-external-peer-${peer}";
|
||||
value = {
|
||||
files.suffix.secret = false;
|
||||
files.publickey.secret = false;
|
||||
files.privatekey.secret = true;
|
||||
files.privatekey.deploy = false;
|
||||
|
||||
# Enable ip forwarding, so wireguard peers can reach eachother
|
||||
# The external peers keys are not deployed and are globally unique.
|
||||
# Even if an external peer is connected to more than one controller,
|
||||
# its private keys will remain the same.
|
||||
share = true;
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
wireguard-tools
|
||||
];
|
||||
|
||||
# Invalidate on hostname changes
|
||||
validation.hostname = peer;
|
||||
|
||||
script = ''
|
||||
${pkgs.python3}/bin/python3 ${./ipv6_allocator.py} "$out" "${instanceName}" peer "${peer}"
|
||||
wg genkey > $out/privatekey
|
||||
wg pubkey < $out/privatekey > $out/publickey
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
# Enable ip forwarding, so wireguard peers can reach each other
|
||||
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
|
||||
|
||||
networking.firewall.allowedUDPPorts = [ settings.port ];
|
||||
@@ -344,19 +418,45 @@ in
|
||||
config.clan.core.vars.generators."wireguard-keys-${instanceName}".files."privatekey".path;
|
||||
|
||||
# Connect to all peers and other controllers
|
||||
peers = lib.mapAttrsToList (
|
||||
name: value:
|
||||
if allPeers ? ${name} then
|
||||
# For peers: they now have our entire /56 subnet
|
||||
{
|
||||
peers =
|
||||
# Peers configuration
|
||||
(lib.mapAttrsToList (name: _value: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow the peer's /96 range in ALL controller subnets
|
||||
allowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _:
|
||||
let
|
||||
controllerPrefix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${ctrlName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/suffix/value"
|
||||
);
|
||||
in
|
||||
"${controllerPrefix}:${peerSuffix}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}) allPeers)
|
||||
++
|
||||
# External peers configuration
|
||||
(map (peer: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-keys-${instanceName}/publickey/value"
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow the peer's /96 range in ALL controller subnets
|
||||
# Allow the external peer's /96 range in ALL controller subnets
|
||||
allowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _:
|
||||
let
|
||||
@@ -366,17 +466,18 @@ in
|
||||
);
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/suffix/value"
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/suffix/value"
|
||||
);
|
||||
in
|
||||
"${controllerPrefix}:${peerSuffix}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
# No endpoint for external peers, they initiate the connection
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
else
|
||||
# For other controllers: use their /56 subnet
|
||||
{
|
||||
}) settings.externalPeers)
|
||||
++
|
||||
# Other controllers configuration
|
||||
(lib.mapAttrsToList (name: value: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
@@ -395,8 +496,7 @@ in
|
||||
|
||||
endpoint = "${value.settings.endpoint}:${toString value.settings.port}";
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
) (allPeers // allOtherControllers);
|
||||
}) allOtherControllers);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
@@ -10,7 +11,23 @@ let
|
||||
"peer1"
|
||||
"peer2"
|
||||
"peer3"
|
||||
# external machine for external peer testing
|
||||
"external1"
|
||||
];
|
||||
|
||||
controllerPrefix =
|
||||
controllerName:
|
||||
builtins.readFile (
|
||||
config.clan.directory
|
||||
+ "/vars/per-machine/${controllerName}/wireguard-network-wg-test-one/prefix/value"
|
||||
);
|
||||
# external peer suffixes are stored via shared vars
|
||||
externalPeerSuffix =
|
||||
externalName:
|
||||
builtins.readFile (
|
||||
config.clan.directory
|
||||
+ "/vars/shared/wireguard-network-wg-test-one-external-peer-${externalName}/suffix/value"
|
||||
);
|
||||
in
|
||||
{
|
||||
name = "wireguard";
|
||||
@@ -47,6 +64,8 @@ in
|
||||
|
||||
roles.controller.machines."controller1".settings = {
|
||||
endpoint = "192.168.1.1";
|
||||
# add an external peer to controller1 only
|
||||
externalPeers = [ "external1" ];
|
||||
};
|
||||
|
||||
roles.controller.machines."controller2".settings = {
|
||||
@@ -77,6 +96,48 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
nodes.external1 =
|
||||
let
|
||||
controller1Prefix = controllerPrefix "controller1";
|
||||
external1Suffix = externalPeerSuffix "external1";
|
||||
in
|
||||
{
|
||||
networking.extraHosts = ''
|
||||
${controller1Prefix}::1 controller1.wg-test-one
|
||||
'';
|
||||
networking.wireguard.interfaces."wg0" = {
|
||||
|
||||
ips = [ "${controller1Prefix + ":" + external1Suffix}/56" ];
|
||||
|
||||
privateKeyFile =
|
||||
builtins.toFile "wg-priv-key"
|
||||
# This needs to be updated whenever update-vars was executed
|
||||
# Get the value from the generated vars via this command:
|
||||
# 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=";
|
||||
|
||||
peers = [
|
||||
{
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.directory + "/vars/per-machine/controller1/wireguard-keys-wg-test-one/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# Allow each controller's /56 subnet
|
||||
allowedIPs = [
|
||||
# "${controller1Prefix}::/56"
|
||||
"::/0"
|
||||
];
|
||||
|
||||
endpoint = "controller1:51820";
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
@@ -94,9 +155,13 @@ in
|
||||
print("="*60)
|
||||
|
||||
for m1 in machines:
|
||||
# ping all other machines
|
||||
for m2 in 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")
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user