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