wireguard: optional internet access for external peers
Add an option to allow external peers to route all their internet traffic through a controller. Also add options to enable ipv4 and assign v4 addresses manually as this is needed for proper internet access for some regions
This commit is contained in:
@@ -133,6 +133,75 @@ graph TB
|
||||
|
||||
### Advanced Options
|
||||
|
||||
#### External Peers
|
||||
|
||||
External peers are devices outside of your clan (like phones, laptops, etc.) that can connect to the mesh network through controllers. Each external peer gets its own keypair and can be configured with specific options.
|
||||
|
||||
##### IPv6-only external peers
|
||||
|
||||
```nix
|
||||
{
|
||||
instances = {
|
||||
wireguard = {
|
||||
module.name = "wireguard";
|
||||
module.input = "clan-core";
|
||||
roles.controller.machines.server1.settings = {
|
||||
endpoint = "vpn.example.com";
|
||||
# Define external peers with configuration options
|
||||
externalPeers = {
|
||||
dave = {
|
||||
# No internet access - can only reach clan mesh
|
||||
allowInternetAccess = false;
|
||||
};
|
||||
moms-phone = {
|
||||
# Internet access enabled - IPv6 traffic routed through VPN
|
||||
allowInternetAccess = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer.machines.laptop1 = {};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### IPv4 support for external peers
|
||||
|
||||
If you need IPv4 internet access for external peers, you can enable IPv4 on the controller and assign IPv4 addresses to external peers:
|
||||
|
||||
```nix
|
||||
{
|
||||
instances = {
|
||||
wireguard = {
|
||||
module.name = "wireguard";
|
||||
module.input = "clan-core";
|
||||
roles.controller.machines.server1.settings = {
|
||||
endpoint = "vpn.example.com";
|
||||
# Enable IPv4 with controller's address
|
||||
ipv4.enable = true;
|
||||
ipv4.address = "10.42.1.1/24";
|
||||
externalPeers = {
|
||||
dave = {
|
||||
# No internet access - can only reach clan mesh
|
||||
allowInternetAccess = false;
|
||||
ipv4.address = "10.42.1.50/32";
|
||||
};
|
||||
moms-phone = {
|
||||
# Internet access enabled - IPv4 and IPv6 traffic routed through VPN
|
||||
allowInternetAccess = true;
|
||||
ipv4.address = "10.42.1.51/32";
|
||||
};
|
||||
};
|
||||
};
|
||||
roles.peer.machines.laptop1 = {};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** IPv4 addresses for external peers are only used for internet access through the controller, not for mesh communication (which uses IPv6).
|
||||
|
||||
External peers can connect to multiple controllers by adding the same peer name to multiple controllers' `externalPeers` configuration.
|
||||
|
||||
### Automatic Hostname Resolution
|
||||
|
||||
|
||||
@@ -105,14 +105,14 @@ let
|
||||
peerIP = controllerPrefix + ":" + peerSuffix;
|
||||
in
|
||||
"${peerIP} ${peerName}.${domain}"
|
||||
) roles.peer.machines;
|
||||
) roles.peer.machines or { };
|
||||
|
||||
# External peers
|
||||
externalPeerHosts = lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
ctrlName: _ctrlValue:
|
||||
lib.map (
|
||||
peer:
|
||||
lib.mapAttrsToList (
|
||||
peer: _peerSettings:
|
||||
let
|
||||
peerSuffix = builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
@@ -298,21 +298,75 @@ in
|
||||
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.
|
||||
ipv4 = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable IPv4 support for external peers on this controller.
|
||||
When enabled, the controller will have an IPv4 address and can route IPv4 traffic.
|
||||
|
||||
For ever entry here, a key pair for an external device will be generated.
|
||||
This key pair can then then be displayed via `clan vars get` and inserted into an external device, like a phone or laptop.
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "10.42.1.1/24";
|
||||
description = ''
|
||||
IPv4 address for this controller in CIDR notation.
|
||||
External peers with IPv4 addresses must be within the same subnet.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
};
|
||||
externalPeers = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
allowInternetAccess = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to allow this external peer to access the internet through the controller.
|
||||
When enabled, the controller will route internet traffic for this peer.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
ipv4.address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.42.1.50/32";
|
||||
description = ''
|
||||
IPv4 address for this external peer in CIDR notation.
|
||||
The peer must be within the controller's IPv4 subnet.
|
||||
Only used when the controller has IPv4 enabled.
|
||||
|
||||
IPv4 is only used for internet access, not for mesh communication (which uses IPv6).
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
dave = {
|
||||
allowInternetAccess = false;
|
||||
};
|
||||
"moms-phone" = {
|
||||
allowInternetAccess = true;
|
||||
ipv4.address = "10.42.1.51/32";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
External peers that are not part of the clan.
|
||||
|
||||
For every entry here, a key pair for an external device will be generated.
|
||||
This key pair can then be displayed via `clan vars get` and inserted into an external device, like a phone or laptop.
|
||||
|
||||
Each external peer can connect to the mesh through one or more controllers.
|
||||
To connect to multiple controllers, set `roles.controller.settings.externalPeers`.
|
||||
To connect to multiple controllers, add the same peer name to multiple controllers' `externalPeers`, or simply set 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.
|
||||
@@ -341,11 +395,37 @@ in
|
||||
}:
|
||||
let
|
||||
allOtherControllers = lib.filterAttrs (name: _v: name != machine.name) roles.controller.machines;
|
||||
allPeers = roles.peer.machines;
|
||||
allPeers = roles.peer.machines or { };
|
||||
# Collect all external peers from all controllers
|
||||
allExternalPeers = lib.unique (
|
||||
lib.flatten (lib.mapAttrsToList (_: ctrl: ctrl.settings.externalPeers) roles.controller.machines)
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (_: ctrl: lib.attrNames ctrl.settings.externalPeers) roles.controller.machines
|
||||
)
|
||||
);
|
||||
|
||||
controllerPrefix =
|
||||
controllerName:
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${controllerName}/wireguard-network-${instanceName}/prefix/value"
|
||||
);
|
||||
|
||||
peerSuffix =
|
||||
peerName:
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${peerName}/wireguard-network-${instanceName}/suffix/value"
|
||||
);
|
||||
|
||||
externalPeerSuffix =
|
||||
externalName:
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${externalName}/suffix/value"
|
||||
);
|
||||
|
||||
thisControllerPrefix =
|
||||
config.clan.core.vars.generators."wireguard-network-${instanceName}".files.prefix.value;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -377,7 +457,7 @@ in
|
||||
};
|
||||
}
|
||||
# For external peers, generate: suffix, public key, private key
|
||||
// lib.genAttrs' settings.externalPeers (peer: {
|
||||
// lib.genAttrs' (lib.attrNames settings.externalPeers) (peer: {
|
||||
name = "wireguard-network-${instanceName}-external-peer-${peer}";
|
||||
value = {
|
||||
files.suffix.secret = false;
|
||||
@@ -407,18 +487,57 @@ in
|
||||
});
|
||||
|
||||
# Enable ip forwarding, so wireguard peers can reach each other
|
||||
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv6.conf.all.forwarding" = 1;
|
||||
}
|
||||
// lib.optionalAttrs settings.ipv4.enable {
|
||||
"net.ipv4.conf.all.forwarding" = 1;
|
||||
};
|
||||
|
||||
networking.firewall.allowedUDPPorts = [ settings.port ];
|
||||
|
||||
networking.firewall.extraCommands =
|
||||
let
|
||||
peersWithInternetAccess = lib.filterAttrs (
|
||||
_: peerConfig: peerConfig.allowInternetAccess
|
||||
) settings.externalPeers;
|
||||
|
||||
peerInfo = lib.mapAttrs (
|
||||
peer: peerConfig:
|
||||
let
|
||||
ipv6Address = "${thisControllerPrefix}:${externalPeerSuffix peer}";
|
||||
ipv4Address =
|
||||
if settings.ipv4.enable && peerConfig.ipv4.address != null then
|
||||
lib.head (lib.splitString "/" peerConfig.ipv4.address)
|
||||
else
|
||||
null;
|
||||
in
|
||||
{
|
||||
inherit ipv6Address ipv4Address;
|
||||
}
|
||||
) peersWithInternetAccess;
|
||||
|
||||
in
|
||||
lib.concatStringsSep "\n" (
|
||||
(lib.mapAttrsToList (_peer: info: ''
|
||||
ip6tables -t nat -A POSTROUTING -s ${info.ipv6Address}/128 ! -o '${instanceName}' -j MASQUERADE
|
||||
'') peerInfo)
|
||||
++ (lib.mapAttrsToList (
|
||||
_peer: info:
|
||||
lib.optionalString (info.ipv4Address != null) ''
|
||||
iptables -t nat -A POSTROUTING -s ${info.ipv4Address} ! -o '${instanceName}' -j MASQUERADE
|
||||
''
|
||||
) peerInfo)
|
||||
);
|
||||
|
||||
# Single wireguard interface
|
||||
networking.wireguard.interfaces."${instanceName}" = {
|
||||
listenPort = settings.port;
|
||||
|
||||
ips = [
|
||||
# Controller uses ::1 in its /56 subnet but with /40 prefix for proper routing
|
||||
"${config.clan.core.vars.generators."wireguard-network-${instanceName}".files.prefix.value}::1/40"
|
||||
];
|
||||
"${thisControllerPrefix}::1/40"
|
||||
]
|
||||
++ lib.optional settings.ipv4.enable settings.ipv4.address;
|
||||
|
||||
privateKeyFile =
|
||||
config.clan.core.vars.generators."wireguard-keys-${instanceName}".files."privatekey".path;
|
||||
@@ -436,51 +555,41 @@ in
|
||||
|
||||
# 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"
|
||||
ctrlName: _: "${controllerPrefix ctrlName}:${peerSuffix name}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}) allPeers)
|
||||
++
|
||||
# External peers configuration - includes all external peers from all controllers
|
||||
(map (peer: {
|
||||
publicKey = (
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/publickey/value"
|
||||
)
|
||||
);
|
||||
(map (
|
||||
peer:
|
||||
let
|
||||
# IPv6 allowed IPs for mesh communication
|
||||
ipv6AllowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _: "${controllerPrefix ctrlName}:${externalPeerSuffix peer}/96"
|
||||
) roles.controller.machines;
|
||||
|
||||
# Allow the external peer's /96 range in ALL controller subnets
|
||||
allowedIPs = lib.mapAttrsToList (
|
||||
ctrlName: _:
|
||||
let
|
||||
controllerPrefix = builtins.readFile (
|
||||
# IPv4 allowed IP (only if this controller manages this peer and has IPv4 enabled)
|
||||
ipv4AllowedIPs = lib.optional (
|
||||
settings.ipv4.enable
|
||||
&& settings.externalPeers ? ${peer}
|
||||
&& settings.externalPeers.${peer}.ipv4.address != null
|
||||
) settings.externalPeers.${peer}.ipv4.address;
|
||||
in
|
||||
{
|
||||
publicKey = (
|
||||
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/shared/wireguard-network-${instanceName}-external-peer-${peer}/suffix/value"
|
||||
);
|
||||
in
|
||||
"${controllerPrefix}:${peerSuffix}/96"
|
||||
) roles.controller.machines;
|
||||
+ "/vars/shared/wireguard-network-${instanceName}-external-peer-${peer}/publickey/value"
|
||||
)
|
||||
);
|
||||
|
||||
# No endpoint for external peers, they initiate the connection
|
||||
persistentKeepalive = 25;
|
||||
}) allExternalPeers)
|
||||
allowedIPs = ipv6AllowedIPs ++ ipv4AllowedIPs;
|
||||
|
||||
persistentKeepalive = 25;
|
||||
}
|
||||
) allExternalPeers)
|
||||
++
|
||||
# Other controllers configuration
|
||||
(lib.mapAttrsToList (name: value: {
|
||||
@@ -491,14 +600,7 @@ in
|
||||
)
|
||||
);
|
||||
|
||||
allowedIPs = [
|
||||
"${
|
||||
builtins.readFile (
|
||||
config.clan.core.settings.directory
|
||||
+ "/vars/per-machine/${name}/wireguard-network-${instanceName}/prefix/value"
|
||||
)
|
||||
}::/56"
|
||||
];
|
||||
allowedIPs = [ "${controllerPrefix name}::/56" ];
|
||||
|
||||
endpoint = "${value.settings.endpoint}:${toString value.settings.port}";
|
||||
persistentKeepalive = 25;
|
||||
@@ -522,7 +624,7 @@ in
|
||||
let
|
||||
isController =
|
||||
instanceInfo.roles ? controller && instanceInfo.roles.controller.machines ? ${machine.name};
|
||||
isPeer = instanceInfo.roles ? peer && instanceInfo.roles.peer.machines ? ${machine.name};
|
||||
isPeer = instanceInfo.roles ? peer && instanceInfo.roles.peer.machines or { } ? ${machine.name};
|
||||
in
|
||||
lib.optional (isController && isPeer) {
|
||||
inherit instanceName;
|
||||
|
||||
@@ -69,14 +69,19 @@ in
|
||||
|
||||
roles.controller.machines."controller1".settings = {
|
||||
endpoint = "192.168.1.1";
|
||||
# add an external peer to controller1 only
|
||||
externalPeers = [ "external1" ];
|
||||
# Enable IPv4 for external peers
|
||||
ipv4.enable = true;
|
||||
ipv4.address = "10.42.1.1/24";
|
||||
# add an external peer to controller1 with IPv4
|
||||
externalPeers.external1 = {
|
||||
ipv4.address = "10.42.1.50/32";
|
||||
};
|
||||
};
|
||||
|
||||
roles.controller.machines."controller2".settings = {
|
||||
endpoint = "192.168.1.2";
|
||||
# add the same external peer to controller2 to test multi-controller connection
|
||||
externalPeers = [ "external1" ];
|
||||
externalPeers.external1 = { };
|
||||
};
|
||||
|
||||
roles.peer.machines = {
|
||||
@@ -116,10 +121,11 @@ in
|
||||
'';
|
||||
networking.wireguard.interfaces."wg0" = {
|
||||
|
||||
# Multiple IPs, one in each controller's subnet
|
||||
# Multiple IPs, one in each controller's subnet (IPv6) plus IPv4
|
||||
ips = [
|
||||
"${controller1Prefix + ":" + external1Suffix}/56"
|
||||
"${controller2Prefix + ":" + external1Suffix}/56"
|
||||
"10.42.1.50/32" # IPv4 address for controller1
|
||||
];
|
||||
|
||||
privateKeyFile =
|
||||
@@ -139,8 +145,11 @@ in
|
||||
)
|
||||
);
|
||||
|
||||
# Allow controller1's /56 subnet
|
||||
allowedIPs = [ "${controller1Prefix}::/56" ];
|
||||
# Allow controller1's /56 subnet (IPv6) and IPv4 subnet
|
||||
allowedIPs = [
|
||||
"${controller1Prefix}::/56"
|
||||
"10.42.1.0/24" # IPv4 subnet for internet access
|
||||
];
|
||||
|
||||
endpoint = "controller1:51820";
|
||||
|
||||
@@ -196,6 +205,10 @@ in
|
||||
print("\n--- Testing external1 -> controller2 (direct connection) ---")
|
||||
external1.wait_until_succeeds("ping -c1 controller2.wg-test-one >&2")
|
||||
|
||||
# Test IPv4 connectivity
|
||||
print("\n--- Testing external1 -> controller1 (IPv4) ---")
|
||||
external1.wait_until_succeeds("ping -c1 10.42.1.1 >&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 ---")
|
||||
|
||||
Reference in New Issue
Block a user