From a13232f46fa7147137a225dbb86a34095cf2dedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ci=C4=99=C5=BCarkiewicz?= Date: Wed, 26 Jun 2024 22:17:12 -0700 Subject: [PATCH] nixos/fedimintd: init services --- nixos/modules/module-list.nix | 1 + .../modules/services/networking/fedimintd.nix | 365 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/fedimintd.nix | 34 ++ 4 files changed, 401 insertions(+) create mode 100644 nixos/modules/services/networking/fedimintd.nix create mode 100644 nixos/tests/fedimintd.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 31bd31bca0246d6..ff1c3883857766a 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1018,6 +1018,7 @@ ./services/networking/expressvpn.nix ./services/networking/fakeroute.nix ./services/networking/fastnetmon-advanced.nix + ./services/networking/fedimintd.nix ./services/networking/ferm.nix ./services/networking/firefox-syncserver.nix ./services/networking/fireqos.nix diff --git a/nixos/modules/services/networking/fedimintd.nix b/nixos/modules/services/networking/fedimintd.nix new file mode 100644 index 000000000000000..c950ac4c8bb9abe --- /dev/null +++ b/nixos/modules/services/networking/fedimintd.nix @@ -0,0 +1,365 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + filterAttrs + flatten + mapAttrs' + mapAttrsToList + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + nameValuePair + recursiveUpdate + types + ; + + eachFedimintd = filterAttrs (fedimintdName: cfg: cfg.enable) config.services.fedimintd; + eachFedimintdNginx = filterAttrs (fedimintdName: cfg: cfg.nginx.enable) eachFedimintd; + + fedimintdOpts = + { + config, + lib, + name, + ... + }: + { + options = { + enable = mkEnableOption "fedimintd"; + + package = mkPackageOption pkgs "fedimint" { }; + + user = mkOption { + type = types.str; + default = "fedimintd-${name}"; + description = "The user as which to run fedimintd."; + }; + + group = mkOption { + type = types.str; + default = config.user; + description = "The group as which to run fedimintd."; + }; + + environment = mkOption { + type = types.attrsOf types.str; + description = "Extra Environment variables to pass to the fedimintd."; + default = { + RUST_BACKTRACE = "1"; + }; + example = { + RUST_LOG = "info,fm=debug"; + RUST_BACKTRACE = "1"; + }; + }; + + p2p = { + openFirewall = mkOption { + type = types.bool; + default = true; + description = "Opens port in firewall for fedimintd's p2p port"; + }; + port = mkOption { + type = types.port; + default = 8173; + description = "Port to bind on for p2p connections from peers"; + }; + bind = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Address to bind on for p2p connections from peers"; + }; + fqdn = mkOption { + type = types.nullOr types.str; + default = null; + example = "p2p.myfedimint.com"; + description = "Public domain for p2p connections from peers"; + }; + url = mkOption { + type = types.nullOr types.str; + default = if config.p2p.fqdn != null then "fedimint://${config.p2p.fqdn}" else null; + example = "fedimint://p2p.myfedimint.com"; + description = '' + Public address for p2p connections from peers + + Typically you want to set `fqdn` instead. + ''; + }; + }; + api = { + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Opens port in firewall for fedimintd's api port"; + }; + port = mkOption { + type = types.port; + default = 8174; + description = "Port to bind on for API connections relied by the reverse proxy/tls terminator."; + }; + bind = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to bind on for API connections relied by the reverse proxy/tls terminator."; + }; + fqdn = mkOption { + type = types.nullOr types.str; + default = null; + example = "api.myfedimint.com"; + description = "Public domain of the API address of the reverse proxy/tls terminator."; + }; + url = mkOption { + type = types.nullOr types.str; + default = if config.api.fqdn != null then "wss://${config.api.fqdn}" else null; + description = '' + Public URL of the API address of the reverse proxy/tls terminator. Usually starting with `wss://`. + + Typically you want to override `fqdn` instead. + ''; + }; + }; + bitcoin = { + network = mkOption { + type = types.str; + default = "signet"; + example = "bitcoin"; + description = "Bitcoin network to participate in."; + }; + rpc = { + url = mkOption { + type = types.str; + default = "http://127.0.0.1:38332"; + example = "signet"; + description = "Bitcoin node (bitcoind/electrum/esplora) address to connect to"; + }; + + kind = mkOption { + type = types.str; + default = "bitcoind"; + example = "electrum"; + description = "Kind of a bitcoin node."; + }; + + secretFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If set the URL specified in `bitcoin.rpc.url` will get the content of this file added + as an URL password, so `http://user@example.com` will turn into `http://user:SOMESECRET@example.com`. + + Example: + + `/etc/nix-bitcoin-secrets/bitcoin-rpcpassword-public` (for nix-bitcoin default) + ''; + }; + }; + }; + + consensus.finalityDelay = mkOption { + type = types.number; + default = 10; + description = "Consensus peg-in finality delay."; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/fedimintd-${name}/"; + readOnly = true; + description = '' + Path to the data dir fedimintd will use to store its data. + Note that due to using the DynamicUser feature of systemd, this value should not be changed + and is set to be read only. + ''; + }; + + nginx = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Wether to configure nginx for fedimintd + ''; + }; + config = mkOption { + type = types.nullOr ( + types.submodule ( + recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { + options.serverName = { + default = "fedimintd-${name}"; + defaultText = "fedimintd-\${name}"; + }; + } + ) + ); + default = null; + description = "Overrides to the nginx vhost section for api"; + }; + }; + }; + }; +in +{ + options = { + services.fedimintd = mkOption { + type = types.attrsOf (types.submodule fedimintdOpts); + default = { }; + description = "Specification of one or more fedimintd instances."; + }; + }; + + config = mkIf (eachFedimintd != { }) { + + assertions = flatten ( + mapAttrsToList (fedimintdName: cfg: [ + { + assertion = cfg.p2p.url != null; + message = '' + `services.fedimintd.${fedimintdName}.p2p.url` must be set to address reachable by other peers. + + Example: `fedimint://p2p.mymint.org`. + ''; + } + { + assertion = cfg.api.url != null; + message = '' + `services.fedimintd.${fedimintdName}.api.url` must be set to address reachable by the clients, with TLS terminated by external service (typically nginx), and relayed to the fedimintd bind address. + + Example: `wss://api.mymint.org`. + ''; + } + ]) eachFedimintd + ); + + networking.firewall.allowedTCPPorts = flatten ( + mapAttrsToList ( + fedimintdName: cfg: + (lib.optional cfg.api.openFirewall cfg.api.port ++ lib.optional cfg.p2p.openFirewall cfg.p2p.port) + ) eachFedimintd + ); + + systemd.services = mapAttrs' ( + fedimintdName: cfg: + (nameValuePair "fedimintd-${fedimintdName}" ( + let + startScript = pkgs.writeShellScript "fedimintd-start" ( + ( + if cfg.bitcoin.rpc.secretFile != null then + '' + secret=$(${pkgs.coreutils}/bin/head -n 1 "${cfg.bitcoin.rpc.secretFile}") + prefix="''${FM_BITCOIN_RPC_URL%*@*}" # Everything before the last '@' + suffix="''${FM_BITCOIN_RPC_URL##*@}" # Everything after the last '@' + FM_BITCOIN_RPC_URL="''${prefix}:''${secret}@''${suffix}" + '' + else + "" + ) + + '' + exec ${cfg.package}/bin/fedimintd + '' + ); + in + { + description = "Fedimint Server"; + documentation = [ "https://github.com/fedimint/fedimint/" ]; + wantedBy = [ "multi-user.target" ]; + environment = lib.mkMerge [ + { + FM_BIND_P2P = "${cfg.p2p.bind}:${toString cfg.p2p.port}"; + FM_BIND_API = "${cfg.api.bind}:${toString cfg.api.port}"; + FM_P2P_URL = cfg.p2p.url; + FM_API_URL = cfg.api.url; + FM_DATA_DIR = cfg.dataDir; + FM_BITCOIN_NETWORK = cfg.bitcoin.network; + FM_BITCOIN_RPC_URL = cfg.bitcoin.rpc.url; + FM_BITCOIN_RPC_KIND = cfg.bitcoin.rpc.kind; + } + cfg.environment + ]; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + + StateDirectory = "fedimintd-${fedimintdName}"; + StateDirectoryMode = "0700"; + ExecStart = startScript; + + Restart = "always"; + RestartSec = 10; + StartLimitBurst = 5; + UMask = "007"; + LimitNOFILE = "100000"; + + LockPersonality = true; + MemoryDenyWriteExecute = "true"; + NoNewPrivileges = "true"; + PrivateDevices = "true"; + PrivateMounts = true; + PrivateTmp = "true"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "full"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + }; + } + )) + ) eachFedimintd; + + users.users = mapAttrs' ( + fedimintdName: cfg: + (nameValuePair "fedimintd-${fedimintdName}" { + name = cfg.user; + group = cfg.group; + description = "Fedimint daemon user"; + home = cfg.dataDir; + isSystemUser = true; + }) + ) eachFedimintd; + + users.groups = mapAttrs' (fedimintdName: cfg: (nameValuePair "${cfg.group}" { })) eachFedimintd; + + services.nginx.virtualHosts = mapAttrs' ( + fedimintdName: cfg: + (nameValuePair cfg.api.fqdn ( + lib.mkMerge [ + cfg.nginx.config + + { + enableACME = mkDefault true; + forceSSL = mkDefault true; + # Currently Fedimint API only support JsonRPC on `/ws/` endpoint, so no need to handle `/` + locations."/ws/" = { + proxyPass = "http://127.0.0.1:${builtins.toString cfg.api.port}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_pass_header Authorization; + ''; + }; + } + ] + )) + ) eachFedimintdNginx; + }; + + meta.maintainers = with lib.maintainers; [ dpc ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 423ae872155b0bc..d96b2bd06762d92 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -318,6 +318,7 @@ in { fancontrol = handleTest ./fancontrol.nix {}; fanout = handleTest ./fanout.nix {}; fcitx5 = handleTest ./fcitx5 {}; + fedimintd = runTest ./fedimintd.nix; fenics = handleTest ./fenics.nix {}; ferm = handleTest ./ferm.nix {}; ferretdb = handleTest ./ferretdb.nix {}; diff --git a/nixos/tests/fedimintd.nix b/nixos/tests/fedimintd.nix new file mode 100644 index 000000000000000..a65c294bbe4e2d0 --- /dev/null +++ b/nixos/tests/fedimintd.nix @@ -0,0 +1,34 @@ +# This test runs the fedimintd and verifies that it starts + +{ pkgs, ... }: + +{ + name = "fedimintd"; + + meta = with pkgs.lib.maintainers; { + maintainers = [ dpc ]; + }; + + nodes.machine = + { ... }: + { + services.fedimintd."mainnet" = { + enable = true; + p2p = { + fqdn = "example.com"; + }; + api = { + fqdn = "example.com"; + }; + }; + }; + + testScript = + { nodes, ... }: + '' + start_all() + + machine.wait_for_unit("fedimintd-mainnet.service") + machine.wait_for_open_port(${toString nodes.machine.services.fedimintd.mainnet.api.port}) + ''; +}