diff --git a/config-remapper.nix b/config-remapper.nix index e053874..3d5357a 100644 --- a/config-remapper.nix +++ b/config-remapper.nix @@ -1,4 +1,8 @@ {lib}: wirenix-config: +with lib.attrsets; +with lib.lists; +with lib.trivial; +with builtins; let # Math # extract-key :: string -> list -> attrSet @@ -6,52 +10,79 @@ let # listOfSetsToSetByKey "primary" [ {primary = "foo"; secondary = 1; tertiary = "one"} {primary = "bar"; secondary = 2; tertiary = "two"} ] # {foo = {secondary = 1; tertiary = "one"}; bar = {secondary = 2; tertiary = "two"};} listOfSetsToSetByKey = key: list: - builtins.listToAttrs ( - lib.lists.forEach list (elem: { + listToAttrs ( + forEach list (elem: { name = elem."${key}"; - value = lib.attrsets.filterAttrs (n: v: n != key) elem; + value = removeAttrs elem [ key ]; }) ); # 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 add-colons = string: - if ((builtins.stringLength string) > 4) + if ((stringLength string) > 4) 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; # Peer Information # Helper functions for querying data about peers from the config peerInfo = { # 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 - 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 - groupConnections = peer: builtins.catAttrs "group" peer.connections; + groupConnections = peer: catAttrs "group" peer.connections; # 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 - 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 - 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 - 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 - 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 - 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 # Helper functions for querying data about subnets from the config 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 - 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 @@ -63,7 +94,7 @@ let # ip: the subnet ip in CIDR notation subnetMap = subnet: { 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"; }; @@ -71,29 +102,44 @@ let # 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 # 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..*.persistentKeepalive + # dynamicEndpointRefreshSeconds: (optional) see networking.wireguard.interfaces..*.dynamicEndpointRefreshSeconds + # dynamicEndpointRefreshRestartSeconds: (optional) see networking.wireguard.interfaces..*.dynamicEndpointRefreshRestartSeconds # connections: a set of all peers (as recursive attrset) which the current peer connects to, keyed by name # publicKey: the peer's public key # privateKeyFile: the location of the peer's private key peerMap = peer: { name = peer.name; subnets = listOfSetsToSetByKey "name" ( - builtins.map (subnet: { + map (subnet: { name = subnet.name; - subnet = lib.attrsets.filterAttrs (n: v: n != "name") (subnetMap subnet); - ip = peerInfo.IP subnet.name peer.name; - peers = listOfSetsToSetByKey "name" (builtins.map (peerMap) (builtins.filter (otherPeer: inList subnet.name otherPeer.subnets) (peerInfo.connections peer))); + subnet = removeAttrs (subnetMap subnet) [ "name" ]; + ip = peerInfo.ip subnet.name peer.name; + 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) ); - connections = listOfSetsToSetByKey "name" (builtins.map (peerMap) (peerInfo.connections peer)); + connections = listOfSetsToSetByKey "name" (map (peerMap) (peerInfo.connections peer)); publicKey = peer.publicKey; privateKeyFile = peer.privateKeyFile; }; }; in { - peerFromName = lib.attrsets.filterAttrs (n: v: n != "name") (mappers.peerMap peerInfo.peerFromName); - subnetFromName = lib.attrsets.filterAttrs (n: v: n != "name") (mappers.subnetMap subnetInfo.subnetFromName); - peers = listOfSetsToSetByKey "name" (builtins.map (mappers.peerMap) (wirenix-config.peers)); - subnets = listOfSetsToSetByKey "name" (builtins.map (mappers.subnetMap) (wirenix-config.subnets)); + peerFromName = removeAttrs (mappers.peerMap peerInfo.peerFromName) [ "name" ]; + subnetFromName = removeAttrs (mappers.subnetMap subnetInfo.subnetFromName) [ "name" ]; + peers = listOfSetsToSetByKey "name" (map (mappers.peerMap) (wirenix-config.peers)); + subnets = listOfSetsToSetByKey "name" (map (mappers.subnetMap) (wirenix-config.subnets)); } \ No newline at end of file diff --git a/peer-config-generator.nix b/peer-config-generator.nix deleted file mode 100644 index 82ae138..0000000 --- a/peer-config-generator.nix +++ /dev/null @@ -1,9 +0,0 @@ - -{config, lib, ...}: -let - has-rekey = config ? rekey; - infoRemapper = import ./config-remapper.nix {inherit lib;} config.modules.wirenix.config; -in -{ - -} \ No newline at end of file diff --git a/static-configuration.nix b/static-configuration.nix new file mode 100644 index 0000000..2fae9e0 --- /dev/null +++ b/static-configuration.nix @@ -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; + } + ]); + };} + ); + }; +} \ No newline at end of file diff --git a/test-data.nix b/test-data.nix index eaae2ba..d0ec138 100644 --- a/test-data.nix +++ b/test-data.nix @@ -1,12 +1,18 @@ { + version = "1"; subnets = [ - { name = "subnet.one"; } - { name = "subnet.two"; } - { name = "subnet.three"; } + { name = "subnet.one"; defaultPort = 51820; } + { name = "subnet.two"; defaultPort = 51821; } + { name = "subnet.three"; defaultPort = 51822; } ]; peers = [ { 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 = [ ]; @@ -22,6 +28,11 @@ } { 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 = [ "subnet.one" ]; @@ -38,6 +49,11 @@ } { 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 = [ "subnet.one" "subnet.two" @@ -56,6 +72,11 @@ } { 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 = [ "subnet.one" "subnet.two" @@ -75,6 +96,11 @@ } { 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 = [ "subnet.three" "subnet.one" diff --git a/wire.nix b/wire.nix index 064e8e1..4c9eae9 100644 --- a/wire.nix +++ b/wire.nix @@ -37,12 +37,36 @@ let ''; }; name = mkOption { - default = config.networking.hostName; - defaultText = literalExpression "hostName"; example = "bernd"; type = types.str; 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 { example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; type = types.singleLineStr; @@ -69,6 +93,14 @@ let type = types.str; 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 = { @@ -77,14 +109,14 @@ let default = {}; type = with types; listOf (submodule subnetOpts); description = '' - Shared configuration file that describes all clients + Subnets in the mesh network(s) ''; }; peers = mkOption { default = {}; type = with types; listOf (submodule peerOpts); description = '' - Shared configuration file that describes all clients + Peers in the mesh network(s) ''; }; }; @@ -92,7 +124,7 @@ let in { options = { - modules.wirenix = { + wirenix = { enable = mkOption { default = true; type = with lib.types; bool; @@ -100,6 +132,16 @@ in 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 { default = {}; type = with types; setOf (submodule configOpts);