diff --git a/.gitignore b/.gitignore index 4bd922a..5598d80 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -secrets/ +/secrets/ diff --git a/.travis.yml b/.travis.yml index 2846259..4fc3a42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,25 @@ -language: minimal +dist: bionic # needed for KVM +language: shell -# broken: -# - PKG=electrs STABLE=0 - -# Retry installing nix due to nondeterministic error -# Fatal error: glibc detected an invalid stdio handle -# see: -# https://github.com/nh2/static-haskell-nix/pull/27#issuecomment-502652181 -# https://github.com/nixos/nix/issues/2733 -install: | - (for i in {1..5}; do bash <(curl https://nixos.org/nix/install) && exit 0; done; exit 1) - . /home/travis/.nix-profile/etc/profile.d/nix.sh - nix-env -iA cachix -f https://cachix.org/api/v1/install - cachix use nix-bitcoin - [ $STABLE -eq 1 ] && export NIX_PATH="nixpkgs=$(nix eval --raw -f pkgs/nixpkgs-pinned.nix nixpkgs)" - [ $STABLE -eq 0 ] && export NIX_PATH="nixpkgs=$(nix eval --raw -f pkgs/nixpkgs-pinned.nix nixpkgs-unstable)" - VER="$(nix eval nixpkgs.lib.version)" +install: + # Retry installing nix due to nondeterministic error + # Fatal error: glibc detected an invalid stdio handle + # see: + # https://github.com/nh2/static-haskell-nix/pull/27#issuecomment-502652181 + # https://github.com/nixos/nix/issues/2733 + - (for i in {1..5}; do bash <(curl https://nixos.org/nix/install) && exit 0; done; exit 1) + - . /home/travis/.nix-profile/etc/profile.d/nix.sh + - if [[ $STABLE == 1 ]]; then export NIX_PATH="nixpkgs=$(nix eval --raw -f pkgs/nixpkgs-pinned.nix nixpkgs)"; fi + - if [[ $STABLE == 0 ]]; then export NIX_PATH="nixpkgs=$(nix eval --raw -f pkgs/nixpkgs-pinned.nix nixpkgs-unstable)"; fi + - nix-env -iA cachix -f https://cachix.org/api/v1/install + - cachix use nix-bitcoin + - VER="$(nix eval nixpkgs.lib.version)" env: - matrix: - - PKG=nodeinfo STABLE=1 + global: + # CACHIX_SIGNING_KEY + - secure: "xXCFZ7g+k5YmCGm8R8l3bZElVmt+RD1KscG3kGr5w4HyyDPTzFetPo+sT8bUpysDU0u3HWhfVhHtpog2mhNhwVl3tQwKXea3dHKC1i6ypBg3gjDngmJRR5wo++ocYDpK8qPaU7m/jHQTNFnTA4CbmMcc05GcYx/1Ai/ZGkNwWFjdIcVeOUoiol33gykMOXIGDg2qlXudt33wP53FHbX8L4fxzodWfAuxKK4AoGprxy5eSnU7LCaXxxJmu4HwuV+Ux2U1NfE/E33cvhlUvTQCswVSZFG06mg8rwhMG1ozsDvlL2itZlu/BeUQH5y3XMMlnJIUXUazkRBibf1w/ebVjpOF+anqkqmq8tcbFEa7T+RJeVTIsvP+L8rE8fcmuZtdg9hNmgRnLmaeT0vVwD1L2UqW9HdRyujdoS0jPYuoc1W7f1JQWfAPhBPQ1SrtKyNNqcbVJ34aN7b+4vCzRpQL1JTbmjzQIWhkiKN1qMo1v/wbIydW8yka4hc4JOfdQLaAJEPI1eAC1MLotSAegMnwKWE1dzm66MuPSipksYjZrvsB28cV4aCVUffIuRhrSr1i2afRHwTpNbK9U4/576hah15ftUdR79Sfkcoi1ekSQTFGRvkRIPYtkKLYwFa3jVA41qz7+IIZCf4TsApy3XDdFx91cRub7yPq9BeZ83A+qYQ=" + jobs: + - TestModules=1 STABLE=1 - PKG=hwi STABLE=1 - PKG=lightning-charge STABLE=1 - PKG=lightning-charge STABLE=0 @@ -28,7 +29,44 @@ env: - PKG=elementsd STABLE=1 - PKG=elementsd STABLE=0 - PKG=electrs STABLE=1 + # broken + # - PKG=electrs STABLE=0 - PKG=liquid-swap STABLE=1 -script: | - printf '%s (%s)\n' "$NIX_PATH" "$VER" - nix-build -A $PKG +script: + - printf '%s (%s)\n' "$NIX_PATH" "$VER" + - | + getBuildExpr() { + if [[ $TestModules ]]; then + if [[ ! -e /dev/kvm ]]; then + >&2 echo "No KVM available on VM Host." + exit 1 + fi + sudo chmod go+rw /dev/kvm + test/run-tests.sh exprForCI + else + echo "(import ./. {}).$PKG" + fi + } + - buildExpr=$(getBuildExpr) + - time nix-instantiate -E "$buildExpr" --add-root ./drv --indirect + - outPath=$(nix-store --query ./drv) + - | + if nix path-info --store https://nix-bitcoin.cachix.org $outPath &>/dev/null; then + echo "$outPath" has already been built successfully. + travis_terminate 0 + fi + # Travis doesn't expose secrets to pull-request builds, + # so skip cache uploading in this case + - | + if [[ $CACHIX_SIGNING_KEY ]]; then + cachix push nix-bitcoin --watch-store & + cachixPid=$! + fi + - nix-build ./drv + - | + if [[ $CACHIX_SIGNING_KEY ]]; then + # Wait until cachix has finished uploading + # Run as root because yama/ptrace_scope != 0 + ruby=$(nix-build '' -A ruby)/bin/ruby + time sudo $ruby helper/wait-for-network-idle.rb $cachixPid + fi diff --git a/default.nix b/default.nix index f5c5399..14030d9 100644 --- a/default.nix +++ b/default.nix @@ -1,16 +1,5 @@ { pkgs ? import {} }: -{ - # 'lib', 'modules' and 'overlays' are special, see - # https://github.com/nix-community/NUR for more. - modules = import ./modules; # NixOS modules - nodeinfo = pkgs.callPackage ./pkgs/nodeinfo { }; - lightning-charge = pkgs.callPackage ./pkgs/lightning-charge { }; - nanopos = pkgs.callPackage ./pkgs/nanopos { }; - spark-wallet = pkgs.callPackage ./pkgs/spark-wallet { }; - electrs = (pkgs.callPackage ./pkgs/electrs { }).rootCrate.build; - elementsd = pkgs.callPackage ./pkgs/elementsd { withGui = false; }; - hwi = pkgs.callPackage ./pkgs/hwi { }; - pylightning = pkgs.python3Packages.callPackage ./pkgs/pylightning { }; - liquid-swap = pkgs.python3Packages.callPackage ./pkgs/liquid-swap { }; +(import ./pkgs { inherit pkgs; }) // { + modules = import ./modules; } diff --git a/helper/wait-for-network-idle.rb b/helper/wait-for-network-idle.rb new file mode 100755 index 0000000..18b5a9a --- /dev/null +++ b/helper/wait-for-network-idle.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'open3' + +# Wait until the given PID had no network activity for `Timeout` seconds, then exit. + +pid = ARGV.first +Timeout = 2 + +stdin, out, err, wait_thread = Open3.popen3("strace -f -e trace=network -s 1 -q -p #{pid}") +while IO.select([err], nil, nil, Timeout) + begin + out = err.read_nonblock(1 << 10) + rescue EOFError + status = wait_thread.value + if status.success? + puts "Monitored process #{pid} exited" + exit 0 + else + puts "Strace failed with exit code #{status.to_i}. Last output:\n#{out}" + # strace often fails with code 256 which looks like success to shells. fail with 1 instead. + exit 1 + end + end +end + +# If we exit without an explicit kill, +# ptrace can fail on reattachment: ptrace(PTRACE_SEIZE, $PID): Operation not permitted +# Only relevant for testing. +Process.kill("TERM", wait_thread.pid) diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index b3baf19..b2a6bf3 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -3,8 +3,8 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.bitcoind; + inherit (config) nix-bitcoin-services; pidFile = "${cfg.dataDir}/bitcoind.pid"; configFile = pkgs.writeText "bitcoin.conf" '' ${optionalString cfg.testnet "testnet=1"} @@ -19,7 +19,7 @@ let 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) @@ -69,7 +69,7 @@ in { package = mkOption { type = types.package; - default = pkgs.blockchains.bitcoind; + default = pkgs.nix-bitcoin.bitcoind; defaultText = "pkgs.blockchains.bitcoind"; description = "The package providing bitcoin binaries."; }; @@ -108,9 +108,9 @@ in { rpc = { port = mkOption { - type = types.nullOr types.ints.u16; - default = null; - description = "Override the default port on which to listen for JSON-RPC connections."; + type = types.ints.u16; + default = 8332; + description = "Port on which to listen for JSON-RPC connections."; }; users = mkOption { default = {}; @@ -225,8 +225,8 @@ in { environment.systemPackages = [ cfg.package ]; systemd.services.bitcoind = { description = "Bitcoin daemon"; - requires = [ "bitcoin-rpcpassword-key.service" ]; - after = [ "network.target" "bitcoin-rpcpassword-key.service" ]; + requires = [ "nix-bitcoin-secrets.target" ]; + after = [ "network.target" "nix-bitcoin-secrets.target" ]; wantedBy = [ "multi-user.target" ]; preStart = '' if ! test -e ${cfg.dataDir}; then @@ -238,9 +238,15 @@ in { cp '${cfg.configFileOption}' '${cfg.dataDir}/bitcoin.conf' chmod o-rw '${cfg.dataDir}/bitcoin.conf' chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' - echo "rpcpassword=$(cat /secrets/bitcoin-rpcpassword)" >> '${cfg.dataDir}/bitcoin.conf' + echo "rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/bitcoin.conf' chmod -R g+rX '${cfg.dataDir}/blocks' ''; + # Wait until RPC port is open. This usually takes just a few ms. + postStart = '' + while ! { exec 3>/dev/tcp/127.0.0.1/${toString cfg.rpc.port}; } &>/dev/null; do + sleep 0.05 + done + ''; serviceConfig = { Type = "simple"; User = "${cfg.user}"; @@ -295,14 +301,16 @@ in { }; users.users.${cfg.user} = { - name = cfg.user; group = cfg.group; - extraGroups = [ "keys" ]; description = "Bitcoin daemon user"; home = cfg.dataDir; }; - users.groups.${cfg.group} = { - name = cfg.group; + users.groups.${cfg.group} = {}; + users.groups.bitcoinrpc = {}; + + nix-bitcoin.secrets.bitcoin-rpcpassword = { + user = "bitcoin"; + group = "bitcoinrpc"; }; }; } diff --git a/modules/clightning.nix b/modules/clightning.nix index 042187b..5b7b15b 100644 --- a/modules/clightning.nix +++ b/modules/clightning.nix @@ -3,8 +3,8 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.clightning; + inherit (config) nix-bitcoin-services; configFile = pkgs.writeText "config" '' autolisten=${if cfg.autolisten then "true" else "false"} network=bitcoin @@ -57,6 +57,16 @@ in { default = "/var/lib/clightning"; description = "The data directory for clightning."; }; + cli = mkOption { + readOnly = true; + default = pkgs.writeScriptBin "lightning-cli" + # Switch user because c-lightning doesn't allow setting the permissions of the rpc socket + # https://github.com/ElementsProject/lightning/issues/1366 + '' + exec sudo -u clightning ${pkgs.nix-bitcoin.clightning}/bin/lightning-cli --lightning-dir='${cfg.dataDir}' "$@" + ''; + description = "Binary to connect with the clightning instance."; + }; enforceTor = nix-bitcoin-services.enforceTor; }; @@ -64,16 +74,14 @@ in { users.users.clightning = { description = "clightning User"; group = "clightning"; - extraGroups = [ "bitcoinrpc" "keys" ]; + extraGroups = [ "bitcoinrpc" ]; home = cfg.dataDir; }; - users.groups.clightning = { - name = "clightning"; - }; + users.groups.clightning = {}; systemd.services.clightning = { description = "Run clightningd"; - path = [ pkgs.blockchains.bitcoind ]; + path = [ pkgs.nix-bitcoin.bitcoind ]; wantedBy = [ "multi-user.target" ]; requires = [ "bitcoind.service" ]; after = [ "bitcoind.service" ]; @@ -85,11 +93,11 @@ in { chmod u=rw,g=r,o= ${cfg.dataDir}/config # The RPC socket has to be removed otherwise we might have stale sockets rm -f ${cfg.dataDir}/lightning-rpc - echo "bitcoin-rpcpassword=$(cat /secrets/bitcoin-rpcpassword)" >> '${cfg.dataDir}/config' + echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/config' ''; serviceConfig = { PermissionsStartOnly = "true"; - ExecStart = "${pkgs.clightning}/bin/lightningd --lightning-dir=${cfg.dataDir}"; + ExecStart = "${pkgs.nix-bitcoin.clightning}/bin/lightningd --lightning-dir=${cfg.dataDir}"; User = "clightning"; Restart = "on-failure"; RestartSec = "10s"; @@ -98,6 +106,11 @@ in { then nix-bitcoin-services.allowTor else nix-bitcoin-services.allowAnyIP ); + # Wait until the rpc socket appears + postStart = '' + while read f; do [[ $f == lightning-rpc ]] && break; done \ + < <(${pkgs.inotifyTools}/bin/inotifywait --quiet --monitor -e create,moved_to --format '%f' '${cfg.dataDir}') + ''; }; }; } diff --git a/modules/default.nix b/modules/default.nix index 621ab8f..6664741 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,4 +1,5 @@ { + modules = ./modules.nix; bitcoind = ./bitcoind.nix; clightning = ./clightning.nix; default = ./default.nix; @@ -7,7 +8,6 @@ liquid = ./liquid.nix; nanopos = ./nanopos.nix; nix-bitcoin = ./nix-bitcoin.nix; - nix-bitcoin-pkgs = ./nix-bitcoin-pkgs.nix; nix-bitcoin-webindex = ./nix-bitcoin-webindex.nix; spark-wallet = ./spark-wallet.nix; recurring-donations = ./recurring-donations.nix; diff --git a/modules/electrs.nix b/modules/electrs.nix index b29ae3c..dfeddc9 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -3,8 +3,9 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.electrs; + inherit (config) nix-bitcoin-services; + secretsDir = config.nix-bitcoin.secretsDir; index-batch-size = "${if cfg.high-memory then "" else "--index-batch-size=10"}"; jsonrpc-import = "${if cfg.high-memory then "" else "--jsonrpc-import"}"; in { @@ -58,15 +59,12 @@ in { config = mkIf cfg.enable { users.users.${cfg.user} = { - name = cfg.user; description = "electrs User"; group = cfg.group; - extraGroups = [ "bitcoinrpc" "keys" "bitcoin"]; + extraGroups = [ "bitcoinrpc" "bitcoin"]; home = cfg.dataDir; }; - users.groups.electrs = { - name = cfg.group; - }; + users.groups.${cfg.group} = {}; systemd.services.electrs = { description = "Run electrs"; @@ -77,7 +75,7 @@ in { preStart = '' mkdir -m 0770 -p ${cfg.dataDir} chown -R '${cfg.user}:${cfg.group}' ${cfg.dataDir} - echo "${pkgs.electrs}/bin/electrs -vvv ${index-batch-size} ${jsonrpc-import} --timestamp --db-dir ${cfg.dataDir} --daemon-dir /var/lib/bitcoind --cookie=${config.services.bitcoind.rpcuser}:$(cat /secrets/bitcoin-rpcpassword) --electrum-rpc-addr=127.0.0.1:${toString cfg.port}" > /run/electrs/startscript.sh + echo "${pkgs.nix-bitcoin.electrs}/bin/electrs -vvv ${index-batch-size} ${jsonrpc-import} --timestamp --db-dir ${cfg.dataDir} --daemon-dir /var/lib/bitcoind --cookie=${config.services.bitcoind.rpcuser}:$(cat ${secretsDir}/bitcoin-rpcpassword) --electrum-rpc-addr=127.0.0.1:${toString cfg.port}" > /run/electrs/startscript.sh ''; serviceConfig = rec { RuntimeDirectory = "electrs"; @@ -106,8 +104,8 @@ in { listen ${toString config.services.electrs.nginxport} ssl; proxy_pass electrs; - ssl_certificate /secrets/nginx_cert; - ssl_certificate_key /secrets/nginx_key; + ssl_certificate ${secretsDir}/nginx-cert; + ssl_certificate_key ${secretsDir}/nginx-key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 4h; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; @@ -116,5 +114,16 @@ in { } ''; }; + systemd.services.nginx = { + requires = [ "nix-bitcoin-secrets.target" ]; + after = [ "nix-bitcoin-secrets.target" ]; + }; + nix-bitcoin.secrets = rec { + nginx-key = { + user = "nginx"; + group = "root"; + }; + nginx-cert = nginx-key; + }; }; } diff --git a/modules/lightning-charge.nix b/modules/lightning-charge.nix index 003a1b3..5f43ed0 100644 --- a/modules/lightning-charge.nix +++ b/modules/lightning-charge.nix @@ -3,8 +3,8 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.lightning-charge; + inherit (config) nix-bitcoin-services; in { options.services.lightning-charge = { enable = mkOption { @@ -30,8 +30,8 @@ in { requires = [ "clightning.service" ]; after = [ "clightning.service" ]; serviceConfig = { - EnvironmentFile = "/secrets/lightning-charge-api-token"; - ExecStart = "${pkgs.lightning-charge}/bin/charged -l ${config.services.clightning.dataDir} -d ${config.services.clightning.dataDir}/lightning-charge.db"; + EnvironmentFile = "${config.nix-bitcoin.secretsDir}/lightning-charge-env"; + ExecStart = "${pkgs.nix-bitcoin.lightning-charge}/bin/charged -l ${config.services.clightning.dataDir} -d ${config.services.clightning.dataDir}/lightning-charge.db"; # Unfortunately c-lightning doesn't allow setting the permissions of the rpc socket, # so this must run as the clightning user # https://github.com/ElementsProject/lightning/issues/1366 @@ -42,5 +42,6 @@ in { // nix-bitcoin-services.nodejs // nix-bitcoin-services.allowTor; }; + nix-bitcoin.secrets.lightning-charge-env.user = "clightning"; }; } diff --git a/modules/liquid.nix b/modules/liquid.nix index 968618d..3dce19a 100644 --- a/modules/liquid.nix +++ b/modules/liquid.nix @@ -3,8 +3,9 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.liquidd; + inherit (config) nix-bitcoin-services; + secretsDir = config.nix-bitcoin.secretsDir; pidFile = "${cfg.dataDir}/liquidd.pid"; configFile = pkgs.writeText "elements.conf" '' chain=liquidv1 @@ -175,16 +176,30 @@ in { Validate pegin claims. All functionaries must run this. ''; }; + cli = mkOption { + readOnly = true; + default = pkgs.writeScriptBin "elements-cli" '' + exec ${pkgs.nix-bitcoin.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@" + ''; + description = "Binary to connect with the liquidd instance."; + }; + swap-cli = mkOption { + readOnly = true; + default = pkgs.writeScriptBin "liquidswap-cli" '' + exec ${pkgs.nix-bitcoin.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@" + ''; + description = "Binary for managing liquid swaps."; + }; enforceTor = nix-bitcoin-services.enforceTor; }; }; config = mkIf cfg.enable { - environment.systemPackages = [ pkgs.elementsd ]; + environment.systemPackages = [ pkgs.nix-bitcoin.elementsd ]; systemd.services.liquidd = { description = "Elements daemon providing access to the Liquid sidechain"; - requires = [ "liquid-rpcpassword-key.service" ]; - after = [ "network.target" "liquid-rpcpassword-key.service" ]; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" ]; wantedBy = [ "multi-user.target" ]; preStart = '' if ! test -e ${cfg.dataDir}; then @@ -193,14 +208,14 @@ in { cp '${configFile}' '${cfg.dataDir}/elements.conf' chmod o-rw '${cfg.dataDir}/elements.conf' chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' - echo "rpcpassword=$(cat /secrets/liquid-rpcpassword)" >> '${cfg.dataDir}/elements.conf' - echo "mainchainrpcpassword=$(cat /secrets/bitcoin-rpcpassword)" >> '${cfg.dataDir}/elements.conf' + echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)" >> '${cfg.dataDir}/elements.conf' + echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/elements.conf' ''; serviceConfig = { Type = "simple"; User = "${cfg.user}"; Group = "${cfg.group}"; - ExecStart = "${pkgs.elementsd}/bin/elementsd ${cmdlineOptions}"; + ExecStart = "${pkgs.nix-bitcoin.elementsd}/bin/elementsd ${cmdlineOptions}"; StateDirectory = "liquidd"; PIDFile = "${pidFile}"; Restart = "on-failure"; @@ -214,14 +229,11 @@ in { ); }; users.users.${cfg.user} = { - name = cfg.user; group = cfg.group; - extraGroups = [ "keys" ]; description = "Liquid sidechain user"; home = cfg.dataDir; }; - users.groups.${cfg.group} = { - name = cfg.group; - }; + users.groups.${cfg.group} = {}; + nix-bitcoin.secrets.liquid-rpcpassword.user = "liquid"; }; } diff --git a/modules/lnd.nix b/modules/lnd.nix index 5131834..651883f 100644 --- a/modules/lnd.nix +++ b/modules/lnd.nix @@ -3,14 +3,17 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.lnd; + inherit (config) nix-bitcoin-services; + secretsDir = config.nix-bitcoin.secretsDir; configFile = pkgs.writeText "lnd.conf" '' datadir=${cfg.dataDir} logdir=${cfg.dataDir}/logs bitcoin.mainnet=1 - tlscertpath=/secrets/lnd_cert - tlskeypath=/secrets/lnd_key + tlscertpath=${secretsDir}/lnd-cert + tlskeypath=${secretsDir}/lnd-key + + rpclisten=localhost:${toString cfg.rpcPort} bitcoin.active=1 bitcoin.node=bitcoind @@ -26,45 +29,6 @@ let ${cfg.extraConfig} ''; - init-lnd-wallet-script = pkgs.writeScript "init-lnd-wallet.sh" '' -#!/bin/sh - -set -e -umask 377 - -${pkgs.coreutils}/bin/sleep 5 - -if [ ! -f /secrets/lnd-seed-mnemonic ] -then - ${pkgs.coreutils}/bin/echo Creating lnd seed - - ${pkgs.curl}/bin/curl -s \ - --cacert /secrets/lnd_cert \ - -X GET https://127.0.0.1:8080/v1/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > /secrets/lnd-seed-mnemonic -fi - -if [ ! -f ${cfg.dataDir}/chain/bitcoin/mainnet/wallet.db ] -then - ${pkgs.coreutils}/bin/echo Creating lnd wallet - - ${pkgs.curl}/bin/curl -s \ - --cacert /secrets/lnd_cert \ - -X POST -d "{\"wallet_password\": \"$(${pkgs.coreutils}/bin/cat /secrets/lnd-wallet-password | ${pkgs.coreutils}/bin/tr -d '\n' | ${pkgs.coreutils}/bin/base64 -w0)\", \ - \"cipher_seed_mnemonic\": $(${pkgs.coreutils}/bin/cat /secrets/lnd-seed-mnemonic | ${pkgs.coreutils}/bin/tr -d '\n')}" \ - https://127.0.0.1:8080/v1/initwallet -else - ${pkgs.coreutils}/bin/echo Unlocking lnd wallet - - ${pkgs.curl}/bin/curl -s \ - -H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 ${cfg.dataDir}/chain/bitcoin/mainnet/admin.macaroon)" \ - --cacert /secrets/lnd_cert \ - -X POST \ - -d "{\"wallet_password\": \"$(${pkgs.coreutils}/bin/cat /secrets/lnd-wallet-password | ${pkgs.coreutils}/bin/tr -d '\n' | ${pkgs.coreutils}/bin/base64 -w0)\"}" \ - https://127.0.0.1:8080/v1/unlockwallet -fi - -exit 0 -''; in { options.services.lnd = { @@ -80,6 +44,11 @@ in { default = "/var/lib/lnd"; description = "The data directory for LND."; }; + rpcPort = mkOption { + type = types.ints.u16; + default = 10009; + description = "Port on which to listen for gRPC connections."; + }; extraConfig = mkOption { type = types.lines; default = ""; @@ -88,23 +57,23 @@ in { ''; description = "Additional configurations to be appended to lnd.conf."; }; + cli = mkOption { + readOnly = true; + default = pkgs.writeScriptBin "lncli" + # Switch user because lnd makes datadir contents readable by user only + '' + exec sudo -u lnd ${pkgs.nix-bitcoin.lnd}/bin/lncli --tlscertpath ${secretsDir}/lnd-cert \ + --macaroonpath '${cfg.dataDir}/chain/bitcoin/mainnet/admin.macaroon' "$@" + ''; + description = "Binary to connect with the lnd instance."; + }; enforceTor = nix-bitcoin-services.enforceTor; }; config = mkIf cfg.enable { - users.users.lnd = { - description = "LND User"; - group = "lnd"; - extraGroups = [ "bitcoinrpc" "keys" ]; - home = cfg.dataDir; - }; - users.groups.lnd = { - name = "lnd"; - }; - systemd.services.lnd = { description = "Run LND"; - path = [ pkgs.blockchains.bitcoind ]; + path = [ pkgs.nix-bitcoin.bitcoind ]; wantedBy = [ "multi-user.target" ]; requires = [ "bitcoind.service" ]; after = [ "bitcoind.service" ]; @@ -113,12 +82,11 @@ in { cp ${configFile} ${cfg.dataDir}/lnd.conf chown -R 'lnd:lnd' '${cfg.dataDir}' chmod u=rw,g=r,o= ${cfg.dataDir}/lnd.conf - echo "bitcoind.rpcpass=$(cat /secrets/bitcoin-rpcpassword)" >> '${cfg.dataDir}/lnd.conf' + echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/lnd.conf' ''; serviceConfig = { PermissionsStartOnly = "true"; - ExecStart = "${pkgs.lnd}/bin/lnd --configfile=${cfg.dataDir}/lnd.conf"; - ExecStartPost = "${pkgs.bash}/bin/bash ${init-lnd-wallet-script}"; + ExecStart = "${pkgs.nix-bitcoin.lnd}/bin/lnd --configfile=${cfg.dataDir}/lnd.conf"; User = "lnd"; Restart = "on-failure"; RestartSec = "10s"; @@ -127,6 +95,67 @@ in { then nix-bitcoin-services.allowTor else nix-bitcoin-services.allowAnyIP ) // nix-bitcoin-services.allowAnyProtocol; # For ZMQ + postStart = let + mainnetDir = "${cfg.dataDir}/chain/bitcoin/mainnet"; + in '' + umask 377 + + attempts=50 + while ! { exec 3>/dev/tcp/127.0.0.1/8080 && exec 3>&-; } &>/dev/null; do + ((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; } + sleep 0.1 + done + + if [[ ! -f ${secretsDir}/lnd-seed-mnemonic ]]; then + echo Create lnd seed + + ${pkgs.curl}/bin/curl -s \ + --cacert ${secretsDir}/lnd-cert \ + -X GET https://127.0.0.1:8080/v1/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > ${secretsDir}/lnd-seed-mnemonic + fi + + if [[ ! -f ${mainnetDir}/wallet.db ]]; then + echo Create lnd wallet + + ${pkgs.curl}/bin/curl -s --output /dev/null --show-error \ + --cacert ${secretsDir}/lnd-cert \ + -X POST -d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\", \ + \"cipher_seed_mnemonic\": $(cat ${secretsDir}/lnd-seed-mnemonic | tr -d '\n')}" \ + https://127.0.0.1:8080/v1/initwallet + + # Guarantees that RPC calls with cfg.cli succeed after the service is started + echo Wait until wallet is created + while [[ ! -f ${mainnetDir}/admin.macaroon ]]; do + sleep 0.1 + done + else + echo Unlock lnd wallet + + ${pkgs.curl}/bin/curl -s \ + -H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${mainnetDir}/admin.macaroon')" \ + --cacert ${secretsDir}/lnd-cert \ + -X POST \ + -d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\"}" \ + https://127.0.0.1:8080/v1/unlockwallet + fi + + # Wait until the RPC port is open + while ! { exec 3>/dev/tcp/127.0.0.1/${toString cfg.rpcPort}; } &>/dev/null; do + sleep 0.1 + done + ''; + }; + users.users.lnd = { + description = "LND User"; + group = "lnd"; + extraGroups = [ "bitcoinrpc" ]; + home = cfg.dataDir; + }; + users.groups.lnd = {}; + nix-bitcoin.secrets = { + lnd-wallet-password.user = "lnd"; + lnd-key.user = "lnd"; + lnd-cert.user = "lnd"; }; }; } diff --git a/modules/modules.nix b/modules/modules.nix new file mode 100644 index 0000000..0b85f32 --- /dev/null +++ b/modules/modules.nix @@ -0,0 +1,43 @@ +{ config, pkgs, lib, ... }: +let + nixpkgs-pinned = import ../pkgs/nixpkgs-pinned.nix; + unstable = import nixpkgs-pinned.nixpkgs-unstable {}; + + allPackages = pkgs: (import ../pkgs { inherit pkgs; }) // { + bitcoin = unstable.bitcoin.override { miniupnpc = null; }; + bitcoind = unstable.bitcoind.override { miniupnpc = null; }; + clightning = unstable.clightning; + lnd = unstable.lnd; + }; +in { + imports = [ + ./bitcoind.nix + ./clightning.nix + ./lightning-charge.nix + ./nanopos.nix + ./nix-bitcoin-webindex.nix + ./liquid.nix + ./spark-wallet.nix + ./electrs.nix + ./onion-chef.nix + ./recurring-donations.nix + ./hardware-wallets.nix + ./lnd.nix + ./secrets/secrets.nix + ]; + + disabledModules = [ "services/networking/bitcoind.nix" ]; + + options = { + nix-bitcoin-services = lib.mkOption { + readOnly = true; + default = import ./nix-bitcoin-services.nix lib; + }; + }; + + config = { + nixpkgs.overlays = [ (self: super: { + nix-bitcoin = allPackages super; + }) ]; + }; +} diff --git a/modules/nanopos.nix b/modules/nanopos.nix index 300da4b..b34cf6d 100644 --- a/modules/nanopos.nix +++ b/modules/nanopos.nix @@ -3,8 +3,8 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.nanopos; + inherit (config) nix-bitcoin-services; defaultItemsFile = pkgs.writeText "items.yaml" '' tea: price: 0.02 # denominated in the currency specified by --currency @@ -52,24 +52,14 @@ in { }; config = mkIf cfg.enable { - users.users.nanopos = - { - description = "nanopos User"; - group = "nanopos"; - extraGroups = [ "keys" ]; - }; - users.groups.nanopos = { - name = "nanopos"; - }; - systemd.services.nanopos = { description = "Run nanopos"; wantedBy = [ "multi-user.target" ]; requires = [ "lightning-charge.service" ]; after = [ "lightning-charge.service" ]; serviceConfig = { - EnvironmentFile = "/secrets/lightning-charge-api-token-for-nanopos"; - ExecStart = "${pkgs.nanopos}/bin/nanopos -y ${cfg.itemsFile} -p ${toString cfg.port} --show-bolt11"; + EnvironmentFile = "${config.nix-bitcoin.secretsDir}/nanopos-env"; + ExecStart = "${pkgs.nix-bitcoin.nanopos}/bin/nanopos -y ${cfg.itemsFile} -p ${toString cfg.port} --show-bolt11"; User = "nanopos"; Restart = "on-failure"; @@ -78,5 +68,11 @@ in { // nix-bitcoin-services.nodejs // nix-bitcoin-services.allowTor; }; + users.users.nanopos = { + description = "nanopos User"; + group = "nanopos"; + }; + users.groups.nanopos = {}; + nix-bitcoin.secrets.nanopos-env.user = "nanopos"; }; } diff --git a/modules/nix-bitcoin-pkgs.nix b/modules/nix-bitcoin-pkgs.nix deleted file mode 100644 index d5c8bd5..0000000 --- a/modules/nix-bitcoin-pkgs.nix +++ /dev/null @@ -1,17 +0,0 @@ -{ config, pkgs, ... }: -let - nixpkgs-pinned = import ../pkgs/nixpkgs-pinned.nix; - nixpkgs-unstable = import nixpkgs-pinned.nixpkgs-unstable { }; -in { - disabledModules = [ "services/networking/bitcoind.nix" ]; - - nixpkgs.overlays = [ (import ../overlay.nix) ]; - - nixpkgs.config.packageOverrides = pkgs: { - # Use bitcoin and clightning from unstable - bitcoin = nixpkgs-unstable.bitcoin.override { miniupnpc = null; }; - blockchains.bitcoind = nixpkgs-unstable.bitcoind.override { miniupnpc = null; }; - clightning = nixpkgs-unstable.clightning.override { }; - lnd = nixpkgs-unstable.lnd.override { }; - }; -} diff --git a/modules/nix-bitcoin-services.nix b/modules/nix-bitcoin-services.nix index da401e6..c04e14c 100644 --- a/modules/nix-bitcoin-services.nix +++ b/modules/nix-bitcoin-services.nix @@ -1,11 +1,10 @@ # See `man systemd.exec` and `man systemd.resource-control` for an explanation # of the various systemd options available through this module. -{ config, lib, pkgs, ... }: +lib: with lib; - -let +{ defaultHardening = { PrivateTmp = "true"; ProtectSystem = "full"; @@ -23,9 +22,7 @@ let SystemCallFilter= "accept accept4 access adjtimex alarm bind brk capget capset chdir chmod chown chown32 clock_getres clock_gettime clock_nanosleep close connect copy_file_range creat dup dup2 dup3 epoll_create epoll_create1 epoll_ctl epoll_ctl_old epoll_pwait epoll_wait epoll_wait_old eventfd eventfd2 execve execveat exit exit_group faccessat fadvise64 fadvise64_64 fallocate fanotify_mark fchdir fchmod fchmodat fchown fchown32 fchownat fcntl fcntl64 fdatasync fgetxattr flistxattr flock fork fremovexattr fsetxattr fstat fstat64 fstatat64 fstatfs fstatfs64 fsync ftruncate ftruncate64 futex futimesat getcpu getcwd getdents getdents64 getegid getegid32 geteuid geteuid32 getgid getgid32 getgroups getgroups32 getitimer getpeername getpgid getpgrp getpid getppid getpriority getrandom getresgid getresgid32 getresuid getresuid32 getrlimit get_robust_list getrusage getsid getsockname getsockopt get_thread_area gettid gettimeofday getuid getuid32 getxattr inotify_add_watch inotify_init inotify_init1 inotify_rm_watch io_cancel ioctl io_destroy io_getevents io_pgetevents ioprio_get ioprio_set io_setup io_submit ipc kill lchown lchown32 lgetxattr link linkat listen listxattr llistxattr _llseek lremovexattr lseek lsetxattr lstat lstat64 madvise memfd_create mincore mkdir mkdirat mknod mknodat mlock mlock2 mlockall mmap mmap2 mprotect mq_getsetattr mq_notify mq_open mq_timedreceive mq_timedsend mq_unlink mremap msgctl msgget msgrcv msgsnd msync munlock munlockall munmap nanosleep newfstatat _newselect open openat pause pipe pipe2 poll ppoll prctl pread64 preadv preadv2 prlimit64 pselect6 pwrite64 pwritev pwritev2 read readahead readlink readlinkat readv recv recvfrom recvmmsg recvmsg remap_file_pages removexattr rename renameat renameat2 restart_syscall rmdir rt_sigaction rt_sigpending rt_sigprocmask rt_sigqueueinfo rt_sigreturn rt_sigsuspend rt_sigtimedwait rt_tgsigqueueinfo sched_getaffinity sched_getattr sched_getparam sched_get_priority_max sched_get_priority_min sched_getscheduler sched_rr_get_interval sched_setaffinity sched_setattr sched_setparam sched_setscheduler sched_yield seccomp select semctl semget semop semtimedop send sendfile sendfile64 sendmmsg sendmsg sendto setfsgid setfsgid32 setfsuid setfsuid32 setgid setgid32 setgroups setgroups32 setitimer setpgid setpriority setregid setregid32 setresgid setresgid32 setresuid setresuid32 setreuid setreuid32 setrlimit set_robust_list setsid setsockopt set_thread_area set_tid_address setuid setuid32 setxattr shmat shmctl shmdt shmget shutdown sigaltstack signalfd signalfd4 sigreturn socket socketcall socketpair splice stat stat64 statfs statfs64 statx symlink symlinkat sync sync_file_range syncfs sysinfo tee tgkill time timer_create timer_delete timerfd_create timerfd_gettime timerfd_settime timer_getoverrun timer_gettime timer_settime times tkill truncate truncate64 ugetrlimit umask uname unlink unlinkat utime utimensat utimes vfork vmsplice wait4 waitid waitpid write writev arm_fadvise64_64 arm_sync_file_range sync_file_range2 breakpoint cacheflush set_tls arch_prctl modify_ldt clone"; SystemCallArchitectures= "native"; }; -in -{ - inherit defaultHardening; + # nodejs applications apparently rely on memory write execute nodejs = { MemoryDenyWriteExecute = "false"; }; # Allow tor traffic. Allow takes precedence over Deny. diff --git a/modules/nix-bitcoin-webindex.nix b/modules/nix-bitcoin-webindex.nix index 96849bf..3371438 100644 --- a/modules/nix-bitcoin-webindex.nix +++ b/modules/nix-bitcoin-webindex.nix @@ -3,8 +3,8 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.nix-bitcoin-webindex; + inherit (config) nix-bitcoin-services; indexFile = pkgs.writeText "index.html" '' @@ -74,7 +74,13 @@ in { description = "Get node info"; wantedBy = [ "multi-user.target" ]; after = [ "nodeinfo.service" ]; - path = [ pkgs.nodeinfo pkgs.clightning pkgs.jq pkgs.sudo ]; + path = with pkgs; [ + nix-bitcoin.nodeinfo + config.services.clightning.cli + config.services.lnd.cli + jq + sudo + ]; serviceConfig = { ExecStart="${pkgs.bash}/bin/bash ${createWebIndex}"; User = "root"; diff --git a/modules/nix-bitcoin.nix b/modules/nix-bitcoin.nix index 8b7e61a..753e146 100644 --- a/modules/nix-bitcoin.nix +++ b/modules/nix-bitcoin.nix @@ -15,21 +15,7 @@ let chown -R operator ${config.users.users.operator.home}/.ssh ''; in { - imports = [ - ./nix-bitcoin-pkgs.nix - ./bitcoind.nix - ./clightning.nix - ./lightning-charge.nix - ./nanopos.nix - ./nix-bitcoin-webindex.nix - ./liquid.nix - ./spark-wallet.nix - ./electrs.nix - ./onion-chef.nix - ./recurring-donations.nix - ./hardware-wallets.nix - ./lnd.nix - ]; + imports = [ ./modules.nix ]; options.services.nix-bitcoin = { enable = mkOption { @@ -42,6 +28,8 @@ in { }; config = mkIf cfg.enable { + nix-bitcoin.secretsDir = mkDefault "/secrets"; + networking.firewall.enable = true; # Tor @@ -85,9 +73,6 @@ in { version = 3; }; - # Add bitcoinrpc group - users.groups.bitcoinrpc = {}; - # clightning services.clightning.bitcoin-rpcuser = config.services.bitcoind.rpcuser; services.clightning.proxy = config.services.tor.client.socksListenAddress; @@ -118,29 +103,15 @@ in { services.onion-chef.enable = true; services.onion-chef.access.operator = [ "bitcoind" "clightning" "nginx" "liquidd" "spark-wallet" "electrs" "sshd" ]; - environment.interactiveShellInit = '' - ${optionalString (config.services.clightning.enable) '' - alias lightning-cli='sudo -u clightning lightning-cli --lightning-dir=${config.services.clightning.dataDir}' - ''} - ${optionalString (config.services.lnd.enable) '' - alias lncli='sudo -u lnd lncli --tlscertpath /secrets/lnd_cert --macaroonpath ${config.services.lnd.dataDir}/chain/bitcoin/mainnet/admin.macaroon' - ''} - ${optionalString (config.services.liquidd.enable) '' - alias elements-cli='elements-cli -datadir=${config.services.liquidd.dataDir}' - alias liquidswap-cli='liquidswap-cli -c ${config.services.liquidd.dataDir}/elements.conf' - ''} - ''; # Unfortunately c-lightning doesn't allow setting the permissions of the rpc socket # https://github.com/ElementsProject/lightning/issues/1366 - security.sudo.configFile = ( - if config.services.clightning.enable then '' - operator ALL=(clightning) NOPASSWD: ALL - '' - else if config.services.lnd.enable then '' - operator ALL=(lnd) NOPASSWD: ALL - '' - else "" - ); + security.sudo.configFile = + (optionalString config.services.clightning.enable '' + operator ALL=(clightning) NOPASSWD: ALL + '') + + (optionalString config.services.lnd.enable '' + operator ALL=(lnd) NOPASSWD: ALL + ''); # Give root ssh access to the operator account systemd.services.copy-root-authorized-keys = { @@ -184,30 +155,32 @@ in { }]; version = 3; }; - environment.systemPackages = with pkgs; [ + environment.systemPackages = with pkgs; with nix-bitcoin; let + s = config.services; + in + [ tor - blockchains.bitcoind - (hiPrio config.services.bitcoind.cli) + bitcoind + (hiPrio s.bitcoind.cli) nodeinfo jq qrencode ] - ++ optionals config.services.clightning.enable [clightning] - ++ optionals config.services.lnd.enable [lnd] - ++ optionals config.services.lightning-charge.enable [lightning-charge] - ++ optionals config.services.nanopos.enable [nanopos] - ++ optionals config.services.nix-bitcoin-webindex.enable [nginx] - ++ optionals config.services.liquidd.enable [elementsd liquid-swap] - ++ optionals config.services.spark-wallet.enable [spark-wallet] - ++ optionals config.services.electrs.enable [electrs] - ++ optionals (config.services.hardware-wallets.ledger || config.services.hardware-wallets.trezor) [ + ++ optionals s.clightning.enable [clightning (hiPrio s.clightning.cli)] + ++ optionals s.lnd.enable [lnd (hiPrio s.lnd.cli)] + ++ optionals s.lightning-charge.enable [lightning-charge] + ++ optionals s.nanopos.enable [nanopos] + ++ optionals s.nix-bitcoin-webindex.enable [nginx] + ++ optionals s.liquidd.enable [elementsd (hiPrio s.liquidd.cli) (hiPrio s.liquidd.swap-cli)] + ++ optionals s.spark-wallet.enable [spark-wallet] + ++ optionals s.electrs.enable [electrs] + ++ optionals (s.hardware-wallets.ledger || s.hardware-wallets.trezor) [ hwi - # To allow debugging issues with lsusb: + # To allow debugging issues with lsusb usbutils ] - ++ optionals config.services.hardware-wallets.trezor [ + ++ optionals s.hardware-wallets.trezor [ python3.pkgs.trezor ]; }; } - diff --git a/modules/onion-chef.nix b/modules/onion-chef.nix index c222015..df9c36f 100644 --- a/modules/onion-chef.nix +++ b/modules/onion-chef.nix @@ -8,8 +8,8 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; 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 @@ -70,14 +70,13 @@ in { config = mkIf cfg.enable { systemd.services.onion-chef = { description = "Run onion-chef"; - wantedBy = [ "multi-user.target" ]; - requires = [ "tor.service" ]; - partOf = [ "tor.service" ]; + wantedBy = [ "tor.service" ]; + bindsTo = [ "tor.service" ]; after = [ "tor.service" ]; serviceConfig = { ExecStart = "${pkgs.bash}/bin/bash ${onion-chef-script}"; - User = "root"; Type = "oneshot"; + RemainAfterExit = true; } // nix-bitcoin-services.defaultHardening; }; }; diff --git a/modules/recurring-donations.nix b/modules/recurring-donations.nix index 1f9e491..03896ee 100644 --- a/modules/recurring-donations.nix +++ b/modules/recurring-donations.nix @@ -3,10 +3,10 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.recurring-donations; + inherit (config) nix-bitcoin-services; recurring-donations-script = pkgs.writeScript "recurring-donations.sh" '' - LNCLI="lightning-cli --lightning-dir=${config.services.clightning.dataDir}" + LNCLI="${pkgs.nix-bitcoin.clightning}/bin/lightning-cli --lightning-dir=${config.services.clightning.dataDir}" pay_tallycoin() { NAME=$1 AMOUNT=$2 @@ -82,7 +82,7 @@ in { description = "Run recurring-donations"; requires = [ "clightning.service" ]; after = [ "clightning.service" ]; - path = [ pkgs.clightning pkgs.curl pkgs.torsocks pkgs.sudo pkgs.jq ]; + path = with pkgs; [ nix-bitcoin.clightning curl torsocks sudo jq ]; serviceConfig = { ExecStart = "${pkgs.bash}/bin/bash ${recurring-donations-script}"; # TODO: would be better if this was operator, but I don't get sudo diff --git a/modules/secrets/generate-secrets.nix b/modules/secrets/generate-secrets.nix new file mode 100644 index 0000000..fa72110 --- /dev/null +++ b/modules/secrets/generate-secrets.nix @@ -0,0 +1,26 @@ +{ config, pkgs, lib, ... }: + +# This is mainly for testing. +# When using this for regular deployments, make sure to create a backup of the +# generated secrets. + +with lib; +{ + nix-bitcoin.setup-secrets = true; + + systemd.services.generate-secrets = { + requiredBy = [ "setup-secrets.service" ]; + before = [ "setup-secrets.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + } // config.nix-bitcoin-services.defaultHardening; + script = '' + mkdir -p "${config.nix-bitcoin.secretsDir}" + cd "${config.nix-bitcoin.secretsDir}" + chown root: . + chmod 0700 . + ${pkgs.nix-bitcoin.generate-secrets} + ''; + }; +} diff --git a/modules/secrets/secrets.nix b/modules/secrets/secrets.nix new file mode 100644 index 0000000..163346d --- /dev/null +++ b/modules/secrets/secrets.nix @@ -0,0 +1,97 @@ +{ config, pkgs, lib, ... }: + +with lib; +let + cfg = config.nix-bitcoin; + setupSecrets = concatStrings (mapAttrsToList (n: v: '' + setupSecret ${n} ${v.user} ${v.group} ${v.permissions} } + '') cfg.secrets); +in +{ + options.nix-bitcoin = { + secretsDir = mkOption { + type = types.path; + default = "/etc/nix-bitcoin-secrets"; + description = "Directory to store secrets"; + }; + + secrets = mkOption { + default = {}; + type = with types; attrsOf (submodule ( + { config, ... }: { + options = { + user = mkOption { + type = str; + default = "root"; + }; + group = mkOption { + type = str; + default = config.user; + }; + permissions = mkOption { + type = str; + default = "0440"; + }; + }; + } + )); + }; + + setup-secrets = mkEnableOption "Set permissions for secrets generated by 'generate-secrets.sh'"; + }; + + config = mkIf cfg.setup-secrets { + systemd.targets.nix-bitcoin-secrets = { + requires = [ "setup-secrets.service" ]; + after = [ "setup-secrets.service" ]; + }; + + # Operation of this service: + # - Create missing secrets that are composed of attrs from secrets.nix + # - Set owner and permissions for all used secrets + # - Make all other secrets accessible to root only + # For all steps make sure that no secrets are copied to the nix store. + # + systemd.services.setup-secrets = { + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + } // config.nix-bitcoin-services.defaultHardening; + script = '' + setupSecret() { + file="$1" + user="$2" + group="$3" + permissions="$4" + if [[ ! -e $file ]]; then + echo "Error: Secret file '$file' is missing" + exit 1 + fi + chown "$user:$group" "$file" + chmod "$permissions" "$file" + processedFiles+=("$file") + } + + dir="${cfg.secretsDir}" + if [[ ! -e $dir ]]; then + echo "Error: Secrets dir '$dir' is missing" + exit 1 + fi + chown root: "$dir" + cd "$dir" + + processedFiles=() + ${setupSecrets} + + # Make all other files accessible to root only + unprocessedFiles=$(comm -23 <(printf '%s\n' *) <(printf '%s\n' "''${processedFiles[@]}" | sort)) + IFS=$'\n' + chown root: $unprocessedFiles + chmod 0440 $unprocessedFiles + + # Now make the secrets dir accessible to other users + chmod 0751 "$dir" + ''; + }; + }; +} diff --git a/modules/spark-wallet.nix b/modules/spark-wallet.nix index e978c7c..bef8d45 100644 --- a/modules/spark-wallet.nix +++ b/modules/spark-wallet.nix @@ -3,12 +3,12 @@ with lib; let - nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; cfg = config.services.spark-wallet; + inherit (config) nix-bitcoin-services; dataDir = "/var/lib/spark-wallet/"; onion-chef-service = (if cfg.onion-service then [ "onion-chef.service" ] else []); run-spark-wallet = pkgs.writeScript "run-spark-wallet" '' - CMD="${pkgs.spark-wallet}/bin/spark-wallet --ln-path ${cfg.ln-path} -Q -k -c /secrets/spark-wallet-login" + CMD="${pkgs.nix-bitcoin.spark-wallet}/bin/spark-wallet --ln-path ${cfg.ln-path} -Q -k -c ${config.nix-bitcoin.secretsDir}/spark-wallet-login" ${optionalString cfg.onion-service '' echo Getting onion hostname @@ -73,5 +73,6 @@ in { // nix-bitcoin-services.nodejs // nix-bitcoin-services.allowTor; }; + nix-bitcoin.secrets.spark-wallet-login.user = "clightning"; }; } diff --git a/network/network.nix b/network/network.nix index 96997ca..fa69939 100644 --- a/network/network.nix +++ b/network/network.nix @@ -1,92 +1,31 @@ -let - secrets = import ../secrets/secrets.nix; - bitcoin-rpcpassword = { - text = secrets.bitcoinrpcpassword; - destDir = "/secrets/"; - user = "bitcoin"; - group = "bitcoinrpc"; - permissions = "0440"; - }; - lnd-wallet-password = { - text = secrets.lnd-wallet-password; - destDir = "/secrets/"; - user = "lnd"; - group = "lnd"; - permissions = "0440"; - }; - lightning-charge-api-token = { - text = "API_TOKEN=" + secrets.lightning-charge-api-token; - destDir = "/secrets/"; - user = "clightning"; - group = "clightning"; - permissions = "0440"; - }; - # variable is called CHARGE_TOKEN instead of API_TOKEN - lightning-charge-api-token-for-nanopos = { - text = "CHARGE_TOKEN=" + secrets.lightning-charge-api-token; - destDir = "/secrets/"; - user = "nanopos"; - group = "nanopos"; - permissions = "0440"; - }; - liquid-rpcpassword = { - text = secrets.liquidrpcpassword; - destDir = "/secrets/"; - user = "liquid"; - group = "liquid"; - permissions = "0440"; - }; - spark-wallet-login = { - text = "login=" + "spark-wallet:" + secrets.spark-wallet-password; - destDir = "/secrets/"; - user = "clightning"; - group = "clightning"; - permissions = "0440"; - }; - nginx_key = { - keyFile = ../secrets/nginx.key; - destDir = "/secrets/"; - user = "nginx"; - group = "root"; - permissions = "0440"; - }; - nginx_cert = { - keyFile = ../secrets/nginx.cert; - destDir = "/secrets/"; - user = "nginx"; - group = "root"; - permissions = "0440"; - }; - lnd_key = { - keyFile = ../secrets/lnd.key; - destDir = "/secrets/"; - user = "lnd"; - group = "lnd"; - permissions = "0440"; - }; - lnd_cert = { - keyFile = ../secrets/lnd.cert; - destDir = "/secrets/"; - user = "lnd"; - group = "lnd"; - permissions = "0440"; - }; -in { +{ network.description = "Bitcoin Core node"; bitcoin-node = - { config, pkgs, ... }: - let - bitcoin-node = import ../configuration.nix; - in { - deployment.keys = { - inherit bitcoin-rpcpassword; - } - // (if (config.services.lnd.enable) then { inherit lnd-wallet-password lnd_key lnd_cert; } else { }) - // (if (config.services.lightning-charge.enable) then { inherit lightning-charge-api-token; } else { }) - // (if (config.services.nanopos.enable) then { inherit lightning-charge-api-token-for-nanopos; } else { }) - // (if (config.services.liquidd.enable) then { inherit liquid-rpcpassword; } else { }) - // (if (config.services.spark-wallet.enable) then { inherit spark-wallet-login; } else { }) - // (if (config.services.electrs.enable) then { inherit nginx_key nginx_cert; } else { }); - } // (bitcoin-node { inherit config pkgs; }); + { config, pkgs, lib, ... }: { + imports = [ ../configuration.nix ]; + + deployment.keys = builtins.mapAttrs (n: v: { + keyFile = "${toString ../secrets}/${n}"; + destDir = config.nix-bitcoin.secretsDir; + inherit (v) user group permissions; + }) config.nix-bitcoin.secrets; + + # nixops makes the secrets directory accessible only for users with group 'key'. + # For compatibility with other deployment methods besides nixops, we forego the + # use of the 'key' group and make the secrets dir world-readable instead. + # This is safe because all containing files have their specific private + # permissions set. + systemd.services.allowSecretsDirAccess = { + requires = [ "keys.target" ]; + after = [ "keys.target" ]; + script = "chmod o+x ${config.nix-bitcoin.secretsDir}"; + serviceConfig.Type = "oneshot"; + }; + + systemd.targets.nix-bitcoin-secrets = { + requires = [ "allowSecretsDirAccess.service" ]; + after = [ "allowSecretsDirAccess.service" ]; + }; + }; } diff --git a/overlay.nix b/overlay.nix index 082b038..ca61067 100644 --- a/overlay.nix +++ b/overlay.nix @@ -1,21 +1 @@ -# You can use this file as a nixpkgs overlay. -# It's useful in the case where you don't want to add the whole NUR namespace -# to your configuration. - -self: super: - -let filterSet = - (f: g: s: builtins.listToAttrs - (map - (n: { name = n; value = builtins.getAttr n s; }) - (builtins.filter - (n: f n && g (builtins.getAttr n s)) - (builtins.attrNames s) - ) - ) - ); -in filterSet - (n: !(n=="lib"||n=="overlays"||n=="modules")) # filter out non-packages - (p: true) # all packages are ok - (import ./default.nix { pkgs = super; }) - +self: super: import ./pkgs { pkgs = super; } diff --git a/pkgs/default.nix b/pkgs/default.nix new file mode 100644 index 0000000..2864369 --- /dev/null +++ b/pkgs/default.nix @@ -0,0 +1,13 @@ +{ pkgs ? import {} }: +{ + nodeinfo = pkgs.callPackage ./nodeinfo { }; + lightning-charge = pkgs.callPackage ./lightning-charge { }; + nanopos = pkgs.callPackage ./nanopos { }; + spark-wallet = pkgs.callPackage ./spark-wallet { }; + electrs = (pkgs.callPackage ./electrs { }).rootCrate.build; + elementsd = pkgs.callPackage ./elementsd { withGui = false; }; + hwi = pkgs.callPackage ./hwi { }; + pylightning = pkgs.python3Packages.callPackage ./pylightning { }; + liquid-swap = pkgs.python3Packages.callPackage ./liquid-swap { }; + generate-secrets = pkgs.callPackage ./generate-secrets { }; +} diff --git a/pkgs/generate-secrets/default.nix b/pkgs/generate-secrets/default.nix new file mode 100644 index 0000000..8222057 --- /dev/null +++ b/pkgs/generate-secrets/default.nix @@ -0,0 +1,6 @@ +{ pkgs }: with pkgs; + +writeScript "generate-secrets" '' + export PATH=${lib.makeBinPath [ coreutils apg openssl ]} + . ${./generate-secrets.sh} ${./openssl.cnf} +'' diff --git a/pkgs/generate-secrets/generate-secrets.sh b/pkgs/generate-secrets/generate-secrets.sh new file mode 100755 index 0000000..4eac950 --- /dev/null +++ b/pkgs/generate-secrets/generate-secrets.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +opensslConf=${1:-openssl.cnf} + +makePasswordSecret() { + [[ -e $1 ]] || apg -m 20 -x 20 -M Ncl -n 1 > "$1" +} + +makePasswordSecret bitcoin-rpcpassword +makePasswordSecret lnd-wallet-password +makePasswordSecret liquid-rpcpassword +makePasswordSecret lightning-charge-token +makePasswordSecret spark-wallet-password + +[[ -e lightning-charge-env ]] || echo "API_TOKEN=$(cat lightning-charge-token)" > lightning-charge-env +[[ -e nanopos-env ]] || echo "CHARGE_TOKEN=$(cat lightning-charge-token)" > nanopos-env +[[ -e spark-wallet-login ]] || echo "login=spark-wallet:$(cat spark-wallet-password)" > spark-wallet-login + +if [[ ! -e nginx-key || ! -e nginx-cert ]]; then + openssl genrsa -out nginx-key 2048 + openssl req -new -key nginx-key -out nginx.csr -subj "/C=KN" + openssl x509 -req -days 1825 -in nginx.csr -signkey nginx-key -out nginx-cert + rm nginx.csr +fi + +if [[ ! -e lnd-key || ! -e lnd-cert ]]; then + openssl ecparam -genkey -name prime256v1 -out lnd-key + openssl req -config $opensslConf -new -sha256 -key lnd-key -out lnd.csr -subj '/CN=localhost/O=lnd' + openssl req -config $opensslConf -x509 -sha256 -days 1825 -key lnd-key -in lnd.csr -out lnd-cert + rm lnd.csr +fi diff --git a/secrets/openssl.cnf b/pkgs/generate-secrets/openssl.cnf similarity index 100% rename from secrets/openssl.cnf rename to pkgs/generate-secrets/openssl.cnf diff --git a/pkgs/generate-secrets/update-and-generate.nix b/pkgs/generate-secrets/update-and-generate.nix new file mode 100644 index 0000000..c2cfae9 --- /dev/null +++ b/pkgs/generate-secrets/update-and-generate.nix @@ -0,0 +1,10 @@ +{ pkgs }: with pkgs; + +let + generate-secrets = callPackage ./. {}; +in +writeScript "make-secrets" '' + # Update from old secrets format + [[ -e secrets.nix ]] && . ${./update-secrets.sh} + ${generate-secrets} +'' diff --git a/pkgs/generate-secrets/update-secrets.sh b/pkgs/generate-secrets/update-secrets.sh new file mode 100644 index 0000000..c9600aa --- /dev/null +++ b/pkgs/generate-secrets/update-secrets.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# Update secrets from the old format to the current one where each secret +# has a local source file. + +reportError() { + echo "Updating secrets failed. (Error in line $1)" + echo "The secret files have been moved to secrets/old-secrets" +} +trap 'reportError $LINENO' ERR + +echo "Updating old secrets to the current format." + +mkdir old-secrets +# move all files into old-secrets +shopt -s extglob dotglob +mv !(old-secrets) old-secrets +shopt -u dotglob + +secrets=$(cat old-secrets/secrets.nix) + +extractPassword() { + pwName="$1" + destFile="${2:-$pwName}" + echo "$secrets" | sed -nE "s/.*?$pwName = \"(.*?)\".*/\1/p" > "$destFile" +} + +rename() { + old="old-secrets/$1" + if [[ -e $old ]]; then + cp "$old" "$2" + fi +} + +extractPassword bitcoinrpcpassword bitcoin-rpcpassword +extractPassword lnd-wallet-password +extractPassword liquidrpcpassword liquid-rpcpassword +extractPassword lightning-charge-api-token lightning-charge-token +extractPassword spark-wallet-password + +rename nginx.key nginx-key +rename nginx.cert nginx-cert +rename lnd.key lnd-key +rename lnd.cert lnd-cert + +rm -r old-secrets diff --git a/pkgs/nixops/release.nix.patch b/pkgs/nixops/release.nix.patch index 91a81ab..c54432a 100644 --- a/pkgs/nixops/release.nix.patch +++ b/pkgs/nixops/release.nix.patch @@ -32,3 +32,14 @@ # For "nix-build --run-env". + +--- a/nixops/backends/__init__.py ++++ b/nixops/backends/__init__.py +@@ -24,6 +24,7 @@ class MachineDefinition(nixops.resources.ResourceDefinition): + opts = {} + for (key, xmlType) in (('text', 'string'), + ('keyFile', 'path'), ++ ('keyFile', 'string'), + ('destDir', 'string'), + ('user', 'string'), + ('group', 'string'), diff --git a/pkgs/nodeinfo/nodeinfo.sh b/pkgs/nodeinfo/nodeinfo.sh index 6b62ff0..bb6dcdb 100644 --- a/pkgs/nodeinfo/nodeinfo.sh +++ b/pkgs/nodeinfo/nodeinfo.sh @@ -4,8 +4,8 @@ set -o pipefail BITCOIND_ONION="$(cat /var/lib/onion-chef/operator/bitcoind)" echo BITCOIND_ONION="$BITCOIND_ONION" -if [ -x "$(command -v lightning-cli)" ]; then - CLIGHTNING_NODEID=$(sudo -u clightning lightning-cli --lightning-dir=/var/lib/clightning getinfo | jq -r '.id') +if systemctl is-active --quiet clightning; then + CLIGHTNING_NODEID=$(lightning-cli getinfo | jq -r '.id') CLIGHTNING_ONION="$(cat /var/lib/onion-chef/operator/clightning)" CLIGHTNING_ID="$CLIGHTNING_NODEID@$CLIGHTNING_ONION:9735" echo CLIGHTNING_NODEID="$CLIGHTNING_NODEID" @@ -13,8 +13,8 @@ if [ -x "$(command -v lightning-cli)" ]; then echo CLIGHTNING_ID="$CLIGHTNING_ID" fi -if [ -x "$(command -v lncli)" ]; then - LND_NODEID=$(sudo -u lnd lncli --tlscertpath /secrets/lnd_cert --macaroonpath /var/lib/lnd/chain/bitcoin/mainnet/admin.macaroon getinfo | jq -r '.uris[0]') +if systemctl is-active --quiet lnd; then + LND_NODEID=$(lncli getinfo | jq -r '.uris[0]') echo LND_NODEID="$LND_NODEID" fi diff --git a/secrets/generate_secrets.sh b/secrets/generate_secrets.sh deleted file mode 100755 index 3b5f15b..0000000 --- a/secrets/generate_secrets.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh - -SECRETSFILE=secrets/secrets.nix - -if [ ! -e "$SECRETSFILE" ]; then - echo Write secrets to $SECRETSFILE - { - echo \{ - echo " bitcoinrpcpassword = \"$(apg -m 20 -x 20 -M Ncl -n 1)\";" - echo " lnd-wallet-password = \"$(apg -m 20 -x 20 -M Ncl -n 1)\";" - echo " lightning-charge-api-token = \"$(apg -m 20 -x 20 -M Ncl -n 1)\";" - echo " liquidrpcpassword = \"$(apg -m 20 -x 20 -M Ncl -n 1)\";" - echo " spark-wallet-password = \"$(apg -m 20 -x 20 -M Ncl -n 1)\";" - echo \} - } >> $SECRETSFILE - echo Done -else - echo $SECRETSFILE already exists. Skipping. -fi - -if [ ! -e secrets/nginx.key ] || [ ! -e secrets/nginx.cert ]; then - echo Generate Nginx Self-Signed Cert - openssl genrsa -out secrets/nginx.key 2048 - openssl req -new -key secrets/nginx.key -out secrets/nginx.csr -subj "/C=KN" - openssl x509 -req -days 1825 -in secrets/nginx.csr -signkey secrets/nginx.key -out secrets/nginx.cert - rm secrets/nginx.csr - echo Done -else - echo Nginx Cert already exists. Skipping. -fi - -if [ ! -e secrets/lnd.key ] || [ ! -e secrets/lnd.cert ]; then - echo Generate LND compatible TLS Cert - openssl ecparam -genkey -name prime256v1 -out secrets/lnd.key - openssl req -config secrets/openssl.cnf -new -sha256 -key secrets/lnd.key -out secrets/lnd.csr -subj '/CN=localhost/O=lnd' - openssl req -config secrets/openssl.cnf -x509 -sha256 -days 1825 -key secrets/lnd.key -in secrets/lnd.csr -out secrets/lnd.cert - rm secrets/lnd.csr - echo Done -else - echo LND cert already exists. Skipping. -fi diff --git a/shell.nix b/shell.nix index f48a5c5..45ab264 100644 --- a/shell.nix +++ b/shell.nix @@ -7,8 +7,9 @@ stdenv.mkDerivation rec { name = "nix-bitcoin-environment"; nixops19_09 = callPackage ./pkgs/nixops {}; + make-secrets = callPackage ./pkgs/generate-secrets/update-and-generate.nix {}; - buildInputs = with pkgs; [ nixops19_09 figlet apg openssl ]; + buildInputs = [ nixops19_09 figlet ]; shellHook = '' export NIX_PATH="nixpkgs=${nixpkgs}:." @@ -18,6 +19,6 @@ stdenv.mkDerivation rec { # keys already added to my ssh-agent. export SSH_AUTH_SOCK="" figlet "nix-bitcoin" - ./secrets/generate_secrets.sh + (mkdir -p secrets; cd secrets; ${make-secrets}) ''; } diff --git a/test/make-test.nix b/test/make-test.nix new file mode 100644 index 0000000..018ab27 --- /dev/null +++ b/test/make-test.nix @@ -0,0 +1,44 @@ +testArgs: + +let + pkgs = import { config = {}; overlays = []; }; + + # Stable nixpkgs doesn't yet include the Python testing framework. + # Use unstable nixpkgs and patch it so that it uses stable nixpkgs for the VM + # machine configuration. + testingPkgs = let + # unstable as of 2020-01-09 + rev = "9beb0d1ac2ebd6063efbdc4d3631f8ce137bbf90"; + src = builtins.fetchTarball { + url = "https://github.com/nixos/nixpkgs-channels/archive/${rev}.tar.gz"; + sha256 = "1v95779di35qhrz70p2v27kmwm09h8pgh74i1wc72v0zp3bdrf50"; + }; + in + pkgs.runCommand "nixpkgs-testing" {} '' + cp -r ${src} $out + cd $out + chmod +w -R . + patch -p1 < ${./use-stable-pkgs.patch} + ''; + + test = (import "${testingPkgs}/nixos/tests/make-test-python.nix") testArgs; + + # Fix the black Python code formatter that's used in the test to allow the test + # script to have longer lines. The default width of 88 chars is too restrictive for + # our script. + fixedTest = { system ? builtins.currentSystem, ... }@args: + let + pkgs = (import testingPkgs { inherit system; config = {}; overlays = []; } ); + pkgsFixed = pkgs // { + python3Packages = pkgs.python3Packages // { + black = pkgs.writeScriptBin "black" '' + fileToCheck=''${@:$#} + [[ $fileToCheck = *test-script ]] && extraArgs='--line-length 100' + exec ${pkgs.python3Packages.black}/bin/black $extraArgs "$@" + ''; + }; + }; + in + test (args // { pkgs = pkgsFixed; }); +in + fixedTest diff --git a/test/run-tests.sh b/test/run-tests.sh new file mode 100755 index 0000000..69a969f --- /dev/null +++ b/test/run-tests.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +# Modules integration test runner. +# The test (./test.nix) uses the NixOS testing framework and is executed in a VM. +# +# Usage: +# Run test +# ./run-tests.sh +# +# Run test and save result to avoid garbage collection +# ./run-tests.sh build --out-link /tmp/nix-bitcoin-test +# +# Run interactive test debugging +# ./run-tests.sh debug +# +# This starts the testing VM and drops you into a Python REPL where you can +# manually execute the tests from ./test-script.py + +set -eo pipefail + +numCPUs=${numCPUs:-$(nproc)} +# Min. 800 MiB needed to avoid 'out of memory' errors +memoryMiB=${memoryMiB:-2048} + +scriptDir=$(cd "${BASH_SOURCE[0]%/*}" && pwd) + +getPkgs() { + nix eval --raw -f "$scriptDir/../pkgs/nixpkgs-pinned.nix" $1 +} +export NIX_PATH=nixpkgs=$(getPkgs nixpkgs):nixpkgs-unstable=$(getPkgs nixpkgs-unstable) + +# Run the test. No temporary files are left on the host system. +run() { + # TMPDIR is also used by the test driver for VM tmp files + export TMPDIR=$(mktemp -d -p /tmp nix-bitcoin-test.XXXXXX) + trap "rm -rf $TMPDIR" EXIT + + nix-build --out-link $TMPDIR/driver "$scriptDir/test.nix" -A driver + + # Variable 'tests' contains the Python code that is executed by the driver on startup + if [[ $1 == --interactive ]]; then + echo "Running interactive testing environment" + tests=$( + echo 'is_interactive = True' + # The test script raises an error when 'is_interactive' is defined so + # that it just loads the initial helper functions and stops before + # executing the actual tests + echo 'try:' + echo ' exec(os.environ["testScript"])' + echo 'except:' + echo ' pass' + # Start VM + echo 'start_all()' + # Start REPL + echo 'import code' + echo 'code.interact(local=globals())' + ) + else + tests='exec(os.environ["testScript"])' + fi + + echo "VM stats: CPUs: $numCPUs, memory: $memoryMiB MiB" + [[ $NB_TEST_ENABLE_NETWORK ]] || QEMU_NET_OPTS='restrict=on' + env -i \ + NIX_PATH="$NIX_PATH" \ + TMPDIR="$TMPDIR" \ + tests="$tests" \ + QEMU_OPTS="-smp $numCPUs -m $memoryMiB -nographic $QEMU_OPTS" \ + QEMU_NET_OPTS="$QEMU_NET_OPTS" \ + $TMPDIR/driver/bin/nixos-test-driver +} + +debug() { + run --interactive +} + +# Run the test by building the test derivation +build() { + vmTestNixExpr | nix-build --no-out-link "$@" - +} + +# On continuous integration nodes there are few other processes running alongside the +# test, so use more memory here for maximum performance. +exprForCI() { + memoryMiB=3072 + memTotalKiB=$(awk '/MemTotal/ { print $2 }' /proc/meminfo) + memAvailableKiB=$(awk '/MemAvailable/ { print $2 }' /proc/meminfo) + # Round down to nearest multiple of 50 MiB for improved test build caching + ((memAvailableMiB = memAvailableKiB / (1024 * 50) * 50)) + ((memAvailableMiB < memoryMiB)) && memoryMiB=$memAvailableMiB + >&2 echo "Host memory: total $((memTotalKiB / 1024)) MiB, available $memAvailableMiB MiB, VM $memoryMiB MiB" + vmTestNixExpr +} + +vmTestNixExpr() { + cat < ]; + security.allowUserNamespaces = true; # reenable disabled option + }; + + machine = { pkgs, lib, ... }: with lib; { + imports = [ + ../modules/nix-bitcoin.nix + ../modules/secrets/generate-secrets.nix + # using the hardened profile increases total test duration by ~50%, so disable it for now + # hardened + ]; + + services.nix-bitcoin.enable = true; + services.bitcoind.extraConfig = mkForce "connect=0"; + + services.clightning.enable = true; + services.spark-wallet.enable = true; + services.lightning-charge.enable = true; + services.nanopos.enable = true; + + services.lnd.enable = true; + systemd.services.lnd.wantedBy = mkForce []; + + services.electrs.enable = true; + + services.liquidd = { + enable = true; + listen = mkForce false; + extraConfig = "noconnect=1"; + }; + + services.nix-bitcoin-webindex.enable = true; + + services.hardware-wallets = { + trezor = true; + ledger = true; + }; + + # to test that unused secrets are made inaccessible by 'setup-secrets' + systemd.services.generate-secrets.postStart = '' + install -o nobody -g nogroup -m777 <(:) /secrets/dummy + ''; + }; + + testScript = builtins.readFile ./test-script.py; +} diff --git a/test/use-stable-pkgs.patch b/test/use-stable-pkgs.patch new file mode 100644 index 0000000..d89cf2d --- /dev/null +++ b/test/use-stable-pkgs.patch @@ -0,0 +1,41 @@ +--- a/nixos/lib/build-vms.nix ++++ b/nixos/lib/build-vms.nix +@@ -30,10 +30,10 @@ rec { + buildVM = + nodes: configurations: + +- import ./eval-config.nix { ++ import { + inherit system; + modules = configurations ++ extraConfigurations; +- baseModules = (import ../modules/module-list.nix) ++ ++ baseModules = (import ) ++ + [ ../modules/virtualisation/qemu-vm.nix + ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs + { key = "no-manual"; documentation.nixos.enable = false; } + + +services.connman doesn't exist in stable nixpkgs +--- a/nixos/modules/virtualisation/qemu-vm.nix ++++ b/nixos/modules/virtualisation/qemu-vm.nix +@@ -620,7 +620,6 @@ in + + # Wireless won't work in the VM. + networking.wireless.enable = mkVMOverride false; +- services.connman.enable = mkVMOverride false; + + # Speed up booting by not waiting for ARP. + networking.dhcpcd.extraConfig = "noarp"; + +The test driver assumed coreutils to be in PATH. This fix will be ported to upstream. +--- a/nixos/lib/testing-python.nix ++++ b/nixos/lib/testing-python.nix +@@ -155,7 +155,7 @@ in rec { + --add-flags "''${vms[*]}" \ + ${lib.optionalString enableOCR + "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \ +- --run "export testScript=\"\$(cat $out/test-script)\"" \ ++ --run "export testScript=\"\$(${coreutils}/bin/cat $out/test-script)\"" \ + --set VLANS '${toString vlans}' + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms + wrapProgram $out/bin/nixos-run-vms \