diff --git a/modules/joinmarket-jmwalletd.nix b/modules/joinmarket-jmwalletd.nix new file mode 100644 index 000000000..8d91f35ea --- /dev/null +++ b/modules/joinmarket-jmwalletd.nix @@ -0,0 +1,156 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services.joinmarket-jmwalletd = { + enable = mkEnableOption "JoinMarket jmwalletd"; + + # Unfortunately it's not possible to set the listening address for + # jmwalletd. It's used only internally. + address = mkOption { + type = types.str; + readOnly = true; + internal = true; + default = "127.0.0.1"; + description = mdDoc '' + The address where the jmwalletd listens to. + ''; + }; + port = mkOption { + type = types.port; + default = 28183; + description = mdDoc "The port over which to serve RPC."; + }; + wssPort = mkOption { + type = types.port; + default = 28283; + description = mdDoc "The port over which to serve websocket subscriptions."; + }; + extraArgs = mkOption { + type = types.separatedString " "; + default = ""; + description = mdDoc "Extra coomand line arguments passed to jmwalletd."; + }; + user = mkOption { + type = types.str; + default = "joinmarket-jmwalletd"; + description = mdDoc "The user as which to run JoinMarket jmwalletd."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = mdDoc "The group as which to run JoinMarket jmwalletd."; + }; + dataDir = mkOption { + readOnly = true; + type = types.path; + default = config.services.joinmarket.dataDir; + description = mdDoc "The JoinMarket data directory."; + }; + sslDir = mkOption { + readOnly = true; + type = types.path; + default = "${cfg.dataDir}/ssl"; + description = mdDoc "The SSL directory for jmwalled."; + }; + certPath = mkOption { + readOnly = true; + default = "${secretsDir}/joinmarket-jmwalletd"; + description = mdDoc "JoinMarket jmwalletd TLS certificate path."; + }; + recoverSync = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Choose to do detailed wallet sync, used for recovering on new Core + instance. + ''; + }; + certificate = { + extraIPs = mkOption { + type = with types; listOf str; + default = []; + example = [ "60.100.0.1" ]; + description = mdDoc '' + Extra `subjectAltName` IPs added to the certificate. + ''; + }; + extraDomains = mkOption { + type = with types; listOf str; + default = []; + example = [ "example.com" ]; + description = mdDoc '' + Extra `subjectAltName` domain names added to the certificate. + ''; + }; + }; + }; + + cfg = config.services.joinmarket-jmwalletd; + nbLib = config.nix-bitcoin.lib; + nbPkgs = config.nix-bitcoin.pkgs; + secretsDir = config.nix-bitcoin.secretsDir; +in { + inherit options; + + config = mkIf cfg.enable (mkMerge [{ + # TODO: possible recursion? + services.joinmarket.enable = true; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + # Allow access to the joinmarket dataDir. + extraGroups = [ config.services.joinmarket.group ]; + }; + users.groups.${cfg.group} = {}; + + nix-bitcoin.secrets.joinmarket-jmwalletd-password.user = cfg.user; + nix-bitcoin.generateSecretsCmds.joinmarket-jmwalletd-password = '' + makePasswordSecret joinmarket-jmwalletd-password + ''; + } + + (mkIf cfg.enable { + nix-bitcoin.secrets.joinmarket-jmwalletd-cert.user = cfg.user; + nix-bitcoin.secrets.joinmarket-jmwalletd-key.user = cfg.user; + nix-bitcoin.generateSecretsCmds.joinmarket-jmwalletd = '' + makeCert joinmarket-jmwalletd '${nbLib.mkCertExtraAltNames cfg.certificate}' + ''; + + systemd.tmpfiles.rules = [ + "d '${cfg.sslDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.joinmarket-jmwalletd = { + wantedBy = [ "joinmarket.service" ]; + requires = [ "joinmarket.service" ]; + after = [ "joinmarket.service" "nix-bitcoin-secrets.target" ]; + preStart = '' + # Copy the certificates into a data directory under the `ssl` dir + mkdir -p '${cfg.sslDir}' + install -m600 '${cfg.certPath}-cert' '${cfg.sslDir}/cert.pem' + install -m600 '${cfg.certPath}-key' '${cfg.sslDir}/key.pem' + ''; + serviceConfig = nbLib.defaultHardening // { + WorkingDirectory = cfg.dataDir; + User = cfg.user; + ExecStart = '' + ${config.nix-bitcoin.pkgs.joinmarket}/bin/jm-jmwalletd \ + --port='${toString cfg.port}' \ + --wss-port='${toString cfg.wssPort}' \ + --datadir='${cfg.dataDir}' \ + ${optionalString (cfg.recoverSync) "--recoversync \\"} + ${cfg.extraArgs} + ''; + SyslogIdentifier = "joinmarket-jmwalletd"; + ReadWritePaths = [ cfg.dataDir ]; + Restart = "on-failure"; + RestartSec = "10s"; + MemoryDenyWriteExecute = false; + } // nbLib.allowTor; + }; + }) + ]); +} diff --git a/modules/modules.nix b/modules/modules.nix index 1b3c204d1..6c409e589 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -27,6 +27,7 @@ ./btcpayserver.nix ./joinmarket.nix ./joinmarket-ob-watcher.nix + ./joinmarket-jmwalletd.nix ./hardware-wallets.nix # Support features diff --git a/modules/netns-isolation.nix b/modules/netns-isolation.nix index 832bb9444..6e6bdf698 100644 --- a/modules/netns-isolation.nix +++ b/modules/netns-isolation.nix @@ -345,8 +345,14 @@ in { messagingAddress = netns.joinmarket.address; cliExec = mkCliExec "joinmarket"; }; - systemd.services.joinmarket-yieldgenerator = mkIf config.services.joinmarket.yieldgenerator.enable { - serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-joinmarket"; + systemd.services = { + joinmarket-yieldgenerator = mkIf config.services.joinmarket.yieldgenerator.enable { + serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-joinmarket"; + }; + + joinmarket-jmwalletd = mkIf config.services.joinmarket-jmwalletd.enable { + serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-joinmarket"; + }; }; services.joinmarket-ob-watcher.address = netns.joinmarket-ob-watcher.address; diff --git a/modules/nodeinfo.nix b/modules/nodeinfo.nix index f219664ff..bb4ae8ee2 100644 --- a/modules/nodeinfo.nix +++ b/modules/nodeinfo.nix @@ -148,6 +148,7 @@ in { btcpayserver = mkInfo ""; liquidd = mkInfo ""; joinmarket-ob-watcher = mkInfo ""; + joinmarket-jmwalletd = mkInfo ""; rtl = mkInfo ""; mempool = mkInfo ""; mempool-frontend = name: cfg: mkInfoLong { diff --git a/test/tests.nix b/test/tests.nix index fb92119da..caa581b12 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -122,6 +122,9 @@ let tests.joinmarket = cfg.joinmarket.enable; tests.joinmarket-yieldgenerator = cfg.joinmarket.yieldgenerator.enable; + tests.joinmarket-jmwalletd = cfg.joinmarket-jmwalletd.enable; + services.joinmarket-jmwalletd.enable = config.services.joinmarket.enable; + tests.joinmarket-ob-watcher = cfg.joinmarket-ob-watcher.enable; services.joinmarket.yieldgenerator = { enable = config.services.joinmarket.enable; @@ -133,7 +136,6 @@ let }; tests.nodeinfo = config.nix-bitcoin.nodeinfo.enable; - tests.backups = cfg.backups.enable; # To test that unused secrets are made inaccessible by 'setup-secrets' diff --git a/test/tests.py b/test/tests.py index ef38b9de5..e71f555ce 100644 --- a/test/tests.py +++ b/test/tests.py @@ -281,6 +281,17 @@ def _(): assert_running("joinmarket-ob-watcher") machine.wait_until_succeeds(log_has_string("joinmarket-ob-watcher", "Starting ob-watcher")) +@test("joinmarket-jmwalletd") +def _(): + assert_running("joinmarket-jmwalletd") + machine.wait_until_succeeds(log_has_string("joinmarket-jmwalletd", "Started joinmarket-jmwalletd.service.")) + machine.wait_until_succeeds(log_has_string("joinmarket-jmwalletd", "Starting jmwalletd on port: 28183")) + wait_for_open_port(ip("joinmarket"), 28183) # RPC port + wait_for_open_port(ip("joinmarket"), 28283) # Websocket SSL port + + # Test web server response + assert_full_match(f"curl -fsSL --insecure https://{ip('joinmarket')}:28183/api/v1/getinfo | jq -jr keys[0]", "version") + @test("nodeinfo") def _(): status, _ = machine.execute("systemctl is-enabled --quiet onion-addresses 2> /dev/null")