diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5c2dc3f64bc47f6..347bf20003acd5b 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1030,6 +1030,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..362152d56617070 --- /dev/null +++ b/nixos/modules/services/networking/fedimintd.nix @@ -0,0 +1,333 @@ +{ config +, lib +, pkgs +, ... +}: +let + inherit (lib) + concatLists + filterAttrs + mapAttrs' + mapAttrsToList + mkEnableOption + mkIf + mkOption + mkOverride + mkPackageOption + nameValuePair + recursiveUpdate + types; + + 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"; + }; + url = mkOption { + type = types.str; + example = "fedimint://p2p.myfedimint.com"; + description = '' + Public address for p2p connections from peers + ''; + }; + }; + 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."; + }; + url = mkOption { + type = types.str; + description = '' + Public URL of the API address of the reverse proxy/tls terminator. Usually starting with `wss://`. + ''; + }; + }; + 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.ints.unsigned; + 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 = '' + Whether to configure nginx for fedimintd + ''; + }; + fqdn = mkOption { + type = types.str; + example = "api.myfedimint.com"; + description = "Public domain of the API address of the reverse proxy/tls terminator."; + }; + config = mkOption { + type = types.submodule ( + recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { } + ); + default = { }; + 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 = + let + eachFedimintd = filterAttrs (fedimintdName: cfg: cfg.enable) config.services.fedimintd; + eachFedimintdNginx = filterAttrs (fedimintdName: cfg: cfg.nginx.enable) eachFedimintd; + in + mkIf (eachFedimintd != { }) { + + networking.firewall.allowedTCPPorts = concatLists ( + 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.nginx.fqdn ( + lib.mkMerge [ + cfg.nginx.config + + { + # Note: we want by default to enable OpenSSL, but it seems anything 100 and above is + # overriden by default value from vhost-options.nix + enableACME = mkOverride 99 true; + forceSSL = mkOverride 99 true; + # Currently Fedimint API only support JsonRPC on `/ws/` endpoint, so no need to handle `/` + locations."/ws/" = { + proxyPass = "http://127.0.0.1:${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 d10efd01113a2c3..655561a45efb581 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -322,6 +322,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..19e92b43da6533b --- /dev/null +++ b/nixos/tests/fedimintd.nix @@ -0,0 +1,37 @@ +# 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 = { + url = "fedimint://example.com"; + }; + api = { + url = "wss://example.com"; + }; + environment = { + "FM_REL_NOTES_ACK" = "0_4_xyz"; + }; + }; + }; + + testScript = + { nodes, ... }: + '' + start_all() + + machine.wait_for_unit("fedimintd-mainnet.service") + machine.wait_for_open_port(${toString nodes.machine.services.fedimintd.mainnet.api.port}) + ''; +}