diff --git a/README.md b/README.md index 62d2970..2f5d5f9 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,7 @@ See the [examples directory](examples/README.md). Features --- A [configuration preset](modules/presets/secure-node.nix) for setting up a secure node -* All applications use Tor for outbound connections and accept inbound connections via onion services. -* Includes a [nodeinfo](modules/nodeinfo.nix) script which prints basic info about the node. +* All applications use Tor for outbound connections and support accepting inbound connections via onion services. NixOS modules * Application services @@ -74,9 +73,9 @@ NixOS modules * [bitcoin-core-hwi](https://github.com/bitcoin-core/HWI) * Helper * [netns-isolation](modules/netns-isolation.nix): isolates applications on the network-level via network namespaces + * [nodeinfo](modules/nodeinfo.nix): script which prints info about the node's services * [backups](modules/backups.nix): daily duplicity backups of all your node's important files * [operator](modules/operator.nix): adds non-root user `operator` who has access to client tools (e.g. `bitcoin-cli`, `lightning-cli`) - * [nix-bitcoin webindex](modules/nix-bitcoin-webindex.nix): a local website to display node information Security --- diff --git a/docs/usage.md b/docs/usage.md index 53d1c8b..5d3985d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -8,7 +8,7 @@ fetch-release > nix-bitcoin-release.nix Nodeinfo --- -Run `nodeinfo` to see your onion addresses for the webindex, spark, etc. if they are enabled. +Run `nodeinfo` to see onion addresses and local addresses for enabled services. Connect to spark-wallet --- @@ -86,10 +86,10 @@ Connect to electrs nixops deploy -d bitcoin-node ``` -3. Get electrs onion address +3. Get electrs onion address with format `:` ``` - nodeinfo | grep 'ELECTRS_ONION' + nodeinfo | jq -r .electrs.onion_address ``` 4. Connect to electrs @@ -98,7 +98,7 @@ Connect to electrs On Desktop ``` - electrum --oneserver -1 -s ":50001:t" -p socks5:localhost:9050 + electrum --oneserver -1 -s ":t" -p socks5:localhost:9050 ``` On Android @@ -107,16 +107,16 @@ Connect to electrs Network > Proxy mode: socks5, Host: 127.0.0.1, Port: 9050 Network > Auto-connect: OFF Network > One-server mode: ON - Network > Server: :50001:t + Network > Server: :t ``` -Connect to nix-bitcoin node through ssh Tor Hidden Service +Connect to nix-bitcoin node through the SSH onion service --- -1. Run `nodeinfo` on your nix-bitcoin node and note the `SSHD_ONION` +1. Get the SSH onion address (excluding the port suffix) ``` nixops ssh operator@bitcoin-node - nodeinfo | grep 'SSHD_ONION' + nodeinfo | jq -r .sshd.onion_address | sed 's/:.*//' ``` 2. Create a SSH key @@ -131,14 +131,14 @@ Connect to nix-bitcoin node through ssh Tor Hidden Service # FIXME: Add your SSH pubkey services.openssh.enable = true; users.users.root = { - openssh.authorizedKeys.keys = [ "[contents of ~/.ssh/id_ed25519.pub]" ]; + openssh.authorizedKeys.keys = [ "" ]; }; ``` -4. Connect to your nix-bitcoin node's ssh Tor Hidden Service, forwarding a local port to the nix-bitcoin node's ssh server +4. Connect to your nix-bitcoin node's SSH onion service, forwarding a local port to the nix-bitcoin node's SSH server ``` - ssh -i ~/.ssh/id_ed25519 -L [random port of your choosing]:localhost:22 root@[your SSHD_ONION] + ssh -i ~/.ssh/id_ed25519 -L :localhost:22 root@ ``` 5. Edit your `network-nixos.nix` to look like this @@ -148,12 +148,12 @@ Connect to nix-bitcoin node through ssh Tor Hidden Service bitcoin-node = { config, pkgs, ... }: { deployment.targetHost = "127.0.0.1"; - deployment.targetPort = [random port of your choosing]; + deployment.targetPort = ; }; } ``` -6. Now you can run `nixops deploy -d bitcoin-node` and it will connect through the ssh tunnel you established in step iv. This also allows you to do more complex ssh setups that `nixops ssh` doesn't support. An example would be authenticating with [Trezor's ssh agent](https://github.com/romanz/trezor-agent), which provides extra security. +6. Now you can run `nixops deploy -d bitcoin-node` and it will connect through the SSH tunnel you established in step iv. This also allows you to do more complex SSH setups that `nixops ssh` doesn't support. An example would be authenticating with [Trezor's SSH agent](https://github.com/romanz/trezor-agent), which provides extra security. Initialize a Trezor for Bitcoin Core's Hardware Wallet Interface --- @@ -263,7 +263,7 @@ you. If however, you want to manually initialize your wallet, follow these steps ## Run the tumbler The tumbler needs to be able to run in the background for a long time, use screen -to run it accross ssh sessions. You can also use tmux in the same fashion. +to run it accross SSH sessions. You can also use tmux in the same fashion. 1. Add screen to your `environment.systemPackages`, for example diff --git a/examples/README.md b/examples/README.md index 1280afb..328c6aa 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,7 +11,7 @@ nix-shell The following example scripts set up a nix-bitcoin node according to [`configuration.nix`](configuration.nix) and then shut down immediately. They leave no traces (outside of `/nix/store`) on the host system.\ -By default, [`configuration.nix`](configuration.nix) enables `bitcoind` and `clightning` (with an onion service). +By default, [`configuration.nix`](configuration.nix) enables `bitcoind` and `clightning`. - [`./deploy-container.sh`](deploy-container.sh) creates a [NixOS container](https://github.com/erikarvstedt/extra-container).\ This is the fastest way to set up a node.\ diff --git a/examples/configuration.nix b/examples/configuration.nix index 9545d0f..8ebd037 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -37,11 +37,12 @@ # Enable this module to use clightning, a Lightning Network implementation # in C. services.clightning.enable = true; - # == TOR - # Enable this option to announce our Tor Hidden Service. By default clightning - # offers outgoing functionality, but doesn't announce the Tor Hidden Service - # under which peers can reach us. - # services.clightning.announce-tor = true; + # + # Set this to create an onion service by which clightning can accept incoming connections + # via Tor. + # The onion service is automatically announced to peers. + # nix-bitcoin.onionServices.clightning.public = true; + # # == Plugins # See ../docs/usage.md for the list of available plugins. # services.clightning.plugins.prometheus.enable = true; @@ -49,13 +50,15 @@ ### LND # Uncomment the following line in order to enable lnd, a lightning # implementation written in Go. In order to avoid collisions with clightning - # you must disable clightning or change the services.clightning.bindport or - # services.lnd.listenPort to a port other than 9735. + # you must disable clightning or change the services.clightning.port or + # services.lnd.port to a port other than 9735. # services.lnd.enable = true; - # Enable this option to announce our Tor Hidden Service. By default lnd - # offers outgoing functionality, but doesn't announce the Tor Hidden Service - # under which peers can reach us. - # services.lnd.announce-tor = true; + # + # Set this to create an onion service by which lnd can accept incoming connections + # via Tor. + # The onion service is automatically announced to peers. + # nix-bitcoin.onionServices.lnd.public = true; + # ## WARNING # If you use lnd, you should manually backup your wallet mnemonic # seed. This will allow you to recover on-chain funds. You can run the @@ -93,6 +96,12 @@ # The lightning backend service automatically enabled. # Afterwards you need to go into Store > General Settings > Lightning Nodes # and click to use "the internal lightning node of this BTCPay Server". + # + # Set this to create an onion service to make the btcpayserver web interface + # accessible via Tor. + # Security WARNING: Create a btcpayserver administrator account before allowing + # public access to the web interface. + # nix-bitcoin.onionServices.btcpayserver.enable = true; ### LIQUIDD # Enable this module to use Liquid, a sidechain for an inter-exchange @@ -101,11 +110,6 @@ # tool run as user operator. # services.liquidd.enable = true; - ### WEBINDEX - # Enable this module to use the nix-bitcoin-webindex, a simple website - # displaying your node information. Only available if clightning is enabled. - # services.nix-bitcoin-webindex.enable = true; - ### RECURRING-DONATIONS # Enable this module to send recurring donations. This is EXPERIMENTAL; it's # not guaranteed that payments are succeeding or that you will notice payment @@ -203,5 +207,5 @@ # The nix-bitcoin release version that your config is compatible with. # When upgrading to a backwards-incompatible release, nix-bitcoin will display an # an error and provide hints for migrating your config to the new release. - nix-bitcoin.configVersion = "0.0.26"; + nix-bitcoin.configVersion = "0.0.30"; } diff --git a/modules/backups.nix b/modules/backups.nix index 7060980..322a84c 100644 --- a/modules/backups.nix +++ b/modules/backups.nix @@ -31,13 +31,6 @@ let in { options.services.backups = { enable = mkEnableOption "Backups service"; - program = mkOption { - type = types.enum [ "duplicity" ]; - default = "duplicity"; - description = '' - Program with which to do backups. - ''; - }; with-bulk-data = mkOption { type = types.bool; default = false; @@ -69,7 +62,7 @@ in { }; }; - config = mkIf (cfg.enable && cfg.program == "duplicity") (mkMerge [ + config = mkIf cfg.enable (mkMerge [ { environment.systemPackages = [ pkgs.duplicity ]; diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index c8650eb..92f3516 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -22,16 +22,18 @@ let ${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"} # Connection options - ${optionalString cfg.listen "bind=${cfg.bind}"} - ${optionalString (cfg.port != null) "port=${toString cfg.port}"} + ${optionalString cfg.listen "bind=${cfg.address}"} + port=${toString cfg.port} ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} listen=${if cfg.listen then "1" else "0"} ${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"} ${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes} # RPC server options - ${optionalString (cfg.rpcthreads != null) "rpcthreads=${toString cfg.rpcthreads}"} + rpcbind=${cfg.rpc.address} rpcport=${toString cfg.rpc.port} + rpcconnect=${cfg.rpc.address} + ${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"} rpcwhitelistdefault=0 ${concatMapStrings (user: '' ${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${passwordHMAC}"} @@ -39,9 +41,7 @@ let "rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"} '') (builtins.attrValues cfg.rpc.users) } - rpcbind=${cfg.rpcbind} - rpcconnect=${cfg.rpcbind} - ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} + ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip} # Wallet options ${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"} @@ -57,6 +57,16 @@ in { options = { services.bitcoind = { enable = mkEnableOption "Bitcoin daemon"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for peer connections."; + }; + port = mkOption { + type = types.port; + default = 8333; + description = "Port to listen for peer connections."; + }; package = mkOption { type = types.package; default = config.nix-bitcoin.pkgs.bitcoind; @@ -77,13 +87,6 @@ in { default = "/var/lib/bitcoind"; description = "The data directory for bitcoind."; }; - bind = mkOption { - type = types.str; - default = "127.0.0.1"; - description = '' - Bind to given address and always listen on it. - ''; - }; user = mkOption { type = types.str; default = "bitcoin"; @@ -95,10 +98,29 @@ in { description = "The group as which to run bitcoind."; }; rpc = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Address to listen for JSON-RPC connections. + ''; + }; port = mkOption { type = types.port; default = 8332; - description = "Port on which to listen for JSON-RPC connections."; + description = "Port to listen for JSON-RPC connections."; + }; + threads = mkOption { + type = types.nullOr types.ints.u16; + default = null; + description = "The number of threads to service RPC calls."; + }; + allowip = mkOption { + type = types.listOf types.str; + default = [ "127.0.0.1" ]; + description = '' + Allow JSON-RPC connections from specified sources. + ''; }; users = mkOption { default = {}; @@ -144,25 +166,6 @@ in { ''; }; }; - rpcthreads = mkOption { - type = types.nullOr types.ints.u16; - default = null; - description = "Set the number of threads to service RPC calls"; - }; - rpcbind = mkOption { - type = types.str; - default = "127.0.0.1"; - description = '' - Bind to given address to listen for JSON-RPC connections. - ''; - }; - rpcallowip = mkOption { - type = types.listOf types.str; - default = [ "127.0.0.1" ]; - description = '' - Allow JSON-RPC connections from specified source. - ''; - }; regtest = mkOption { type = types.bool; default = false; @@ -176,11 +179,6 @@ in { readOnly = true; default = mainnet: regtest: if cfg.regtest then regtest else mainnet; }; - port = mkOption { - type = types.nullOr types.port; - default = null; - description = "Override the default port on which to listen for connections."; - }; proxy = mkOption { type = types.nullOr types.str; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null; diff --git a/modules/btcpayserver.nix b/modules/btcpayserver.nix index 427a08a..3600930 100644 --- a/modules/btcpayserver.nix +++ b/modules/btcpayserver.nix @@ -14,6 +14,16 @@ in { default = nbPkgs.nbxplorer; description = "The package providing nbxplorer binaries."; }; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen on."; + }; + port = mkOption { + type = types.port; + default = 24444; + description = "Port to listen on."; + }; dataDir = mkOption { type = types.path; default = "/var/lib/nbxplorer"; @@ -29,16 +39,6 @@ in { default = cfg.nbxplorer.user; description = "The group as which to run nbxplorer."; }; - bind = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "The address on which to bind."; - }; - port = mkOption { - type = types.port; - default = 24444; - description = "Port on which to bind."; - }; enable = mkOption { # This option is only used by netns-isolation internal = true; @@ -49,6 +49,16 @@ in { btcpayserver = { enable = mkEnableOption "btcpayserver"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen on."; + }; + port = mkOption { + type = types.port; + default = 23000; + description = "Port to listen on."; + }; package = mkOption { type = types.package; default = nbPkgs.btcpayserver; @@ -69,16 +79,6 @@ in { default = cfg.btcpayserver.user; description = "The group as which to run btcpayserver."; }; - bind = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "The address on which to bind."; - }; - port = mkOption { - type = types.port; - default = 23000; - description = "Port on which to bind."; - }; lightningBackend = mkOption { type = types.nullOr (types.enum [ "clightning" "lnd" ]); default = null; @@ -117,9 +117,9 @@ in { configFile = builtins.toFile "config" '' network=${config.services.bitcoind.network} btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name} - btcrpcurl=http://${config.services.bitcoind.rpcbind}:${toString cfg.bitcoind.rpc.port} - btcnodeendpoint=${config.services.bitcoind.bind}:8333 - bind=${cfg.nbxplorer.bind} + btcrpcurl=http://${config.services.bitcoind.rpc.address}:${toString cfg.bitcoind.rpc.port} + btcnodeendpoint=${config.services.bitcoind.address}:${toString config.services.bitcoind.port} + bind=${cfg.nbxplorer.address} port=${toString cfg.nbxplorer.port} ''; in { @@ -153,9 +153,9 @@ in { network=${config.services.bitcoind.network} postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpaydb socksendpoint=${cfg.tor.client.socksListenAddress} - btcexplorerurl=http://${cfg.nbxplorer.bind}:${toString cfg.nbxplorer.port}/ + btcexplorerurl=http://${cfg.nbxplorer.address}:${toString cfg.nbxplorer.port}/ btcexplorercookiefile=${cfg.nbxplorer.dataDir}/${config.services.bitcoind.makeNetworkName "Main" "RegTest"}/.cookie - bind=${cfg.btcpayserver.bind} + bind=${cfg.btcpayserver.address} ${optionalString (cfg.btcpayserver.rootpath != null) "rootpath=${cfg.btcpayserver.rootpath}"} port=${toString cfg.btcpayserver.port} '' + optionalString (cfg.btcpayserver.lightningBackend == "clightning") '' @@ -163,7 +163,7 @@ in { ''); lndConfig = "btclightning=type=lnd-rest;" + - "server=https://${toString cfg.lnd.listen}:${toString cfg.lnd.restPort}/;" + + "server=https://${cfg.lnd.restAddress}:${toString cfg.lnd.restPort}/;" + "macaroonfilepath=/run/lnd/btcpayserver.macaroon;" + "certthumbprint="; in let self = { diff --git a/modules/clightning.nix b/modules/clightning.nix index 38f3b9d..544782b 100644 --- a/modules/clightning.nix +++ b/modules/clightning.nix @@ -6,15 +6,14 @@ let cfg = config.services.clightning; inherit (config) nix-bitcoin-services; nbPkgs = config.nix-bitcoin.pkgs; - onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []); network = config.services.bitcoind.makeNetworkName "bitcoin" "regtest"; configFile = pkgs.writeText "config" '' network=${network} bitcoin-datadir=${config.services.bitcoind.dataDir} ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} always-use-proxy=${if cfg.always-use-proxy then "true" else "false"} - bind-addr=${cfg.bind-addr}:${toString cfg.bindport} - bitcoin-rpcconnect=${config.services.bitcoind.rpcbind} + bind-addr=${cfg.address}:${toString cfg.port} + bitcoin-rpcconnect=${config.services.bitcoind.rpc.address} bitcoin-rpcport=${toString config.services.bitcoind.rpc.port} bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name} rpc-file-mode=0660 @@ -29,13 +28,15 @@ in { If enabled, the clightning service will be installed. ''; }; - autolisten = mkOption { - type = types.bool; - default = false; - description = '' - Bind (and maybe announce) on IPv4 and IPv6 interfaces if no addr, - bind-addr or announce-addr options are specified. - ''; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "IP address or UNIX domain socket to listen for peer connections."; + }; + port = mkOption { + type = types.port; + default = 9735; + description = "Port to listen for peer connections."; }; proxy = mkOption { type = types.nullOr types.str; @@ -49,21 +50,6 @@ in { Always use the *proxy*, even to connect to normal IP addresses (you can still connect to Unix domain sockets manually). This also disables all DNS lookups, to avoid leaking information. ''; }; - bind-addr = mkOption { - type = nbPkgs.lib.ipv4Address; - default = "127.0.0.1"; - description = "Set an IP address or UNIX domain socket to listen to"; - }; - bindport = mkOption { - type = types.port; - default = 9735; - description = "Set a Port to listen to locally"; - }; - announce-tor = mkOption { - type = types.bool; - default = false; - description = "Announce clightning Tor Hidden Service"; - }; dataDir = mkOption { type = types.path; default = "/var/lib/clightning"; @@ -97,11 +83,24 @@ in { ''; description = "Binary to connect with the clightning instance."; }; - enforceTor = nix-bitcoin-services.enforceTor; + getPublicAddressCmd = mkOption { + type = types.str; + default = ""; + description = '' + Bash expression which outputs the public service address to announce to peers. + If left empty, no address is announced. + ''; + }; + inherit (nix-bitcoin-services) enforceTor; }; config = mkIf cfg.enable { - services.bitcoind.enable = true; + services.bitcoind = { + enable = true; + # Increase rpc thread count due to reports that lightning implementations fail + # under high bitcoind rpc load + rpc.threads = 16; + }; environment.systemPackages = [ nbPkgs.clightning (hiPrio cfg.cli) ]; users.users.${cfg.user} = { @@ -116,21 +115,25 @@ in { "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" ]; - services.onion-chef.access.clightning = if cfg.announce-tor then [ "clightning" ] else []; systemd.services.clightning = { description = "Run clightningd"; path = [ nbPkgs.bitcoind ]; wantedBy = [ "multi-user.target" ]; - requires = [ "bitcoind.service" ] ++ onion-chef-service; - after = [ "bitcoind.service" ] ++ onion-chef-service; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" ]; preStart = '' cp ${configFile} ${cfg.dataDir}/config chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' # The RPC socket has to be removed otherwise we might have stale sockets rm -f ${cfg.networkDir}/lightning-rpc chmod 640 ${cfg.dataDir}/config - echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/config' - ${optionalString cfg.announce-tor "echo announce-addr=$(cat /var/lib/onion-chef/clightning/clightning) >> '${cfg.dataDir}/config'"} + { + echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-public)" + ${optionalString (cfg.getPublicAddressCmd != "") '' + echo "announce-addr=$(${cfg.getPublicAddressCmd})" + ''} + } >> '${cfg.dataDir}/config' + ''; serviceConfig = nix-bitcoin-services.defaultHardening // { ExecStart = "${nbPkgs.clightning}/bin/lightningd --lightning-dir=${cfg.dataDir}"; diff --git a/modules/default.nix b/modules/default.nix index 72d7661..cc013a5 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -6,7 +6,6 @@ electrs = ./electrs.nix; liquid = ./liquid.nix; presets.secure-node = ./presets/secure-node.nix; - nix-bitcoin-webindex = ./nix-bitcoin-webindex.nix; spark-wallet = ./spark-wallet.nix; recurring-donations = ./recurring-donations.nix; lnd = ./lnd.nix; diff --git a/modules/electrs.nix b/modules/electrs.nix index 5258317..5d03c10 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -9,6 +9,16 @@ let in { options.services.electrs = { enable = mkEnableOption "electrs"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for RPC connections."; + }; + port = mkOption { + type = types.port; + default = 50001; + description = "RPC port."; + }; dataDir = mkOption { type = types.path; default = "/var/lib/electrs"; @@ -31,16 +41,6 @@ in { If enabled, the electrs service will sync faster on high-memory systems (≥ 8GB). ''; }; - address = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "RPC and monitoring listening address."; - }; - port = mkOption { - type = types.port; - default = 50001; - description = "RPC port."; - }; monitoringPort = mkOption { type = types.port; default = 4224; @@ -95,7 +95,7 @@ in { --daemon-dir='${bitcoind.dataDir}' \ --electrum-rpc-addr=${cfg.address}:${toString cfg.port} \ --monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \ - --daemon-rpc-addr=${bitcoind.rpcbind}:${toString bitcoind.rpc.port} \ + --daemon-rpc-addr=${bitcoind.rpc.address}:${toString bitcoind.rpc.port} \ ${cfg.extraArgs} ''; User = cfg.user; diff --git a/modules/joinmarket.nix b/modules/joinmarket.nix index 66d36a7..d1c2367 100644 --- a/modules/joinmarket.nix +++ b/modules/joinmarket.nix @@ -21,7 +21,7 @@ let [BLOCKCHAIN] blockchain_source = bitcoin-rpc network = ${bitcoind.network} - rpc_host = ${bitcoind.rpcbind} + rpc_host = ${bitcoind.rpc.address} rpc_port = ${toString bitcoind.rpc.port} rpc_user = ${bitcoind.rpc.users.privileged.name} @@RPC_PASSWORD@@ diff --git a/modules/lightning-loop.nix b/modules/lightning-loop.nix index 8702297..37d9448 100644 --- a/modules/lightning-loop.nix +++ b/modules/lightning-loop.nix @@ -17,7 +17,7 @@ let tlscertpath=${secretsDir}/loop-cert tlskeypath=${secretsDir}/loop-key - lnd.host=${config.services.lnd.rpclisten}:${toString config.services.lnd.rpcPort} + lnd.host=${config.services.lnd.rpcAddress}:${toString config.services.lnd.rpcPort} lnd.macaroondir=${config.services.lnd.networkDir} lnd.tlspath=${secretsDir}/lnd-cert diff --git a/modules/liquid.nix b/modules/liquid.nix index ec3ee6e..6e83321 100644 --- a/modules/liquid.nix +++ b/modules/liquid.nix @@ -16,23 +16,22 @@ let ${optionalString (cfg.validatepegin != null) "validatepegin=${if cfg.validatepegin then "1" else "0"}"} # Connection options - ${optionalString cfg.listen "bind=${cfg.bind}"} - ${optionalString (cfg.port != null) "port=${toString cfg.port}"} + ${optionalString cfg.listen "bind=${cfg.address}"} + port=${toString cfg.port} ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} listen=${if cfg.listen then "1" else "0"} # RPC server options - ${optionalString (cfg.rpc.port != null) "rpcport=${toString cfg.rpc.port}"} + rpcport=${toString cfg.rpc.port} ${concatMapStringsSep "\n" (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (attrValues cfg.rpc.users) } - rpcbind=${cfg.rpcbind} - rpcconnect=${cfg.rpcbind} + rpcbind=${cfg.rpc.address} + rpcconnect=${cfg.rpc.address} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} - ${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"} - ${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"} - mainchainrpchost=${config.services.bitcoind.rpcbind} + rpcuser=${cfg.rpcuser} + mainchainrpchost=${config.services.bitcoind.rpc.address} mainchainrpcport=${toString config.services.bitcoind.rpc.port} mainchainrpcuser=${config.services.bitcoind.rpc.users.public.name} @@ -71,7 +70,16 @@ in { services.liquidd = { enable = mkEnableOption "Liquid sidechain"; - + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for peer connections."; + }; + port = mkOption { + type = types.port; + default = 7042; + description = "Override the default port on which to listen for connections."; + }; extraConfig = mkOption { type = types.lines; default = ""; @@ -88,14 +96,6 @@ in { default = "/var/lib/liquidd"; description = "The data directory for liquidd."; }; - bind = mkOption { - type = types.str; - default = "127.0.0.1"; - description = '' - Bind to given address and always listen on it. - ''; - }; - user = mkOption { type = types.str; default = "liquid"; @@ -106,12 +106,16 @@ in { default = cfg.user; description = "The group as which to run liquidd."; }; - rpc = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for JSON-RPC connections."; + }; port = mkOption { - type = types.nullOr types.port; - default = null; - description = "Override the default port on which to listen for JSON-RPC connections."; + type = types.port; + default = 7041; + description = "Port to listen for JSON-RPC connections."; }; users = mkOption { default = {}; @@ -125,14 +129,6 @@ in { ''; }; }; - - rpcbind = mkOption { - type = types.str; - default = "127.0.0.1"; - description = '' - Bind to given address to listen for JSON-RPC connections. - ''; - }; rpcallowip = mkOption { type = types.listOf types.str; default = [ "127.0.0.1" ]; @@ -141,25 +137,15 @@ in { ''; }; rpcuser = mkOption { - type = types.nullOr types.str; - default = null; + type = types.str; + default = "liquidrpc"; description = "Username for JSON-RPC connections"; }; - rpcpassword = mkOption { - type = types.nullOr types.str; - default = null; - description = "Password for JSON-RPC connections"; - }; testnet = mkOption { type = types.bool; default = false; description = "Whether to use the test chain."; }; - port = mkOption { - type = types.nullOr types.port; - default = null; - description = "Override the default port on which to listen for connections."; - }; proxy = mkOption { type = types.nullOr types.str; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null; diff --git a/modules/lnd.nix b/modules/lnd.nix index 7df7934..e8fb9c7 100644 --- a/modules/lnd.nix +++ b/modules/lnd.nix @@ -8,8 +8,7 @@ let secretsDir = config.nix-bitcoin.secretsDir; bitcoind = config.services.bitcoind; - bitcoindRpcAddress = bitcoind.rpcbind; - onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []); + bitcoindRpcAddress = bitcoind.rpc.address; networkDir = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}"; configFile = pkgs.writeText "lnd.conf" '' datadir=${cfg.dataDir} @@ -17,9 +16,9 @@ let tlscertpath=${secretsDir}/lnd-cert tlskeypath=${secretsDir}/lnd-key - listen=${toString cfg.listen}:${toString cfg.listenPort} - rpclisten=${cfg.rpclisten}:${toString cfg.rpcPort} - restlisten=${cfg.restlisten}:${toString cfg.restPort} + listen=${toString cfg.address}:${toString cfg.port} + rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort} + restlisten=${cfg.restAddress}:${toString cfg.restPort} bitcoin.${bitcoind.network}=1 bitcoin.active=1 @@ -55,50 +54,43 @@ in { default = networkDir; description = "The network data directory."; }; - listen = mkOption { - type = config.nix-bitcoin.pkgs.lib.ipv4Address; + address = mkOption { + type = types.str; default = "localhost"; - description = "Bind to given address to listen to peer connections"; + description = "Address to listen for peer connections"; }; - listenPort = mkOption { + port = mkOption { type = types.port; default = 9735; - description = "Bind to given port to listen to peer connections"; + description = "Port to listen for peer connections"; }; - rpclisten = mkOption { + rpcAddress = mkOption { type = types.str; default = "localhost"; - description = '' - Bind to given address to listen to RPC connections. - ''; - }; - restlisten = mkOption { - type = types.str; - default = "localhost"; - description = '' - Bind to given address to listen to REST connections. - ''; + description = "Address to listen for RPC connections."; }; rpcPort = mkOption { type = types.port; default = 10009; - description = "Port on which to listen for gRPC connections."; + description = "Port to listen for gRPC connections."; + }; + restAddress = mkOption { + type = types.str; + default = "localhost"; + description = '' + Address to listen for REST connections. + ''; }; restPort = mkOption { type = types.port; default = 8080; - description = "Port on which to listen for REST connections."; + description = "Port to listen for REST connections."; }; tor-socks = mkOption { type = types.nullOr types.str; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null; description = "Set a socks proxy to use to connect to Tor nodes"; }; - announce-tor = mkOption { - type = types.bool; - default = false; - description = "Announce LND Tor Hidden Service"; - }; macaroons = mkOption { default = {}; type = with types; attrsOf (submodule { @@ -138,13 +130,21 @@ in { # Switch user because lnd makes datadir contents readable by user only '' sudo -u lnd ${cfg.package}/bin/lncli \ - --rpcserver ${cfg.rpclisten}:${toString cfg.rpcPort} \ + --rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \ --tlscertpath '${secretsDir}/lnd-cert' \ --macaroonpath '${networkDir}/admin.macaroon' "$@" ''; description = "Binary to connect with the lnd instance."; }; - enforceTor = nix-bitcoin-services.enforceTor; + getPublicAddressCmd = mkOption { + type = types.str; + default = ""; + description = '' + Bash expression which outputs the public service address to announce to peers. + If left empty, no address is announced. + ''; + }; + inherit (nix-bitcoin-services) enforceTor; }; config = mkIf cfg.enable { @@ -154,7 +154,12 @@ in { } ]; - services.bitcoind.enable = true; + services.bitcoind = { + enable = true; + # Increase rpc thread count due to reports that lightning implementations fail + # under high bitcoind rpc load + rpc.threads = 16; + }; environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ]; @@ -167,16 +172,19 @@ in { zmqpubrawtx = "tcp://${bitcoindRpcAddress}:28333"; }; - services.onion-chef.access.lnd = if cfg.announce-tor then [ "lnd" ] else []; systemd.services.lnd = { description = "Run LND"; wantedBy = [ "multi-user.target" ]; - requires = [ "bitcoind.service" ] ++ onion-chef-service; - after = [ "bitcoind.service" ] ++ onion-chef-service; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" ]; preStart = '' install -m600 ${configFile} '${cfg.dataDir}/lnd.conf' - echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/lnd.conf' - ${optionalString cfg.announce-tor "echo externalip=$(cat /var/lib/onion-chef/lnd/lnd) >> '${cfg.dataDir}/lnd.conf'"} + { + echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" + ${optionalString (cfg.getPublicAddressCmd != "") '' + echo "externalip=$(${cfg.getPublicAddressCmd})" + ''} + } >> '${cfg.dataDir}/lnd.conf' ''; serviceConfig = nix-bitcoin-services.defaultHardening // { RuntimeDirectory = "lnd"; # Only used to store custom macaroons @@ -187,12 +195,12 @@ in { RestartSec = "10s"; ReadWritePaths = "${cfg.dataDir}"; ExecStartPost = let - restUrl = "https://${cfg.restlisten}:${toString cfg.restPort}/v1"; + restUrl = "https://${cfg.restAddress}:${toString cfg.restPort}/v1"; in [ # Run fully privileged for secrets dir write access "+${nix-bitcoin-services.script '' attempts=250 - while ! { exec 3>/dev/tcp/${cfg.restlisten}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do + while ! { exec 3>/dev/tcp/${cfg.restAddress}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do ((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; } sleep 0.1 done @@ -234,7 +242,7 @@ in { fi # Wait until the RPC port is open - while ! { exec 3>/dev/tcp/${cfg.rpclisten}/${toString cfg.rpcPort}; } &>/dev/null; do + while ! { exec 3>/dev/tcp/${cfg.rpcAddress}/${toString cfg.rpcPort}; } &>/dev/null; do sleep 0.1 done diff --git a/modules/modules.nix b/modules/modules.nix index 96fc869..4788da6 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -24,9 +24,11 @@ with lib; # Support features ./versioning.nix ./security.nix + ./onion-addresses.nix + ./onion-services.nix ./netns-isolation.nix + ./nodeinfo.nix ./backups.nix - ./onion-chef.nix ]; disabledModules = [ "services/networking/bitcoind.nix" ]; @@ -58,11 +60,11 @@ with lib; config = { assertions = [ - { assertion = (config.services.lnd.enable -> ( !config.services.clightning.enable || config.services.clightning.bindport != config.services.lnd.listenPort)); + { assertion = (config.services.lnd.enable -> ( !config.services.clightning.enable || config.services.clightning.port != config.services.lnd.port)); message = '' LND and clightning can't both bind to lightning port 9735. Either disable LND/clightning or change services.clightning.bindPort or - services.lnd.listenPort to a port other than 9735. + services.lnd.port to a port other than 9735. ''; } ]; diff --git a/modules/netns-isolation.nix b/modules/netns-isolation.nix index 0d335f5..36e5300 100644 --- a/modules/netns-isolation.nix +++ b/modules/netns-isolation.nix @@ -245,26 +245,26 @@ in { }; services.bitcoind = { - bind = netns.bitcoind.address; - rpcbind = netns.bitcoind.address; - rpcallowip = [ + address = netns.bitcoind.address; + rpc.address = netns.bitcoind.address; + rpc.allowip = [ bridgeIp # For operator user netns.bitcoind.address ] ++ map (n: netns.${n}.address) netns.bitcoind.availableNetns; }; systemd.services.bitcoind-import-banlist.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-bitcoind"; - services.clightning.bind-addr = netns.clightning.address; + services.clightning.address = netns.clightning.address; services.lnd = { - listen = netns.lnd.address; - rpclisten = netns.lnd.address; - restlisten = netns.lnd.address; + address = netns.lnd.address; + rpcAddress = netns.lnd.address; + restAddress = netns.lnd.address; }; services.liquidd = { - bind = netns.liquidd.address; - rpcbind = netns.liquidd.address; + address = netns.liquidd.address; + rpc.address = netns.liquidd.address; rpcallowip = [ bridgeIp # For operator user netns.liquidd.address @@ -274,14 +274,14 @@ in { services.electrs.address = netns.electrs.address; services.spark-wallet = { - host = netns.spark-wallet.address; + address = netns.spark-wallet.address; extraArgs = "--no-tls"; }; services.lightning-loop.rpcAddress = netns.lightning-loop.address; - services.nbxplorer.bind = netns.nbxplorer.address; - services.btcpayserver.bind = netns.btcpayserver.address; + services.nbxplorer.address = netns.nbxplorer.address; + services.btcpayserver.address = netns.btcpayserver.address; services.joinmarket.cliExec = mkCliExec "joinmarket"; systemd.services.joinmarket-yieldgenerator.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-joinmarket"; diff --git a/modules/nix-bitcoin-webindex.nix b/modules/nix-bitcoin-webindex.nix deleted file mode 100644 index 4224243..0000000 --- a/modules/nix-bitcoin-webindex.nix +++ /dev/null @@ -1,105 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.services.nix-bitcoin-webindex; - inherit (config) nix-bitcoin-services; - indexFile = pkgs.writeText "index.html" '' - - -

-

- nix-bitcoin -

-

-

-

- lightning node: CLIGHTNING_ID -

-

- - - ''; - createWebIndex = pkgs.writeText "make-index.sh" '' - set -e - cp ${indexFile} /var/www/index.html - chown -R nginx:nginx /var/www/ - nodeinfo - . <(nodeinfo) - sed -i "s/CLIGHTNING_ID/$CLIGHTNING_ID/g" /var/www/index.html - ''; -in { - options.services.nix-bitcoin-webindex = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - If enabled, the webindex service will be installed. - ''; - }; - host = mkOption { - type = types.str; - default = if config.nix-bitcoin.netns-isolation.enable then - config.nix-bitcoin.netns-isolation.netns.nginx.address - else - "localhost"; - description = "HTTP server listen address."; - }; - enforceTor = nix-bitcoin-services.enforceTor; - }; - - config = mkIf cfg.enable { - assertions = [ - { assertion = config.services.clightning.enable; - message = "nix-bitcoin-webindex requires clightning."; - } - ]; - - systemd.tmpfiles.rules = [ - "d /var/www 0755 nginx nginx - -" - ]; - - services.nginx = { - enable = true; - virtualHosts."_" = { - root = "/var/www"; - }; - }; - services.tor.hiddenServices.nginx = { - map = [{ - port = 80; toHost = cfg.host; - } { - port = 443; toHost = cfg.host; - }]; - version = 3; - }; - - # create-web-index - systemd.services.create-web-index = { - description = "Get node info"; - wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ - config.programs.nodeinfo - jq - sudo - ] ++ optional config.services.lnd.enable config.services.lnd.cli - ++ optional config.services.clightning.enable config.services.clightning.cli; - serviceConfig = nix-bitcoin-services.defaultHardening // { - ExecStart="${pkgs.bash}/bin/bash ${createWebIndex}"; - User = "root"; - Type = "simple"; - RemainAfterExit="yes"; - Restart = "on-failure"; - RestartSec = "10s"; - PrivateNetwork = "true"; # This service needs no network access - PrivateUsers = "false"; - ReadWritePaths = "/var/www"; - CapabilityBoundingSet = "CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_SYS_ADMIN CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER"; - } // (if cfg.enforceTor - then nix-bitcoin-services.allowTor - else nix-bitcoin-services.allowAnyIP - ); - }; - }; -} diff --git a/modules/nodeinfo.nix b/modules/nodeinfo.nix index 86f4174..afd3fa6 100644 --- a/modules/nodeinfo.nix +++ b/modules/nodeinfo.nix @@ -1,74 +1,117 @@ { config, lib, pkgs, ... }: with lib; - let - operatorName = config.nix-bitcoin.operator.name; + cfg = config.nix-bitcoin.nodeinfo; + + # Services included in the output + services = { + bitcoind = mkInfo ""; + clightning = mkInfo '' + info["nodeid"] = shell("lightning-cli getinfo | jq -r '.id'") + if 'onion_address' in info: + info["id"] = f"{info['nodeid']}@{info['onion_address']}" + ''; + lnd = mkInfo '' + info["nodeid"] = shell("lightning-cli getinfo | jq -r '.id'") + ''; + electrs = mkInfo ""; + spark-wallet = mkInfo ""; + btcpayserver = mkInfo ""; + liquidd = mkInfo ""; + # Only add sshd when it has an onion service + sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: '' + add_service("sshd", """set_onion_address(info, "sshd", ${onionPort})""") + ''); + }; + script = pkgs.writeScriptBin "nodeinfo" '' - set -eo pipefail + #!${pkgs.python3}/bin/python - BITCOIND_ONION="$(cat /var/lib/onion-chef/${operatorName}/bitcoind)" - echo BITCOIND_ONION="$BITCOIND_ONION" + import json + import subprocess + from collections import OrderedDict - if systemctl is-active --quiet clightning; then - CLIGHTNING_NODEID=$(lightning-cli getinfo | jq -r '.id') - CLIGHTNING_ONION="$(cat /var/lib/onion-chef/${operatorName}/clightning)" - CLIGHTNING_ID="$CLIGHTNING_NODEID@$CLIGHTNING_ONION:9735" - echo CLIGHTNING_NODEID="$CLIGHTNING_NODEID" - echo CLIGHTNING_ONION="$CLIGHTNING_ONION" - echo CLIGHTNING_ID="$CLIGHTNING_ID" - fi + def success(*args): + return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 - if systemctl is-active --quiet lnd; then - LND_NODEID=$(lncli getinfo | jq -r '.uris[0]') - echo LND_NODEID="$LND_NODEID" - fi + def is_active(unit): + return success("systemctl", "is-active", "--quiet", unit) - NGINX_ONION_FILE=/var/lib/onion-chef/${operatorName}/nginx - if [ -e "$NGINX_ONION_FILE" ]; then - NGINX_ONION="$(cat $NGINX_ONION_FILE)" - echo NGINX_ONION="$NGINX_ONION" - fi + def is_enabled(unit): + return success("systemctl", "is-enabled", "--quiet", unit) - LIQUIDD_ONION_FILE=/var/lib/onion-chef/${operatorName}/liquidd - if [ -e "$LIQUIDD_ONION_FILE" ]; then - LIQUIDD_ONION="$(cat $LIQUIDD_ONION_FILE)" - echo LIQUIDD_ONION="$LIQUIDD_ONION" - fi + def cmd(*args): + return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8') - SPARKWALLET_ONION_FILE=/var/lib/onion-chef/${operatorName}/spark-wallet - if [ -e "$SPARKWALLET_ONION_FILE" ]; then - SPARKWALLET_ONION="$(cat $SPARKWALLET_ONION_FILE)" - echo SPARKWALLET_ONION="http://$SPARKWALLET_ONION" - fi + def shell(*args): + return cmd("bash", "-c", *args).strip() - ELECTRS_ONION_FILE=/var/lib/onion-chef/${operatorName}/electrs - if [ -e "$ELECTRS_ONION_FILE" ]; then - ELECTRS_ONION="$(cat $ELECTRS_ONION_FILE)" - echo ELECTRS_ONION="$ELECTRS_ONION" - fi + infos = OrderedDict() + operator = "${config.nix-bitcoin.operator.name}" - BTCPAYSERVER_ONION_FILE=/var/lib/onion-chef/${operatorName}/btcpayserver - if [ -e "$BTCPAYSERVER_ONION_FILE" ]; then - BTCPAYSERVER_ONION="$(cat $BTCPAYSERVER_ONION_FILE)" - echo BTCPAYSERVER_ONION="$BTCPAYSERVER_ONION" - fi + def set_onion_address(info, name, port): + path = f"/var/lib/onion-addresses/{operator}/{name}" + try: + with open(path, "r") as f: + onion_address = f.read().strip() + except OSError: + print(f"error reading file {path}", file=sys.stderr) + return + info["onion_address"] = f"{onion_address}:{port}" - SSHD_ONION_FILE=/var/lib/onion-chef/${operatorName}/sshd - if [ -e "$SSHD_ONION_FILE" ]; then - SSHD_ONION="$(cat $SSHD_ONION_FILE)" - echo SSHD_ONION="$SSHD_ONION" - fi + def add_service(service, make_info): + if not is_active(service): + infos[service] = "service is not running" + else: + info = OrderedDict() + exec(make_info, globals(), locals()) + infos[service] = info + + if is_enabled("onion-adresses") and not is_active("onion-adresses"): + print("error: service 'onion-adresses' is not running") + exit(1) + + ${concatStrings infos} + + print(json.dumps(infos, indent=2)) ''; + + infos = map (service: + let cfg = config.services.${service}; + in optionalString cfg.enable (services.${service} service cfg) + ) (builtins.attrNames services); + + mkInfo = extraCode: name: cfg: + '' + add_service("${name}", """ + info["local_address"] = "${cfg.address}:${toString cfg.port}" + '' + mkIfOnionPort name (onionPort: '' + set_onion_address(info, "${name}", ${onionPort}) + '') + extraCode + '' + + """) + ''; + + mkIfOnionPort = name: fn: + if hiddenServices ? ${name} then + fn (toString (builtins.elemAt hiddenServices.${name}.map 0).port) + else + ""; + + inherit (config.services.tor) hiddenServices; in { options = { - programs.nodeinfo = mkOption { - readOnly = true; - default = script; + nix-bitcoin.nodeinfo = { + enable = mkEnableOption "nodeinfo"; + program = mkOption { + readOnly = true; + default = script; + }; }; }; config = { - environment.systemPackages = [ script ]; + environment.systemPackages = optional cfg.enable script; }; } diff --git a/modules/obsolete-options.nix b/modules/obsolete-options.nix new file mode 100644 index 0000000..4c39aff --- /dev/null +++ b/modules/obsolete-options.nix @@ -0,0 +1,28 @@ +{ lib, ... }: + +with lib; +let + mkRenamedAnnounceTorOption = service: + # use mkRemovedOptionModule because mkRenamedOptionModule fails with an infinite recursion error + mkRemovedOptionModule [ "services" service "announce-tor" ] '' + Use option `nix-bitcoin.onionServices.${service}.public` instead. + ''; +in { + imports = [ + (mkRenamedOptionModule [ "services" "bitcoind" "bind" ] [ "services" "bitcoind" "address" ]) + (mkRenamedOptionModule [ "services" "bitcoind" "rpcallowip" ] [ "services" "bitcoind" "rpc" "allowip" ]) + (mkRenamedOptionModule [ "services" "bitcoind" "rpcthreads" ] [ "services" "bitcoind" "rpc" "threads" ]) + (mkRenamedOptionModule [ "services" "clightning" "bind-addr" ] [ "services" "clightning" "address" ]) + (mkRenamedOptionModule [ "services" "clightning" "bindport" ] [ "services" "clightning" "port" ]) + (mkRenamedOptionModule [ "services" "spark-wallet" "host" ] [ "services" "spark-wallet" "address" ]) + (mkRenamedOptionModule [ "services" "lnd" "rpclisten" ] [ "services" "lnd" "rpcAddress" ]) + (mkRenamedOptionModule [ "services" "lnd" "listen" ] [ "services" "lnd" "address" ]) + (mkRenamedOptionModule [ "services" "lnd" "listenPort" ] [ "services" "lnd" "port" ]) + (mkRenamedOptionModule [ "services" "btcpayserver" "bind" ] [ "services" "btcpayserver" "address" ]) + (mkRenamedOptionModule [ "services" "liquidd" "bind" ] [ "services" "liquidd" "address" ]) + (mkRenamedOptionModule [ "services" "liquidd" "rpcbind" ] [ "services" "liquidd" "rpc" "address" ]) + + (mkRenamedAnnounceTorOption "clightning") + (mkRenamedAnnounceTorOption "lnd") + ]; +} diff --git a/modules/onion-addresses.nix b/modules/onion-addresses.nix new file mode 100644 index 0000000..f715337 --- /dev/null +++ b/modules/onion-addresses.nix @@ -0,0 +1,76 @@ +# This module enables unprivileged users to read onion addresses. +# By default, onion addresses in /var/lib/tor/onion are only readable by the +# tor user. +# The included service copies onion addresses to /var/lib/onion-addresses// +# and sets permissions according to option 'access'. + +{ config, lib, ... }: + +with lib; + +let + cfg = config.nix-bitcoin.onionAddresses; + inherit (config) nix-bitcoin-services; +in { + options.nix-bitcoin.onionAddresses = { + access = mkOption { + type = with types; attrsOf (listOf str); + default = {}; + description = '' + This option controls who is allowed to access onion addresses. + For example, the following allows user 'myuser' to access bitcoind + and clightning onion addresses: + { + "myuser" = [ "bitcoind" "clightning" ]; + }; + The onion hostnames can then be read from + /var/lib/onion-addresses/myuser. + ''; + }; + dataDir = mkOption { + readOnly = true; + default = "/var/lib/onion-addresses"; + }; + }; + + config = mkIf (cfg.access != {}) { + systemd.services.onion-addresses = { + wantedBy = [ "tor.service" ]; + bindsTo = [ "tor.service" ]; + after = [ "tor.service" ]; + serviceConfig = nix-bitcoin-services.defaultHardening // { + Type = "oneshot"; + RemainAfterExit = true; + StateDirectory = "onion-addresses"; + PrivateNetwork = "true"; # This service needs no network access + PrivateUsers = "false"; + CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER"; + }; + script = '' + # Wait until tor is up + until [[ -e /var/lib/tor/state ]]; do sleep 0.1; done + + cd ${cfg.dataDir} + rm -rf * + + ${concatMapStrings + (user: '' + mkdir -p -m 0700 ${user} + chown ${user} ${user} + ${concatMapStrings + (service: '' + onionFile=/var/lib/tor/onion/${service}/hostname + if [[ -e $onionFile ]]; then + cp $onionFile ${user}/${service} + chown ${user} ${user}/${service} + fi + '') + cfg.access.${user} + } + '') + (builtins.attrNames cfg.access) + } + ''; + }; + }; +} diff --git a/modules/onion-chef.nix b/modules/onion-chef.nix deleted file mode 100644 index 2fe3839..0000000 --- a/modules/onion-chef.nix +++ /dev/null @@ -1,90 +0,0 @@ -# The onion chef module allows unprivileged users to read onion hostnames. -# By default the onion hostnames in /var/lib/tor/onion are only readable by the -# tor user. The onion chef copies the onion hostnames into into -# /var/lib/onion-chef and sets permissions according to the access option. - -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.services.onion-chef; - inherit (config) nix-bitcoin-services; - dataDir = "/var/lib/onion-chef/"; - onion-chef-script = pkgs.writeScript "onion-chef.sh" '' - # wait until tor is up - until ls -l /var/lib/tor/state; do sleep 1; done - - cd ${dataDir} - - # Create directory for every user and set permissions - ${ builtins.foldl' - (x: user: x + - '' - mkdir -p -m 0700 ${user} - chown ${user} ${user} - # Copy onion hostnames into the user's directory - ${ builtins.foldl' - (x: onion: x + - '' - ONION_FILE=/var/lib/tor/onion/${onion}/hostname - if [ -e "$ONION_FILE" ]; then - cp $ONION_FILE ${user}/${onion} - chown ${user} ${user}/${onion} - fi - '') - "" - (builtins.getAttr user cfg.access) - } - '') - "" - (builtins.attrNames cfg.access) - } - ''; -in { - options.services.onion-chef = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - If enabled, the onion-chef service will be installed. - ''; - }; - access = mkOption { - type = types.attrs; - default = {}; - description = '' - This option controls who is allowed to access onion hostnames. For - example the following allows the user operator to access the bitcoind - and clightning onion. - { - "operator" = [ "bitcoind" "clightning" ]; - }; - The onion hostnames can then be read from - /var/lib/onion-chef/. - ''; - }; - }; - - config = mkIf cfg.enable { - systemd.tmpfiles.rules = [ - "d '${dataDir}' 0755 root root - -" - ]; - - systemd.services.onion-chef = { - description = "Run onion-chef"; - wantedBy = [ "tor.service" ]; - bindsTo = [ "tor.service" ]; - after = [ "tor.service" ]; - serviceConfig = nix-bitcoin-services.defaultHardening // { - ExecStart = "${pkgs.bash}/bin/bash ${onion-chef-script}"; - Type = "oneshot"; - RemainAfterExit = true; - PrivateNetwork = "true"; # This service needs no network access - PrivateUsers = "false"; - ReadWritePaths = "${dataDir}"; - CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER"; - }; - }; - }; -} diff --git a/modules/onion-services.nix b/modules/onion-services.nix new file mode 100644 index 0000000..db0dac1 --- /dev/null +++ b/modules/onion-services.nix @@ -0,0 +1,121 @@ +# This module creates onion-services for NixOS services. +# An onion service can be enabled for every service that defines +# options 'address', 'port' and optionally 'getPublicAddressCmd'. +# +# See it in use at ./presets/enable-tor.nix + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.nix-bitcoin.onionServices; + + services = builtins.attrNames cfg; + + activeServices = builtins.filter (service: + config.services.${service}.enable && cfg.${service}.enable + ) services; + + publicServices = builtins.filter (service: cfg.${service}.public) activeServices; +in { + options.nix-bitcoin.onionServices = mkOption { + default = {}; + type = with types; attrsOf (submodule ( + { config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = config.public; + description = '' + Create an onion service for the given service. + The service must define options 'address' and 'port'. + ''; + }; + public = mkOption { + type = types.bool; + default = false; + description = '' + Make the onion address accessible to the service. + If enabled, the onion service is automatically enabled. + Only available for services that define option `getPublicAddressCmd`. + ''; + }; + externalPort = mkOption { + type = types.nullOr types.port; + default = null; + description = "Override the external port of the onion service."; + }; + }; + } + )); + }; + + config = mkMerge [ + (mkIf (cfg != {}) { + # Define hidden services + services.tor = { + enable = true; + hiddenServices = genAttrs activeServices (name: + let + service = config.services.${name}; + inherit (cfg.${name}) externalPort; + in { + map = [{ + port = if externalPort != null then externalPort else service.port; + toPort = service.port; + toHost = if service.address == "0.0.0.0" then "127.0.0.1" else service.address; + }]; + version = 3; + } + ); + }; + + # Enable public services to access their own onion addresses + nix-bitcoin.onionAddresses.access = ( + genAttrs publicServices singleton + ) // { + # Allow the operator user to access onion addresses for all active services + ${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices; + }; + systemd.services = let + onionAddresses = [ "onion-addresses.service" ]; + in genAttrs publicServices (service: { + requires = onionAddresses; + after = onionAddresses; + }); + }) + + # Set getPublicAddressCmd for public services + { + services = let + # publicServices' doesn't depend on config.services.*.enable, + # so we can use it to define config.services without causing infinite recursion + publicServices' = builtins.filter (service: + let srv = cfg.${service}; + in srv.public && srv.enable + ) services; + in genAttrs publicServices' (service: { + getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/${service}/${service}"; + }); + } + + # Set sensible defaults for some services + { + nix-bitcoin.onionServices = { + spark-wallet = { + externalPort = 80; + # Enable 'public' by default, but don't auto-enable the onion service. + # When the onion service is enabled, 'public' lets spark-wallet generate + # a QR code for accessing the web interface. + public = true; + # Low priority so we can override this with mkDefault in ./presets/enable-tor.nix + enable = mkOverride 1400 false; + }; + btcpayserver = { + externalPort = 80; + }; + }; + } + ]; +} diff --git a/modules/presets/enable-tor.nix b/modules/presets/enable-tor.nix new file mode 100644 index 0000000..8d16a9e --- /dev/null +++ b/modules/presets/enable-tor.nix @@ -0,0 +1,32 @@ +{ lib, ... }: +let + defaultTrue = lib.mkDefault true; +in { + services.tor = { + enable = true; + client.enable = true; + }; + + # Use Tor for all outgoing connections + services = { + bitcoind.enforceTor = true; + clightning.enforceTor = true; + lnd.enforceTor = true; + lightning-loop.enforceTor = true; + liquidd.enforceTor = true; + electrs.enforceTor = true; + # disable Tor enforcement until btcpayserver can fetch rates over Tor + # btcpayserver.enforceTor = true; + nbxplorer.enforceTor = true; + spark-wallet.enforceTor = true; + recurring-donations.enforceTor = true; + }; + + # Add onion services for incoming connections + nix-bitcoin.onionServices = { + bitcoind.enable = defaultTrue; + liquidd.enable = defaultTrue; + electrs.enable = defaultTrue; + spark-wallet.enable = defaultTrue; + }; +} diff --git a/modules/presets/secure-node.nix b/modules/presets/secure-node.nix index c4f06fa..a0472c4 100644 --- a/modules/presets/secure-node.nix +++ b/modules/presets/secure-node.nix @@ -14,23 +14,9 @@ let in { imports = [ ../modules.nix - ../nodeinfo.nix - ../nix-bitcoin-webindex.nix + ./enable-tor.nix ]; - options = { - services.clightning.onionport = mkOption { - type = types.port; - default = 9735; - description = "Port on which to listen for tor client connections."; - }; - services.lnd.onionport = mkOption { - type = types.ints.u16; - default = 9735; - description = "Port on which to listen for tor client connections."; - }; - }; - config = { # For backwards compatibility only nix-bitcoin.secretsDir = mkDefault "/secrets"; @@ -39,99 +25,36 @@ in { nix-bitcoin.security.hideProcessInformation = true; - # Tor - services.tor = { - enable = true; - client.enable = true; + environment.systemPackages = with pkgs; [ + jq + ]; - hiddenServices.sshd = mkHiddenService { port = 22; }; - }; + # sshd + services.tor.hiddenServices.sshd = mkHiddenService { port = 22; }; + nix-bitcoin.onionAddresses.access.${operatorName} = [ "sshd" ]; - # bitcoind services.bitcoind = { enable = true; listen = true; dataDirReadableByGroup = mkIf cfg.electrs.high-memory true; - enforceTor = true; - port = 8333; assumevalid = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6"; addnodes = [ "ecoc5q34tmbq54wl.onion" ]; discover = false; addresstype = "bech32"; dbCache = 1000; - # higher rpcthread count due to reports that lightning implementations fail - # under high bitcoind rpc load - rpcthreads = 16; }; - services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; }; - # clightning - services.clightning.enforceTor = true; - services.tor.hiddenServices.clightning = mkIf cfg.clightning.enable (mkHiddenService { - port = cfg.clightning.onionport; - toHost = cfg.clightning.bind-addr; - toPort = cfg.clightning.bindport; - }); - - # lnd - services.lnd.enforceTor = true; - services.tor.hiddenServices.lnd = mkIf cfg.lnd.enable (mkHiddenService { port = cfg.lnd.onionport; toHost = cfg.lnd.listen; toPort = cfg.lnd.listenPort; }); - - # lightning-loop - services.lightning-loop.enforceTor = true; - - # liquidd services.liquidd = { - rpcuser = "liquidrpc"; prune = 1000; validatepegin = true; listen = true; - enforceTor = true; - port = 7042; - }; - services.tor.hiddenServices.liquidd = mkIf cfg.liquidd.enable (mkHiddenService { port = cfg.liquidd.port; toHost = cfg.liquidd.bind; }); - - # electrs - services.electrs = { - port = 50001; - enforceTor = true; - }; - services.tor.hiddenServices.electrs = mkIf cfg.electrs.enable (mkHiddenService { - port = cfg.electrs.port; toHost = cfg.electrs.address; - }); - - # btcpayserver - # disable tor enforcement until btcpayserver can fetch rates over Tor - services.btcpayserver.enforceTor = false; - services.nbxplorer.enforceTor = true; - services.tor.hiddenServices.btcpayserver = mkIf cfg.btcpayserver.enable (mkHiddenService { port = 80; toPort = 23000; toHost = cfg.btcpayserver.bind; }); - - services.spark-wallet = { - onion-service = true; - enforceTor = true; }; - services.recurring-donations.enforceTor = true; + nix-bitcoin.nodeinfo.enable = true; - services.nix-bitcoin-webindex.enforceTor = true; - - # Backups - services.backups = { - program = "duplicity"; - frequency = "daily"; - }; - - environment.systemPackages = with pkgs; [ - tor - jq - qrencode - ]; - - services.onion-chef = { - enable = true; - access.${operatorName} = [ "bitcoind" "clightning" "nginx" "liquidd" "spark-wallet" "electrs" "btcpayserver" "sshd" ]; - }; + services.backups.frequency = "daily"; + # operator nix-bitcoin.operator.enable = true; users.users.${operatorName} = { openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys; diff --git a/modules/spark-wallet.nix b/modules/spark-wallet.nix index 6f9ca68..a130dd2 100644 --- a/modules/spark-wallet.nix +++ b/modules/spark-wallet.nix @@ -5,18 +5,17 @@ with lib; let cfg = config.services.spark-wallet; inherit (config) nix-bitcoin-services; - onion-chef-service = (if cfg.onion-service then [ "onion-chef.service" ] else []); # Use wasabi rate provider because the default (bitstamp) doesn't accept # connections through Tor torRateProvider = "--rate-provider wasabi --proxy socks5h://${config.services.tor.client.socksListenAddress}"; startScript = '' - ${optionalString cfg.onion-service '' - publicURL="--public-url http://$(cat /var/lib/onion-chef/spark-wallet/spark-wallet)" + ${optionalString (cfg.getPublicAddressCmd != "") '' + publicURL="--public-url http://$(${cfg.getPublicAddressCmd})" ''} exec ${config.nix-bitcoin.pkgs.spark-wallet}/bin/spark-wallet \ --ln-path '${config.services.clightning.networkDir}' \ - --host ${cfg.host} \ + --host ${cfg.address} --port ${toString cfg.port} \ --config '${config.nix-bitcoin.secretsDir}/spark-wallet-login' \ ${optionalString cfg.enforceTor torRateProvider} \ $publicURL \ @@ -31,24 +30,31 @@ in { If enabled, the spark-wallet service will be installed. ''; }; - host = mkOption { + address = mkOption { type = types.str; default = "localhost"; - description = "http(s) server listen address."; + description = "http(s) server address."; }; - onion-service = mkOption { - type = types.bool; - default = false; - description = '' - "If enabled, configures spark-wallet to be reachable through an onion service."; - ''; + port = mkOption { + type = types.port; + default = 9737; + description = "http(s) server port."; }; extraArgs = mkOption { type = types.separatedString " "; default = ""; description = "Extra command line arguments passed to spark-wallet."; }; - enforceTor = nix-bitcoin-services.enforceTor; + getPublicAddressCmd = mkOption { + type = types.str; + default = ""; + description = '' + Bash expression which outputs the public service address. + If set, spark-wallet prints a QR code to the systemd journal which + encodes an URL for accessing the web interface. + ''; + }; + inherit (nix-bitcoin-services) enforceTor; }; config = mkIf cfg.enable { @@ -61,25 +67,16 @@ in { }; users.groups.spark-wallet = {}; - services.tor.hiddenServices.spark-wallet = mkIf cfg.onion-service { - map = [{ - port = 80; toPort = 9737; toHost = cfg.host; - }]; - version = 3; - }; - services.onion-chef.enable = cfg.onion-service; - services.onion-chef.access.spark-wallet = if cfg.onion-service then [ "spark-wallet" ] else []; systemd.services.spark-wallet = { description = "Run spark-wallet"; wantedBy = [ "multi-user.target" ]; - requires = [ "clightning.service" ] ++ onion-chef-service; - after = [ "clightning.service" ] ++ onion-chef-service; + requires = [ "clightning.service" ]; + after = [ "clightning.service" ]; script = startScript; serviceConfig = nix-bitcoin-services.defaultHardening // { User = "spark-wallet"; Restart = "on-failure"; RestartSec = "10s"; - ReadWritePaths = mkIf cfg.onion-service "/var/lib/onion-chef"; } // (if cfg.enforceTor then nix-bitcoin-services.allowTor else nix-bitcoin-services.allowAnyIP) diff --git a/modules/versioning.nix b/modules/versioning.nix index ff44728..f06a593 100644 --- a/modules/versioning.nix +++ b/modules/versioning.nix @@ -5,7 +5,19 @@ let version = config.nix-bitcoin.configVersion; # Sorted by increasing version numbers - changes = [ + changes = let + mkOnionServiceChange = service: { + version = "0.0.30"; + condition = config.services.${service}.enable; + message = '' + The onion service for ${service} has been disabled in the default + configuration (`secure-node.nix`). + + To enable the onion service, add the following to your configuration: + nix-bitcon.onionServices.${service}.enable = true; + ''; + }; + in [ { version = "0.0.26"; condition = config.services.joinmarket.enable; @@ -54,6 +66,9 @@ let https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.8.0/docs/NATIVE-SEGWIT-UPGRADE.md ''; } + (mkOnionServiceChange "clightning") + (mkOnionServiceChange "lnd") + (mkOnionServiceChange "btcpayserver") ]; incompatibleChanges = optionals @@ -76,6 +91,10 @@ let lastChange = builtins.elemAt changes (builtins.length changes - 1); in { + imports = [ + ./obsolete-options.nix + ]; + options = { nix-bitcoin.configVersion = mkOption { type = with types; nullOr str; @@ -93,6 +112,6 @@ in config = { # Force evaluation. An actual option value is never assigned - system.extraDependencies = optional (builtins.length incompatibleChanges > 0) (builtins.throw errorMsg); + system = optionalAttrs (builtins.length incompatibleChanges > 0) (builtins.throw errorMsg); }; } diff --git a/pkgs/default.nix b/pkgs/default.nix index 639b6b2..db178c1 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -20,7 +20,5 @@ let self = { pinned = import ./pinned.nix; - lib = import ./lib.nix { inherit (pkgs) lib; }; - modulesPkgs = self // self.pinned; }; in self diff --git a/pkgs/lib.nix b/pkgs/lib.nix deleted file mode 100644 index 43dbe45..0000000 --- a/pkgs/lib.nix +++ /dev/null @@ -1,5 +0,0 @@ -{ lib }: -{ - # An address type that checks that there's no port - ipv4Address = lib.types.addCheck lib.types.str (s: builtins.length (builtins.split ":" s) == 1); -} diff --git a/test/tests.nix b/test/tests.nix index 10c608d..83e4629 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -44,7 +44,7 @@ let testEnv = rec { tests.spark-wallet = cfg.spark-wallet.enable; tests.lnd = cfg.lnd.enable; - services.lnd.listenPort = 9736; + services.lnd.port = 9736; tests.lightning-loop = cfg.lightning-loop.enable; @@ -68,6 +68,8 @@ let testEnv = rec { ''; }; + tests.nodeinfo = config.nix-bitcoin.nodeinfo.enable; + tests.backups = cfg.backups.enable; # To test that unused secrets are made inaccessible by 'setup-secrets' @@ -119,6 +121,8 @@ let testEnv = rec { services.joinmarket.enable = true; services.backups.enable = true; + nix-bitcoin.nodeinfo.enable = true; + services.hardware-wallets = { trezor = true; ledger = true; @@ -130,7 +134,6 @@ let testEnv = rec { scenarios.full ../modules/presets/secure-node.nix ]; - services.nix-bitcoin-webindex.enable = true; tests.secure-node = true; tests.banlist-and-restart = true; diff --git a/test/tests.py b/test/tests.py index 53165ed..329dcc6 100644 --- a/test/tests.py +++ b/test/tests.py @@ -216,17 +216,19 @@ def _(): ) +@test("nodeinfo") +def _(): + status, _ = machine.execute("systemctl is-enabled --quiet onion-addresses 2> /dev/null") + if status == 0: + machine.wait_for_unit("onion-addresses") + json_info = succeed("sudo -u operator nodeinfo") + info = json.loads(json_info) + assert info["bitcoind"]["local_address"] + + @test("secure-node") def _(): - assert_running("onion-chef") - - # FIXME: use 'wait_for_unit' because 'create-web-index' always fails during startup due - # to incomplete unit dependencies. - # 'create-web-index' implicitly tests 'nodeinfo'. - machine.wait_for_unit("create-web-index") - assert_running("nginx") - wait_for_open_port(ip("nginx"), 80) - assert_matches(f"curl {ip('nginx')}", "nix-bitcoin") + assert_running("onion-addresses") # Run this test before the following tests that shut down services