added options for specifying endpoints, other refactoring

release
Matthew Salerno 1 year ago
parent 60ac560c64
commit e58ea8655e

@ -1,4 +1,8 @@
{lib}: wirenix-config: {lib}: wirenix-config:
with lib.attrsets;
with lib.lists;
with lib.trivial;
with builtins;
let let
# Math # Math
# extract-key :: string -> list -> attrSet # extract-key :: string -> list -> attrSet
@ -6,52 +10,79 @@ let
# listOfSetsToSetByKey "primary" [ {primary = "foo"; secondary = 1; tertiary = "one"} {primary = "bar"; secondary = 2; tertiary = "two"} ] # listOfSetsToSetByKey "primary" [ {primary = "foo"; secondary = 1; tertiary = "one"} {primary = "bar"; secondary = 2; tertiary = "two"} ]
# {foo = {secondary = 1; tertiary = "one"}; bar = {secondary = 2; tertiary = "two"};} # {foo = {secondary = 1; tertiary = "one"}; bar = {secondary = 2; tertiary = "two"};}
listOfSetsToSetByKey = key: list: listOfSetsToSetByKey = key: list:
builtins.listToAttrs ( listToAttrs (
lib.lists.forEach list (elem: { forEach list (elem: {
name = elem."${key}"; name = elem."${key}";
value = lib.attrsets.filterAttrs (n: v: n != key) elem; value = removeAttrs elem [ key ];
}) })
); );
# returns true if `elem` is in `list` # returns true if `elem` is in `list`
inList = elem: list: builtins.any (e: e == elem) list; inList = elem: list: any (e: e == elem) list;
# adds colons to a string every 4 characters for IPv6 shenanigans # adds colons to a string every 4 characters for IPv6 shenanigans
add-colons = string: add-colons = string:
if ((builtins.stringLength string) > 4) if ((stringLength string) > 4)
then then
((builtins.substring 0 4 string) + ":" + (add-colons (builtins.substring 4 32 string))) ((substring 0 4 string) + ":" + (add-colons (substring 4 32 string)))
else string; else string;
# Peer Information # Peer Information
# Helper functions for querying data about peers from the config # Helper functions for querying data about peers from the config
peerInfo = { peerInfo = {
# last 20 (hardcoded atm) characters (80 bits) of the peer's IPv6 address # last 20 (hardcoded atm) characters (80 bits) of the peer's IPv6 address
IPSuffix = peerName: builtins.substring 0 20 (builtins.hashString "sha256" peerName); ipSuffix = peerName: substring 0 20 (hashString "sha256" peerName);
# list of subnets (as subnet attrset in wirenix config) the peer belongs to # list of subnets (as subnet attrset in wirenix config) the peer belongs to
subnets = peer: builtins.filter (subnet: inList subnet.name peer.subnets) wirenix-config.subnets; subnets = peer: filter (subnet: inList subnet.name peer.subnets) wirenix-config.subnets;
# list of groups the peer connects to # list of groups the peer connects to
groupConnections = peer: builtins.catAttrs "group" peer.connections; groupConnections = peer: catAttrs "group" peer.connections;
# list of peers the peer connects to # list of peers the peer connects to
directConnections = peer: builtins.catAttrs "peer" peer.connections; directConnections = peer: catAttrs "peer" peer.connections;
# list of subnets for which the peer will try to connect to all peer in the subnet
subnetConnections = peer: catAttrs "subnet" peer.connections;
# gets the peer (as peer attrset in wirenix config) from a name # gets the peer (as peer attrset in wirenix config) from a name
peerFromName = peerName: builtins.head (builtins.filter (peer: peer.name == peerName) wirenix-config.peers); peerFromName = peerName: head (filter (peer: peer.name == peerName) wirenix-config.peers);
# gets all peers (as peer attrset in wirenix config) the are in a group # gets all peers (as peer attrset in wirenix config) the are in a group
peersInGroup = groupName: builtins.filter (peer: inList groupName peer.groups) wirenix-config.peers; peersInGroup = groupName: filter (peer: inList groupName peer.groups) wirenix-config.peers;
# returns true if peer is in group
peerIsInGroup = peer: groupName: inList peer (peerInfo.peersInGroup groupName);
# gets all peers (as peer attrset in wirenix config) that the given peer connects to, may contain the peer itself and duplicates # gets all peers (as peer attrset in wirenix config) that the given peer connects to, may contain the peer itself and duplicates
connectionsUnfiltered = peer: (builtins.map (peerInfo.peerFromName) (peerInfo.directConnections peer)) ++ (builtins.concatMap (group: peerInfo.peersInGroup group) (peerInfo.groupConnections peer)); connectionsUnfiltered = peer:
(map (peerInfo.peerFromName) (peerInfo.directConnections peer)) ++
(concatMap (group: peerInfo.peersInGroup group) (peerInfo.groupConnections peer)) ++
(concatMap (subnet: subnetInfo.peersInSubnet subnet) (peerInfo.subnetConnections peer));
# gets all peers (as peer attrset in wirenix config) that the given peer connects to, will not contain the peer itself or duplicates # gets all peers (as peer attrset in wirenix config) that the given peer connects to, will not contain the peer itself or duplicates
connections = peer: lib.lists.remove peer (lib.lists.unique (peerInfo.connectionsUnfiltered peer)); connections = peer: remove peer (unique (peerInfo.connectionsUnfiltered peer));
# returns the peer's IP when given the peer's name and subnet name # returns the peer's IP when given the peer's name and subnet name
IP = subnetName: PeerName: (add-colons ((subnetInfo.prefix subnetName) + (peerInfo.IPSuffix PeerName))) + "/80"; ip = subnetName: PeerName: (add-colons ((subnetInfo.prefix subnetName) + (peerInfo.ipSuffix PeerName))) + "/80";
# returns the endpoint for peerTo that peerFrom will connect with
endpointMatches = peerFrom: peerTo: map (matched: removeAttrs matched [ "match" ]) (
filter (endpoint:
if endpoint == {} then true else
all (id) (
mapAttrsToList (type: value:
if (type == "group") then
(peerInfo.peerIsInGroup peerFrom value)
else if (type == "peer") then
(peerFrom.name == endpoint.match.peer)
else if (type == "subnet") then
(peerInfo.peerIsInSubnet peerFrom (subnetInfo.subnetFromName value))
else throw "Unexpected type "+type+" in endpoints config."
) endpoint
)
) peerTo.endpoints
);
endpoint = foldl' (mergeAttrs) {} endpointMatches;
}; };
# Subnet Information # Subnet Information
# Helper functions for querying data about subnets from the config # Helper functions for querying data about subnets from the config
subnetInfo = { subnetInfo = {
# gets the subnet (as subnet attrset in wirenix config) from a name
subnetFromName = subnetName: builtins.head (builtins.filter (subnet: subnet.name == subnetName) wirenix-config.subnets);
# gets the first 10 characters of the IPV6 address for the subnet
prefix = subnetName: "fd" + (builtins.substring 0 10 (builtins.hashString "sha256" subnetName));
# gets all peers (as peer attrset in wirenix config) that belong to the subnet # gets all peers (as peer attrset in wirenix config) that belong to the subnet
peers = subnet: builtins.filter (peer: inList subnet.name peer.subnets) wirenix-config.peers; peersInSubnet = subnet: filter (peer: inList subnet.name peer.subnets) wirenix-config.peers;
# returns true if peer is in subnet
peerIsInSubnet = subnet: peer: inList peer (subnetInfo.peersInSubnet subnet);
# gets the subnet (as subnet attrset in wirenix config) from a name
subnetFromName = subnetName: head (filter (subnet: subnet.name == subnetName) wirenix-config.subnets);
# gets the first 10 characters of the IPV6 address for the subnet name
prefix = subnetName: "fd" + (substring 0 10 (hashString "sha256" subnetName));
}; };
# Mappers take a peer or subnet from the config and convert it # Mappers take a peer or subnet from the config and convert it
@ -63,7 +94,7 @@ let
# ip: the subnet ip in CIDR notation # ip: the subnet ip in CIDR notation
subnetMap = subnet: { subnetMap = subnet: {
name = subnet.name; name = subnet.name;
peers = listOfSetsToSetByKey "name" (builtins.map (peerMap) (subnetInfo.peers subnet)); peers = listOfSetsToSetByKey "name" (map (peerMap) (subnetInfo.peersInSubnet subnet));
ip = (add-colons (subnetInfo.prefix subnet.name)) + "::/80"; ip = (add-colons (subnetInfo.prefix subnet.name)) + "::/80";
}; };
@ -71,29 +102,44 @@ let
# subnets: a set of subnets (as similar recursive attrsets), keyed by name, with the following structure: # subnets: a set of subnets (as similar recursive attrsets), keyed by name, with the following structure:
# subnet: the subnet as described in the subnetMap function's description # subnet: the subnet as described in the subnetMap function's description
# ip: the peer's ip on the subnet (can only be one IP at the moment) # ip: the peer's ip on the subnet (can only be one IP at the moment)
# peers: peers (as recursive attrset) on the subnet that the current peer has connections with # peers: peers (as recursive attrset) on the subnet that the current peer has connections to, keyed by name, with the following structure:
# peer: the peer as described by this description
# ip: the connected peers ip on the subnet (same as subnets[subnetName].peers[peerName].subnets[subnetName].ip)
# endpoint: the endpoint to connect to, described as follows:
# ip: the ip to connect to
# port: the port to connect to
# persistentKeepalive: (optional) see networking.wireguard.interfaces.<name>.*.persistentKeepalive
# dynamicEndpointRefreshSeconds: (optional) see networking.wireguard.interfaces.<name>.*.dynamicEndpointRefreshSeconds
# dynamicEndpointRefreshRestartSeconds: (optional) see networking.wireguard.interfaces.<name>.*.dynamicEndpointRefreshRestartSeconds
# connections: a set of all peers (as recursive attrset) which the current peer connects to, keyed by name # connections: a set of all peers (as recursive attrset) which the current peer connects to, keyed by name
# publicKey: the peer's public key # publicKey: the peer's public key
# privateKeyFile: the location of the peer's private key # privateKeyFile: the location of the peer's private key
peerMap = peer: { peerMap = peer: {
name = peer.name; name = peer.name;
subnets = listOfSetsToSetByKey "name" ( subnets = listOfSetsToSetByKey "name" (
builtins.map (subnet: { map (subnet: {
name = subnet.name; name = subnet.name;
subnet = lib.attrsets.filterAttrs (n: v: n != "name") (subnetMap subnet); subnet = removeAttrs (subnetMap subnet) [ "name" ];
ip = peerInfo.IP subnet.name peer.name; ip = peerInfo.ip subnet.name peer.name;
peers = listOfSetsToSetByKey "name" (builtins.map (peerMap) (builtins.filter (otherPeer: inList subnet.name otherPeer.subnets) (peerInfo.connections peer))); peers = listOfSetsToSetByKey "name" (
(map (peerTo: {
name = peerTo.name;
peer = removeAttrs (peerMap peerTo) [ "name" ];
ip = peerInfo.ip subnet.name peerTo.name;
endpoint = peerInfo.endpoint peer peerTo;
}) (filter (otherPeer: inList subnet.name otherPeer.subnets) (peerInfo.connections peer)))
);
}) (peerInfo.subnets peer) }) (peerInfo.subnets peer)
); );
connections = listOfSetsToSetByKey "name" (builtins.map (peerMap) (peerInfo.connections peer)); connections = listOfSetsToSetByKey "name" (map (peerMap) (peerInfo.connections peer));
publicKey = peer.publicKey; publicKey = peer.publicKey;
privateKeyFile = peer.privateKeyFile; privateKeyFile = peer.privateKeyFile;
}; };
}; };
in in
{ {
peerFromName = lib.attrsets.filterAttrs (n: v: n != "name") (mappers.peerMap peerInfo.peerFromName); peerFromName = removeAttrs (mappers.peerMap peerInfo.peerFromName) [ "name" ];
subnetFromName = lib.attrsets.filterAttrs (n: v: n != "name") (mappers.subnetMap subnetInfo.subnetFromName); subnetFromName = removeAttrs (mappers.subnetMap subnetInfo.subnetFromName) [ "name" ];
peers = listOfSetsToSetByKey "name" (builtins.map (mappers.peerMap) (wirenix-config.peers)); peers = listOfSetsToSetByKey "name" (map (mappers.peerMap) (wirenix-config.peers));
subnets = listOfSetsToSetByKey "name" (builtins.map (mappers.subnetMap) (wirenix-config.subnets)); subnets = listOfSetsToSetByKey "name" (map (mappers.subnetMap) (wirenix-config.subnets));
} }

@ -1,9 +0,0 @@
{config, lib, ...}:
let
has-rekey = config ? rekey;
infoRemapper = import ./config-remapper.nix {inherit lib;} config.modules.wirenix.config;
in
{
}

@ -0,0 +1,44 @@
{config, lib, ...}:
with lib.trivial;
with lib.attrsets;
with lib.lists;
with lib;
let
# check whether or not agenix-rekey exists
has-rekey = config ? rekey;
# The remapper transforms the config in a way that makes filling out configs more easy
remapper = import ./config-remapper.nix {inherit lib;} config.modules.wirenix.config;
thisPeer = remapper.peerFromName config.wirenix.peerName;
# these aren't really important, I just wanted to reverse the argument order
forEachAttr = flip mapAttrs';
forEachAttrToList = flip mapAttrsToList;
in
{
networking.wireguard = {
interfaces = forEachAttr thisPeer.subnets (name: subnetConnection: { name = "wg-${name}";
value = {
ips = [ subnetConnection.ip ];
listenPort = subnetConnection.subnet.defaultPort;
privateKeyFile = thisPeer.privateKeyFile;
peers = forEachAttrToList subnetConnection.peers (peerName: peerConnection: mkMerge [
{
name = peerName;
publicKey = peerConnection.peer.publicKey;
allowedIPs = [ peerConnection.ip ];
endpoint = "${peerConnection.endpoint.ip}:${peerConnection.endpoint.port}";
}
mkIf (peerConnection.endpoint ? persistentKeepalive) {
persistentKeepalive = peerConnection.endpoint.persistentKeepalive;
}
mkIf (peerConnection.endpoint ? dynamicEndpointRefreshSeconds) {
dynamicEndpointRefreshSeconds = peerConnection.endpoint.dynamicEndpointRefreshSeconds;
}
mkIf (peerConnection.endpoint ? dynamicEndpointRefreshRestartSeconds) {
dynamicEndpointRefreshRestartSeconds = peerConnection.endpoint.dynamicEndpointRefreshRestartSeconds;
}
]);
};}
);
};
}

@ -1,12 +1,18 @@
{ {
version = "1";
subnets = [ subnets = [
{ name = "subnet.one"; } { name = "subnet.one"; defaultPort = 51820; }
{ name = "subnet.two"; } { name = "subnet.two"; defaultPort = 51821; }
{ name = "subnet.three"; } { name = "subnet.three"; defaultPort = 51822; }
]; ];
peers = [ peers = [
{ {
name = "peer.zero"; name = "peer.zero";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [ subnets = [
]; ];
@ -22,6 +28,11 @@
} }
{ {
name = "peer.one"; name = "peer.one";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820; persistentKeepalive = 15;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [ subnets = [
"subnet.one" "subnet.one"
]; ];
@ -38,6 +49,11 @@
} }
{ {
name = "peer.two"; name = "peer.two";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [ subnets = [
"subnet.one" "subnet.one"
"subnet.two" "subnet.two"
@ -56,6 +72,11 @@
} }
{ {
name = "peer.three"; name = "peer.three";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [ subnets = [
"subnet.one" "subnet.one"
"subnet.two" "subnet.two"
@ -75,6 +96,11 @@
} }
{ {
name = "peer.four"; name = "peer.four";
endpoints = [
{match = {group = "subnet two group";}; ip = "1.1.1.1"; port = 51820;}
{match = {peer = "peer.one";}; ip = "2.2.2.2"; port = 51820;}
{match = {}; ip = "3.3.3.3"; port = 51820;}
];
subnets = [ subnets = [
"subnet.three" "subnet.three"
"subnet.one" "subnet.one"

@ -37,12 +37,36 @@ let
''; '';
}; };
name = mkOption { name = mkOption {
default = config.networking.hostName;
defaultText = literalExpression "hostName";
example = "bernd"; example = "bernd";
type = types.str; type = types.str;
description = mdDoc "Unique name for the peer (must be unique for all subdomains this peer is a member of)"; description = mdDoc "Unique name for the peer (must be unique for all subdomains this peer is a member of)";
}; };
endpoints = mkOption {
example = ''
[
{match = {}; ip = "192.168.1.10"; port = 51820;} # default case
{match = {group = "location1"; subnet = "lanNet";}; ip = "192.168.1.10"; port = 51820; }
{match = {peer = "offSitePeer1";}; ip = "123.123.123.123"; port = 51825; persistentKeepalive = 15;}
];
'';
type = with types; listOf attrset;
description = mdDoc ''
The endpoints clients use to reach this host with rules to match by
group name `match = {group = "groupName";};`
peer name `match = {peer = "peerName";};`
or a default match at the end `match = {};`
All rules in `match` must be true for a match to happen.
Multiple matches will be merged top to bottom, so rules at the top
should be your most general rules which get overridden.
Values other than `match` specify options for the connection,
possible values are:
- ip
- port
- persistentKeepalive
- dynamicEndpointRefreshSeconds
- dynamicEndpointRefreshRestartSeconds
'';
};
publicKey = mkOption { publicKey = mkOption {
example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
type = types.singleLineStr; type = types.singleLineStr;
@ -69,6 +93,14 @@ let
type = types.str; type = types.str;
description = mdDoc "Unique name for the subnet"; description = mdDoc "Unique name for the subnet";
}; };
defaultPort = mkOption {
example = 51820;
type = types.int;
description = mdDoc ''
The port peers will use when communicating over this subnet.
Currently there is no way to set the ports for peers individually.
'';
};
}; };
}; };
configOpts = { configOpts = {
@ -77,14 +109,14 @@ let
default = {}; default = {};
type = with types; listOf (submodule subnetOpts); type = with types; listOf (submodule subnetOpts);
description = '' description = ''
Shared configuration file that describes all clients Subnets in the mesh network(s)
''; '';
}; };
peers = mkOption { peers = mkOption {
default = {}; default = {};
type = with types; listOf (submodule peerOpts); type = with types; listOf (submodule peerOpts);
description = '' description = ''
Shared configuration file that describes all clients Peers in the mesh network(s)
''; '';
}; };
}; };
@ -92,7 +124,7 @@ let
in in
{ {
options = { options = {
modules.wirenix = { wirenix = {
enable = mkOption { enable = mkOption {
default = true; default = true;
type = with lib.types; bool; type = with lib.types; bool;
@ -100,6 +132,16 @@ in
Wirenix Wirenix
''; '';
}; };
peerName = mkOption {
default = config.networking.hostName;
defaultText = literalExpression "hostName";
example = "bernd";
type = types.str;
description = mdDoc ''
Name of the peer using this module, to match the name in
`wirenix.config.peers.*.name`
'';
};
config = mkOption { config = mkOption {
default = {}; default = {};
type = with types; setOf (submodule configOpts); type = with types; setOf (submodule configOpts);

Loading…
Cancel
Save