at master 12 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 inherit (lib) 9 concatLists 10 filterAttrs 11 mapAttrs' 12 mapAttrsToList 13 mkEnableOption 14 mkIf 15 mkOption 16 mkOverride 17 mkPackageOption 18 nameValuePair 19 recursiveUpdate 20 types 21 ; 22 23 fedimintdOpts = 24 { 25 config, 26 lib, 27 name, 28 ... 29 }: 30 { 31 options = { 32 enable = mkEnableOption "fedimintd"; 33 34 package = mkPackageOption pkgs "fedimint" { }; 35 36 environment = mkOption { 37 type = types.attrsOf types.str; 38 description = "Extra Environment variables to pass to the fedimintd."; 39 default = { 40 RUST_BACKTRACE = "1"; 41 }; 42 example = { 43 RUST_LOG = "info,fm=debug"; 44 RUST_BACKTRACE = "1"; 45 }; 46 }; 47 48 p2p = { 49 openFirewall = mkOption { 50 type = types.bool; 51 default = true; 52 description = "Opens port in firewall for fedimintd's p2p port (both TCP and UDP)"; 53 }; 54 port = mkOption { 55 type = types.port; 56 default = 8173; 57 description = "Port to bind on for p2p connections from peers (both TCP and UDP)"; 58 }; 59 bind = mkOption { 60 type = types.str; 61 default = "0.0.0.0"; 62 description = "Address to bind on for p2p connections from peers (both TCP and UDP)"; 63 }; 64 url = mkOption { 65 type = types.nullOr types.str; 66 example = "fedimint://p2p.myfedimint.com:8173"; 67 description = '' 68 Public address for p2p connections from peers (if TCP is used) 69 ''; 70 }; 71 }; 72 api_ws = { 73 openFirewall = mkOption { 74 type = types.bool; 75 default = false; 76 description = "Opens TCP port in firewall for fedimintd's Websocket API"; 77 }; 78 port = mkOption { 79 type = types.port; 80 default = 8174; 81 description = "TCP Port to bind on for API connections relayed by the reverse proxy/tls terminator."; 82 }; 83 bind = mkOption { 84 type = types.str; 85 default = "127.0.0.1"; 86 description = "Address to bind on for API connections relied by the reverse proxy/tls terminator."; 87 }; 88 url = mkOption { 89 type = types.nullOr types.str; 90 description = '' 91 Public URL of the API address of the reverse proxy/tls terminator. Usually starting with `wss://`. 92 ''; 93 }; 94 }; 95 api_iroh = { 96 openFirewall = mkOption { 97 type = types.bool; 98 default = true; 99 description = "Opens UDP port in firewall for fedimintd's API Iroh endpoint"; 100 }; 101 port = mkOption { 102 type = types.port; 103 default = 8174; 104 description = "UDP Port to bind Iroh endpoint for API connections"; 105 }; 106 bind = mkOption { 107 type = types.str; 108 default = "0.0.0.0"; 109 description = "Address to bind on for Iroh endpoint for API connections"; 110 }; 111 }; 112 ui = { 113 openFirewall = mkOption { 114 type = types.bool; 115 default = false; 116 description = "Opens TCP port in firewall for built-in UI"; 117 }; 118 port = mkOption { 119 type = types.port; 120 default = 8175; 121 description = "TCP Port to bind on for UI connections"; 122 }; 123 bind = mkOption { 124 type = types.str; 125 default = "127.0.0.1"; 126 description = "Address to bind on for UI connections"; 127 }; 128 }; 129 bitcoin = { 130 network = mkOption { 131 type = types.str; 132 default = "signet"; 133 example = "bitcoin"; 134 description = "Bitcoin network to participate in."; 135 }; 136 rpc = { 137 url = mkOption { 138 type = types.str; 139 default = "http://127.0.0.1:38332"; 140 example = "signet"; 141 description = "Bitcoin node (bitcoind/electrum/esplora) address to connect to"; 142 }; 143 144 kind = mkOption { 145 type = types.str; 146 default = "bitcoind"; 147 example = "electrum"; 148 description = "Kind of a bitcoin node."; 149 }; 150 151 secretFile = mkOption { 152 type = types.nullOr types.path; 153 default = null; 154 description = '' 155 If set the URL specified in `bitcoin.rpc.url` will get the content of this file added 156 as an URL password, so `http://user@example.com` will turn into `http://user:SOMESECRET@example.com`. 157 158 Example: 159 160 `/etc/nix-bitcoin-secrets/bitcoin-rpcpassword-public` (for nix-bitcoin default) 161 ''; 162 }; 163 }; 164 }; 165 166 consensus.finalityDelay = mkOption { 167 type = types.ints.unsigned; 168 default = 10; 169 description = "Consensus peg-in finality delay."; 170 }; 171 172 dataDir = mkOption { 173 type = types.path; 174 default = "/var/lib/fedimintd-${name}/"; 175 readOnly = true; 176 description = '' 177 Path to the data dir fedimintd will use to store its data. 178 Note that due to using the DynamicUser feature of systemd, this value should not be changed 179 and is set to be read only. 180 ''; 181 }; 182 183 nginx = { 184 enable = mkOption { 185 type = types.bool; 186 default = false; 187 description = '' 188 Whether to configure nginx for fedimintd 189 ''; 190 }; 191 fqdn = mkOption { 192 type = types.str; 193 example = "api.myfedimint.com"; 194 description = "Public domain of the API address of the reverse proxy/tls terminator."; 195 }; 196 path_ui = mkOption { 197 type = types.str; 198 example = "/"; 199 default = "/"; 200 description = "Path to host the built-in UI on and forward to the daemon's api port"; 201 }; 202 path_ws = mkOption { 203 type = types.str; 204 example = "/"; 205 default = "/ws/"; 206 description = "Path to host the API on and forward to the daemon's api port"; 207 }; 208 config = mkOption { 209 type = types.submodule ( 210 recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { 211 inherit config lib; 212 }) { } 213 ); 214 default = { }; 215 description = "Overrides to the nginx vhost section for api"; 216 }; 217 }; 218 }; 219 }; 220in 221{ 222 options = { 223 services.fedimintd = mkOption { 224 type = types.attrsOf (types.submodule fedimintdOpts); 225 default = { }; 226 description = "Specification of one or more fedimintd instances."; 227 }; 228 }; 229 230 config = 231 let 232 eachFedimintd = filterAttrs (fedimintdName: cfg: cfg.enable) config.services.fedimintd; 233 eachFedimintdNginx = filterAttrs (fedimintdName: cfg: cfg.nginx.enable) eachFedimintd; 234 in 235 mkIf (eachFedimintd != { }) { 236 237 networking.firewall.allowedTCPPorts = concatLists ( 238 mapAttrsToList ( 239 fedimintdName: cfg: 240 ( 241 lib.optional cfg.api_ws.openFirewall cfg.api_ws.port 242 ++ lib.optional cfg.p2p.openFirewall cfg.p2p.port 243 ++ lib.optional cfg.ui.openFirewall cfg.ui.port 244 ) 245 ) eachFedimintd 246 ); 247 248 networking.firewall.allowedUDPPorts = concatLists ( 249 mapAttrsToList ( 250 fedimintdName: cfg: 251 ( 252 lib.optional cfg.api_iroh.openFirewall cfg.api_iroh.port 253 ++ lib.optional cfg.p2p.openFirewall cfg.p2p.port 254 ) 255 ) eachFedimintd 256 ); 257 258 systemd.services = mapAttrs' ( 259 fedimintdName: cfg: 260 (nameValuePair "fedimintd-${fedimintdName}" ( 261 let 262 startScript = pkgs.writeShellScriptBin "fedimintd" ( 263 ( 264 if cfg.bitcoin.rpc.secretFile != null then 265 '' 266 >&2 echo "Setting FM_FORCE_BITCOIN_RPC_URL using password from ${cfg.bitcoin.rpc.secretFile}" 267 secret=$(${pkgs.coreutils}/bin/head -n 1 "${cfg.bitcoin.rpc.secretFile}" || exit 1) 268 export FM_FORCE_BITCOIN_RPC_URL=$(echo "$FM_BITCOIN_RPC_URL" | sed "s|^\(\w\+://[^@]\+\)\(@.*\)|\1:''${secret}\2|") 269 '' 270 else 271 "" 272 ) 273 + '' 274 exec ${cfg.package}/bin/fedimintd 275 '' 276 ); 277 in 278 { 279 description = "Fedimint Server"; 280 documentation = [ "https://github.com/fedimint/fedimint/" ]; 281 wantedBy = [ "multi-user.target" ]; 282 environment = lib.mkMerge [ 283 { 284 FM_BIND_P2P = "${cfg.p2p.bind}:${toString cfg.p2p.port}"; 285 FM_BIND_API_WS = "${cfg.api_ws.bind}:${toString cfg.api_ws.port}"; 286 FM_BIND_API_IROH = "${cfg.api_iroh.bind}:${toString cfg.api_iroh.port}"; 287 FM_BIND_UI = "${cfg.ui.bind}:${toString cfg.ui.port}"; 288 FM_DATA_DIR = cfg.dataDir; 289 FM_BITCOIN_NETWORK = cfg.bitcoin.network; 290 FM_BITCOIN_RPC_URL = cfg.bitcoin.rpc.url; 291 FM_BITCOIN_RPC_KIND = cfg.bitcoin.rpc.kind; 292 } 293 294 (lib.optionalAttrs (cfg.p2p.url != null) { 295 FM_P2P_URL = cfg.p2p.url; 296 }) 297 298 (lib.optionalAttrs (cfg.api_ws.url != null) { 299 FM_API_URL = cfg.api_ws.url; 300 }) 301 302 cfg.environment 303 ]; 304 serviceConfig = { 305 DynamicUser = true; 306 307 StateDirectory = "fedimintd-${fedimintdName}"; 308 StateDirectoryMode = "0700"; 309 ExecStart = "${startScript}/bin/fedimintd"; 310 311 Restart = "always"; 312 RestartSec = 10; 313 UMask = "007"; 314 LimitNOFILE = "100000"; 315 316 LockPersonality = true; 317 MemoryDenyWriteExecute = true; 318 NoNewPrivileges = true; 319 PrivateDevices = true; 320 PrivateMounts = true; 321 PrivateTmp = true; 322 ProtectClock = true; 323 ProtectControlGroups = true; 324 ProtectHostname = true; 325 ProtectKernelLogs = true; 326 ProtectKernelModules = true; 327 ProtectKernelTunables = true; 328 ProtectSystem = "full"; 329 RestrictAddressFamilies = [ 330 "AF_INET" 331 "AF_INET6" 332 "AF_NETLINK" 333 ]; 334 RestrictNamespaces = true; 335 RestrictRealtime = true; 336 SocketBindAllow = "udp:${builtins.toString cfg.api_iroh.port}"; 337 SystemCallArchitectures = "native"; 338 SystemCallFilter = [ 339 "@system-service" 340 "~@privileged" 341 ]; 342 }; 343 unitConfig = { 344 StartLimitBurst = 5; 345 }; 346 } 347 )) 348 ) eachFedimintd; 349 350 services.nginx.virtualHosts = mapAttrs' ( 351 fedimintdName: cfg: 352 (nameValuePair cfg.nginx.fqdn ( 353 lib.mkMerge [ 354 cfg.nginx.config 355 356 { 357 # Note: we want by default to enable OpenSSL, but it seems anything 100 and above is 358 # overridden by default value from vhost-options.nix 359 enableACME = mkOverride 99 true; 360 forceSSL = mkOverride 99 true; 361 locations.${cfg.nginx.path_ws} = { 362 proxyPass = "http://127.0.0.1:${builtins.toString cfg.api_ws.port}/"; 363 proxyWebsockets = true; 364 extraConfig = '' 365 proxy_pass_header Authorization; 366 ''; 367 }; 368 locations.${cfg.nginx.path_ui} = { 369 proxyPass = "http://127.0.0.1:${builtins.toString cfg.ui.port}/"; 370 extraConfig = '' 371 proxy_pass_header Authorization; 372 ''; 373 }; 374 } 375 ] 376 )) 377 ) eachFedimintdNginx; 378 }; 379 380 meta.maintainers = with lib.maintainers; [ dpc ]; 381}