diff --git a/configuration.nix b/configuration.nix index 5bbed3b..66451b6 100644 --- a/configuration.nix +++ b/configuration.nix @@ -30,6 +30,19 @@ # default nix-bitcoin nodes offer outgoing connectivity. # services.clightning.autolisten = true; + ### LND + # Disable clightning and uncomment the following line in order to enable lnd, + # a lightning implementation written in Go. + # services.lnd.enable = assert (!config.services.clightning.enable); 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 + # following command after the lnd service starts: + # nixops scp --from bitcoin-node /secrets/lnd-seed-mnemonic ./secrets/lnd-seed-mnemonic + # You should also backup your channel state after opening new channels. + # This will allow you to recover off-chain funds, by force-closing channels. + # nixops scp --from bitcoin-node /var/lib/lnd/chain/bitcoin/mainnet/channel.backup /my-backup-path/channel.backup + ### SPARK WALLET # Enable this module to use spark-wallet, a minimalistic wallet GUI for # c-lightning, accessible over the web or through mobile and desktop apps. diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 04d5aee..13bb18a 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -193,6 +193,18 @@ in { to stay under the specified target size in MiB) ''; }; + zmqpubrawblock = mkOption { + type = types.nullOr types.string; + default = null; + example = "tcp://127.0.0.1:28332"; + description = "ZMQ address for zmqpubrawblock notifications"; + }; + zmqpubrawtx = mkOption { + type = types.nullOr types.string; + default = null; + example = "tcp://127.0.0.1:28333"; + description = "ZMQ address for zmqpubrawtx notifications"; + }; enforceTor = nix-bitcoin-services.enforceTor; }; }; @@ -239,7 +251,7 @@ in { // (if cfg.enforceTor then nix-bitcoin-services.allowTor else nix-bitcoin-services.allowAnyIP - ); + ) // optionalAttrs config.services.lnd.enable nix-bitcoin-services.allowAnyProtocol; # FOR ZMQ }; systemd.services.bitcoind-import-banlist = { description = "Bitcoin daemon banlist importer"; diff --git a/modules/default.nix b/modules/default.nix index c4f0079..621ab8f 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -11,4 +11,5 @@ 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 810d815..b29ae3c 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -106,8 +106,8 @@ in { listen ${toString config.services.electrs.nginxport} ssl; proxy_pass electrs; - ssl_certificate /secrets/ssl_certificate; - ssl_certificate_key /secrets/ssl_certificate_key; + ssl_certificate /secrets/nginx_cert; + ssl_certificate_key /secrets/nginx_key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 4h; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; diff --git a/modules/lnd.nix b/modules/lnd.nix new file mode 100644 index 0000000..58e7848 --- /dev/null +++ b/modules/lnd.nix @@ -0,0 +1,132 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + nix-bitcoin-services = pkgs.callPackage ./nix-bitcoin-services.nix { }; + cfg = config.services.lnd; + configFile = pkgs.writeText "lnd.conf" '' + datadir=${cfg.dataDir} + logdir=${cfg.dataDir}/logs + bitcoin.mainnet=1 + tlscertpath=/secrets/lnd_cert + tlskeypath=/secrets/lnd_key + + bitcoin.active=1 + bitcoin.node=bitcoind + + tor.active=true + tor.v3=true + tor.streamisolation=true + tor.privatekeypath=${cfg.dataDir}/v3_onion_private_key + + bitcoind.rpcuser=${config.services.bitcoind.rpcuser} + bitcoind.zmqpubrawblock=${config.services.bitcoind.zmqpubrawblock} + bitcoind.zmqpubrawtx=${config.services.bitcoind.zmqpubrawtx} + + ${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 = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, the LND service will be installed. + ''; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/lnd"; + description = "The data directory for LND."; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + autopilot.active=1 + ''; + description = "Additional configurations to be appended to lnd.conf."; + }; + 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.altcoins.bitcoind ]; + wantedBy = [ "multi-user.target" ]; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" ]; + preStart = '' + mkdir -m 0770 -p ${cfg.dataDir} + 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' + ''; + serviceConfig = { + PermissionsStartOnly = "true"; + ExecStart = "${pkgs.lnd}/bin/lnd --configfile=${cfg.dataDir}/lnd.conf"; + ExecStartPost = "${pkgs.bash}/bin/bash ${init-lnd-wallet-script}"; + User = "lnd"; + Restart = "on-failure"; + RestartSec = "10s"; + } // nix-bitcoin-services.defaultHardening + // (if cfg.enforceTor + then nix-bitcoin-services.allowTor + else nix-bitcoin-services.allowAnyIP + ) // nix-bitcoin-services.allowAnyProtocol; # For ZMQ + }; + }; +} diff --git a/modules/nix-bitcoin-pkgs.nix b/modules/nix-bitcoin-pkgs.nix index 918a257..b88274c 100644 --- a/modules/nix-bitcoin-pkgs.nix +++ b/modules/nix-bitcoin-pkgs.nix @@ -15,5 +15,6 @@ in { bitcoin = nixpkgs-unstable.bitcoin.override { miniupnpc = null; }; altcoins.bitcoind = nixpkgs-unstable.altcoins.bitcoind.override { miniupnpc = null; }; clightning = nixpkgs-unstable.clightning.override { }; + lnd = nixpkgs-unstable.lnd.override { }; }; } diff --git a/modules/nix-bitcoin.nix b/modules/nix-bitcoin.nix index 530a347..85c869f 100644 --- a/modules/nix-bitcoin.nix +++ b/modules/nix-bitcoin.nix @@ -28,6 +28,7 @@ in { ./onion-chef.nix ./recurring-donations.nix ./hardware-wallets.nix + ./lnd.nix ]; options.services.nix-bitcoin = { @@ -46,6 +47,8 @@ in { # Tor services.tor.enable = true; services.tor.client.enable = true; + # LND uses ControlPort to create onion services + services.tor.controlPort = if config.services.lnd.enable then 9051 else null; # Tor SSH service services.tor.hiddenServices.sshd = { @@ -64,12 +67,16 @@ in { services.bitcoind.enforceTor = true; services.bitcoind.port = 8333; services.bitcoind.rpcuser = "bitcoinrpc"; + services.bitcoind.zmqpubrawblock = "tcp://127.0.0.1:28332"; + services.bitcoind.zmqpubrawtx = "tcp://127.0.0.1:28333"; services.bitcoind.extraConfig = '' assumevalid=0000000000000000000726d186d6298b5054b9a5c49639752294b322a305d240 addnode=ecoc5q34tmbq54wl.onion discover=0 addresstype=bech32 changetype=bech32 + ${optionalString (config.services.lnd.enable) "zmqpubrawblock=${config.services.bitcoind.zmqpubrawblock}"} + ${optionalString (config.services.lnd.enable) "zmqpubrawtx=${config.services.bitcoind.zmqpubrawtx}"} ''; services.bitcoind.prune = 0; services.bitcoind.dbCache = 1000; @@ -96,11 +103,15 @@ in { version = 3; }; + # lnd + services.lnd.enforceTor = true; + # Create user operator which can use bitcoin-cli and lightning-cli users.users.operator = { isNormalUser = true; extraGroups = [ config.services.bitcoind.group ] ++ (if config.services.clightning.enable then [ "clightning" ] else [ ]) + ++ (if config.services.lnd.enable then [ "lnd" ] else [ ]) ++ (if config.services.liquidd.enable then [ config.services.liquidd.group ] else [ ]) ++ (if (config.services.hardware-wallets.ledger || config.services.hardware-wallets.trezor) then [ config.services.hardware-wallets.group ] else [ ]); @@ -111,17 +122,26 @@ in { environment.interactiveShellInit = '' alias bitcoin-cli='bitcoin-cli -datadir=${config.services.bitcoind.dataDir}' - alias lightning-cli='sudo -u clightning lightning-cli --lightning-dir=${config.services.clightning.dataDir}' - '' + (if config.services.liquidd.enable then '' - alias elements-cli='elements-cli -datadir=${config.services.liquidd.dataDir}' - alias liquidswap-cli='liquidswap-cli -c ${config.services.liquidd.dataDir}/elements.conf' - '' else ""); + ${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 "" ); @@ -176,6 +196,7 @@ in { 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] diff --git a/network/network.nix b/network/network.nix index 4059e40..96997ca 100644 --- a/network/network.nix +++ b/network/network.nix @@ -7,6 +7,13 @@ let 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/"; @@ -36,20 +43,34 @@ let group = "clightning"; permissions = "0440"; }; - ssl_certificate_key = { - keyFile = ../secrets/ssl_certificate_key.key; + nginx_key = { + keyFile = ../secrets/nginx.key; destDir = "/secrets/"; user = "nginx"; group = "root"; permissions = "0440"; }; - ssl_certificate = { - keyFile = ../secrets/ssl_certificate.crt; + 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"; @@ -61,10 +82,11 @@ 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 ssl_certificate_key ssl_certificate; } else { }); + // (if (config.services.electrs.enable) then { inherit nginx_key nginx_cert; } else { }); } // (bitcoin-node { inherit config pkgs; }); } diff --git a/pkgs/nodeinfo/nodeinfo.sh b/pkgs/nodeinfo/nodeinfo.sh index 62529fa..a013fd3 100644 --- a/pkgs/nodeinfo/nodeinfo.sh +++ b/pkgs/nodeinfo/nodeinfo.sh @@ -2,14 +2,21 @@ set -e set -o pipefail BITCOIND_ONION="$(cat /var/lib/onion-chef/operator/bitcoind)" -CLIGHTNING_NODEID=$(sudo -u clightning lightning-cli --lightning-dir=/var/lib/clightning getinfo | jq -r '.id') -CLIGHTNING_ONION="$(cat /var/lib/onion-chef/operator/clightning)" -CLIGHTNING_ID="$CLIGHTNING_NODEID@$CLIGHTNING_ONION:9735" - echo BITCOIND_ONION="$BITCOIND_ONION" -echo CLIGHTNING_NODEID="$CLIGHTNING_NODEID" -echo CLIGHTNING_ONION="$CLIGHTNING_ONION" -echo CLIGHTNING_ID="$CLIGHTNING_ID" + +if [ -x "$(command -v clightning)" ]; then + CLIGHTNING_NODEID=$(sudo -u clightning lightning-cli --lightning-dir=/var/lib/clightning 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" + echo CLIGHTNING_ONION="$CLIGHTNING_ONION" + 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]') + echo LND_NODEID="$LND_NODEID" +fi NGINX_ONION_FILE=/var/lib/onion-chef/operator/nginx if [ -e "$NGINX_ONION_FILE" ]; then diff --git a/secrets/generate_secrets.sh b/secrets/generate_secrets.sh index 460de78..3b5f15b 100755 --- a/secrets/generate_secrets.sh +++ b/secrets/generate_secrets.sh @@ -2,24 +2,40 @@ SECRETSFILE=secrets/secrets.nix -if [ -e "$SECRETSFILE" ]; then - echo $SECRETSFILE already exists. No new secrets were generated. - exit 1 +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 -echo Write secrets to $SECRETSFILE -{ - echo \{ - echo " bitcoinrpcpassword = \"$(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 +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 -echo Generate Self-Signed Cert -openssl genrsa -out secrets/ssl_certificate_key.key 2048 -openssl req -new -key secrets/ssl_certificate_key.key -out secrets/ssl_certificate.csr -subj "/C=KN" -openssl x509 -req -days 1825 -in secrets/ssl_certificate.csr -signkey secrets/ssl_certificate_key.key -out secrets/ssl_certificate.crt -echo Done +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/secrets/openssl.cnf b/secrets/openssl.cnf new file mode 100644 index 0000000..66f25e4 --- /dev/null +++ b/secrets/openssl.cnf @@ -0,0 +1,32 @@ +[ req ] +#default_bits = 2048 +#default_md = sha256 +#default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_min = 2 +countryName_max = 2 +stateOrProvinceName = State or Province Name (full name) +localityName = Locality Name (eg, city) +0.organizationName = Organization Name (eg, company) +organizationalUnitName = Organizational Unit Name (eg, section) +commonName = Common Name (eg, fully qualified host name) +commonName_max = 64 +emailAddress = Email Address +emailAddress_max = 64 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +[ v3_ca ] +subjectAltName = @alt_names + +[ alt_names ] +IP.1 = 127.0.0.1 +DNS.1 = localhost