Merge #293: Module refactorings, onionServices

e2922eb4ce move rpc thread count setting to lightning modules (Erik Arvstedt)
352fc4e8fe liquid: remove insecure and redundant option 'rpcpassword' (Erik Arvstedt)
757a66b9bd liquid: move rpcuser definition to module (Erik Arvstedt)
0e00c39d47 secure-node: improve layout (Erik Arvstedt)
5f7a7962f7 backups: remove redundant option 'program' (Erik Arvstedt)
04d8560f86 secure-node: remove qrencode, tor from systemPackages (Erik Arvstedt)
323a431aba improve nodeinfo (Erik Arvstedt)
f6b883a9ac remove webindex (Erik Arvstedt)
2a240d6f4a enable-tor: disable default onion services for clightning, lnd, btcpayserver (Erik Arvstedt)
18c7842e1a modules: show warnings for obsolete options (Erik Arvstedt)
45c40c4eb9 versioning: simplify assertion evaluation (Erik Arvstedt)
bed00fe937 lnd: use onionServices for address announcing (Erik Arvstedt)
3980cd5a41 clightning: use onionServices for address announcing (Erik Arvstedt)
bd2a46cb73 spark-wallet: use onionServices (Erik Arvstedt)
87fb9f246b add 'enable-tor' preset (Erik Arvstedt)
05b5402bb1 add nix-bitcoin.onionServices (Erik Arvstedt)
fffe988248 onionAddresses: add readonly option 'dataDir' (Erik Arvstedt)
5f34b094d3 onionAddresses: improve script (Erik Arvstedt)
b266f23251 onionAddresses: use service 'script' option (Erik Arvstedt)
6d13b26d0a onionAddresses: add more precise type for option 'access' (Erik Arvstedt)
93562f76dd onionAddresses: remove redundant option 'enable' (Erik Arvstedt)
43c247e3fe onionAddresses: use StateDirectory instead of tmpfiles (Erik Arvstedt)
5c6977b006 rename onion-chef -> nix-bitcoin.onionAddresses (Erik Arvstedt)
55073eee70 remove nix-bitcoin.pkgs.lib (Erik Arvstedt)
09e0042aa8 spark-wallet: add consistent address options (Erik Arvstedt)
39f16c0b4a liquidd: add consistent address options (Erik Arvstedt)
b5d76ba1b3 electrs: add consistent address options (Erik Arvstedt)
8fa32b7f91 btcpayserver: add consistent address options (Erik Arvstedt)
e78a609687 clightning: add consistent address options (Erik Arvstedt)
b41a720c28 lnd: add consistent address options (Erik Arvstedt)
dd4a0238f9 bitcoind: group rpc options under parent option 'rpc' (Erik Arvstedt)
5b7e0d09b2 bitcoind: add consistent address options (Erik Arvstedt)

Pull request description:

ACKs for top commit:
  nixbitcoin:
    ACK e2922eb4ce
  jonasnick:
    ACK e2922eb4ce

Tree-SHA512: a85b33efe66048f06699b3997f83c9427f70f278fa66d30ee9a29c91f50723ff8bd1ffb9d968d7f08818742c8c6afb0b40dbfc14b95a4b8c3302caf9bede4198
This commit is contained in:
Jonas Nick 2021-01-14 20:42:02 +00:00
commit c6c14889eb
No known key found for this signature in database
GPG Key ID: 4861DBF262123605
30 changed files with 659 additions and 625 deletions

View File

@ -48,8 +48,7 @@ See the [examples directory](examples/README.md).
Features
---
A [configuration preset](modules/presets/secure-node.nix) for setting up a secure node
* All applications use Tor for outbound connections and accept inbound connections via onion services.
* Includes a [nodeinfo](modules/nodeinfo.nix) script which prints basic info about the node.
* All applications use Tor for outbound connections and support accepting inbound connections via onion services.
NixOS modules
* Application services
@ -74,9 +73,9 @@ NixOS modules
* [bitcoin-core-hwi](https://github.com/bitcoin-core/HWI)
* Helper
* [netns-isolation](modules/netns-isolation.nix): isolates applications on the network-level via network namespaces
* [nodeinfo](modules/nodeinfo.nix): script which prints info about the node's services
* [backups](modules/backups.nix): daily duplicity backups of all your node's important files
* [operator](modules/operator.nix): adds non-root user `operator` who has access to client tools (e.g. `bitcoin-cli`, `lightning-cli`)
* [nix-bitcoin webindex](modules/nix-bitcoin-webindex.nix): a local website to display node information
Security
---

View File

@ -8,7 +8,7 @@ fetch-release > nix-bitcoin-release.nix
Nodeinfo
---
Run `nodeinfo` to see your onion addresses for the webindex, spark, etc. if they are enabled.
Run `nodeinfo` to see onion addresses and local addresses for enabled services.
Connect to spark-wallet
---
@ -86,10 +86,10 @@ Connect to electrs
nixops deploy -d bitcoin-node
```
3. Get electrs onion address
3. Get electrs onion address with format `<onion-address>:<port>`
```
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 "<ELECTRS_ONION>:50001:t" -p socks5:localhost:9050
electrum --oneserver -1 -s "<electrs onion address>: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: <ELECTRS_ONION>:50001:t
Network > Server: <electrs onion address>: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 = [ "<contents of ~/.ssh/id_ed25519.pub>" ];
};
```
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 <random port of your choosing>:localhost:22 root@<SSH onion address>
```
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 = <random port of your choosing>;
};
}
```
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

View File

@ -11,7 +11,7 @@ nix-shell
The following example scripts set up a nix-bitcoin node according to [`configuration.nix`](configuration.nix) and then
shut down immediately. They leave no traces (outside of `/nix/store`) on the host system.\
By default, [`configuration.nix`](configuration.nix) enables `bitcoind` and `clightning` (with an onion service).
By default, [`configuration.nix`](configuration.nix) enables `bitcoind` and `clightning`.
- [`./deploy-container.sh`](deploy-container.sh) creates a [NixOS container](https://github.com/erikarvstedt/extra-container).\
This is the fastest way to set up a node.\

View File

@ -37,11 +37,12 @@
# Enable this module to use clightning, a Lightning Network implementation
# in C.
services.clightning.enable = true;
# == TOR
# Enable this option to announce our Tor Hidden Service. By default clightning
# offers outgoing functionality, but doesn't announce the Tor Hidden Service
# under which peers can reach us.
# services.clightning.announce-tor = true;
#
# Set this to create an onion service by which clightning can accept incoming connections
# via Tor.
# The onion service is automatically announced to peers.
# nix-bitcoin.onionServices.clightning.public = true;
#
# == Plugins
# See ../docs/usage.md for the list of available plugins.
# services.clightning.plugins.prometheus.enable = true;
@ -49,13 +50,15 @@
### LND
# Uncomment the following line in order to enable lnd, a lightning
# implementation written in Go. In order to avoid collisions with clightning
# you must disable clightning or change the services.clightning.bindport or
# services.lnd.listenPort to a port other than 9735.
# you must disable clightning or change the services.clightning.port or
# services.lnd.port to a port other than 9735.
# services.lnd.enable = true;
# Enable this option to announce our Tor Hidden Service. By default lnd
# offers outgoing functionality, but doesn't announce the Tor Hidden Service
# under which peers can reach us.
# services.lnd.announce-tor = true;
#
# Set this to create an onion service by which lnd can accept incoming connections
# via Tor.
# The onion service is automatically announced to peers.
# nix-bitcoin.onionServices.lnd.public = true;
#
## WARNING
# If you use lnd, you should manually backup your wallet mnemonic
# seed. This will allow you to recover on-chain funds. You can run the
@ -93,6 +96,12 @@
# The lightning backend service automatically enabled.
# Afterwards you need to go into Store > General Settings > Lightning Nodes
# and click to use "the internal lightning node of this BTCPay Server".
#
# Set this to create an onion service to make the btcpayserver web interface
# accessible via Tor.
# Security WARNING: Create a btcpayserver administrator account before allowing
# public access to the web interface.
# nix-bitcoin.onionServices.btcpayserver.enable = true;
### LIQUIDD
# Enable this module to use Liquid, a sidechain for an inter-exchange
@ -101,11 +110,6 @@
# tool run as user operator.
# services.liquidd.enable = true;
### WEBINDEX
# Enable this module to use the nix-bitcoin-webindex, a simple website
# displaying your node information. Only available if clightning is enabled.
# services.nix-bitcoin-webindex.enable = true;
### RECURRING-DONATIONS
# Enable this module to send recurring donations. This is EXPERIMENTAL; it's
# not guaranteed that payments are succeeding or that you will notice payment
@ -203,5 +207,5 @@
# The nix-bitcoin release version that your config is compatible with.
# When upgrading to a backwards-incompatible release, nix-bitcoin will display an
# an error and provide hints for migrating your config to the new release.
nix-bitcoin.configVersion = "0.0.26";
nix-bitcoin.configVersion = "0.0.30";
}

View File

@ -31,13 +31,6 @@ let
in {
options.services.backups = {
enable = mkEnableOption "Backups service";
program = mkOption {
type = types.enum [ "duplicity" ];
default = "duplicity";
description = ''
Program with which to do backups.
'';
};
with-bulk-data = mkOption {
type = types.bool;
default = false;
@ -69,7 +62,7 @@ in {
};
};
config = mkIf (cfg.enable && cfg.program == "duplicity") (mkMerge [
config = mkIf cfg.enable (mkMerge [
{
environment.systemPackages = [ pkgs.duplicity ];

View File

@ -22,16 +22,18 @@ let
${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"}
# Connection options
${optionalString cfg.listen "bind=${cfg.bind}"}
${optionalString (cfg.port != null) "port=${toString cfg.port}"}
${optionalString cfg.listen "bind=${cfg.address}"}
port=${toString cfg.port}
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
listen=${if cfg.listen then "1" else "0"}
${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"}
${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes}
# RPC server options
${optionalString (cfg.rpcthreads != null) "rpcthreads=${toString cfg.rpcthreads}"}
rpcbind=${cfg.rpc.address}
rpcport=${toString cfg.rpc.port}
rpcconnect=${cfg.rpc.address}
${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"}
rpcwhitelistdefault=0
${concatMapStrings (user: ''
${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${passwordHMAC}"}
@ -39,9 +41,7 @@ let
"rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"}
'') (builtins.attrValues cfg.rpc.users)
}
rpcbind=${cfg.rpcbind}
rpcconnect=${cfg.rpcbind}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip}
# Wallet options
${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"}
@ -57,6 +57,16 @@ in {
options = {
services.bitcoind = {
enable = mkEnableOption "Bitcoin daemon";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for peer connections.";
};
port = mkOption {
type = types.port;
default = 8333;
description = "Port to listen for peer connections.";
};
package = mkOption {
type = types.package;
default = config.nix-bitcoin.pkgs.bitcoind;
@ -77,13 +87,6 @@ in {
default = "/var/lib/bitcoind";
description = "The data directory for bitcoind.";
};
bind = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Bind to given address and always listen on it.
'';
};
user = mkOption {
type = types.str;
default = "bitcoin";
@ -95,10 +98,29 @@ in {
description = "The group as which to run bitcoind.";
};
rpc = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Address to listen for JSON-RPC connections.
'';
};
port = mkOption {
type = types.port;
default = 8332;
description = "Port on which to listen for JSON-RPC connections.";
description = "Port to listen for JSON-RPC connections.";
};
threads = mkOption {
type = types.nullOr types.ints.u16;
default = null;
description = "The number of threads to service RPC calls.";
};
allowip = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1" ];
description = ''
Allow JSON-RPC connections from specified sources.
'';
};
users = mkOption {
default = {};
@ -144,25 +166,6 @@ in {
'';
};
};
rpcthreads = mkOption {
type = types.nullOr types.ints.u16;
default = null;
description = "Set the number of threads to service RPC calls";
};
rpcbind = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Bind to given address to listen for JSON-RPC connections.
'';
};
rpcallowip = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1" ];
description = ''
Allow JSON-RPC connections from specified source.
'';
};
regtest = mkOption {
type = types.bool;
default = false;
@ -176,11 +179,6 @@ in {
readOnly = true;
default = mainnet: regtest: if cfg.regtest then regtest else mainnet;
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the default port on which to listen for connections.";
};
proxy = mkOption {
type = types.nullOr types.str;
default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;

View File

@ -14,6 +14,16 @@ in {
default = nbPkgs.nbxplorer;
description = "The package providing nbxplorer binaries.";
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen on.";
};
port = mkOption {
type = types.port;
default = 24444;
description = "Port to listen on.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/nbxplorer";
@ -29,16 +39,6 @@ in {
default = cfg.nbxplorer.user;
description = "The group as which to run nbxplorer.";
};
bind = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The address on which to bind.";
};
port = mkOption {
type = types.port;
default = 24444;
description = "Port on which to bind.";
};
enable = mkOption {
# This option is only used by netns-isolation
internal = true;
@ -49,6 +49,16 @@ in {
btcpayserver = {
enable = mkEnableOption "btcpayserver";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen on.";
};
port = mkOption {
type = types.port;
default = 23000;
description = "Port to listen on.";
};
package = mkOption {
type = types.package;
default = nbPkgs.btcpayserver;
@ -69,16 +79,6 @@ in {
default = cfg.btcpayserver.user;
description = "The group as which to run btcpayserver.";
};
bind = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The address on which to bind.";
};
port = mkOption {
type = types.port;
default = 23000;
description = "Port on which to bind.";
};
lightningBackend = mkOption {
type = types.nullOr (types.enum [ "clightning" "lnd" ]);
default = null;
@ -117,9 +117,9 @@ in {
configFile = builtins.toFile "config" ''
network=${config.services.bitcoind.network}
btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name}
btcrpcurl=http://${config.services.bitcoind.rpcbind}:${toString cfg.bitcoind.rpc.port}
btcnodeendpoint=${config.services.bitcoind.bind}:8333
bind=${cfg.nbxplorer.bind}
btcrpcurl=http://${config.services.bitcoind.rpc.address}:${toString cfg.bitcoind.rpc.port}
btcnodeendpoint=${config.services.bitcoind.address}:${toString config.services.bitcoind.port}
bind=${cfg.nbxplorer.address}
port=${toString cfg.nbxplorer.port}
'';
in {
@ -153,9 +153,9 @@ in {
network=${config.services.bitcoind.network}
postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpaydb
socksendpoint=${cfg.tor.client.socksListenAddress}
btcexplorerurl=http://${cfg.nbxplorer.bind}:${toString cfg.nbxplorer.port}/
btcexplorerurl=http://${cfg.nbxplorer.address}:${toString cfg.nbxplorer.port}/
btcexplorercookiefile=${cfg.nbxplorer.dataDir}/${config.services.bitcoind.makeNetworkName "Main" "RegTest"}/.cookie
bind=${cfg.btcpayserver.bind}
bind=${cfg.btcpayserver.address}
${optionalString (cfg.btcpayserver.rootpath != null) "rootpath=${cfg.btcpayserver.rootpath}"}
port=${toString cfg.btcpayserver.port}
'' + optionalString (cfg.btcpayserver.lightningBackend == "clightning") ''
@ -163,7 +163,7 @@ in {
'');
lndConfig =
"btclightning=type=lnd-rest;" +
"server=https://${toString cfg.lnd.listen}:${toString cfg.lnd.restPort}/;" +
"server=https://${cfg.lnd.restAddress}:${toString cfg.lnd.restPort}/;" +
"macaroonfilepath=/run/lnd/btcpayserver.macaroon;" +
"certthumbprint=";
in let self = {

View File

@ -6,15 +6,14 @@ let
cfg = config.services.clightning;
inherit (config) nix-bitcoin-services;
nbPkgs = config.nix-bitcoin.pkgs;
onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []);
network = config.services.bitcoind.makeNetworkName "bitcoin" "regtest";
configFile = pkgs.writeText "config" ''
network=${network}
bitcoin-datadir=${config.services.bitcoind.dataDir}
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
always-use-proxy=${if cfg.always-use-proxy then "true" else "false"}
bind-addr=${cfg.bind-addr}:${toString cfg.bindport}
bitcoin-rpcconnect=${config.services.bitcoind.rpcbind}
bind-addr=${cfg.address}:${toString cfg.port}
bitcoin-rpcconnect=${config.services.bitcoind.rpc.address}
bitcoin-rpcport=${toString config.services.bitcoind.rpc.port}
bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name}
rpc-file-mode=0660
@ -29,13 +28,15 @@ in {
If enabled, the clightning service will be installed.
'';
};
autolisten = mkOption {
type = types.bool;
default = false;
description = ''
Bind (and maybe announce) on IPv4 and IPv6 interfaces if no addr,
bind-addr or announce-addr options are specified.
'';
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "IP address or UNIX domain socket to listen for peer connections.";
};
port = mkOption {
type = types.port;
default = 9735;
description = "Port to listen for peer connections.";
};
proxy = mkOption {
type = types.nullOr types.str;
@ -49,21 +50,6 @@ in {
Always use the *proxy*, even to connect to normal IP addresses (you can still connect to Unix domain sockets manually). This also disables all DNS lookups, to avoid leaking information.
'';
};
bind-addr = mkOption {
type = nbPkgs.lib.ipv4Address;
default = "127.0.0.1";
description = "Set an IP address or UNIX domain socket to listen to";
};
bindport = mkOption {
type = types.port;
default = 9735;
description = "Set a Port to listen to locally";
};
announce-tor = mkOption {
type = types.bool;
default = false;
description = "Announce clightning Tor Hidden Service";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/clightning";
@ -97,11 +83,24 @@ in {
'';
description = "Binary to connect with the clightning instance.";
};
enforceTor = nix-bitcoin-services.enforceTor;
getPublicAddressCmd = mkOption {
type = types.str;
default = "";
description = ''
Bash expression which outputs the public service address to announce to peers.
If left empty, no address is announced.
'';
};
inherit (nix-bitcoin-services) enforceTor;
};
config = mkIf cfg.enable {
services.bitcoind.enable = true;
services.bitcoind = {
enable = true;
# Increase rpc thread count due to reports that lightning implementations fail
# under high bitcoind rpc load
rpc.threads = 16;
};
environment.systemPackages = [ nbPkgs.clightning (hiPrio cfg.cli) ];
users.users.${cfg.user} = {
@ -116,21 +115,25 @@ in {
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
services.onion-chef.access.clightning = if cfg.announce-tor then [ "clightning" ] else [];
systemd.services.clightning = {
description = "Run clightningd";
path = [ nbPkgs.bitcoind ];
wantedBy = [ "multi-user.target" ];
requires = [ "bitcoind.service" ] ++ onion-chef-service;
after = [ "bitcoind.service" ] ++ onion-chef-service;
requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" ];
preStart = ''
cp ${configFile} ${cfg.dataDir}/config
chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}'
# The RPC socket has to be removed otherwise we might have stale sockets
rm -f ${cfg.networkDir}/lightning-rpc
chmod 640 ${cfg.dataDir}/config
echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/config'
${optionalString cfg.announce-tor "echo announce-addr=$(cat /var/lib/onion-chef/clightning/clightning) >> '${cfg.dataDir}/config'"}
{
echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-public)"
${optionalString (cfg.getPublicAddressCmd != "") ''
echo "announce-addr=$(${cfg.getPublicAddressCmd})"
''}
} >> '${cfg.dataDir}/config'
'';
serviceConfig = nix-bitcoin-services.defaultHardening // {
ExecStart = "${nbPkgs.clightning}/bin/lightningd --lightning-dir=${cfg.dataDir}";

View File

@ -6,7 +6,6 @@
electrs = ./electrs.nix;
liquid = ./liquid.nix;
presets.secure-node = ./presets/secure-node.nix;
nix-bitcoin-webindex = ./nix-bitcoin-webindex.nix;
spark-wallet = ./spark-wallet.nix;
recurring-donations = ./recurring-donations.nix;
lnd = ./lnd.nix;

View File

@ -9,6 +9,16 @@ let
in {
options.services.electrs = {
enable = mkEnableOption "electrs";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for RPC connections.";
};
port = mkOption {
type = types.port;
default = 50001;
description = "RPC port.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/electrs";
@ -31,16 +41,6 @@ in {
If enabled, the electrs service will sync faster on high-memory systems ( 8GB).
'';
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "RPC and monitoring listening address.";
};
port = mkOption {
type = types.port;
default = 50001;
description = "RPC port.";
};
monitoringPort = mkOption {
type = types.port;
default = 4224;
@ -95,7 +95,7 @@ in {
--daemon-dir='${bitcoind.dataDir}' \
--electrum-rpc-addr=${cfg.address}:${toString cfg.port} \
--monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \
--daemon-rpc-addr=${bitcoind.rpcbind}:${toString bitcoind.rpc.port} \
--daemon-rpc-addr=${bitcoind.rpc.address}:${toString bitcoind.rpc.port} \
${cfg.extraArgs}
'';
User = cfg.user;

View File

@ -21,7 +21,7 @@ let
[BLOCKCHAIN]
blockchain_source = bitcoin-rpc
network = ${bitcoind.network}
rpc_host = ${bitcoind.rpcbind}
rpc_host = ${bitcoind.rpc.address}
rpc_port = ${toString bitcoind.rpc.port}
rpc_user = ${bitcoind.rpc.users.privileged.name}
@@RPC_PASSWORD@@

View File

@ -17,7 +17,7 @@ let
tlscertpath=${secretsDir}/loop-cert
tlskeypath=${secretsDir}/loop-key
lnd.host=${config.services.lnd.rpclisten}:${toString config.services.lnd.rpcPort}
lnd.host=${config.services.lnd.rpcAddress}:${toString config.services.lnd.rpcPort}
lnd.macaroondir=${config.services.lnd.networkDir}
lnd.tlspath=${secretsDir}/lnd-cert

View File

@ -16,23 +16,22 @@ let
${optionalString (cfg.validatepegin != null) "validatepegin=${if cfg.validatepegin then "1" else "0"}"}
# Connection options
${optionalString cfg.listen "bind=${cfg.bind}"}
${optionalString (cfg.port != null) "port=${toString cfg.port}"}
${optionalString cfg.listen "bind=${cfg.address}"}
port=${toString cfg.port}
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
listen=${if cfg.listen then "1" else "0"}
# RPC server options
${optionalString (cfg.rpc.port != null) "rpcport=${toString cfg.rpc.port}"}
rpcport=${toString cfg.rpc.port}
${concatMapStringsSep "\n"
(rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
(attrValues cfg.rpc.users)
}
rpcbind=${cfg.rpcbind}
rpcconnect=${cfg.rpcbind}
rpcbind=${cfg.rpc.address}
rpcconnect=${cfg.rpc.address}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip}
${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"}
${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"}
mainchainrpchost=${config.services.bitcoind.rpcbind}
rpcuser=${cfg.rpcuser}
mainchainrpchost=${config.services.bitcoind.rpc.address}
mainchainrpcport=${toString config.services.bitcoind.rpc.port}
mainchainrpcuser=${config.services.bitcoind.rpc.users.public.name}
@ -71,7 +70,16 @@ in {
services.liquidd = {
enable = mkEnableOption "Liquid sidechain";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for peer connections.";
};
port = mkOption {
type = types.port;
default = 7042;
description = "Override the default port on which to listen for connections.";
};
extraConfig = mkOption {
type = types.lines;
default = "";
@ -88,14 +96,6 @@ in {
default = "/var/lib/liquidd";
description = "The data directory for liquidd.";
};
bind = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Bind to given address and always listen on it.
'';
};
user = mkOption {
type = types.str;
default = "liquid";
@ -106,12 +106,16 @@ in {
default = cfg.user;
description = "The group as which to run liquidd.";
};
rpc = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for JSON-RPC connections.";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the default port on which to listen for JSON-RPC connections.";
type = types.port;
default = 7041;
description = "Port to listen for JSON-RPC connections.";
};
users = mkOption {
default = {};
@ -125,14 +129,6 @@ in {
'';
};
};
rpcbind = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Bind to given address to listen for JSON-RPC connections.
'';
};
rpcallowip = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1" ];
@ -141,25 +137,15 @@ in {
'';
};
rpcuser = mkOption {
type = types.nullOr types.str;
default = null;
type = types.str;
default = "liquidrpc";
description = "Username for JSON-RPC connections";
};
rpcpassword = mkOption {
type = types.nullOr types.str;
default = null;
description = "Password for JSON-RPC connections";
};
testnet = mkOption {
type = types.bool;
default = false;
description = "Whether to use the test chain.";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the default port on which to listen for connections.";
};
proxy = mkOption {
type = types.nullOr types.str;
default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;

View File

@ -8,8 +8,7 @@ let
secretsDir = config.nix-bitcoin.secretsDir;
bitcoind = config.services.bitcoind;
bitcoindRpcAddress = bitcoind.rpcbind;
onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []);
bitcoindRpcAddress = bitcoind.rpc.address;
networkDir = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
configFile = pkgs.writeText "lnd.conf" ''
datadir=${cfg.dataDir}
@ -17,9 +16,9 @@ let
tlscertpath=${secretsDir}/lnd-cert
tlskeypath=${secretsDir}/lnd-key
listen=${toString cfg.listen}:${toString cfg.listenPort}
rpclisten=${cfg.rpclisten}:${toString cfg.rpcPort}
restlisten=${cfg.restlisten}:${toString cfg.restPort}
listen=${toString cfg.address}:${toString cfg.port}
rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort}
restlisten=${cfg.restAddress}:${toString cfg.restPort}
bitcoin.${bitcoind.network}=1
bitcoin.active=1
@ -55,50 +54,43 @@ in {
default = networkDir;
description = "The network data directory.";
};
listen = mkOption {
type = config.nix-bitcoin.pkgs.lib.ipv4Address;
address = mkOption {
type = types.str;
default = "localhost";
description = "Bind to given address to listen to peer connections";
description = "Address to listen for peer connections";
};
listenPort = mkOption {
port = mkOption {
type = types.port;
default = 9735;
description = "Bind to given port to listen to peer connections";
description = "Port to listen for peer connections";
};
rpclisten = mkOption {
rpcAddress = mkOption {
type = types.str;
default = "localhost";
description = ''
Bind to given address to listen to RPC connections.
'';
};
restlisten = mkOption {
type = types.str;
default = "localhost";
description = ''
Bind to given address to listen to REST connections.
'';
description = "Address to listen for RPC connections.";
};
rpcPort = mkOption {
type = types.port;
default = 10009;
description = "Port on which to listen for gRPC connections.";
description = "Port to listen for gRPC connections.";
};
restAddress = mkOption {
type = types.str;
default = "localhost";
description = ''
Address to listen for REST connections.
'';
};
restPort = mkOption {
type = types.port;
default = 8080;
description = "Port on which to listen for REST connections.";
description = "Port to listen for REST connections.";
};
tor-socks = mkOption {
type = types.nullOr types.str;
default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
description = "Set a socks proxy to use to connect to Tor nodes";
};
announce-tor = mkOption {
type = types.bool;
default = false;
description = "Announce LND Tor Hidden Service";
};
macaroons = mkOption {
default = {};
type = with types; attrsOf (submodule {
@ -138,13 +130,21 @@ in {
# Switch user because lnd makes datadir contents readable by user only
''
sudo -u lnd ${cfg.package}/bin/lncli \
--rpcserver ${cfg.rpclisten}:${toString cfg.rpcPort} \
--rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \
--tlscertpath '${secretsDir}/lnd-cert' \
--macaroonpath '${networkDir}/admin.macaroon' "$@"
'';
description = "Binary to connect with the lnd instance.";
};
enforceTor = nix-bitcoin-services.enforceTor;
getPublicAddressCmd = mkOption {
type = types.str;
default = "";
description = ''
Bash expression which outputs the public service address to announce to peers.
If left empty, no address is announced.
'';
};
inherit (nix-bitcoin-services) enforceTor;
};
config = mkIf cfg.enable {
@ -154,7 +154,12 @@ in {
}
];
services.bitcoind.enable = true;
services.bitcoind = {
enable = true;
# Increase rpc thread count due to reports that lightning implementations fail
# under high bitcoind rpc load
rpc.threads = 16;
};
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
@ -167,16 +172,19 @@ in {
zmqpubrawtx = "tcp://${bitcoindRpcAddress}:28333";
};
services.onion-chef.access.lnd = if cfg.announce-tor then [ "lnd" ] else [];
systemd.services.lnd = {
description = "Run LND";
wantedBy = [ "multi-user.target" ];
requires = [ "bitcoind.service" ] ++ onion-chef-service;
after = [ "bitcoind.service" ] ++ onion-chef-service;
requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" ];
preStart = ''
install -m600 ${configFile} '${cfg.dataDir}/lnd.conf'
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/lnd.conf'
${optionalString cfg.announce-tor "echo externalip=$(cat /var/lib/onion-chef/lnd/lnd) >> '${cfg.dataDir}/lnd.conf'"}
{
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
${optionalString (cfg.getPublicAddressCmd != "") ''
echo "externalip=$(${cfg.getPublicAddressCmd})"
''}
} >> '${cfg.dataDir}/lnd.conf'
'';
serviceConfig = nix-bitcoin-services.defaultHardening // {
RuntimeDirectory = "lnd"; # Only used to store custom macaroons
@ -187,12 +195,12 @@ in {
RestartSec = "10s";
ReadWritePaths = "${cfg.dataDir}";
ExecStartPost = let
restUrl = "https://${cfg.restlisten}:${toString cfg.restPort}/v1";
restUrl = "https://${cfg.restAddress}:${toString cfg.restPort}/v1";
in [
# Run fully privileged for secrets dir write access
"+${nix-bitcoin-services.script ''
attempts=250
while ! { exec 3>/dev/tcp/${cfg.restlisten}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do
while ! { exec 3>/dev/tcp/${cfg.restAddress}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do
((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; }
sleep 0.1
done
@ -234,7 +242,7 @@ in {
fi
# Wait until the RPC port is open
while ! { exec 3>/dev/tcp/${cfg.rpclisten}/${toString cfg.rpcPort}; } &>/dev/null; do
while ! { exec 3>/dev/tcp/${cfg.rpcAddress}/${toString cfg.rpcPort}; } &>/dev/null; do
sleep 0.1
done

View File

@ -24,9 +24,11 @@ with lib;
# Support features
./versioning.nix
./security.nix
./onion-addresses.nix
./onion-services.nix
./netns-isolation.nix
./nodeinfo.nix
./backups.nix
./onion-chef.nix
];
disabledModules = [ "services/networking/bitcoind.nix" ];
@ -58,11 +60,11 @@ with lib;
config = {
assertions = [
{ assertion = (config.services.lnd.enable -> ( !config.services.clightning.enable || config.services.clightning.bindport != config.services.lnd.listenPort));
{ assertion = (config.services.lnd.enable -> ( !config.services.clightning.enable || config.services.clightning.port != config.services.lnd.port));
message = ''
LND and clightning can't both bind to lightning port 9735. Either
disable LND/clightning or change services.clightning.bindPort or
services.lnd.listenPort to a port other than 9735.
services.lnd.port to a port other than 9735.
'';
}
];

View File

@ -245,26 +245,26 @@ in {
};
services.bitcoind = {
bind = netns.bitcoind.address;
rpcbind = netns.bitcoind.address;
rpcallowip = [
address = netns.bitcoind.address;
rpc.address = netns.bitcoind.address;
rpc.allowip = [
bridgeIp # For operator user
netns.bitcoind.address
] ++ map (n: netns.${n}.address) netns.bitcoind.availableNetns;
};
systemd.services.bitcoind-import-banlist.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-bitcoind";
services.clightning.bind-addr = netns.clightning.address;
services.clightning.address = netns.clightning.address;
services.lnd = {
listen = netns.lnd.address;
rpclisten = netns.lnd.address;
restlisten = netns.lnd.address;
address = netns.lnd.address;
rpcAddress = netns.lnd.address;
restAddress = netns.lnd.address;
};
services.liquidd = {
bind = netns.liquidd.address;
rpcbind = netns.liquidd.address;
address = netns.liquidd.address;
rpc.address = netns.liquidd.address;
rpcallowip = [
bridgeIp # For operator user
netns.liquidd.address
@ -274,14 +274,14 @@ in {
services.electrs.address = netns.electrs.address;
services.spark-wallet = {
host = netns.spark-wallet.address;
address = netns.spark-wallet.address;
extraArgs = "--no-tls";
};
services.lightning-loop.rpcAddress = netns.lightning-loop.address;
services.nbxplorer.bind = netns.nbxplorer.address;
services.btcpayserver.bind = netns.btcpayserver.address;
services.nbxplorer.address = netns.nbxplorer.address;
services.btcpayserver.address = netns.btcpayserver.address;
services.joinmarket.cliExec = mkCliExec "joinmarket";
systemd.services.joinmarket-yieldgenerator.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-joinmarket";

View File

@ -1,105 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.nix-bitcoin-webindex;
inherit (config) nix-bitcoin-services;
indexFile = pkgs.writeText "index.html" ''
<html>
<body>
<p>
<h1>
nix-bitcoin
</h1>
</p>
<p>
<h3>
lightning node: CLIGHTNING_ID
</h3>
</p>
</body>
</html>
'';
createWebIndex = pkgs.writeText "make-index.sh" ''
set -e
cp ${indexFile} /var/www/index.html
chown -R nginx:nginx /var/www/
nodeinfo
. <(nodeinfo)
sed -i "s/CLIGHTNING_ID/$CLIGHTNING_ID/g" /var/www/index.html
'';
in {
options.services.nix-bitcoin-webindex = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, the webindex service will be installed.
'';
};
host = mkOption {
type = types.str;
default = if config.nix-bitcoin.netns-isolation.enable then
config.nix-bitcoin.netns-isolation.netns.nginx.address
else
"localhost";
description = "HTTP server listen address.";
};
enforceTor = nix-bitcoin-services.enforceTor;
};
config = mkIf cfg.enable {
assertions = [
{ assertion = config.services.clightning.enable;
message = "nix-bitcoin-webindex requires clightning.";
}
];
systemd.tmpfiles.rules = [
"d /var/www 0755 nginx nginx - -"
];
services.nginx = {
enable = true;
virtualHosts."_" = {
root = "/var/www";
};
};
services.tor.hiddenServices.nginx = {
map = [{
port = 80; toHost = cfg.host;
} {
port = 443; toHost = cfg.host;
}];
version = 3;
};
# create-web-index
systemd.services.create-web-index = {
description = "Get node info";
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
config.programs.nodeinfo
jq
sudo
] ++ optional config.services.lnd.enable config.services.lnd.cli
++ optional config.services.clightning.enable config.services.clightning.cli;
serviceConfig = nix-bitcoin-services.defaultHardening // {
ExecStart="${pkgs.bash}/bin/bash ${createWebIndex}";
User = "root";
Type = "simple";
RemainAfterExit="yes";
Restart = "on-failure";
RestartSec = "10s";
PrivateNetwork = "true"; # This service needs no network access
PrivateUsers = "false";
ReadWritePaths = "/var/www";
CapabilityBoundingSet = "CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_SYS_ADMIN CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER";
} // (if cfg.enforceTor
then nix-bitcoin-services.allowTor
else nix-bitcoin-services.allowAnyIP
);
};
};
}

View File

@ -1,74 +1,117 @@
{ config, lib, pkgs, ... }:
with lib;
let
operatorName = config.nix-bitcoin.operator.name;
script = pkgs.writeScriptBin "nodeinfo" ''
set -eo pipefail
cfg = config.nix-bitcoin.nodeinfo;
BITCOIND_ONION="$(cat /var/lib/onion-chef/${operatorName}/bitcoind)"
echo BITCOIND_ONION="$BITCOIND_ONION"
if systemctl is-active --quiet clightning; then
CLIGHTNING_NODEID=$(lightning-cli getinfo | jq -r '.id')
CLIGHTNING_ONION="$(cat /var/lib/onion-chef/${operatorName}/clightning)"
CLIGHTNING_ID="$CLIGHTNING_NODEID@$CLIGHTNING_ONION:9735"
echo CLIGHTNING_NODEID="$CLIGHTNING_NODEID"
echo CLIGHTNING_ONION="$CLIGHTNING_ONION"
echo CLIGHTNING_ID="$CLIGHTNING_ID"
fi
if systemctl is-active --quiet lnd; then
LND_NODEID=$(lncli getinfo | jq -r '.uris[0]')
echo LND_NODEID="$LND_NODEID"
fi
NGINX_ONION_FILE=/var/lib/onion-chef/${operatorName}/nginx
if [ -e "$NGINX_ONION_FILE" ]; then
NGINX_ONION="$(cat $NGINX_ONION_FILE)"
echo NGINX_ONION="$NGINX_ONION"
fi
LIQUIDD_ONION_FILE=/var/lib/onion-chef/${operatorName}/liquidd
if [ -e "$LIQUIDD_ONION_FILE" ]; then
LIQUIDD_ONION="$(cat $LIQUIDD_ONION_FILE)"
echo LIQUIDD_ONION="$LIQUIDD_ONION"
fi
SPARKWALLET_ONION_FILE=/var/lib/onion-chef/${operatorName}/spark-wallet
if [ -e "$SPARKWALLET_ONION_FILE" ]; then
SPARKWALLET_ONION="$(cat $SPARKWALLET_ONION_FILE)"
echo SPARKWALLET_ONION="http://$SPARKWALLET_ONION"
fi
ELECTRS_ONION_FILE=/var/lib/onion-chef/${operatorName}/electrs
if [ -e "$ELECTRS_ONION_FILE" ]; then
ELECTRS_ONION="$(cat $ELECTRS_ONION_FILE)"
echo ELECTRS_ONION="$ELECTRS_ONION"
fi
BTCPAYSERVER_ONION_FILE=/var/lib/onion-chef/${operatorName}/btcpayserver
if [ -e "$BTCPAYSERVER_ONION_FILE" ]; then
BTCPAYSERVER_ONION="$(cat $BTCPAYSERVER_ONION_FILE)"
echo BTCPAYSERVER_ONION="$BTCPAYSERVER_ONION"
fi
SSHD_ONION_FILE=/var/lib/onion-chef/${operatorName}/sshd
if [ -e "$SSHD_ONION_FILE" ]; then
SSHD_ONION="$(cat $SSHD_ONION_FILE)"
echo SSHD_ONION="$SSHD_ONION"
fi
# 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" ''
#!${pkgs.python3}/bin/python
import json
import subprocess
from collections import OrderedDict
def success(*args):
return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
def is_active(unit):
return success("systemctl", "is-active", "--quiet", unit)
def is_enabled(unit):
return success("systemctl", "is-enabled", "--quiet", unit)
def cmd(*args):
return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8')
def shell(*args):
return cmd("bash", "-c", *args).strip()
infos = OrderedDict()
operator = "${config.nix-bitcoin.operator.name}"
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}"
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 {
nix-bitcoin.nodeinfo = {
enable = mkEnableOption "nodeinfo";
program = mkOption {
readOnly = true;
default = script;
};
};
};
config = {
environment.systemPackages = [ script ];
environment.systemPackages = optional cfg.enable script;
};
}

View File

@ -0,0 +1,28 @@
{ lib, ... }:
with lib;
let
mkRenamedAnnounceTorOption = service:
# use mkRemovedOptionModule because mkRenamedOptionModule fails with an infinite recursion error
mkRemovedOptionModule [ "services" service "announce-tor" ] ''
Use option `nix-bitcoin.onionServices.${service}.public` instead.
'';
in {
imports = [
(mkRenamedOptionModule [ "services" "bitcoind" "bind" ] [ "services" "bitcoind" "address" ])
(mkRenamedOptionModule [ "services" "bitcoind" "rpcallowip" ] [ "services" "bitcoind" "rpc" "allowip" ])
(mkRenamedOptionModule [ "services" "bitcoind" "rpcthreads" ] [ "services" "bitcoind" "rpc" "threads" ])
(mkRenamedOptionModule [ "services" "clightning" "bind-addr" ] [ "services" "clightning" "address" ])
(mkRenamedOptionModule [ "services" "clightning" "bindport" ] [ "services" "clightning" "port" ])
(mkRenamedOptionModule [ "services" "spark-wallet" "host" ] [ "services" "spark-wallet" "address" ])
(mkRenamedOptionModule [ "services" "lnd" "rpclisten" ] [ "services" "lnd" "rpcAddress" ])
(mkRenamedOptionModule [ "services" "lnd" "listen" ] [ "services" "lnd" "address" ])
(mkRenamedOptionModule [ "services" "lnd" "listenPort" ] [ "services" "lnd" "port" ])
(mkRenamedOptionModule [ "services" "btcpayserver" "bind" ] [ "services" "btcpayserver" "address" ])
(mkRenamedOptionModule [ "services" "liquidd" "bind" ] [ "services" "liquidd" "address" ])
(mkRenamedOptionModule [ "services" "liquidd" "rpcbind" ] [ "services" "liquidd" "rpc" "address" ])
(mkRenamedAnnounceTorOption "clightning")
(mkRenamedAnnounceTorOption "lnd")
];
}

View File

@ -0,0 +1,76 @@
# This module enables unprivileged users to read onion addresses.
# By default, onion addresses in /var/lib/tor/onion are only readable by the
# tor user.
# The included service copies onion addresses to /var/lib/onion-addresses/<user>/
# and sets permissions according to option 'access'.
{ config, lib, ... }:
with lib;
let
cfg = config.nix-bitcoin.onionAddresses;
inherit (config) nix-bitcoin-services;
in {
options.nix-bitcoin.onionAddresses = {
access = mkOption {
type = with types; attrsOf (listOf str);
default = {};
description = ''
This option controls who is allowed to access onion addresses.
For example, the following allows user 'myuser' to access bitcoind
and clightning onion addresses:
{
"myuser" = [ "bitcoind" "clightning" ];
};
The onion hostnames can then be read from
/var/lib/onion-addresses/myuser.
'';
};
dataDir = mkOption {
readOnly = true;
default = "/var/lib/onion-addresses";
};
};
config = mkIf (cfg.access != {}) {
systemd.services.onion-addresses = {
wantedBy = [ "tor.service" ];
bindsTo = [ "tor.service" ];
after = [ "tor.service" ];
serviceConfig = nix-bitcoin-services.defaultHardening // {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "onion-addresses";
PrivateNetwork = "true"; # This service needs no network access
PrivateUsers = "false";
CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER";
};
script = ''
# Wait until tor is up
until [[ -e /var/lib/tor/state ]]; do sleep 0.1; done
cd ${cfg.dataDir}
rm -rf *
${concatMapStrings
(user: ''
mkdir -p -m 0700 ${user}
chown ${user} ${user}
${concatMapStrings
(service: ''
onionFile=/var/lib/tor/onion/${service}/hostname
if [[ -e $onionFile ]]; then
cp $onionFile ${user}/${service}
chown ${user} ${user}/${service}
fi
'')
cfg.access.${user}
}
'')
(builtins.attrNames cfg.access)
}
'';
};
};
}

View File

@ -1,90 +0,0 @@
# The onion chef module allows unprivileged users to read onion hostnames.
# By default the onion hostnames in /var/lib/tor/onion are only readable by the
# tor user. The onion chef copies the onion hostnames into into
# /var/lib/onion-chef and sets permissions according to the access option.
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.onion-chef;
inherit (config) nix-bitcoin-services;
dataDir = "/var/lib/onion-chef/";
onion-chef-script = pkgs.writeScript "onion-chef.sh" ''
# wait until tor is up
until ls -l /var/lib/tor/state; do sleep 1; done
cd ${dataDir}
# Create directory for every user and set permissions
${ builtins.foldl'
(x: user: x +
''
mkdir -p -m 0700 ${user}
chown ${user} ${user}
# Copy onion hostnames into the user's directory
${ builtins.foldl'
(x: onion: x +
''
ONION_FILE=/var/lib/tor/onion/${onion}/hostname
if [ -e "$ONION_FILE" ]; then
cp $ONION_FILE ${user}/${onion}
chown ${user} ${user}/${onion}
fi
'')
""
(builtins.getAttr user cfg.access)
}
'')
""
(builtins.attrNames cfg.access)
}
'';
in {
options.services.onion-chef = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, the onion-chef service will be installed.
'';
};
access = mkOption {
type = types.attrs;
default = {};
description = ''
This option controls who is allowed to access onion hostnames. For
example the following allows the user operator to access the bitcoind
and clightning onion.
{
"operator" = [ "bitcoind" "clightning" ];
};
The onion hostnames can then be read from
/var/lib/onion-chef/<user>.
'';
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${dataDir}' 0755 root root - -"
];
systemd.services.onion-chef = {
description = "Run onion-chef";
wantedBy = [ "tor.service" ];
bindsTo = [ "tor.service" ];
after = [ "tor.service" ];
serviceConfig = nix-bitcoin-services.defaultHardening // {
ExecStart = "${pkgs.bash}/bin/bash ${onion-chef-script}";
Type = "oneshot";
RemainAfterExit = true;
PrivateNetwork = "true"; # This service needs no network access
PrivateUsers = "false";
ReadWritePaths = "${dataDir}";
CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER";
};
};
};
}

121
modules/onion-services.nix Normal file
View File

@ -0,0 +1,121 @@
# This module creates onion-services for NixOS services.
# An onion service can be enabled for every service that defines
# options 'address', 'port' and optionally 'getPublicAddressCmd'.
#
# See it in use at ./presets/enable-tor.nix
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.nix-bitcoin.onionServices;
services = builtins.attrNames cfg;
activeServices = builtins.filter (service:
config.services.${service}.enable && cfg.${service}.enable
) services;
publicServices = builtins.filter (service: cfg.${service}.public) activeServices;
in {
options.nix-bitcoin.onionServices = mkOption {
default = {};
type = with types; attrsOf (submodule (
{ config, ... }: {
options = {
enable = mkOption {
type = types.bool;
default = config.public;
description = ''
Create an onion service for the given service.
The service must define options 'address' and 'port'.
'';
};
public = mkOption {
type = types.bool;
default = false;
description = ''
Make the onion address accessible to the service.
If enabled, the onion service is automatically enabled.
Only available for services that define option `getPublicAddressCmd`.
'';
};
externalPort = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the external port of the onion service.";
};
};
}
));
};
config = mkMerge [
(mkIf (cfg != {}) {
# Define hidden services
services.tor = {
enable = true;
hiddenServices = genAttrs activeServices (name:
let
service = config.services.${name};
inherit (cfg.${name}) externalPort;
in {
map = [{
port = if externalPort != null then externalPort else service.port;
toPort = service.port;
toHost = if service.address == "0.0.0.0" then "127.0.0.1" else service.address;
}];
version = 3;
}
);
};
# Enable public services to access their own onion addresses
nix-bitcoin.onionAddresses.access = (
genAttrs publicServices singleton
) // {
# Allow the operator user to access onion addresses for all active services
${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices;
};
systemd.services = let
onionAddresses = [ "onion-addresses.service" ];
in genAttrs publicServices (service: {
requires = onionAddresses;
after = onionAddresses;
});
})
# Set getPublicAddressCmd for public services
{
services = let
# publicServices' doesn't depend on config.services.*.enable,
# so we can use it to define config.services without causing infinite recursion
publicServices' = builtins.filter (service:
let srv = cfg.${service};
in srv.public && srv.enable
) services;
in genAttrs publicServices' (service: {
getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/${service}/${service}";
});
}
# Set sensible defaults for some services
{
nix-bitcoin.onionServices = {
spark-wallet = {
externalPort = 80;
# Enable 'public' by default, but don't auto-enable the onion service.
# When the onion service is enabled, 'public' lets spark-wallet generate
# a QR code for accessing the web interface.
public = true;
# Low priority so we can override this with mkDefault in ./presets/enable-tor.nix
enable = mkOverride 1400 false;
};
btcpayserver = {
externalPort = 80;
};
};
}
];
}

View File

@ -0,0 +1,32 @@
{ lib, ... }:
let
defaultTrue = lib.mkDefault true;
in {
services.tor = {
enable = true;
client.enable = true;
};
# Use Tor for all outgoing connections
services = {
bitcoind.enforceTor = true;
clightning.enforceTor = true;
lnd.enforceTor = true;
lightning-loop.enforceTor = true;
liquidd.enforceTor = true;
electrs.enforceTor = true;
# disable Tor enforcement until btcpayserver can fetch rates over Tor
# btcpayserver.enforceTor = true;
nbxplorer.enforceTor = true;
spark-wallet.enforceTor = true;
recurring-donations.enforceTor = true;
};
# Add onion services for incoming connections
nix-bitcoin.onionServices = {
bitcoind.enable = defaultTrue;
liquidd.enable = defaultTrue;
electrs.enable = defaultTrue;
spark-wallet.enable = defaultTrue;
};
}

View File

@ -14,23 +14,9 @@ let
in {
imports = [
../modules.nix
../nodeinfo.nix
../nix-bitcoin-webindex.nix
./enable-tor.nix
];
options = {
services.clightning.onionport = mkOption {
type = types.port;
default = 9735;
description = "Port on which to listen for tor client connections.";
};
services.lnd.onionport = mkOption {
type = types.ints.u16;
default = 9735;
description = "Port on which to listen for tor client connections.";
};
};
config = {
# For backwards compatibility only
nix-bitcoin.secretsDir = mkDefault "/secrets";
@ -39,99 +25,36 @@ in {
nix-bitcoin.security.hideProcessInformation = true;
# Tor
services.tor = {
enable = true;
client.enable = true;
environment.systemPackages = with pkgs; [
jq
];
hiddenServices.sshd = mkHiddenService { port = 22; };
};
# sshd
services.tor.hiddenServices.sshd = mkHiddenService { port = 22; };
nix-bitcoin.onionAddresses.access.${operatorName} = [ "sshd" ];
# bitcoind
services.bitcoind = {
enable = true;
listen = true;
dataDirReadableByGroup = mkIf cfg.electrs.high-memory true;
enforceTor = true;
port = 8333;
assumevalid = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6";
addnodes = [ "ecoc5q34tmbq54wl.onion" ];
discover = false;
addresstype = "bech32";
dbCache = 1000;
# higher rpcthread count due to reports that lightning implementations fail
# under high bitcoind rpc load
rpcthreads = 16;
};
services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; };
# clightning
services.clightning.enforceTor = true;
services.tor.hiddenServices.clightning = mkIf cfg.clightning.enable (mkHiddenService {
port = cfg.clightning.onionport;
toHost = cfg.clightning.bind-addr;
toPort = cfg.clightning.bindport;
});
# lnd
services.lnd.enforceTor = true;
services.tor.hiddenServices.lnd = mkIf cfg.lnd.enable (mkHiddenService { port = cfg.lnd.onionport; toHost = cfg.lnd.listen; toPort = cfg.lnd.listenPort; });
# lightning-loop
services.lightning-loop.enforceTor = true;
# liquidd
services.liquidd = {
rpcuser = "liquidrpc";
prune = 1000;
validatepegin = true;
listen = true;
enforceTor = true;
port = 7042;
};
services.tor.hiddenServices.liquidd = mkIf cfg.liquidd.enable (mkHiddenService { port = cfg.liquidd.port; toHost = cfg.liquidd.bind; });
# electrs
services.electrs = {
port = 50001;
enforceTor = true;
};
services.tor.hiddenServices.electrs = mkIf cfg.electrs.enable (mkHiddenService {
port = cfg.electrs.port; toHost = cfg.electrs.address;
});
# btcpayserver
# disable tor enforcement until btcpayserver can fetch rates over Tor
services.btcpayserver.enforceTor = false;
services.nbxplorer.enforceTor = true;
services.tor.hiddenServices.btcpayserver = mkIf cfg.btcpayserver.enable (mkHiddenService { port = 80; toPort = 23000; toHost = cfg.btcpayserver.bind; });
services.spark-wallet = {
onion-service = true;
enforceTor = true;
};
services.recurring-donations.enforceTor = true;
nix-bitcoin.nodeinfo.enable = true;
services.nix-bitcoin-webindex.enforceTor = true;
# Backups
services.backups = {
program = "duplicity";
frequency = "daily";
};
environment.systemPackages = with pkgs; [
tor
jq
qrencode
];
services.onion-chef = {
enable = true;
access.${operatorName} = [ "bitcoind" "clightning" "nginx" "liquidd" "spark-wallet" "electrs" "btcpayserver" "sshd" ];
};
services.backups.frequency = "daily";
# operator
nix-bitcoin.operator.enable = true;
users.users.${operatorName} = {
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;

View File

@ -5,18 +5,17 @@ with lib;
let
cfg = config.services.spark-wallet;
inherit (config) nix-bitcoin-services;
onion-chef-service = (if cfg.onion-service then [ "onion-chef.service" ] else []);
# Use wasabi rate provider because the default (bitstamp) doesn't accept
# connections through Tor
torRateProvider = "--rate-provider wasabi --proxy socks5h://${config.services.tor.client.socksListenAddress}";
startScript = ''
${optionalString cfg.onion-service ''
publicURL="--public-url http://$(cat /var/lib/onion-chef/spark-wallet/spark-wallet)"
${optionalString (cfg.getPublicAddressCmd != "") ''
publicURL="--public-url http://$(${cfg.getPublicAddressCmd})"
''}
exec ${config.nix-bitcoin.pkgs.spark-wallet}/bin/spark-wallet \
--ln-path '${config.services.clightning.networkDir}' \
--host ${cfg.host} \
--host ${cfg.address} --port ${toString cfg.port} \
--config '${config.nix-bitcoin.secretsDir}/spark-wallet-login' \
${optionalString cfg.enforceTor torRateProvider} \
$publicURL \
@ -31,24 +30,31 @@ in {
If enabled, the spark-wallet service will be installed.
'';
};
host = mkOption {
address = mkOption {
type = types.str;
default = "localhost";
description = "http(s) server listen address.";
description = "http(s) server address.";
};
onion-service = mkOption {
type = types.bool;
default = false;
description = ''
"If enabled, configures spark-wallet to be reachable through an onion service.";
'';
port = mkOption {
type = types.port;
default = 9737;
description = "http(s) server port.";
};
extraArgs = mkOption {
type = types.separatedString " ";
default = "";
description = "Extra command line arguments passed to spark-wallet.";
};
enforceTor = nix-bitcoin-services.enforceTor;
getPublicAddressCmd = mkOption {
type = types.str;
default = "";
description = ''
Bash expression which outputs the public service address.
If set, spark-wallet prints a QR code to the systemd journal which
encodes an URL for accessing the web interface.
'';
};
inherit (nix-bitcoin-services) enforceTor;
};
config = mkIf cfg.enable {
@ -61,25 +67,16 @@ in {
};
users.groups.spark-wallet = {};
services.tor.hiddenServices.spark-wallet = mkIf cfg.onion-service {
map = [{
port = 80; toPort = 9737; toHost = cfg.host;
}];
version = 3;
};
services.onion-chef.enable = cfg.onion-service;
services.onion-chef.access.spark-wallet = if cfg.onion-service then [ "spark-wallet" ] else [];
systemd.services.spark-wallet = {
description = "Run spark-wallet";
wantedBy = [ "multi-user.target" ];
requires = [ "clightning.service" ] ++ onion-chef-service;
after = [ "clightning.service" ] ++ onion-chef-service;
requires = [ "clightning.service" ];
after = [ "clightning.service" ];
script = startScript;
serviceConfig = nix-bitcoin-services.defaultHardening // {
User = "spark-wallet";
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = mkIf cfg.onion-service "/var/lib/onion-chef";
} // (if cfg.enforceTor
then nix-bitcoin-services.allowTor
else nix-bitcoin-services.allowAnyIP)

View File

@ -5,7 +5,19 @@ let
version = config.nix-bitcoin.configVersion;
# Sorted by increasing version numbers
changes = [
changes = let
mkOnionServiceChange = service: {
version = "0.0.30";
condition = config.services.${service}.enable;
message = ''
The onion service for ${service} has been disabled in the default
configuration (`secure-node.nix`).
To enable the onion service, add the following to your configuration:
nix-bitcon.onionServices.${service}.enable = true;
'';
};
in [
{
version = "0.0.26";
condition = config.services.joinmarket.enable;
@ -54,6 +66,9 @@ let
https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.8.0/docs/NATIVE-SEGWIT-UPGRADE.md
'';
}
(mkOnionServiceChange "clightning")
(mkOnionServiceChange "lnd")
(mkOnionServiceChange "btcpayserver")
];
incompatibleChanges = optionals
@ -76,6 +91,10 @@ let
lastChange = builtins.elemAt changes (builtins.length changes - 1);
in
{
imports = [
./obsolete-options.nix
];
options = {
nix-bitcoin.configVersion = mkOption {
type = with types; nullOr str;
@ -93,6 +112,6 @@ in
config = {
# Force evaluation. An actual option value is never assigned
system.extraDependencies = optional (builtins.length incompatibleChanges > 0) (builtins.throw errorMsg);
system = optionalAttrs (builtins.length incompatibleChanges > 0) (builtins.throw errorMsg);
};
}

View File

@ -20,7 +20,5 @@ let self = {
pinned = import ./pinned.nix;
lib = import ./lib.nix { inherit (pkgs) lib; };
modulesPkgs = self // self.pinned;
}; in self

View File

@ -1,5 +0,0 @@
{ lib }:
{
# An address type that checks that there's no port
ipv4Address = lib.types.addCheck lib.types.str (s: builtins.length (builtins.split ":" s) == 1);
}

View File

@ -44,7 +44,7 @@ let testEnv = rec {
tests.spark-wallet = cfg.spark-wallet.enable;
tests.lnd = cfg.lnd.enable;
services.lnd.listenPort = 9736;
services.lnd.port = 9736;
tests.lightning-loop = cfg.lightning-loop.enable;
@ -68,6 +68,8 @@ let testEnv = rec {
'';
};
tests.nodeinfo = config.nix-bitcoin.nodeinfo.enable;
tests.backups = cfg.backups.enable;
# To test that unused secrets are made inaccessible by 'setup-secrets'
@ -119,6 +121,8 @@ let testEnv = rec {
services.joinmarket.enable = true;
services.backups.enable = true;
nix-bitcoin.nodeinfo.enable = true;
services.hardware-wallets = {
trezor = true;
ledger = true;
@ -130,7 +134,6 @@ let testEnv = rec {
scenarios.full
../modules/presets/secure-node.nix
];
services.nix-bitcoin-webindex.enable = true;
tests.secure-node = true;
tests.banlist-and-restart = true;

View File

@ -216,17 +216,19 @@ def _():
)
@test("nodeinfo")
def _():
status, _ = machine.execute("systemctl is-enabled --quiet onion-addresses 2> /dev/null")
if status == 0:
machine.wait_for_unit("onion-addresses")
json_info = succeed("sudo -u operator nodeinfo")
info = json.loads(json_info)
assert info["bitcoind"]["local_address"]
@test("secure-node")
def _():
assert_running("onion-chef")
# FIXME: use 'wait_for_unit' because 'create-web-index' always fails during startup due
# to incomplete unit dependencies.
# 'create-web-index' implicitly tests 'nodeinfo'.
machine.wait_for_unit("create-web-index")
assert_running("nginx")
wait_for_open_port(ip("nginx"), 80)
assert_matches(f"curl {ip('nginx')}", "nix-bitcoin")
assert_running("onion-addresses")
# Run this test before the following tests that shut down services