at 24.11-pre 8.3 kB view raw
1{ config 2, lib 3, options 4, pkgs 5, ... 6}: 7let 8 inherit (lib) 9 attrValues 10 concatLines 11 concatMap 12 filter 13 filterAttrsRecursive 14 flatten 15 getExe 16 mkIf 17 optional 18 ; 19 20 cfg = config.services.rosenpass; 21 opt = options.services.rosenpass; 22 settingsFormat = pkgs.formats.toml { }; 23in 24{ 25 options.services.rosenpass = 26 let 27 inherit (lib) 28 literalExpression 29 mkOption 30 ; 31 inherit (lib.types) 32 enum 33 listOf 34 nullOr 35 path 36 str 37 submodule 38 ; 39 in 40 { 41 enable = lib.mkEnableOption "Rosenpass"; 42 43 package = lib.mkPackageOption pkgs "rosenpass" { }; 44 45 defaultDevice = mkOption { 46 type = nullOr str; 47 description = "Name of the network interface to use for all peers by default."; 48 example = "wg0"; 49 }; 50 51 settings = mkOption { 52 type = submodule { 53 freeformType = settingsFormat.type; 54 55 options = { 56 public_key = mkOption { 57 type = path; 58 description = "Path to a file containing the public key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`."; 59 }; 60 61 secret_key = mkOption { 62 type = path; 63 description = "Path to a file containing the secret key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`."; 64 }; 65 66 listen = mkOption { 67 type = listOf str; 68 description = "List of local endpoints to listen for connections."; 69 default = [ ]; 70 example = literalExpression "[ \"0.0.0.0:10000\" ]"; 71 }; 72 73 verbosity = mkOption { 74 type = enum [ "Verbose" "Quiet" ]; 75 default = "Quiet"; 76 description = "Verbosity of output produced by the service."; 77 }; 78 79 peers = 80 let 81 peer = submodule { 82 freeformType = settingsFormat.type; 83 84 options = { 85 public_key = mkOption { 86 type = path; 87 description = "Path to a file containing the public key of the remote Rosenpass peer."; 88 }; 89 90 endpoint = mkOption { 91 type = nullOr str; 92 default = null; 93 description = "Endpoint of the remote Rosenpass peer."; 94 }; 95 96 device = mkOption { 97 type = str; 98 default = cfg.defaultDevice; 99 defaultText = literalExpression "config.${opt.defaultDevice}"; 100 description = "Name of the local WireGuard interface to use for this peer."; 101 }; 102 103 peer = mkOption { 104 type = str; 105 description = "WireGuard public key corresponding to the remote Rosenpass peer."; 106 }; 107 }; 108 }; 109 in 110 mkOption { 111 type = listOf peer; 112 description = "List of peers to exchange keys with."; 113 default = [ ]; 114 }; 115 }; 116 }; 117 default = { }; 118 description = "Configuration for Rosenpass, see <https://rosenpass.eu/> for further information."; 119 }; 120 }; 121 122 config = mkIf cfg.enable { 123 warnings = 124 let 125 # NOTE: In the descriptions below, we tried to refer to e.g. 126 # options.systemd.network.netdevs."<name>".wireguardPeers.*.PublicKey 127 # directly, but don't know how to traverse "<name>" and * in this path. 128 extractions = [ 129 { 130 relevant = config.systemd.network.enable; 131 root = config.systemd.network.netdevs; 132 peer = (x: x.wireguardPeers); 133 key = (x: if x.wireguardPeerConfig ? PublicKey then x.wireguardPeerConfig.PublicKey else null); 134 description = "${options.systemd.network.netdevs}.\"<name>\".wireguardPeers.*.wireguardPeerConfig.PublicKey"; 135 } 136 { 137 relevant = config.networking.wireguard.enable; 138 root = config.networking.wireguard.interfaces; 139 peer = (x: x.peers); 140 key = (x: x.publicKey); 141 description = "${options.networking.wireguard.interfaces}.\"<name>\".peers.*.publicKey"; 142 } 143 rec { 144 relevant = root != { }; 145 root = config.networking.wg-quick.interfaces; 146 peer = (x: x.peers); 147 key = (x: x.publicKey); 148 description = "${options.networking.wg-quick.interfaces}.\"<name>\".peers.*.publicKey"; 149 } 150 ]; 151 relevantExtractions = filter (x: x.relevant) extractions; 152 extract = { root, peer, key, ... }: 153 filter (x: x != null) (flatten (concatMap (x: (map key (peer x))) (attrValues root))); 154 configuredKeys = flatten (map extract relevantExtractions); 155 itemize = xs: concatLines (map (x: " - ${x}") xs); 156 descriptions = map (x: "`${x.description}`"); 157 missingKeys = filter (key: !builtins.elem key configuredKeys) (map (x: x.peer) cfg.settings.peers); 158 unusual = '' 159 While this may work as expected, e.g. you want to manually configure WireGuard, 160 such a scenario is unusual. Please double-check your configuration. 161 ''; 162 in 163 (optional (relevantExtractions != [ ] && missingKeys != [ ]) '' 164 You have configured Rosenpass peers with the WireGuard public keys: 165 ${itemize missingKeys} 166 But there is no corresponding active Wireguard peer configuration in any of: 167 ${itemize (descriptions relevantExtractions)} 168 ${unusual} 169 '') 170 ++ 171 optional (relevantExtractions == [ ]) '' 172 You have configured Rosenpass, but you have not configured Wireguard via any of: 173 ${itemize (descriptions extractions)} 174 ${unusual} 175 ''; 176 177 environment.systemPackages = [ cfg.package pkgs.wireguard-tools ]; 178 179 systemd.services.rosenpass = 180 let 181 filterNonNull = filterAttrsRecursive (_: v: v != null); 182 config = settingsFormat.generate "config.toml" ( 183 filterNonNull (cfg.settings 184 // 185 ( 186 let 187 credentialPath = id: "$CREDENTIALS_DIRECTORY/${id}"; 188 # NOTE: We would like to remove all `null` values inside `cfg.settings` 189 # recursively, since `settingsFormat.generate` cannot handle `null`. 190 # This would require to traverse both attribute sets and lists recursively. 191 # `filterAttrsRecursive` only recurses into attribute sets, but not 192 # into values that might contain other attribute sets (such as lists, 193 # e.g. `cfg.settings.peers`). Here, we just specialize on `cfg.settings.peers`, 194 # and this may break unexpectedly whenever a `null` value is contained 195 # in a list in `cfg.settings`, other than `cfg.settings.peers`. 196 peersWithoutNulls = map filterNonNull cfg.settings.peers; 197 in 198 { 199 secret_key = credentialPath "pqsk"; 200 public_key = credentialPath "pqpk"; 201 peers = peersWithoutNulls; 202 } 203 ) 204 ) 205 ); 206 in 207 rec { 208 wantedBy = [ "multi-user.target" ]; 209 wants = [ "network-online.target" ]; 210 after = [ "network-online.target" ]; 211 path = [ cfg.package pkgs.wireguard-tools ]; 212 213 serviceConfig = { 214 User = "rosenpass"; 215 Group = "rosenpass"; 216 RuntimeDirectory = "rosenpass"; 217 DynamicUser = true; 218 AmbientCapabilities = [ "CAP_NET_ADMIN" ]; 219 LoadCredential = [ 220 "pqsk:${cfg.settings.secret_key}" 221 "pqpk:${cfg.settings.public_key}" 222 ]; 223 }; 224 225 # See <https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers> 226 environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml"; 227 228 script = '' 229 ${getExe pkgs.envsubst} -i ${config} -o "$CONFIG" 230 rosenpass exchange-config "$CONFIG" 231 ''; 232 }; 233 }; 234}