diff --git a/README.md b/README.md index 8ebba91..2f5d5f9 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,6 @@ Features --- A [configuration preset](modules/presets/secure-node.nix) for setting up a secure node * All applications use Tor for outbound connections and support accepting inbound connections via onion services. -* Includes a [nodeinfo](modules/nodeinfo.nix) script which prints basic info about the node. NixOS modules * Application services @@ -74,6 +73,7 @@ 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`) diff --git a/docs/usage.md b/docs/usage.md index d6baab4..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 the onion addresses for enabled services. +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/modules/modules.nix b/modules/modules.nix index d2e6690..4788da6 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -27,6 +27,7 @@ with lib; ./onion-addresses.nix ./onion-services.nix ./netns-isolation.nix + ./nodeinfo.nix ./backups.nix ]; diff --git a/modules/nodeinfo.nix b/modules/nodeinfo.nix index 254ad06..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-addresses/${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-addresses/${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-addresses/${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-addresses/${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-addresses/${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-addresses/${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-addresses/${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-addresses/${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/presets/secure-node.nix b/modules/presets/secure-node.nix index 1f1d012..335c364 100644 --- a/modules/presets/secure-node.nix +++ b/modules/presets/secure-node.nix @@ -14,7 +14,6 @@ let in { imports = [ ../modules.nix - ../nodeinfo.nix ./enable-tor.nix ]; @@ -75,5 +74,7 @@ in { cp "${config.users.users.root.home}/.vbox-nixops-client-key" "${config.users.users.${operatorName}.home}" ''; }; + + nix-bitcoin.nodeinfo.enable = true; }; } diff --git a/test/tests.nix b/test/tests.nix index 98b12ff..83e4629 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -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; diff --git a/test/tests.py b/test/tests.py index 01b27e5..329dcc6 100644 --- a/test/tests.py +++ b/test/tests.py @@ -216,6 +216,16 @@ 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-addresses")