diff options
| author | Gregor Kleen <gkleen@yggdrasil.li> | 2021-11-03 23:28:00 +0100 |
|---|---|---|
| committer | Gregor Kleen <gkleen@yggdrasil.li> | 2021-11-03 23:28:00 +0100 |
| commit | 3d5b1509be78c0e9d8923af7e63f38d9dcbdefdf (patch) | |
| tree | ab98231775abe2dc7ec29b98d0f6444a7b7d56a7 /modules/yggdrasil-wg/default.nix | |
| parent | c47b09353f424eed1ef99bb41a9285ac87b051f2 (diff) | |
| download | nixos-3d5b1509be78c0e9d8923af7e63f38d9dcbdefdf.tar nixos-3d5b1509be78c0e9d8923af7e63f38d9dcbdefdf.tar.gz nixos-3d5b1509be78c0e9d8923af7e63f38d9dcbdefdf.tar.bz2 nixos-3d5b1509be78c0e9d8923af7e63f38d9dcbdefdf.tar.xz nixos-3d5b1509be78c0e9d8923af7e63f38d9dcbdefdf.zip | |
yggdrasil-wg: dual stack
Diffstat (limited to 'modules/yggdrasil-wg/default.nix')
| -rw-r--r-- | modules/yggdrasil-wg/default.nix | 173 |
1 files changed, 96 insertions, 77 deletions
diff --git a/modules/yggdrasil-wg/default.nix b/modules/yggdrasil-wg/default.nix index cef1ce4e..55064baa 100644 --- a/modules/yggdrasil-wg/default.nix +++ b/modules/yggdrasil-wg/default.nix | |||
| @@ -4,10 +4,13 @@ with lib; | |||
| 4 | 4 | ||
| 5 | let | 5 | let |
| 6 | listenPort = 51820; | 6 | listenPort = 51820; |
| 7 | wgSubnet = "2a03:4000:52:ada:1"; | 7 | wgSubnet = { |
| 8 | "4" = "2a03:4000:52:ada:2"; | ||
| 9 | "6" = "2a03:4000:52:ada:3"; | ||
| 10 | }; | ||
| 8 | wgSubnetLength = 80; | 11 | wgSubnetLength = 80; |
| 9 | wgHostLength = wgSubnetLength + 16; | 12 | wgHostLength = wgSubnetLength + 16; |
| 10 | batSubnet = "2a03:4000:52:ada:2"; | 13 | batSubnet = "2a03:4000:52:ada:1"; |
| 11 | batSubnetLength = 80; | 14 | batSubnetLength = 80; |
| 12 | batHostLength = batSubnetLength + 16; | 15 | batHostLength = batSubnetLength + 16; |
| 13 | 16 | ||
| @@ -16,27 +19,49 @@ let | |||
| 16 | to = "surtr"; | 19 | to = "surtr"; |
| 17 | endpointHost = "202.61.241.61"; | 20 | endpointHost = "202.61.241.61"; |
| 18 | PersistentKeepalive = 25; | 21 | PersistentKeepalive = 25; |
| 22 | family = "4"; | ||
| 23 | } | ||
| 24 | { from = "vidhar"; | ||
| 25 | to = "surtr"; | ||
| 26 | endpointHost = "2a03:4000:52:ada::"; | ||
| 27 | PersistentKeepalive = 25; | ||
| 28 | family = "6"; | ||
| 29 | } | ||
| 30 | { from = "sif"; | ||
| 31 | to = "surtr"; | ||
| 32 | endpointHost = "202.61.241.61"; | ||
| 33 | PersistentKeepalive = 25; | ||
| 34 | family = "4"; | ||
| 19 | } | 35 | } |
| 20 | { from = "sif"; | 36 | { from = "sif"; |
| 21 | to = "surtr"; | 37 | to = "surtr"; |
| 22 | endpointHost = "2a03:4000:52:ada::"; | 38 | endpointHost = "2a03:4000:52:ada::"; |
| 23 | PersistentKeepalive = 25; | 39 | PersistentKeepalive = 25; |
| 40 | family = "6"; | ||
| 24 | } | 41 | } |
| 25 | { from = "sif"; | 42 | { from = "sif"; |
| 26 | to = "vidhar"; | 43 | to = "vidhar"; |
| 27 | endpointHost = "192.168.2.168"; | 44 | endpointHost = "192.168.2.168"; |
| 28 | PersistentKeepalive = 25; | 45 | PersistentKeepalive = 25; |
| 46 | family = "4"; | ||
| 29 | } | 47 | } |
| 30 | ]; | 48 | ]; |
| 31 | wgHostIPs = { | 49 | wgHostIPs = mapAttrs (_family: wgSubnet: { |
| 32 | surtr = "${wgSubnet}::/${toString wgHostLength}"; | 50 | surtr = "${wgSubnet}::/${toString wgHostLength}"; |
| 33 | vidhar = "${wgSubnet}:1::/${toString wgHostLength}"; | 51 | vidhar = "${wgSubnet}:1::/${toString wgHostLength}"; |
| 34 | sif = "${wgSubnet}:2::/${toString wgHostLength}"; | 52 | sif = "${wgSubnet}:2::/${toString wgHostLength}"; |
| 35 | }; | 53 | }) wgSubnet; |
| 36 | greHostMACPrefixes = { | 54 | greHostMACPrefixes = { |
| 37 | surtr = "02:00:01:00:00"; | 55 | "4" = { |
| 38 | vidhar = "02:00:01:00:01"; | 56 | surtr = "02:00:01:00:00"; |
| 39 | sif = "02:00:01:00:02"; | 57 | vidhar = "02:00:01:00:01"; |
| 58 | sif = "02:00:01:00:02"; | ||
| 59 | }; | ||
| 60 | "6" = { | ||
| 61 | surtr = "02:00:02:00:00"; | ||
| 62 | vidhar = "02:00:02:00:01"; | ||
| 63 | sif = "02:00:02:00:02"; | ||
| 64 | }; | ||
| 40 | }; | 65 | }; |
| 41 | batHostMACs = { | 66 | batHostMACs = { |
| 42 | surtr = "02:00:00:00:00:00"; | 67 | surtr = "02:00:00:00:00:00"; |
| @@ -49,46 +74,47 @@ let | |||
| 49 | sif = ["${batSubnet}:2::/${toString batHostLength}"]; | 74 | sif = ["${batSubnet}:2::/${toString batHostLength}"]; |
| 50 | }; | 75 | }; |
| 51 | 76 | ||
| 52 | mkPublicKeyPath = host: ./hosts + "/${host}.pub"; | 77 | mkPublicKeyPath = family: host: ./hosts + "/${family}" + "/${host}.pub"; |
| 53 | mkPrivateKeyPath = host: ./hosts + "/${host}.priv"; | 78 | mkPrivateKeyPath = family: host: ./hosts + "/${family}" + "/${host}.priv"; |
| 54 | 79 | ||
| 55 | kernel = config.boot.kernelPackages; | 80 | kernel = config.boot.kernelPackages; |
| 56 | 81 | ||
| 57 | publicKeyPath = mkPublicKeyPath hostName; | 82 | publicKeyPath = family: mkPublicKeyPath family hostName; |
| 58 | privateKeyPath = mkPrivateKeyPath hostName; | 83 | privateKeyPath = family: mkPrivateKeyPath family hostName; |
| 59 | inNetwork = pathExists privateKeyPath && pathExists publicKeyPath; | 84 | inNetwork' = family: pathExists (privateKeyPath family) && pathExists (publicKeyPath family); |
| 60 | hostLinks = filter ({ from, to, ... }: thisHost from || thisHost to) links; | 85 | inNetwork = any inNetwork' families; |
| 61 | linkToPeer = opts@{from, to, ...}: | 86 | hostLinks = filterAttrs (_family: links: links != []) (mapAttrs (_family: filter ({ from, to, ... }: thisHost from || thisHost to)) links); |
| 87 | linkToPeer = family: opts@{from, to, ...}: | ||
| 62 | let | 88 | let |
| 63 | other = if thisHost from then to else from; | 89 | other = if thisHost from then to else from; |
| 64 | in { | 90 | in { |
| 65 | AllowedIPs = wgHostIPs.${other}; | 91 | AllowedIPs = wgHostIPs.${family}.${other}; |
| 66 | PublicKey = trim (readFile (mkPublicKeyPath other)); | 92 | PublicKey = trim (readFile (mkPublicKeyPath family other)); |
| 67 | } // (optionalAttrs (thisHost from) (linkCfgFilterCustom opts // linkMkEndpointCfg opts)); | 93 | } // (optionalAttrs (thisHost from) (linkCfgFilterCustom opts // linkMkEndpointCfg opts)); |
| 68 | linkCfgFilterCustom = filterAttrs (n: _v: !(elem n ["from" "to" "endpointHost"])); | 94 | linkCfgFilterCustom = filterAttrs (n: _v: !(elem n ["from" "to" "endpointHost"])); |
| 69 | linkMkEndpointCfg = opts@{from, ...}: optionalAttrs (opts ? "endpointHost" && thisHost from) { Endpoint = "${opts.endpointHost}:${toString listenPort}"; }; | 95 | linkMkEndpointCfg = opts@{from, ...}: optionalAttrs (opts ? "endpointHost" && thisHost from) { Endpoint = "${opts.endpointHost}:${toString listenPort}"; }; |
| 70 | linkToGreDev = opts@{from, to, ...}: | 96 | linkToGreDev = family: opts@{from, to, ...}: |
| 71 | let | 97 | let |
| 72 | other = if thisHost from then to else from; | 98 | other = if thisHost from then to else from; |
| 73 | in nameValuePair "yggre-${other}" { | 99 | in nameValuePair "yggre-${other}-${family}" { |
| 74 | netdevConfig = { | 100 | netdevConfig = { |
| 75 | Name = "yggre-${other}"; | 101 | Name = "yggre-${other}-${family}"; |
| 76 | Kind = "ip6gretap"; | 102 | Kind = "ip6gretap"; |
| 77 | }; | 103 | }; |
| 78 | tunnelConfig = { | 104 | tunnelConfig = { |
| 79 | Local = stripSubnet wgHostIPs.${hostName}; | 105 | Local = stripSubnet wgHostIPs.${family}.${hostName}; |
| 80 | Remote = stripSubnet wgHostIPs.${other}; | 106 | Remote = stripSubnet wgHostIPs.${family}.${other}; |
| 81 | }; | 107 | }; |
| 82 | }; | 108 | }; |
| 83 | linkToGreNetwork = ix: opts@{from, to, ...}: | 109 | linkToGreNetwork = family: ix: opts@{from, to, ...}: |
| 84 | let | 110 | let |
| 85 | other = if thisHost from then to else from; | 111 | other = if thisHost from then to else from; |
| 86 | in nameValuePair "yggre-${other}" { | 112 | in nameValuePair "yggre-${other}" { |
| 87 | matchConfig = { | 113 | matchConfig = { |
| 88 | Name = "yggre-${other}"; | 114 | Name = "yggre-${other}-${family}"; |
| 89 | }; | 115 | }; |
| 90 | linkConfig = { | 116 | linkConfig = { |
| 91 | MACAddress = "${greHostMACPrefixes.${hostName}}:${toHexByte ix}"; | 117 | MACAddress = "${greHostMACPrefixes.${family}.${hostName}}:${toHexByte ix}"; |
| 92 | RequiredForOnline = false; | 118 | RequiredForOnline = false; |
| 93 | }; | 119 | }; |
| 94 | networkConfig = { | 120 | networkConfig = { |
| @@ -96,71 +122,72 @@ let | |||
| 96 | LinkLocalAddressing = "no"; | 122 | LinkLocalAddressing = "no"; |
| 97 | }; | 123 | }; |
| 98 | }; | 124 | }; |
| 99 | 125 | familyToYggdrasilDev = family: nameValuePair "yggdrasil-wg-${family}" { | |
| 100 | thisHost = host: builtins.match "^(ipv(4|6)\.)?${hostName}$" host != null; | 126 | netdevConfig = { |
| 127 | Name = "yggdrasil-wg-${family}"; | ||
| 128 | Kind = "wireguard"; | ||
| 129 | }; | ||
| 130 | wireguardConfig = { | ||
| 131 | PrivateKeyFile = config.sops.secrets."yggdrasil-wg-${family}.priv".path; | ||
| 132 | ListenPort = listenPort; | ||
| 133 | }; | ||
| 134 | wireguardPeers = map (opts@{to, from, ...}: { wireguardPeerConfig = linkToPeer family opts; }) hostLinks.${family}; | ||
| 135 | }; | ||
| 136 | familyToYggdrasilNetwork = family: nameValuePair "yggdrasil-wg-${family}" { | ||
| 137 | name = "yggdrasil-wg-${family}"; | ||
| 138 | matchConfig = { | ||
| 139 | Name = "yggdrasil-wg-${family}"; | ||
| 140 | }; | ||
| 141 | address = [wgHostIPs.${family}.${hostName}]; | ||
| 142 | routes = [ | ||
| 143 | { routeConfig = { | ||
| 144 | Destination = "${wgSubnet.${family}}::/${toString wgSubnetLength}"; | ||
| 145 | }; | ||
| 146 | } | ||
| 147 | ]; | ||
| 148 | linkConfig = { | ||
| 149 | RequiredForOnline = false; | ||
| 150 | }; | ||
| 151 | networkConfig = { | ||
| 152 | Tunnel = map (opts@{from, to, ...}: let other = if thisHost from then to else from; in "yggre-${other}-${family}") hostLinks.${family}; | ||
| 153 | }; | ||
| 154 | }; | ||
| 155 | familyToSopsSecret = family: nameValuePair "yggdrasil-wg-${family}.priv" (mkIf (pathExists (privateKeyPath family)) { | ||
| 156 | format = "binary"; | ||
| 157 | sopsFile = privateKeyPath family; | ||
| 158 | mode = "0640"; | ||
| 159 | owner = "root"; | ||
| 160 | group = "systemd-network"; | ||
| 161 | }); | ||
| 162 | |||
| 163 | thisHost = host: host == hostName; | ||
| 101 | trim = str: if hasSuffix "\n" str then trim (removeSuffix "\n" str) else str; | 164 | trim = str: if hasSuffix "\n" str then trim (removeSuffix "\n" str) else str; |
| 102 | stripSubnet = addr: let matchRes = builtins.match "^(.*)/[0-9]+$" addr; in if matchRes == null then addr else elemAt matchRes 0; | 165 | stripSubnet = addr: let matchRes = builtins.match "^(.*)/[0-9]+$" addr; in if matchRes == null then addr else elemAt matchRes 0; |
| 103 | optIx = optName: xs: let | 166 | optIx = optName: xs: let |
| 104 | withOpts = listToAttrs (imap0 (ix: x: nameValuePair x.name (x.value // { ${optName} = ix; })) (filter (x: x.value.${optName} or false) (imap0 (ix: nameValuePair (toString ix)) xs))); | 167 | withOpts = listToAttrs (imap0 (ix: x: nameValuePair x.name (x.value // { ${optName} = ix; })) (filter (x: x.value.${optName} or false) (imap0 (ix: nameValuePair (toString ix)) xs))); |
| 105 | withoutOpts = listToAttrs (map (nv: nameValuePair nv.name (removeAttrs nv.value [optName])) (filter (x: !(x.value.${optName} or false)) (imap0 (ix: nameValuePair (toString ix)) xs))); | 168 | withoutOpts = listToAttrs (map (nv: nameValuePair nv.name (removeAttrs nv.value [optName])) (filter (x: !(x.value.${optName} or false)) (imap0 (ix: nameValuePair (toString ix)) xs))); |
| 106 | in genList (ix: withOpts.${toString ix} or withoutOpts.${toString ix}) (length xs); | 169 | in genList (ix: withOpts.${toString ix} or withoutOpts.${toString ix}) (length xs); |
| 107 | mkLinks = id; | 170 | groupFamilies = links: mapAttrs (_name: value: map (filterAttrs (k: _v: k != "family")) value) (groupBy (x: x.family) links); |
| 171 | mkLinks = groupFamilies; | ||
| 172 | families = attrNames links; | ||
| 173 | hostFamilies = attrNames hostLinks; | ||
| 108 | toHexByte = n: let | 174 | toHexByte = n: let |
| 109 | hex = toHexString n; | 175 | hex = toHexString n; |
| 110 | in if (stringLength hex < 2) then "0${hex}" else hex; | 176 | in if (stringLength hex < 2) then "0${hex}" else hex; |
| 111 | in { | 177 | in { |
| 112 | config = { | 178 | config = { |
| 113 | assertions = [ | ||
| 114 | { assertion = inNetwork || !(pathExists privateKeyPath || pathExists publicKeyPath); | ||
| 115 | message = "yggdrasil-wg: Either both public and private keys must exist or neither."; | ||
| 116 | } | ||
| 117 | { assertion = !inNetwork || (wgHostIPs ? "${hostName}"); | ||
| 118 | message = "yggdrasil-wg: Entry in wgHostIPs must exist."; | ||
| 119 | } | ||
| 120 | ] ++ map ({from, to, ...}: let other = if thisHost from then to else from; in { assertion = pathExists (mkPublicKeyPath other); message = "yggdrasil-wg: This host (${hostName}) has a link with ‘${other}’, but no public key is available for ‘${other}’."; }) hostLinks; | ||
| 121 | |||
| 122 | systemd.network = mkIf inNetwork { | 179 | systemd.network = mkIf inNetwork { |
| 123 | enable = true; | 180 | enable = true; |
| 124 | netdevs = { | 181 | netdevs = { |
| 125 | yggdrasil-wg = { | ||
| 126 | netdevConfig = { | ||
| 127 | Name = "yggdrasil-wg"; | ||
| 128 | Kind = "wireguard"; | ||
| 129 | }; | ||
| 130 | wireguardConfig = { | ||
| 131 | PrivateKeyFile = config.sops.secrets."yggdrasil-wg.priv".path; | ||
| 132 | ListenPort = listenPort; | ||
| 133 | }; | ||
| 134 | wireguardPeers = map (opts@{to, from, ...}: { wireguardPeerConfig = linkToPeer opts; }) hostLinks; | ||
| 135 | }; | ||
| 136 | yggdrasil = { | 182 | yggdrasil = { |
| 137 | netdevConfig = { | 183 | netdevConfig = { |
| 138 | Name = "yggdrasil"; | 184 | Name = "yggdrasil"; |
| 139 | Kind = "batadv"; | 185 | Kind = "batadv"; |
| 140 | }; | 186 | }; |
| 141 | }; | 187 | }; |
| 142 | } // listToAttrs (map linkToGreDev hostLinks); | 188 | } // listToAttrs (map familyToYggdrasilDev hostFamilies) // listToAttrs (concatMap (family: map (linkToGreDev family) hostLinks.${family}) hostFamilies); |
| 143 | 189 | ||
| 144 | networks = { | 190 | networks = { |
| 145 | yggdrasil-wg = { | ||
| 146 | name = "yggdrasil-wg"; | ||
| 147 | matchConfig = { | ||
| 148 | Name = "yggdrasil-wg"; | ||
| 149 | }; | ||
| 150 | address = [wgHostIPs.${hostName}]; | ||
| 151 | routes = [ | ||
| 152 | { routeConfig = { | ||
| 153 | Destination = "${wgSubnet}::/${toString wgSubnetLength}"; | ||
| 154 | }; | ||
| 155 | } | ||
| 156 | ]; | ||
| 157 | linkConfig = { | ||
| 158 | RequiredForOnline = false; | ||
| 159 | }; | ||
| 160 | networkConfig = { | ||
| 161 | Tunnel = map (opts@{from, to, ...}: let other = if thisHost from then to else from; in "yggre-${other}") hostLinks; | ||
| 162 | }; | ||
| 163 | }; | ||
| 164 | yggdrasil = { | 191 | yggdrasil = { |
| 165 | name = "yggdrasil"; | 192 | name = "yggdrasil"; |
| 166 | matchConfig = { | 193 | matchConfig = { |
| @@ -178,18 +205,10 @@ in { | |||
| 178 | RequiredForOnline = false; | 205 | RequiredForOnline = false; |
| 179 | }; | 206 | }; |
| 180 | }; | 207 | }; |
| 181 | } // listToAttrs (imap0 linkToGreNetwork hostLinks); | 208 | } // listToAttrs (map familyToYggdrasilNetwork hostFamilies) // listToAttrs (concatMap (family: imap0 (linkToGreNetwork family) hostLinks.${family}) hostFamilies); |
| 182 | }; | 209 | }; |
| 183 | 210 | ||
| 184 | sops.secrets = { | 211 | sops.secrets = listToAttrs (map familyToSopsSecret hostFamilies); |
| 185 | "yggdrasil-wg.priv" = mkIf (pathExists privateKeyPath) { | ||
| 186 | format = "binary"; | ||
| 187 | sopsFile = privateKeyPath; | ||
| 188 | mode = "0640"; | ||
| 189 | owner = "root"; | ||
| 190 | group = "systemd-network"; | ||
| 191 | }; | ||
| 192 | }; | ||
| 193 | 212 | ||
| 194 | networking.hosts = mkIf inNetwork (listToAttrs (concatMap ({name, value}: map (ip: nameValuePair (stripSubnet ip) ["${name}.yggdrasil"]) value) (mapAttrsToList nameValuePair batHostIPs))); | 213 | networking.hosts = mkIf inNetwork (listToAttrs (concatMap ({name, value}: map (ip: nameValuePair (stripSubnet ip) ["${name}.yggdrasil"]) value) (mapAttrsToList nameValuePair batHostIPs))); |
| 195 | 214 | ||
