diff --git a/config-remapper.nix b/config-remapper.nix deleted file mode 100644 index 3d5357a..0000000 --- a/config-remapper.nix +++ /dev/null @@ -1,145 +0,0 @@ -{lib}: wirenix-config: -with lib.attrsets; -with lib.lists; -with lib.trivial; -with builtins; -let - # Math - # extract-key :: string -> list -> attrSet - # Example: - # 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: - listToAttrs ( - forEach list (elem: { - name = elem."${key}"; - value = removeAttrs elem [ key ]; - }) - ); - # returns true if `elem` is in `list` - inList = elem: list: any (e: e == elem) list; - # adds colons to a string every 4 characters for IPv6 shenanigans - add-colons = string: - if ((stringLength string) > 4) - then - ((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: substring 0 20 (hashString "sha256" peerName); - # list of subnets (as subnet attrset in wirenix config) the peer belongs to - subnets = peer: filter (subnet: inList subnet.name peer.subnets) wirenix-config.subnets; - # list of groups the peer connects to - groupConnections = peer: catAttrs "group" peer.connections; - # list of peers the peer connects to - 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: 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: 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: - (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: 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"; - # 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 all peers (as peer attrset in wirenix config) that belong to the subnet - 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 - # into a recursive attrset that is better suited for nix configs - mappers = rec { - - # Maps a wirenix config subnet to a recursive attrset, structure is as follows: - # peers: a set of peers (as similar recursive attrsets), keyed by name - # ip: the subnet ip in CIDR notation - subnetMap = subnet: { - name = subnet.name; - peers = listOfSetsToSetByKey "name" (map (peerMap) (subnetInfo.peersInSubnet subnet)); - ip = (add-colons (subnetInfo.prefix subnet.name)) + "::/80"; - }; - - # Maps a wirenix config peer to a recursive attrset, structure is as follows: - # 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 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" ( - map (subnet: { - name = subnet.name; - 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" (map (peerMap) (peerInfo.connections peer)); - publicKey = peer.publicKey; - privateKeyFile = peer.privateKeyFile; - }; - }; -in -{ - 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/configurers/networkd.nix b/configurers/networkd.nix new file mode 100644 index 0000000..e69de29 diff --git a/configurers/networkmanager.nix b/configurers/networkmanager.nix new file mode 100644 index 0000000..e69de29 diff --git a/static-configuration.nix b/configurers/static.nix similarity index 64% rename from static-configuration.nix rename to configurers/static.nix index 2fae9e0..e51b620 100644 --- a/static-configuration.nix +++ b/configurers/static.nix @@ -1,5 +1,4 @@ - -{config, lib, ...}: +{config, lib, ...}: intermediateConfig: with lib.trivial; with lib.attrsets; with lib.lists; @@ -7,25 +6,23 @@ 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; + thisPeer = intermediateConfig.peers."${config.wirenix.peerName}"; # these aren't really important, I just wanted to reverse the argument order - forEachAttr = flip mapAttrs'; + forEachAttr' = flip mapAttrs'; forEachAttrToList = flip mapAttrsToList; in { networking.wireguard = { - interfaces = forEachAttr thisPeer.subnets (name: subnetConnection: { name = "wg-${name}"; + interfaces = forEachAttr' thisPeer.subnetConnections (name: subnetConnection: { name = "wg-${name}"; value = { - ips = [ subnetConnection.ip ]; - listenPort = subnetConnection.subnet.defaultPort; + ips = subnetConnection.ipAddresses; + listenPort = subnetConnection.listenPort; privateKeyFile = thisPeer.privateKeyFile; - peers = forEachAttrToList subnetConnection.peers (peerName: peerConnection: mkMerge [ + peers = forEachAttrToList subnetConnection.peerConnections (peerName: peerConnection: mkMerge [ { name = peerName; publicKey = peerConnection.peer.publicKey; - allowedIPs = [ peerConnection.ip ]; + allowedIPs = peerConnection.ipAddresses; endpoint = "${peerConnection.endpoint.ip}:${peerConnection.endpoint.port}"; } mkIf (peerConnection.endpoint ? persistentKeepalive) { diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 5999137..0000000 --- a/flake.lock +++ /dev/null @@ -1,7 +0,0 @@ -{ - "nodes": { - "root": {} - }, - "root": "root", - "version": 7 -} diff --git a/lib.nix b/lib.nix index a0da02c..ba915de 100644 --- a/lib.nix +++ b/lib.nix @@ -1,1326 +1,51 @@ -{ lib ? null, ... }: - -let - - net = { - ip = { - - # add :: (ip | mac | integer) -> ip -> ip - # - # Examples: - # - # Adding integer to IPv4: - # > net.ip.add 100 "10.0.0.1" - # "10.0.0.101" - # - # Adding IPv4 to IPv4: - # > net.ip.add "127.0.0.1" "10.0.0.1" - # "137.0.0.2" - # - # Adding IPv6 to IPv4: - # > net.ip.add "::cafe:beef" "10.0.0.1" - # "212.254.186.191" - # - # Adding MAC to IPv4 (overflows): - # > net.ip.add "fe:ed:fa:ce:f0:0d" "10.0.0.1" - # "4.206.240.14" - # - # Adding integer to IPv6: - # > net.ip.add 100 "dead:cafe:beef::" - # "dead:cafe:beef::64" - # - # Adding IPv4 to to IPv6: - # > net.ip.add "127.0.0.1" "dead:cafe:beef::" - # "dead:cafe:beef::7f00:1" - # - # Adding MAC to IPv6: - # > net.ip.add "fe:ed:fa:ce:f0:0d" "dead:cafe:beef::" - # "dead:cafe:beef::feed:face:f00d" - add = delta: ip: - let - function = "net.ip.add"; - delta' = typechecks.numeric function "delta" delta; - ip' = typechecks.ip function "ip" ip; - in - builders.ip (implementations.ip.add delta' ip'); - - # diff :: ip -> ip -> (integer | ipv6) - # - # net.ip.diff is the reverse of net.ip.add: - # - # net.ip.diff (net.ip.add a b) a = b - # net.ip.diff (net.ip.add a b) b = a - # - # The difference between net.ip.diff and net.ip.subtract is that - # net.ip.diff will try its best to return an integer (falling back - # to an IPv6 if the result is too big to fit in an integer). This is - # useful if you have two hosts that you know are on the same network - # and you just want to calculate the offset between them — a result - # like "0.0.0.10" is not very useful (which is what you would get - # from net.ip.subtract). - diff = minuend: subtrahend: - let - function = "net.ip.diff"; - minuend' = typechecks.ip function "minuend" minuend; - subtrahend' = typechecks.ip function "subtrahend" subtrahend; - result = implementations.ip.diff minuend' subtrahend'; - in - if result ? ipv6 - then builders.ipv6 result - else result; - - # subtract :: (ip | mac | integer) -> ip -> ip - # - # net.ip.subtract is also the reverse of net.ip.add: - # - # net.ip.subtract a (net.ip.add a b) = b - # net.ip.subtract b (net.ip.add a b) = a - # - # The difference between net.ip.subtract and net.ip.diff is that - # net.ip.subtract will always return the same type as its "ip" - # parameter. Its implementation takes the "delta" parameter, - # coerces it to be the same type as the "ip" paramter, negates it - # (using two's complement), and then adds it to "ip". - subtract = delta: ip: - let - function = "net.ip.subtract"; - delta' = typechecks.numeric function "delta" delta; - ip' = typechecks.ip function "ip" ip; - in - builders.ip (implementations.ip.subtract delta' ip'); - }; - - mac = { - - # add :: (ip | mac | integer) -> mac -> mac - # - # Examples: - # - # Adding integer to MAC: - # > net.mac.add 100 "fe:ed:fa:ce:f0:0d" - # "fe:ed:fa:ce:f0:71" - # - # Adding IPv4 to MAC: - # > net.mac.add "127.0.0.1" "fe:ed:fa:ce:f0:0d" - # "fe:ee:79:ce:f0:0e" - # - # Adding IPv6 to MAC: - # > net.mac.add "::cafe:beef" "fe:ed:fa:ce:f0:0d" - # "fe:ee:c5:cd:aa:cb - # - # Adding MAC to MAC: - # > net.mac.add "fe:ed:fa:00:00:00" "00:00:00:ce:f0:0d" - # "fe:ed:fa:ce:f0:0d" - add = delta: mac: - let - function = "net.mac.add"; - delta' = typechecks.numeric function "delta" delta; - mac' = typechecks.mac function "mac" mac; - in - builders.mac (implementations.mac.add delta' mac'); - - # diff :: mac -> mac -> integer - # - # net.mac.diff is the reverse of net.mac.add: - # - # net.mac.diff (net.mac.add a b) a = b - # net.mac.diff (net.mac.add a b) b = a - # - # The difference between net.mac.diff and net.mac.subtract is that - # net.mac.diff will always return an integer. - diff = minuend: subtrahend: - let - function = "net.mac.diff"; - minuend' = typechecks.mac function "minuend" minuend; - subtrahend' = typechecks.mac function "subtrahend" subtrahend; - in - implementations.mac.diff minuend' subtrahend'; - - # subtract :: (ip | mac | integer) -> mac -> mac - # - # net.mac.subtract is also the reverse of net.ip.add: - # - # net.mac.subtract a (net.mac.add a b) = b - # net.mac.subtract b (net.mac.add a b) = a - # - # The difference between net.mac.subtract and net.mac.diff is that - # net.mac.subtract will always return a MAC address. - subtract = delta: mac: - let - function = "net.mac.subtract"; - delta' = typechecks.numeric function "delta" delta; - mac' = typechecks.mac function "mac" mac; - in - builders.mac (implementations.mac.subtract delta' mac'); - }; - - cidr = { - # add :: (ip | mac | integer) -> cidr -> cidr - # - # > net.cidr.add 2 "127.0.0.0/8" - # "129.0.0.0/8" - # - # > net.cidr.add (-2) "127.0.0.0/8" - # "125.0.0.0/8" - add = delta: cidr: - let - function = "net.cidr.add"; - delta' = typechecks.numeric function "delta" delta; - cidr' = typechecks.cidr function "cidr" cidr; - in - builders.cidr (implementations.cidr.add delta' cidr'); - - # child :: cidr -> cidr -> bool - # - # > net.cidr.child "10.10.10.0/24" "10.0.0.0/8" - # true - # - # > net.cidr.child "127.0.0.0/8" "10.0.0.0/8" - # false - child = subcidr: cidr: - let - function = "net.cidr.child"; - subcidr' = typechecks.cidr function "subcidr" subcidr; - cidr' = typechecks.cidr function "cidr" cidr; - in - implementations.cidr.child subcidr' cidr'; - - # contains :: ip -> cidr -> bool - # - # > net.cidr.contains "127.0.0.1" "127.0.0.0/8" - # true - # - # > net.cidr.contains "127.0.0.1" "192.168.0.0/16" - # false - contains = ip: cidr: - let - function = "net.cidr.contains"; - ip' = typechecks.ip function "ip" ip; - cidr' = typechecks.cidr function "cidr" cidr; - in - implementations.cidr.contains ip' cidr'; - - # capacity :: cidr -> integer - # - # > net.cidr.capacity "172.16.0.0/12" - # 1048576 - # - # > net.cidr.capacity "dead:cafe:beef::/96" - # 4294967296 - # - # > net.cidr.capacity "dead:cafe:beef::/48" (saturates to maxBound) - # 9223372036854775807 - capacity = cidr: - let - function = "net.cidr.capacity"; - cidr' = typechecks.cidr function "cidr" cidr; - in - implementations.cidr.capacity cidr'; - - # host :: (ip | mac | integer) -> cidr -> ip - # - # > net.cidr.host 10000 "10.0.0.0/8" - # 10.0.39.16 - # - # > net.cidr.host 10000 "dead:cafe:beef::/64" - # "dead:cafe:beef::2710" - # - # net.cidr.host "127.0.0.1" "dead:cafe:beef::/48" - # > "dead:cafe:beef::7f00:1" - # - # Inpsired by: - # https://www.terraform.io/docs/configuration/functions/cidrhost.html - host = hostnum: cidr: - let - function = "net.cidr.host"; - hostnum' = typechecks.numeric function "hostnum" hostnum; - cidr' = typechecks.cidr function "cidr" cidr; - in - builders.ip (implementations.cidr.host hostnum' cidr'); - - # length :: cidr -> integer - # - # > net.cidr.prefix "127.0.0.0/8" - # 8 - # - # > net.cidr.prefix "dead:cafe:beef::/48" - # 48 - length = cidr: - let - function = "net.cidr.length"; - cidr' = typechecks.cidr function "cidr" cidr; - in - implementations.cidr.length cidr'; - - # make :: integer -> ip -> cidr - # - # > net.cidr.make 24 "192.168.0.150" - # "192.168.0.0/24" - # - # > net.cidr.make 40 "dead:cafe:beef::feed:face:f00d" - # "dead:cafe:be00::/40" - make = length: base: - let - function = "net.cidr.make"; - length' = typechecks.int function "length" length; - base' = typechecks.ip function "base" base; - in - builders.cidr (implementations.cidr.make length' base'); - - # netmask :: cidr -> ip - # - # > net.cidr.netmask "192.168.0.0/24" - # "255.255.255.0" - # - # > net.cidr.netmask "dead:cafe:beef::/64" - # "ffff:ffff:ffff:ffff::" - netmask = cidr: - let - function = "net.cidr.netmask"; - cidr' = typechecks.cidr function "cidr" cidr; - in - builders.ip (implementations.cidr.netmask cidr'); - - # size :: cidr -> integer - # - # > net.cidr.prefix "127.0.0.0/8" - # 24 - # - # > net.cidr.prefix "dead:cafe:beef::/48" - # 80 - size = cidr: - let - function = "net.cidr.size"; - cidr' = typechecks.cidr function "cidr" cidr; - in - implementations.cidr.size cidr'; - - # subnet :: integer -> (ip | mac | integer) -> cidr -> cidr - # - # > net.cidr.subnet 4 2 "172.16.0.0/12" - # "172.18.0.0/16" - # - # > net.cidr.subnet 4 15 "10.1.2.0/24" - # "10.1.2.240/28" - # - # > net.cidr.subnet 16 162 "fd00:fd12:3456:7890::/56" - # "fd00:fd12:3456:7800:a200::/72" - # - # Inspired by: - # https://www.terraform.io/docs/configuration/functions/cidrsubnet.html - subnet = length: netnum: cidr: - let - function = "net.cidr.subnet"; - length' = typechecks.int function "length" length; - netnum' = typechecks.numeric function "netnum" netnum; - cidr' = typechecks.cidr function "cidr" cidr; - in - builders.cidr (implementations.cidr.subnet length' netnum' cidr'); - - }; - } // ( - if builtins.isNull lib then {} else { - types = - let - - mkParsedOptionType = { name, description, parser, builder }: - let - normalize = def: def // { - value = builder (parser def.value); - }; - in - lib.mkOptionType { - inherit name description; - check = x: builtins.isString x && parser x != null; - merge = loc: defs: lib.mergeEqualOption loc (map normalize defs); - }; - - dependent-ip = type: cidr: - let - cidrs = - if builtins.isList cidr - then cidr - else [ cidr ]; - in - lib.types.addCheck type (i: lib.any (net.cidr.contains i) cidrs) // { - description = type.description + " in ${builtins.concatStringsSep " or " cidrs}"; - }; - - dependent-cidr = type: cidr: - let - cidrs = - if builtins.isList cidr - then cidr - else [ cidr ]; - in - lib.types.addCheck type (i: lib.any (net.cidr.child i) cidrs) // { - description = type.description + " in ${builtins.concatStringsSep " or " cidrs}"; - }; - - in - rec { - - ip = mkParsedOptionType { - name = "ip"; - description = "IPv4 or IPv6 address"; - parser = parsers.ip; - builder = builders.ip; - }; - - ip-in = dependent-ip ip; - - ipv4 = mkParsedOptionType { - name = "ipv4"; - description = "IPv4 address"; - parser = parsers.ipv4; - builder = builders.ipv4; - }; - - ipv4-in = dependent-ip ipv4; - - ipv6 = mkParsedOptionType { - name = "ipv6"; - description = "IPv6 address"; - parser = parsers.ipv6; - builder = builders.ipv6; - }; - - ipv6-in = dependent-ip ipv6; - - cidr = mkParsedOptionType { - name = "cidr"; - description = "IPv4 or IPv6 address range in CIDR notation"; - parser = parsers.cidr; - builder = builders.cidr; - }; - - cidr-in = dependent-cidr cidr; - - cidrv4 = mkParsedOptionType { - name = "cidrv4"; - description = "IPv4 address range in CIDR notation"; - parser = parsers.cidrv4; - builder = builders.cidrv4; - }; - - cidrv4-in = dependent-cidr cidrv4; - - cidrv6 = mkParsedOptionType { - name = "cidrv6"; - description = "IPv6 address range in CIDR notation"; - parser = parsers.cidrv6; - builder = builders.cidrv6; - }; - - cidrv6-in = dependent-cidr cidrv6; - - mac = mkParsedOptionType { - name = "mac"; - description = "MAC address"; - parser = parsers.mac; - builder = builders.mac; - }; - - }; - } - ); - - list = { - cons = a: b: [ a ] ++ b; - }; - - bit = - let - shift = n: x: - if n < 0 - then x * math.pow 2 (-n) - else - let - safeDiv = n: d: if d == 0 then 0 else n / d; - d = math.pow 2 n; - in - if x < 0 - then not (safeDiv (not x) d) - else safeDiv x d; - - left = n: shift (-n); - - right = shift; - - and = builtins.bitAnd; - - or = builtins.bitOr; - - xor = builtins.bitXor; - - not = xor (-1); - - mask = n: and (left n 1 - 1); - in - { - inherit left right and or xor not mask; - }; - - math = rec { - max = a: b: - if a > b - then a - else b; - - min = a: b: - if a < b - then a - else b; - - clamp = a: b: c: max a (min b c); - - pow = x: n: - if n == 0 - then 1 - else if bit.and n 1 != 0 - then x * pow (x * x) ((n - 1) / 2) - else pow (x * x) (n / 2); - }; - - parsers = - let - - # fmap :: (a -> b) -> parser a -> parser b - fmap = f: ma: bind ma (a: pure (f a)); - - # pure :: a -> parser a - pure = a: string: { - leftovers = string; - result = a; - }; - - # liftA2 :: (a -> b -> c) -> parser a -> parser b -> parser c - liftA2 = f: ma: mb: bind ma (a: bind mb (b: pure (f a b))); - liftA3 = f: a: b: ap (liftA2 f a b); - liftA4 = f: a: b: c: ap (liftA3 f a b c); - liftA5 = f: a: b: c: d: ap (liftA4 f a b c d); - liftA6 = f: a: b: c: d: e: ap (liftA5 f a b c d e); - - # ap :: parser (a -> b) -> parser a -> parser b - ap = liftA2 (a: a); - - # then_ :: parser a -> parser b -> parser b - then_ = liftA2 (a: b: b); - - # empty :: parser a - empty = string: null; - - # alt :: parser a -> parser a -> parser a - alt = left: right: string: - let - result = left string; - in - if builtins.isNull result - then right string - else result; - - # guard :: bool -> parser {} - guard = condition: if condition then pure {} else empty; - - # mfilter :: (a -> bool) -> parser a -> parser a - mfilter = f: parser: bind parser (a: then_ (guard (f a)) (pure a)); - - # some :: parser a -> parser [a] - some = v: liftA2 list.cons v (many v); - - # many :: parser a -> parser [a] - many = v: alt (some v) (pure []); - - # bind :: parser a -> (a -> parser b) -> parser b - bind = parser: f: string: - let - a = parser string; - in - if builtins.isNull a - then null - else f a.result a.leftovers; - - # run :: parser a -> string -> maybe a - run = parser: string: - let - result = parser string; - in - if builtins.isNull result || result.leftovers != "" - then null - else result.result; - - next = string: - if string == "" - then null - else { - leftovers = builtins.substring 1 (-1) string; - result = builtins.substring 0 1 string; - }; - - # Count how many characters were consumed by a parser - count = parser: string: - let - result = parser string; - in - if builtins.isNull result - then null - else result // { - result = { - inherit (result) result; - count = with result; - builtins.stringLength string - builtins.stringLength leftovers; - }; - }; - - # Limit the parser to n characters at most - limit = n: parser: - fmap (a: a.result) (mfilter (a: a.count <= n) (count parser)); - - # Ensure the parser consumes exactly n characters - exactly = n: parser: - fmap (a: a.result) (mfilter (a: a.count == n) (count parser)); - - char = c: bind next (c': guard (c == c')); - - string = css: - if css == "" - then pure {} - else - let - c = builtins.substring 0 1 css; - cs = builtins.substring 1 (-1) css; - in - then_ (char c) (string cs); - - digit = set: bind next ( - c: then_ - (guard (builtins.hasAttr c set)) - (pure (builtins.getAttr c set)) - ); - - decimalDigits = { - "0" = 0; - "1" = 1; - "2" = 2; - "3" = 3; - "4" = 4; - "5" = 5; - "6" = 6; - "7" = 7; - "8" = 8; - "9" = 9; - }; - - hexadecimalDigits = decimalDigits // { - "a" = 10; - "b" = 11; - "c" = 12; - "d" = 13; - "e" = 14; - "f" = 15; - "A" = 10; - "B" = 11; - "C" = 12; - "D" = 13; - "E" = 14; - "F" = 15; - }; - - fromDecimalDigits = builtins.foldl' (a: c: a * 10 + c) 0; - fromHexadecimalDigits = builtins.foldl' (a: bit.or (bit.left 4 a)) 0; - - # disallow leading zeros - decimal = bind (digit decimalDigits) ( - n: - if n == 0 - then pure 0 - else fmap - (ns: fromDecimalDigits (list.cons n ns)) - (many (digit decimalDigits)) - ); - - hexadecimal = fmap fromHexadecimalDigits (some (digit hexadecimalDigits)); - - ipv4 = - let - dot = char "."; - - octet = mfilter (n: n < 256) decimal; - - octet' = then_ dot octet; - - fromOctets = a: b: c: d: { - ipv4 = bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 a) b)) c)) d; - }; - in - liftA4 fromOctets octet octet' octet' octet'; - - # This is more or less a literal translation of - # https://hackage.haskell.org/package/ip/docs/src/Net.IPv6.html#parser - ipv6 = - let - colon = char ":"; - - hextet = limit 4 hexadecimal; - - hextet' = then_ colon hextet; - - fromHextets = hextets: - if builtins.length hextets != 8 - then empty - else - let - a = builtins.elemAt hextets 0; - b = builtins.elemAt hextets 1; - c = builtins.elemAt hextets 2; - d = builtins.elemAt hextets 3; - e = builtins.elemAt hextets 4; - f = builtins.elemAt hextets 5; - g = builtins.elemAt hextets 6; - h = builtins.elemAt hextets 7; - in - pure { - ipv6 = { - a = bit.or (bit.left 16 a) b; - b = bit.or (bit.left 16 c) d; - c = bit.or (bit.left 16 e) f; - d = bit.or (bit.left 16 g) h; - }; - }; - - ipv4' = fmap - ( - address: - let - upper = bit.right 16 address.ipv4; - lower = bit.mask 16 address.ipv4; - in - [ upper lower ] - ) - ipv4; - - part = n: - let - n' = n + 1; - hex = liftA2 list.cons hextet - ( - then_ colon - ( - alt - (then_ colon (doubleColon n')) - (part n') - ) - ); - in - if n == 7 - then fmap (a: [ a ]) hextet - else - if n == 6 - then alt ipv4' hex - else hex; - - doubleColon = n: - bind (alt afterDoubleColon (pure [])) ( - rest: - let - missing = 8 - n - builtins.length rest; - in - if missing < 0 - then empty - else pure (builtins.genList (_: 0) missing ++ rest) - ); - - afterDoubleColon = - alt ipv4' - ( - liftA2 list.cons hextet - ( - alt - (then_ colon afterDoubleColon) - (pure []) - ) - ); - - in - bind - ( - alt - ( - then_ - (string "::") - (doubleColon 0) - ) - (part 0) - ) - fromHextets; - - cidrv4 = - liftA2 - (base: length: implementations.cidr.make length base) - ipv4 - (then_ (char "/") (mfilter (n: n <= 32) decimal)); - - cidrv6 = - liftA2 - (base: length: implementations.cidr.make length base) - ipv6 - (then_ (char "/") (mfilter (n: n <= 128) decimal)); - - mac = - let - colon = char ":"; - - octet = exactly 2 hexadecimal; - - octet' = then_ colon octet; - - fromOctets = a: b: c: d: e: f: { - mac = bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 a) b)) c)) d)) e)) f; - }; - in - liftA6 fromOctets octet octet' octet' octet' octet' octet'; - - in - { - ipv4 = run ipv4; - ipv6 = run ipv6; - ip = run (alt ipv4 ipv6); - cidrv4 = run cidrv4; - cidrv6 = run cidrv6; - cidr = run (alt cidrv4 cidrv6); - mac = run mac; - numeric = run (alt (alt ipv4 ipv6) mac); - }; - - builders = - let - - ipv4 = address: - let - abcd = address.ipv4; - abc = bit.right 8 abcd; - ab = bit.right 8 abc; - a = bit.right 8 ab; - b = bit.mask 8 ab; - c = bit.mask 8 abc; - d = bit.mask 8 abcd; - in - builtins.concatStringsSep "." (map toString [ a b c d ]); - - # This is more or less a literal translation of - # https://hackage.haskell.org/package/ip/docs/src/Net.IPv6.html#encode - ipv6 = address: - let - - digits = "0123456789abcdef"; - - toHexString = n: - let - rest = bit.right 4 n; - current = bit.mask 4 n; - prefix = - if rest == 0 - then "" - else toHexString rest; - in - "${prefix}${builtins.substring current 1 digits}"; - - in - if (with address.ipv6; a == 0 && b == 0 && c == 0 && d > 65535) - then "::${ipv4 { ipv4 = address.ipv6.d; }}" - else - if (with address.ipv6; a == 0 && b == 0 && c == 65535) - then "::ffff:${ipv4 { ipv4 = address.ipv6.d; }}" - else - let - - a = bit.right 16 address.ipv6.a; - b = bit.mask 16 address.ipv6.a; - c = bit.right 16 address.ipv6.b; - d = bit.mask 16 address.ipv6.b; - e = bit.right 16 address.ipv6.c; - f = bit.mask 16 address.ipv6.c; - g = bit.right 16 address.ipv6.d; - h = bit.mask 16 address.ipv6.d; - - hextets = [ a b c d e f g h ]; - - # calculate the position and size of the longest sequence of - # zeroes within the list of hextets - longest = - let - go = i: current: best: - if i < builtins.length hextets - then - let - n = builtins.elemAt hextets i; - - current' = - if n == 0 - then - if builtins.isNull current - then { - size = 1; - position = i; - } - else current // { - size = current.size + 1; - } - else null; - - best' = - if n == 0 - then - if builtins.isNull best - then current' - else - if current'.size > best.size - then current' - else best - else best; - in - go (i + 1) current' best' - else best; - in - go 0 null null; - - format = hextets: - builtins.concatStringsSep ":" (map toHexString hextets); - in - if builtins.isNull longest - then format hextets - else - let - sublist = i: length: xs: - map - (builtins.elemAt xs) - (builtins.genList (x: x + i) length); - - end = longest.position + longest.size; - - before = sublist 0 longest.position hextets; - - after = sublist end (builtins.length hextets - end) hextets; - in - "${format before}::${format after}"; - - ip = address: - if address ? ipv4 - then ipv4 address - else ipv6 address; - - cidrv4 = cidr: - "${ipv4 cidr.base}/${toString cidr.length}"; - - cidrv6 = cidr: - "${ipv6 cidr.base}/${toString cidr.length}"; - - cidr = cidr: - "${ip cidr.base}/${toString cidr.length}"; - - mac = address: - let - digits = "0123456789abcdef"; - octet = n: - let - upper = bit.right 4 n; - lower = bit.mask 4 n; - in - "${builtins.substring upper 1 digits}${builtins.substring lower 1 digits}"; - in - let - a = bit.mask 8 (bit.right 40 address.mac); - b = bit.mask 8 (bit.right 32 address.mac); - c = bit.mask 8 (bit.right 24 address.mac); - d = bit.mask 8 (bit.right 16 address.mac); - e = bit.mask 8 (bit.right 8 address.mac); - f = bit.mask 8 (bit.right 0 address.mac); - in - "${octet a}:${octet b}:${octet c}:${octet d}:${octet e}:${octet f}"; - - in - { - inherit ipv4 ipv6 ip cidrv4 cidrv6 cidr mac; - }; - - arithmetic = rec { - # or :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) - or = a_: b: - let - a = coerce b a_; - in - if a ? ipv6 - then { - ipv6 = { - a = bit.or a.ipv6.a b.ipv6.a; - b = bit.or a.ipv6.b b.ipv6.b; - c = bit.or a.ipv6.c b.ipv6.c; - d = bit.or a.ipv6.d b.ipv6.d; - }; - } - else if a ? ipv4 - then { - ipv4 = bit.or a.ipv4 b.ipv4; - } - else if a ? mac - then { - mac = bit.or a.mac b.mac; - } - else bit.or a b; - - # and :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) - and = a_: b: - let - a = coerce b a_; - in - if a ? ipv6 - then { - ipv6 = { - a = bit.and a.ipv6.a b.ipv6.a; - b = bit.and a.ipv6.b b.ipv6.b; - c = bit.and a.ipv6.c b.ipv6.c; - d = bit.and a.ipv6.d b.ipv6.d; - }; - } - else if a ? ipv4 - then { - ipv4 = bit.and a.ipv4 b.ipv4; - } - else if a ? mac - then { - mac = bit.and a.mac b.mac; - } - else bit.and a b; - - # not :: (ip | mac | integer) -> (ip | mac | integer) - not = a: - if a ? ipv6 - then { - ipv6 = { - a = bit.mask 32 (bit.not a.ipv6.a); - b = bit.mask 32 (bit.not a.ipv6.b); - c = bit.mask 32 (bit.not a.ipv6.c); - d = bit.mask 32 (bit.not a.ipv6.d); - }; - } - else if a ? ipv4 - then { - ipv4 = bit.mask 32 (bit.not a.ipv4); - } - else if a ? mac - then { - mac = bit.mask 48 (bit.not a.mac); - } - else bit.not a; - - # add :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) - add = - let - split = a: { - fst = bit.mask 32 (bit.right 32 a); - snd = bit.mask 32 a; - }; - in - a_: b: - let - a = coerce b a_; - in - if a ? ipv6 - then - let - a' = split (a.ipv6.a + b.ipv6.a + b'.fst); - b' = split (a.ipv6.b + b.ipv6.b + c'.fst); - c' = split (a.ipv6.c + b.ipv6.c + d'.fst); - d' = split (a.ipv6.d + b.ipv6.d); - in - { - ipv6 = { - a = a'.snd; - b = b'.snd; - c = c'.snd; - d = d'.snd; - }; - } - else if a ? ipv4 - then { - ipv4 = bit.mask 32 (a.ipv4 + b.ipv4); - } - else if a ? mac - then { - mac = bit.mask 48 (a.mac + b.mac); - } - else a + b; - - # subtract :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) - subtract = a: b: add (add 1 (not (coerce b a))) b; - - # diff :: (ip | mac | integer) -> (ip | mac | integer) -> (ipv6 | integer) - diff = a: b: - let - toIPv6 = coerce ({ ipv6.a = 0; }); - result = (subtract b (toIPv6 a)).ipv6; - max32 = bit.left 32 1 - 1; - in - if result.a == 0 && result.b == 0 && bit.right 31 result.c == 0 || result.a == max32 && result.b == max32 && bit.right 31 result.c == 1 - then bit.or (bit.left 32 result.c) result.d - else { - ipv6 = result; - }; - - # left :: integer -> (ip | mac | integer) -> (ip | mac | integer) - left = i: right (-i); - - # right :: integer -> (ip | mac | integer) -> (ip | mac | integer) - right = - let - step = i: x: { - _1 = bit.mask 32 (bit.right (i + 96) x); - _2 = bit.mask 32 (bit.right (i + 64) x); - _3 = bit.mask 32 (bit.right (i + 32) x); - _4 = bit.mask 32 (bit.right i x); - _5 = bit.mask 32 (bit.right (i - 32) x); - _6 = bit.mask 32 (bit.right (i - 64) x); - _7 = bit.mask 32 (bit.right (i - 96) x); - }; - ors = builtins.foldl' bit.or 0; - in - i: x: - if x ? ipv6 - then - let - a' = step i x.ipv6.a; - b' = step i x.ipv6.b; - c' = step i x.ipv6.c; - d' = step i x.ipv6.d; - in - { - ipv6 = { - a = ors [ a'._4 b'._3 c'._2 d'._1 ]; - b = ors [ a'._5 b'._4 c'._3 d'._2 ]; - c = ors [ a'._6 b'._5 c'._4 d'._3 ]; - d = ors [ a'._7 b'._6 c'._5 d'._4 ]; - }; - } - else if x ? ipv4 - then { - ipv4 = bit.mask 32 (bit.right i x.ipv4); - } - else if x ? mac - then { - mac = bit.mask 48 (bit.right i x.mac); - } - else bit.right i x; - - # shadow :: integer -> (ip | mac | integer) -> (ip | mac | integer) - shadow = n: a: and (right n (left n (coerce a (-1)))) a; - - # coshadow :: integer -> (ip | mac | integer) -> (ip | mac | integer) - coshadow = n: a: and (not (right n (left n (coerce a (-1))))) a; - - # coerce :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) - coerce = target: value: - if target ? ipv6 - then - if value ? ipv6 - then value - else if value ? ipv4 - then { - ipv6 = { - a = 0; - b = 0; - c = 0; - d = value.ipv4; - }; - } - else if value ? mac - then { - ipv6 = { - a = 0; - b = 0; - c = bit.right 32 value.mac; - d = bit.mask 32 value.mac; - }; - } - else { - ipv6 = { - a = bit.mask 32 (bit.right 96 value); - b = bit.mask 32 (bit.right 64 value); - c = bit.mask 32 (bit.right 32 value); - d = bit.mask 32 value; - }; - } - else if target ? ipv4 - then - if value ? ipv6 - then { - ipv4 = value.ipv6.d; - } - else if value ? ipv4 - then value - else if value ? mac - then { - ipv4 = bit.mask 32 value.mac; - } - else { - ipv4 = bit.mask 32 value; - } - else if target ? mac - then - if value ? ipv6 - then { - mac = bit.or (bit.left 32 (bit.mask 16 value.ipv6.c)) value.ipv6.d; - } - else if value ? ipv4 - then { - mac = value.ipv4; - } - else if value ? mac - then value - else { - mac = bit.mask 48 value; - } - else - if value ? ipv6 - then builtins.foldl' bit.or 0 - [ - (bit.left 96 value.ipv6.a) - (bit.left 64 value.ipv6.b) - (bit.left 32 value.ipv6.c) - value.ipv6.d - ] - else if value ? ipv4 - then value.ipv4 - else if value ? mac - then value.mac - else value; - }; - - implementations = { - ip = { - # add :: (ip | mac | integer) -> ip -> ip - add = arithmetic.add; - - # diff :: ip -> ip -> (ipv6 | integer) - diff = arithmetic.diff; - - # subtract :: (ip | mac | integer) -> ip -> ip - subtract = arithmetic.subtract; - }; - - mac = { - # add :: (ip | mac | integer) -> mac -> mac - add = arithmetic.add; - - # diff :: mac -> mac -> (ipv6 | integer) - diff = arithmetic.diff; - - # subtract :: (ip | mac | integer) -> mac -> mac - subtract = arithmetic.subtract; - }; - - cidr = rec { - # add :: (ip | mac | integer) -> cidr -> cidr - add = delta: cidr: - let - size' = size cidr; - in - { - base = arithmetic.left size' (arithmetic.add delta (arithmetic.right size' cidr.base)); - inherit (cidr) length; - }; - - # capacity :: cidr -> integer - capacity = cidr: - let - size' = size cidr; - in - if size' > 62 - then 9223372036854775807 # maxBound to prevent overflow - else bit.left size' 1; - - # child :: cidr -> cidr -> bool - child = subcidr: cidr: - length subcidr > length cidr && contains (host 0 subcidr) cidr; - - # contains :: ip -> cidr -> bool - contains = ip: cidr: host 0 (make cidr.length ip) == host 0 cidr; - - # host :: (ip | mac | integer) -> cidr -> ip - host = index: cidr: - let - index' = arithmetic.coerce cidr.base index; - in - arithmetic.or (arithmetic.shadow cidr.length index') cidr.base; - - # length :: cidr -> integer - length = cidr: cidr.length; - - # netmask :: cidr -> ip - netmask = cidr: arithmetic.coshadow cidr.length (arithmetic.coerce cidr.base (-1)); - - # size :: cidr -> integer - size = cidr: (if cidr.base ? ipv6 then 128 else 32) - cidr.length; - - # subnet :: integer -> (ip | mac | integer) -> cidr -> cidr - subnet = length: index: cidr: - let - length' = cidr.length + length; - index' = arithmetic.coerce cidr.base index; - size = (if cidr.base ? ipv6 then 128 else 32) - length'; - in - make length' (host (arithmetic.left size index') cidr); - - # make :: integer -> ip -> cidr - make = length: base: - let - length' = math.clamp 0 (if base ? ipv6 then 128 else 32) length; - in - { - base = arithmetic.coshadow length' base; - length = length'; - }; - }; - }; - - typechecks = - let - - fail = description: function: argument: - builtins.throw "${function}: ${argument} parameter must be ${description}"; - - meta = parser: description: function: argument: input: - let - error = fail description function argument; - in - if !builtins.isString input - then error - else - let - result = parser input; - in - if builtins.isNull result - then error - else result; - - in - { - int = function: argument: input: - if builtins.isInt input - then input - else fail "an integer" function argument; - ip = meta parsers.ip "an IPv4 or IPv6 address"; - cidr = meta parsers.cidr "an IPv4 or IPv6 address range in CIDR notation"; - mac = meta parsers.mac "a MAC address"; - numeric = function: argument: input: - if builtins.isInt input - then input - else meta parsers.numeric "an integer or IPv4, IPv6 or MAC address" function argument input; - }; - -in - -{ - lib = { - inherit net; - }; +with builtins; +/** +ACL independent functions that can be used in parsers. +*/ +rec { + /** listOfSetsToSetByKey :: string -> list -> attrSet + * Example: + * 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: + listToAttrs ( + forEach list (elem: { + name = elem."${key}"; + value = removeAttrs elem [ key ]; + }) + ); + /** */ + mapListOfSetsToSetByKey = function: list: mapAttrs (name: value: function value) (listOfSetsToSetByKey list); + /** adds colons to a string every 4 characters for IPv6 shenanigans */ + addColonsToIPv6 = string: + if ((stringLength string) > 4) + then + ((substring 0 4 string) + ":" + (addColonsToIPv6 (substring 4 32 string))) + else string; + + /** pipeMap :: [(a_(n) -> a_(n+1)] -> [a_0] -> [a_end] + * equivelent to `builtins.map (lib.trivial.flip lib.trivial.pipe funcList) elems` + */ + pipeMap = + let + pipe = item: funcs: + if ((length funcs) == 0) + then item + else pipe ((head funcs) item) (tail funcs); + pipe' = funcs: item: pipe item funcs; + in + funcs: list: map (pipe' funcs) list; + + /** generate last 20 characters (80 bits) of the peer's IPv6 address */ + generateIPv6Suffix = peerName: substring 0 20 (hashString "sha256" peerName); + + /** generate the first 10 characters of the IPV6 address for the subnet name */ + generateIPv6Prefix = subnetName: "fd" + (substring 0 10 (hashString "sha256" subnetName)); + + /** generates a full IPv6 subnet */ + generateIPv6Subnet = subnetName: (addColonsToIPv6 (generateIPv6Prefix subnetName)) + "::/80"; + + /** generates a full IPv6 address */ + generateIPv6Address = subnetName: peerName: (addColonsToIPv6 ((generateIPv6Prefix subnetName) + (generateIPv6Suffix peerName))) + "/80"; } \ No newline at end of file diff --git a/persers/v1.nix b/persers/v1.nix new file mode 100644 index 0000000..db992cc --- /dev/null +++ b/persers/v1.nix @@ -0,0 +1,124 @@ +{lib, ...}: v1_acl: +with lib.attrsets; +with lib.lists; +with lib.trivial; +with (import ../lib.nix); +with builtins; +let + /** parsePeer :: acl_peer -> ic_peer */ + parsePeer = acl_peer: { + subnetConnections = listOfSetsToSetByKey "name" (pipeMap [subnetFromName (getSubnetConnectionAndName acl_peer)] acl_peer.subnets); + publicKey = acl_peer.publicKey; + privateKeyFile = acl_peer.privateKeyFile; + } // + (if acl_peer ? extraArgs then {extraArgs = acl_peer.extraArgs;} else {}) // + { + publicKey = acl_peer.publicKey; + privateKeyFile = acl_peer.privateKeyFile; + } // + (if acl_peer ? groups then {groups = map groupFromName acl_peer.groups;} else {groups = [];}); + + /** parseGroup :: acl_group -> ic_group */ + parseGroup = acl_group: { + peers = mapListOfSetsToSetByKey parsePeer (selectPeers [{type="group"; rule="is"; value="${acl_group.name}";}]); + } // (if acl_group ? extraArgs then {extraArgs = acl_group.extraArgs;} else {}); + + /** parseSubnet :: acl_subnet -> ic_subnet */ + parseSubnet = acl_subnet: { + peers = mapListOfSetsToSetByKey parsePeer (selectPeers [{type="subnet"; rule="is"; value="${acl_subnet.name}";}]); + } // (if acl_subnet ? extraArgs then {extraArgs = acl_subnet.extraArgs;} else {}); + + /** getSubnetConnection :: acl_peer -> acl_subnet -> (subnetConnection // {name}) */ + getSubnetConnectionAndName = acl_peer: acl_subnet: { + name = acl_subnet.name; # name gets removed shortly after, name is not in the actual subnetConnection object + subnet = parseSubnet acl_subnet; + ipAddresses = getIpAddresses acl_peer acl_subnet; + listenPort = acl_peer.subnets."${acl_subnet.name}".listenPort; + peerConnections = getPeerConnections acl_peer acl_subnet; + } // (if acl_peer.subnets."${acl_subnet.name}" ? extraArgs then {extraArgs = acl_peer.subnets."${acl_subnet.name}".extraArgs;} else {}); + + /** getIpAddresses :: acl_peer -> acl_subnet -> [str] */ + getIpAddresses = acl_peer: acl_subnet: + if (acl_peer.subnets."${acl_subnet.name}" ? ipAddresses) then ( + if (elem "auto" acl_peer.subnets."${acl_subnet.name}".ipAddresses) then ( + (remove "auto" acl_peer.subnets."${acl_subnet.name}".ipAddresses) ++ (singleton (generateIPv6Address acl_peer.name acl_subnet.name)) + ) else acl_peer.subnets."${acl_subnet.name}".ipAddresses + ) else (singleton (generateIPv6Address acl_peer.name acl_subnet.name)); + + /** getPeerConnections :: acl_peer -> acl_subnet -> str -> peerConnection */ + getPeerConnections = acl_peerFrom: acl_subnet: + let + filterSubnets = connection: elem acl_subnet.name connection.subnets; + filterPeer = key: acl_peer: connection: elem acl_peer.name (catAttrs "name" (selectPeers connection."${key}")); + getConnectionsX = key: filter (connection: all (x: x connection) [filterSubnets (filterPeer key acl_peerFrom)]) v1_acl.connections; + getConnectionsA = getConnectionsX "a"; + getConnectionsB = getConnectionsX "b"; + allPeers = unique ((concatMap (connection: selectPeers connection.b) getConnectionsA) ++ (concatMap (connection: selectPeers connection.a) getConnectionsB)); + allOtherPeers = remove acl_peerFrom allPeers; + getExtraArgs = acl_peerTo: + let + connections = (filter (filterPeer "a" acl_peerTo) getConnectionsB) ++ (filter (filterPeer "b" acl_peerTo) getConnectionsA); + extraArgsList = catAttrs "extraArgs" connections; + in + foldl' mergeAttrs {} extraArgsList; + in + listOfSetsToSetByKey "name" (map (acl_peerTo: + { + name = acl_peerTo.name; + peer = parsePeer acl_peerTo; + ipAddresses = getIpAddresses acl_peerTo acl_subnet; + endpoint = getEndpoint acl_peerFrom acl_peerTo; + extraArgs = getExtraArgs acl_peerTo; + }) allOtherPeers); + + /** getEndpoint :: acl_peer -> acl_peer -> ic_endpoint */ + getEndpoint = acl_peerFrom: acl_peerTo: + let + getAllEndpointMatches = filter (endpoint: elem acl_peerFrom.name (catAttrs "name" (selectPeers (if endpoint ? match then endpoint.match else [])))) acl_peerTo.endpoints; + in + removeAttrs (foldl' mergeAttrs {} getAllEndpointMatches) [ "match" ]; + + /** selectPeers :: [acl_filters] -> str -> [acl_peer] + * (str -> ic_peer) means it returns an attrset of peers keyed by name, typescript syntax: + * selectPeers(acl: acl, acl_filters: acl_filter[]): {[peerName: string]: ic_peer}; + */ + selectPeers = acl_filters: + if length acl_filters == 0 + then + v1_acl.peers + else + foldl' intersectAttrs (selectPeersSingleFilter (head acl_filters)) (map selectPeersSingleFilter acl_filters); + + /** selectPeersSingleFilter :: acl_filter -> [acl_peer] */ + selectPeersSingleFilter = acl_filter: + with acl_filter; + let + applyRule = comparison: if rule == "is" then comparison else if rule == "not" then !comparison else throw ("Unknown filter rule " + rule); + in + if type == "peer" then + (filter (acl_peer: applyRule (acl_peer.name == value)) v1_acl.peers) + else if type == "group" then + (filter (acl_peer: applyRule (elem value acl_peer.groups)) v1_acl.peers) + else if type == "subnet" then + (filter (acl_peer: applyRule (elem value (attrNames acl_peer.subnets))) v1_acl.peers) + else throw ("Unknown filter type " + type); + + groupFromName = groupName: findSingle + (group: group.name == groupName) + (throw "No group " + groupName) + (throw "Multiply defined group " + groupName) + v1_acl.groups; + + subnetFromName = subnetName: findSingle + (subnet: subnet.name == subnetName) + (throw "No subnet " + subnetName) + (throw "Multiply defined subnet " + subnetName) + v1_acl.subnets; + + +in +{ + peers = mapListOfSetsToSetByKey parsePeer v1_acl.peers; + subnets = mapListOfSetsToSetByKey parseSubnet v1_acl.subnets; + groups = mapListOfSetsToSetByKey parseGroup v1_acl.groups; +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..138bebe --- /dev/null +++ b/readme.md @@ -0,0 +1,307 @@ +WireNix is a Nix Flake designed to make creation of Wireguard mesh networks +easier. The simplist and most likely layout is a full mesh network, but Wirenix +is able to support arbitrary graph topologies. +# Reading the README +Due to Nix's typeless nature, I have opted to define all my configurations in +psuedo-typescript to make options more legible. I have chosen typescript +because it looks somewhat like JSON and is easy to understand. Examples will +still be given in Nix EL. + +You can start by reading the `ACL Configuration` section, then reading +`Quick Start` section for how to use configure your machines. Other sections +exist to provide helpful context and advanced usage, but should not be +necessary for a working setup. + +# Glosary +## ACL +Access Control List: +This is your shared configuration for the network. + +## Subnet +In Wirenix, the word subnet represents any network of connected peers. +In the implementation, subnets are keyed by their `name` property. Subnet names +define the initial 32 bits after `fd` in of an the IPv6 addresses peers +connecting to the subnet will use. Generally speaking, one subnet = one +wireguard interface for each client on the subnet. + +## Peer + In Wirenix, peer is any machine with a unique public key In the + implementation, peer names define last 80 bits of their IPv6 address. + +## Group +In Wirenix, a group is just a tag that peers can have. These are used for +matching peers and can contain arbitrary names. + +## Endpoint +In wirenix, an endpoint specifies external IP of a peer that other peers should +connect to. +In the ACL configuration, endpoints can exist on subnets, groups, and peers, +but these are just for convenience. Think of adding an endpoint to a subnet or +group as being the same as adding the endpoint to all peers in the subnet or +group. +Endpoints have filters, which can specify for which connecting clients the +endpoint will apply to. + +## Filter +In Wirenix, a filter is used to select peers by their subnets, groups, and +names. A filter is made up of filter rules, specifying multiple rules will +yield the intersection of those rules. +Note that selecting by peer name will always return a list of 1 or 0 entries, +on account of names needing to be unique. + +# ACL Configuration +The ACL is a nix attrset designed to be represented in JSON for easy importing +and potential use outside of the nix ecosystem. + +## top level acl: +```typescript +type ACL = { + version?: str; + subnets: subnet[]; + groups: group[]; + peers: peer[]; + connections: connection[]; + extraArgs?: any; +}; +``` +Version is used to check for config compatibility and is recommended. Not +specifying version will parse the configuration with the most recent parser +available and generate a warning. Using an older configuration version than +available will use the parser for that version and generate a warning. Using +a version newer than any parsers available will throw an error. + +## subnet: +```typescript +type subnet = { + name: str; + endpoints?: endpoint[]; + extraArgs?: any; +}; +``` + +## Group: +```typescript +type group = { + name: str; + endpoints?: endpoint[]; + extraArgs?: any; +}; +``` + +## Peer: +```typescript +type peer = { + name: str; + subnets: [subnetName: str]: { + listenPort: int; + ipAddresses?: str[]; + extraArgs?: any; + }; + groups?: str[]; + endpoints?: endpoint[]; + extraArgs?: any; +}; +``` + +## Connection: +```typescript +type connection = { + a: filter; + b: filter; + subnets: str[]; + extraArgs?: any; +}; +``` + +Connections connect all peers matching filter `a` to all peers matching +filter `b`, and all peers matching filter `b` to all peers matching filter `a` +subnets filters the connection to only be made over the subnets listed. It is +recomended to use the subnets property iff the `subnet` filter is also used +(the `subnet` filter on its own will connect all shared subnets of machines in +`a` and `b`, even subnets not mentioned in the filters if they are shared). + +## Endpoint: +```typescript +type endpoint = { + match?: filter; + ip?: str; + port?: int; + persistentKeepalive?: int; + dynamicEndpointRefreshSeconds?: int; + dynamicEndpointRefreshRestartSeconds?: int; + extraArgs?: any; +}; +``` +Endpoints are merged in this order: First lists of endpoints are merged top to +bottom, with the bottom endpoints overriding the top ones. Then, lists are +merged in this order: subnet -> group -> peer; with peer being the highest +priority, overriding others. A good layout is to set ports in subnet, ip in +peer, and leave group empty. group endpoints can be useful for specifying +connection details across port forwarded NATs, however. + +## Filter: +```typescript +type filter = { + type: ["peer" | "group" | "subnet"]; + rule: [ "is" | "not" ]; + value: str; +}[]; // <==== Important! It's a list +``` + +## extraArgs +`extraArgs` is intentionally left alone. I promise I won't ever set +`extraArgs`, but any value in it will be forwarded on to the corresponding +section in the intermediate configuration. Because of this, it can be used to +pass data into user defined Configuration Modules. Most users can ignore +`extraArgs`. + +# Architecture +WireNix consists of 4 main components: +1. The shared ACL Configuration +2. Parser Modules +3. The intermediate Configuration +4. Configuration Modules +The goal of splitting WireNix into modules is both for my own sanity when +developing, and to make it hackable without requiring users to make their own +fork. Users are able to specify their own Parser Modules, enabling them to use +their own preferred ACL syntax if they desire. Users can also specify their own +configuration modules, allowing them to add compatibility to for other network +stacks or to enable their own modules. Using both custom Parser and +Configuration modules enables essentially rewriting this flake however you see +fit, all without making a fork (although at that point I may question why you +don't write your own module from scratch). + +## ACL +The shared ACL configuration should describe the full network topology. It does +not need to consist only of NixOS peers (although at the moment, other peers +will have to be configured manually to conform to the expected settings). The +details of this file are documented in the `Top Level ACL` section. +You can make your own ACL configuration format so long as you keep the +`version` field and set it to some unique name. You can then register your +parser which takes your ACL and produces an intermediate configuration like so: + +TODO + +## Parser Modules +Parser Modules are responsible for taking an ACL and converting it to the +intermediate configuration format. Parser modules are selected by matching the +ACL version field. A parser module must take an ACL and return the +corresponding Intermediate Configuration You can register your own parser +module like so: + +TODO + +## Intermediate Configuration +The Intermediate Configuration is a recursive attrset that is more suited for +being used in a NixOS configuration than the ACL Configuration. +Unlike the ACL, the intermediate configuration is more verbose, easier to +traverse, repeats itself often, and is recursive. This allows cross version +compatibility so long as the intermediate configuration doesn't change. Any +changes will likely only be the addition of optional features that do not +interfere with existing intermediate configuration use, though at this stage +there are no guarentees. +It can be assumed that all types mentioned are types for the intermediate +connection and NOT the related to types in the ACL. The intermediate +configuration has the following structure: + +### Root Structure +```typescript +type intermediateConfiguration = { + peers: {[peerName: string]: peer}; + subnets: {[subnetName: string]: subnet}; + groups: {[groupName: string]: group}; +} +``` + +### Peer +```typescript +type peer = { + subnetConnections: {[subnetName: string]: subnetConnection}; + groups: {[groupName: string]: group} + publicKey: string; + privateKeyFile: string; + extraArgs?: any; +}; +``` + + +### Subnet +```typescript +type subnet = { + peers: {[peerName: string]: peer}; + extraArgs?: any; +}; +``` + +### Group +```typescript +type group = { + peers: {[peerName: string]: peer}; + extraArgs?: any; +}; +``` + +### Peer Connection +```typescript +type peerConnection = { + peer: peer; + ipAddresses: string[]; + endpoint: endpoint; + extraArgs?: any; +}; +``` + +### Subnet Connection +```typescript +type subnetConnection = { + subnet: subnet; + ipAddresses: string[]; + listenPort: int; + peerConnections: {[peerName: string]: peerConnection}; + extraArgs?: any; +}; +``` + +### Endpoint +```typescript +type endpoint = { + ip: str; + port: int; + persistentKeepalive?: int; + dynamicEndpointRefreshSeconds?: int; + dynamicEndpointRefreshRestartSeconds?: int; + extraArgs?: any; +}; +``` + +Unlike the ACL, this structure is recursive, resembling an arbitrary graph. +This graph can be traversed back and forth in circles until you run out of +stack space. + +## Configuration Modules +Configuration Modules take the Intermediate Configuration and produce NixOS +configurations from them. By default, there exist configuration modules for +setting up wireguard with the static network configuration, networkd, and +Network Manager. There is a fourth, "default" configuration module that +intelligently selects which module to use (with priority being networkd > +network manager > static configuration). However, you can manually override +which module is used (or use your own module) in your flake.nix file: + +TODO + +# Integrations: +By default, WireNix supports setting wireguard keypairs with agenix-rekey if +the module is detected (via the existence of `config.rekey`). WireNix also +supports networkd, network manager, and the nixos static network configuration +(in that order of preference). + +# Current Issues / Drawbacks +- To keep configuration and the initial POC simpler, assigning IP addresses +manually on subnets is not supported (yet). +- WireNix does not do NAT traversal, it's up to you to forward the correct +ports on your NAT device(s) and apply the correct firewall rules on your +router(s). +- WireNix does not allow for dynamic addition of peers. If you need something +more dynamic, look into Tailscale/Headscale. +- Peers cannot have multiple keys. If this is a desirable feature I may think +of adding it, but I cannot think of a good reason for it. \ No newline at end of file diff --git a/test-data.nix b/test-data.nix deleted file mode 100644 index d0ec138..0000000 --- a/test-data.nix +++ /dev/null @@ -1,120 +0,0 @@ -{ - version = "1"; - subnets = [ - { 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 = [ - - ]; - groups = [ - - ]; - connections = [ - { group = "everyoneConnectsToMe"; } - ]; - privateKeyFile = "/not/yet"; - publicKey = "testData"; - presharedKeyFile = "testData2"; - } - { - 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" - ]; - groups = [ - - ]; - connections = [ - { group = "everyoneConnectsToMe"; } - { group = "subnet one group"; } - ]; - privateKeyFile = "/not/yet"; - publicKey = "testData"; - presharedKeyFile = "testData2"; - } - { - 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" - ]; - groups = [ - "everyoneConnectsToMe" - "subnet two group" - ]; - connections = [ - { group = "everyoneConnectsToMe"; } - { group = "subnet two group"; } - ]; - privateKeyFile = "/not/yet"; - publicKey = "testData"; - presharedKeyFile = "testData2"; - } - { - 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" - "subnet.three" - ]; - groups = [ - "everyoneConnectsToMe" - "subnet two group" - ]; - connections = [ - { group = "everyoneConnectsToMe"; } - { group = "subnet two group"; } - ]; - privateKeyFile = "/not/yet"; - publicKey = "testData"; - presharedKeyFile = "testData2"; - } - { - 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" - ]; - groups = [ - - ]; - connections = [ - { group = "everyoneConnectsToMe"; } - { peer = "peer.one"; } - ]; - privateKeyFile = "/not/yet"; - publicKey = "testData"; - presharedKeyFile = "testData2"; - } - ]; -} \ No newline at end of file diff --git a/wire.nix b/wire.nix index 4c9eae9..e9d2134 100644 --- a/wire.nix +++ b/wire.nix @@ -1,127 +1,5 @@ -{ config, lib, pkgs, ... }: +{ config, lib, ... }@inputs: with lib; -let - has-rekey = config ? rekey; - peerOpts = { - options = { - subnets = mkOption { - default = []; - type = with types; listOf str; - description = '' - subnets the peer belongs to - ''; - }; - groups = mkOption { - default = true; - type = with types; listOf str; - description = '' - groups the peer belongs to - ''; - }; - peers = { - default = true; - type = with types; listOf set; - description = mdDoc '' - Peers the peer is connected to, can be one of `{ peer = "peerName"; }` - or `{ group = "groupname"; }`. Remember to configure this for *both* peers. - The best way to do this is with a simple full mesh network, where all peers - belong to one group ("groupA"), and their peers are `{ group = "groupA"}`. - ''; - }; - privateKeyFile = mkOption { - example = "/private/wireguard_key"; - type = with types; nullOr str; - default = null; - description = mdDoc '' - Private key file as generated by {command}`wg genkey`. - ''; - }; - name = mkOption { - 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; - description = mdDoc "The base64 public key of the peer."; - }; - presharedKeyFile = mkOption { - default = null; - example = "/private/wireguard_psk"; - type = with types; nullOr str; - description = mdDoc '' - File pointing to preshared key as generated by {command}`wg genpsk`. - Optional, and may be omitted. This option adds an additional layer of - symmetric-key cryptography to be mixed into the already existing - public-key cryptography, for post-quantum resistance. - ''; - }; - }; - }; - subnetOpts = { - options = { - name = mkOption { - default = "wireguard"; - example = "mySubnet.myDomain.me"; - 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 = { - options = { - subnets = mkOption { - default = {}; - type = with types; listOf (submodule subnetOpts); - description = '' - Subnets in the mesh network(s) - ''; - }; - peers = mkOption { - default = {}; - type = with types; listOf (submodule peerOpts); - description = '' - Peers in the mesh network(s) - ''; - }; - }; - }; -in { options = { wirenix = { @@ -142,9 +20,33 @@ in `wirenix.config.peers.*.name` ''; }; - config = mkOption { + configurer = mkOption { + default = "auto"; + type = types.str; + description = mdDoc '' + Configurer to use. Builtin values can be "auto", "networkmanager", or "networkd". + See the `additionalConfigurers` for adding more options. + ''; + }; + additionalConfigurers = mkOption { + default = "auto"; + type = with types; attrsOf (functionTo attrset); + description = mdDoc '' + Additional configurers to load, with their names being used to select from the + configurer option. + ''; + }; + additionalParsers = mkOption { + default = "auto"; + type = with types; attrsOf (functionTo attrset); + description = mdDoc '' + Additional parsers to load, with their names being used to compare to the acl's + "version" feild. + ''; + }; + aclConfig = mkOption { default = {}; - type = with types; setOf (submodule configOpts); + type = types.attrset; description = '' Shared configuration file that describes all clients ''; @@ -154,12 +56,21 @@ in # --------------------------------------------------------------- # - config = lib.mkIf (config.modules.wirenix.enable) (lib.mkMerge [ - (lib.mkIf (has-rekey) { - environment.etc.rekey.text = "yes"; - }) - (lib.mkIf (!has-rekey ) { - environment.etc.rekey.text = "no"; - }) - ]); + config = + let + configurers = rec { + auto = static; + static = import ./configurers/static.nix; + networkd = import ./configurers/networkd.nix; + networkmanager = import ./configurers/networkmanager.nix; + } // config.modules.wirenix.additionalConfigurers; + parsers = { + v1 = import ./parsers/v1.nix; + } // config.modules.wirenix.additionalParsers; + acl = config.modules.wirenix.aclConfig; + parser = parsers."${acl.version}" inputs; + configurer = configurers."${config.modules.wirenix.configurer}" inputs; + in + lib.mkIf (config.modules.wirenix.enable) + configurer (parser acl); } \ No newline at end of file