Merge #255: Improve netns-isolation and Tor config

b4b607dfa5 netns: simplify firewall setup (Erik Arvstedt)
25639cec42 netns: fix error msg when starting netns (Erik Arvstedt)
67068afd6b netns: fix error when stopping netns (Erik Arvstedt)
4ff88efc50 netns: add address binding test (Erik Arvstedt)
8da01fe8a6 lightning-loop: allow RPC access from main netns (Erik Arvstedt)
d76b080b74 lightning-loop: add RPC and REST server options (Erik Arvstedt)
9ddf7864a4 lightning-loop regtest: fix incorrectly succeeding test (Erik Arvstedt)
e66636ef0e liquidd: use type str for rpcbind (Erik Arvstedt)
de23fdd377 lnd: use type str for rpclisten, restlisten (Erik Arvstedt)
8b053326cc bitcoind: use type str for rpcbind (Erik Arvstedt)
6903e8afcc netns-liquidd: allow RPC access from main netns (Erik Arvstedt)
82f4901880 netns-lnd: allow RPC access from main netns (Erik Arvstedt)
58d24e735d netns-bitcoind: allow RPC access from main netns (Erik Arvstedt)
0e2ff948d3 test: add scenario 'netnsRegtest' (Erik Arvstedt)
e0675cb256 move enforceTor logic to service modules (Erik Arvstedt)
0cc8caa737 lnd: only set tor.active on enforceTor (Erik Arvstedt)
9a931483b9 netns test: remove strict dependency on clightning, electrs (Erik Arvstedt)
bae1b7f413 netns test: improve ping test (Erik Arvstedt)
5e0e16529c netns: fix default addressblock value type (Erik Arvstedt)

Pull request description:

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

Tree-SHA512: b290831d9a3fa4de56b0f19cf84a1998e830aa844532d7cba8cd8227c785a23bfa1514123a974652e8e61060e1297b6bfbcff9640580206a04c5292309b1daef
This commit is contained in:
Jonas Nick 2020-11-02 16:11:25 +00:00
commit dbad828851
No known key found for this signature in database
GPG Key ID: 4861DBF262123605
15 changed files with 143 additions and 165 deletions

View File

@ -16,7 +16,7 @@ env:
jobs: jobs:
- TestModules=1 STABLE=1 SCENARIO=default - TestModules=1 STABLE=1 SCENARIO=default
- TestModules=1 STABLE=1 SCENARIO=netns - TestModules=1 STABLE=1 SCENARIO=netns
- EvalModules=1 STABLE=1 - TestModules=1 STABLE=1 SCENARIO=netnsRegtest
- PKG=hwi STABLE=1 - PKG=hwi STABLE=1
- PKG=hwi STABLE=0 - PKG=hwi STABLE=0
- PKG=lightning-charge STABLE=1 - PKG=lightning-charge STABLE=1
@ -35,12 +35,6 @@ env:
- PKG=joinmarket STABLE=0 - PKG=joinmarket STABLE=0
script: script:
- printf '%s (%s)\n' "$NIX_PATH" "$VER" - printf '%s (%s)\n' "$NIX_PATH" "$VER"
#
- |
if [[ $EvalModules ]]; then
test/run-tests.sh --scenario full eval
travis_terminate 0
fi
- | - |
getBuildExpr() { getBuildExpr() {
if [[ $TestModules ]]; then if [[ $TestModules ]]; then

View File

@ -39,7 +39,8 @@ let
"rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"} "rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"}
'') (builtins.attrValues cfg.rpc.users) '') (builtins.attrValues cfg.rpc.users)
} }
${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} rpcbind=${cfg.rpcbind}
rpcconnect=${cfg.rpcbind}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip}
# Wallet options # Wallet options
@ -149,8 +150,8 @@ in {
description = "Set the number of threads to service RPC calls"; description = "Set the number of threads to service RPC calls";
}; };
rpcbind = mkOption { rpcbind = mkOption {
type = types.listOf types.str; type = types.str;
default = [ "127.0.0.1" ]; default = "127.0.0.1";
description = '' description = ''
Bind to given address to listen for JSON-RPC connections. Bind to given address to listen for JSON-RPC connections.
''; '';
@ -182,7 +183,7 @@ in {
}; };
proxy = mkOption { proxy = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
description = "Connect through SOCKS5 proxy"; description = "Connect through SOCKS5 proxy";
}; };
listen = mkOption { listen = mkOption {
@ -275,17 +276,12 @@ in {
description = "What type of addresses to use"; description = "What type of addresses to use";
}; };
cli = mkOption { cli = mkOption {
type = types.package;
# Overriden on netns-isolation
default = cfg.cliBase;
description = "Binary to connect with the bitcoind instance.";
};
cliBase = mkOption {
readOnly = true; readOnly = true;
type = types.package; type = types.package;
default = pkgs.writeScriptBin "bitcoin-cli" '' default = pkgs.writeScriptBin "bitcoin-cli" ''
exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@" exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@"
''; '';
description = "Binary to connect with the bitcoind instance.";
}; };
enforceTor = nix-bitcoin-services.enforceTor; enforceTor = nix-bitcoin-services.enforceTor;
}; };
@ -341,9 +337,8 @@ in {
fi fi
''; '';
postStart = '' postStart = ''
cd ${cfg.cliBase}/bin
# Poll until bitcoind accepts commands. This can take a long time. # Poll until bitcoind accepts commands. This can take a long time.
while ! ./bitcoin-cli getnetworkinfo &> /dev/null; do while ! ${cfg.cli}/bin/bitcoin-cli getnetworkinfo &> /dev/null; do
sleep 1 sleep 1
done done
''; '';
@ -368,7 +363,7 @@ in {
bindsTo = [ "bitcoind.service" ]; bindsTo = [ "bitcoind.service" ];
after = [ "bitcoind.service" ]; after = [ "bitcoind.service" ];
script = '' script = ''
cd ${cfg.cliBase}/bin cd ${cfg.cli}/bin
echo "Importing node banlist..." echo "Importing node banlist..."
cat ${./banlist.cli.txt} | while read line; do cat ${./banlist.cli.txt} | while read line; do
if ! err=$(eval "$line" 2>&1) && [[ $err != *already\ banned* ]]; then if ! err=$(eval "$line" 2>&1) && [[ $err != *already\ banned* ]]; then

View File

@ -112,7 +112,7 @@ in {
configFile = builtins.toFile "config" '' configFile = builtins.toFile "config" ''
network=${config.services.bitcoind.network} network=${config.services.bitcoind.network}
btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name} btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name}
btcrpcurl=http://${builtins.elemAt config.services.bitcoind.rpcbind 0}:${toString cfg.bitcoind.rpc.port} btcrpcurl=http://${config.services.bitcoind.rpcbind}:${toString cfg.bitcoind.rpc.port}
btcnodeendpoint=${config.services.bitcoind.bind}:8333 btcnodeendpoint=${config.services.bitcoind.bind}:8333
bind=${cfg.nbxplorer.bind} bind=${cfg.nbxplorer.bind}
port=${toString cfg.nbxplorer.port} port=${toString cfg.nbxplorer.port}

View File

@ -13,7 +13,7 @@ let
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
always-use-proxy=${if cfg.always-use-proxy then "true" else "false"} always-use-proxy=${if cfg.always-use-proxy then "true" else "false"}
bind-addr=${cfg.bind-addr}:${toString cfg.bindport} bind-addr=${cfg.bind-addr}:${toString cfg.bindport}
bitcoin-rpcconnect=${builtins.elemAt config.services.bitcoind.rpcbind 0} bitcoin-rpcconnect=${config.services.bitcoind.rpcbind}
bitcoin-rpcport=${toString config.services.bitcoind.rpc.port} bitcoin-rpcport=${toString config.services.bitcoind.rpc.port}
bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name} bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name}
rpc-file-mode=0660 rpc-file-mode=0660
@ -38,12 +38,12 @@ in {
}; };
proxy = mkOption { proxy = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
description = "Set a socks proxy to use to connect to Tor nodes (or for all connections if *always-use-proxy* is set)"; description = "Set a socks proxy to use to connect to Tor nodes (or for all connections if *always-use-proxy* is set)";
}; };
always-use-proxy = mkOption { always-use-proxy = mkOption {
type = types.bool; type = types.bool;
default = false; default = cfg.enforceTor;
description = '' description = ''
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. 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.
''; '';

View File

@ -97,7 +97,7 @@ in {
--daemon-dir='${bitcoind.dataDir}' \ --daemon-dir='${bitcoind.dataDir}' \
--electrum-rpc-addr=${cfg.address}:${toString cfg.port} \ --electrum-rpc-addr=${cfg.address}:${toString cfg.port} \
--monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \ --monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \
--daemon-rpc-addr=${builtins.elemAt bitcoind.rpcbind 0}:${toString bitcoind.rpc.port} \ --daemon-rpc-addr=${bitcoind.rpcbind}:${toString bitcoind.rpc.port} \
${cfg.extraArgs} ${cfg.extraArgs}
''; '';
User = cfg.user; User = cfg.user;

View File

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

View File

@ -7,14 +7,17 @@ let
inherit (config) nix-bitcoin-services; inherit (config) nix-bitcoin-services;
secretsDir = config.nix-bitcoin.secretsDir; secretsDir = config.nix-bitcoin.secretsDir;
network = config.services.bitcoind.network; network = config.services.bitcoind.network;
rpclisten = "${cfg.rpcAddress}:${toString cfg.rpcPort}";
configFile = builtins.toFile "loop.conf" '' configFile = builtins.toFile "loop.conf" ''
datadir=${cfg.dataDir} datadir=${cfg.dataDir}
network=${network} network=${network}
rpclisten=${rpclisten}
restlisten=${cfg.restAddress}:${toString cfg.restPort}
logdir=${cfg.dataDir}/logs logdir=${cfg.dataDir}/logs
tlscertpath=${secretsDir}/loop-cert tlscertpath=${secretsDir}/loop-cert
tlskeypath=${secretsDir}/loop-key tlskeypath=${secretsDir}/loop-key
lnd.host=${builtins.elemAt config.services.lnd.rpclisten 0}:${toString config.services.lnd.rpcPort} lnd.host=${config.services.lnd.rpclisten}:${toString config.services.lnd.rpcPort}
lnd.macaroondir=${config.services.lnd.networkDir} lnd.macaroondir=${config.services.lnd.networkDir}
lnd.tlspath=${secretsDir}/lnd-cert lnd.tlspath=${secretsDir}/lnd-cert
@ -25,6 +28,26 @@ let
in { in {
options.services.lightning-loop = { options.services.lightning-loop = {
enable = mkEnableOption "lightning-loop"; enable = mkEnableOption "lightning-loop";
rpcAddress = mkOption {
type = types.str;
default = "localhost";
description = "Address to listen for gRPC connections.";
};
rpcPort = mkOption {
type = types.port;
default = 11010;
description = "Port to listen for gRPC connections.";
};
restAddress = mkOption {
type = types.str;
default = cfg.rpcAddress;
description = "Address to listen for REST connections.";
};
restPort = mkOption {
type = types.port;
default = 8081;
description = "Port to listen for REST connections.";
};
package = mkOption { package = mkOption {
type = types.package; type = types.package;
default = pkgs.nix-bitcoin.lightning-loop; default = pkgs.nix-bitcoin.lightning-loop;
@ -38,7 +61,7 @@ in {
}; };
proxy = mkOption { proxy = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
description = "host:port of SOCKS5 proxy for connnecting to the loop server."; description = "host:port of SOCKS5 proxy for connnecting to the loop server.";
}; };
extraConfig = mkOption { extraConfig = mkOption {
@ -51,13 +74,13 @@ in {
}; };
cli = mkOption { cli = mkOption {
default = pkgs.writeScriptBin "loop" '' default = pkgs.writeScriptBin "loop" ''
${cfg.cliExec} ${cfg.package}/bin/loop \ ${cfg.package}/bin/loop \
--rpcserver ${rpclisten} \
--macaroonpath '${cfg.dataDir}/${network}/loop.macaroon' \ --macaroonpath '${cfg.dataDir}/${network}/loop.macaroon' \
--tlscertpath '${secretsDir}/loop-cert' "$@" --tlscertpath '${secretsDir}/loop-cert' "$@"
''; '';
description = "Binary to connect with the lightning-loop instance."; description = "Binary to connect with the lightning-loop instance.";
}; };
inherit (nix-bitcoin-services) cliExec;
enforceTor = nix-bitcoin-services.enforceTor; enforceTor = nix-bitcoin-services.enforceTor;
}; };

View File

@ -26,11 +26,12 @@ let
(rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
(attrValues cfg.rpc.users) (attrValues cfg.rpc.users)
} }
${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} rpcbind=${cfg.rpcbind}
rpcconnect=${cfg.rpcbind}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip}
${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"} ${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"}
${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"} ${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"}
mainchainrpchost=${builtins.elemAt config.services.bitcoind.rpcbind 0} mainchainrpchost=${config.services.bitcoind.rpcbind}
mainchainrpcport=${toString config.services.bitcoind.rpc.port} mainchainrpcport=${toString config.services.bitcoind.rpc.port}
mainchainrpcuser=${config.services.bitcoind.rpc.users.public.name} mainchainrpcuser=${config.services.bitcoind.rpc.users.public.name}
@ -125,8 +126,8 @@ in {
}; };
rpcbind = mkOption { rpcbind = mkOption {
type = types.listOf types.str; type = types.str;
default = [ "127.0.0.1" ]; default = "127.0.0.1";
description = '' description = ''
Bind to given address to listen for JSON-RPC connections. Bind to given address to listen for JSON-RPC connections.
''; '';
@ -160,7 +161,7 @@ in {
}; };
proxy = mkOption { proxy = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
description = "Connect through SOCKS5 proxy"; description = "Connect through SOCKS5 proxy";
}; };
listen = mkOption { listen = mkOption {
@ -205,17 +206,16 @@ in {
cli = mkOption { cli = mkOption {
readOnly = true; readOnly = true;
default = pkgs.writeScriptBin "elements-cli" '' default = pkgs.writeScriptBin "elements-cli" ''
${cfg.cliExec} ${pkgs.nix-bitcoin.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@" ${pkgs.nix-bitcoin.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@"
''; '';
description = "Binary to connect with the liquidd instance."; description = "Binary to connect with the liquidd instance.";
}; };
swapCli = mkOption { swapCli = mkOption {
default = pkgs.writeScriptBin "liquidswap-cli" '' default = pkgs.writeScriptBin "liquidswap-cli" ''
${cfg.cliExec} ${pkgs.nix-bitcoin.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@" ${pkgs.nix-bitcoin.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@"
''; '';
description = "Binary for managing liquid swaps."; description = "Binary for managing liquid swaps.";
}; };
inherit (nix-bitcoin-services) cliExec;
enforceTor = nix-bitcoin-services.enforceTor; enforceTor = nix-bitcoin-services.enforceTor;
}; };
}; };

View File

@ -8,7 +8,7 @@ let
secretsDir = config.nix-bitcoin.secretsDir; secretsDir = config.nix-bitcoin.secretsDir;
bitcoind = config.services.bitcoind; bitcoind = config.services.bitcoind;
bitcoindRpcAddress = builtins.elemAt bitcoind.rpcbind 0; bitcoindRpcAddress = bitcoind.rpcbind;
onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []); onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []);
networkDir = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}"; networkDir = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
configFile = pkgs.writeText "lnd.conf" '' configFile = pkgs.writeText "lnd.conf" ''
@ -18,14 +18,14 @@ let
tlskeypath=${secretsDir}/lnd-key tlskeypath=${secretsDir}/lnd-key
listen=${toString cfg.listen}:${toString cfg.listenPort} listen=${toString cfg.listen}:${toString cfg.listenPort}
${lib.concatMapStrings (rpclisten: "rpclisten=${rpclisten}:${toString cfg.rpcPort}\n") cfg.rpclisten} rpclisten=${cfg.rpclisten}
${lib.concatMapStrings (restlisten: "restlisten=${restlisten}:${toString cfg.restPort}\n") cfg.restlisten} restlisten=${cfg.restlisten}
bitcoin.${bitcoind.network}=1 bitcoin.${bitcoind.network}=1
bitcoin.active=1 bitcoin.active=1
bitcoin.node=bitcoind bitcoin.node=bitcoind
tor.active=true ${optionalString (cfg.enforceTor) "tor.active=true"}
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"} ${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port} bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port}
@ -66,15 +66,15 @@ in {
description = "Bind to given port to listen to peer connections"; description = "Bind to given port to listen to peer connections";
}; };
rpclisten = mkOption { rpclisten = mkOption {
type = types.listOf types.str; type = types.str;
default = [ "localhost" ]; default = "localhost";
description = '' description = ''
Bind to given address to listen to RPC connections. Bind to given address to listen to RPC connections.
''; '';
}; };
restlisten = mkOption { restlisten = mkOption {
type = types.listOf types.str; type = types.str;
default = [ "localhost" ]; default = "localhost";
description = '' description = ''
Bind to given address to listen to REST connections. Bind to given address to listen to REST connections.
''; '';
@ -91,7 +91,7 @@ in {
}; };
tor-socks = mkOption { tor-socks = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = if cfg.enforceTor then config.services.tor.client.socksListenAddress else null;
description = "Set a socks proxy to use to connect to Tor nodes"; description = "Set a socks proxy to use to connect to Tor nodes";
}; };
announce-tor = mkOption { announce-tor = mkOption {
@ -138,12 +138,13 @@ in {
default = pkgs.writeScriptBin "lncli" default = pkgs.writeScriptBin "lncli"
# Switch user because lnd makes datadir contents readable by user only # Switch user because lnd makes datadir contents readable by user only
'' ''
${cfg.cliExec} sudo -u lnd ${cfg.package}/bin/lncli --tlscertpath ${secretsDir}/lnd-cert \ sudo -u lnd ${cfg.package}/bin/lncli \
--rpcserver ${cfg.rpclisten}:${toString cfg.rpcPort} \
--tlscertpath '${secretsDir}/lnd-cert' \
--macaroonpath '${networkDir}/admin.macaroon' "$@" --macaroonpath '${networkDir}/admin.macaroon' "$@"
''; '';
description = "Binary to connect with the lnd instance."; description = "Binary to connect with the lnd instance.";
}; };
inherit (nix-bitcoin-services) cliExec;
enforceTor = nix-bitcoin-services.enforceTor; enforceTor = nix-bitcoin-services.enforceTor;
}; };
@ -188,12 +189,12 @@ in {
RestartSec = "10s"; RestartSec = "10s";
ReadWritePaths = "${cfg.dataDir}"; ReadWritePaths = "${cfg.dataDir}";
ExecStartPost = let ExecStartPost = let
restPort = toString cfg.restPort; restUrl = "https://${cfg.restlisten}:${toString cfg.restPort}/v1";
in [ in [
# Run fully privileged for secrets dir write access # Run fully privileged for secrets dir write access
"+${nix-bitcoin-services.script '' "+${nix-bitcoin-services.script ''
attempts=250 attempts=250
while ! { exec 3>/dev/tcp/127.0.0.1/${restPort} && exec 3>&-; } &>/dev/null; do while ! { exec 3>/dev/tcp/${cfg.restlisten}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do
((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; } ((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; }
sleep 0.1 sleep 0.1
done done
@ -204,7 +205,7 @@ in {
umask u=r,go= umask u=r,go=
${pkgs.curl}/bin/curl -s \ ${pkgs.curl}/bin/curl -s \
--cacert ${secretsDir}/lnd-cert \ --cacert ${secretsDir}/lnd-cert \
-X GET https://127.0.0.1:${restPort}/v1/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > "$mnemonic" -X GET ${restUrl}/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > "$mnemonic"
fi fi
chown lnd: "$mnemonic" chown lnd: "$mnemonic"
''}" ''}"
@ -216,7 +217,7 @@ in {
--cacert ${secretsDir}/lnd-cert \ --cacert ${secretsDir}/lnd-cert \
-X POST -d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\", \ -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')}" \ \"cipher_seed_mnemonic\": $(cat ${secretsDir}/lnd-seed-mnemonic | tr -d '\n')}" \
https://127.0.0.1:${restPort}/v1/initwallet ${restUrl}/initwallet
# Guarantees that RPC calls with cfg.cli succeed after the service is started # Guarantees that RPC calls with cfg.cli succeed after the service is started
echo Wait until wallet is created echo Wait until wallet is created
@ -231,11 +232,11 @@ in {
--cacert ${secretsDir}/lnd-cert \ --cacert ${secretsDir}/lnd-cert \
-X POST \ -X POST \
-d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\"}" \ -d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\"}" \
https://127.0.0.1:${restPort}/v1/unlockwallet ${restUrl}/unlockwallet
fi fi
# Wait until the RPC port is open # Wait until the RPC port is open
while ! { exec 3>/dev/tcp/127.0.0.1/${toString cfg.rpcPort}; } &>/dev/null; do while ! { exec 3>/dev/tcp/${cfg.rpclisten}/${toString cfg.rpcPort}; } &>/dev/null; do
sleep 0.1 sleep 0.1
done done
@ -251,7 +252,7 @@ in {
--cacert ${secretsDir}/lnd-cert \ --cacert ${secretsDir}/lnd-cert \
-X POST \ -X POST \
-d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \ -d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \
https://127.0.0.1:${restPort}/v1/macaroon |\ ${restUrl}/macaroon |\
${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath" ${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath"
chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath" chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath"
'') (attrNames cfg.macaroons)} '') (attrNames cfg.macaroons)}

View File

@ -50,7 +50,7 @@ in {
addressblock = mkOption { addressblock = mkOption {
type = types.ints.u8; type = types.ints.u8;
default = "1"; default = 1;
description = '' description = ''
The address block N in 169.254.N.0/24, used as the prefix for netns addresses. The address block N in 169.254.N.0/24, used as the prefix for netns addresses.
''; '';
@ -141,6 +141,7 @@ in {
inherit (v) netnsName; inherit (v) netnsName;
ipNetns = "${ip} -n ${netnsName}"; ipNetns = "${ip} -n ${netnsName}";
netnsIptables = "${ip} netns exec ${netnsName} ${config.networking.firewall.package}/bin/iptables"; netnsIptables = "${ip} netns exec ${netnsName} ${config.networking.firewall.package}/bin/iptables";
allowedAddresses = concatMapStringsSep "," (available: netns.${available}.address) v.availableNetns;
in { in {
"${n}".serviceConfig.NetworkNamespacePath = "/var/run/netns/${netnsName}"; "${n}".serviceConfig.NetworkNamespacePath = "/var/run/netns/${netnsName}";
@ -151,6 +152,7 @@ in {
requiredBy = bindsTo; requiredBy = bindsTo;
before = bindsTo; before = bindsTo;
script = '' script = ''
${ip} netns delete ${netnsName} 2> /dev/null || true
${ip} netns add ${netnsName} ${ip} netns add ${netnsName}
${ipNetns} link set lo up ${ipNetns} link set lo up
${ip} link add ${veth} type veth peer name ${peer} ${ip} link add ${veth} type veth peer name ${peer}
@ -164,23 +166,19 @@ in {
${netnsIptables} -w -A INPUT -s 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT ${netnsIptables} -w -A INPUT -s 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT
# allow return traffic to outgoing connections initiated by the service itself # allow return traffic to outgoing connections initiated by the service itself
${netnsIptables} -w -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT ${netnsIptables} -w -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
'' + (optionalString (config.services.${n}.enforceTor or false)) '' '' + optionalString (config.services.${n}.enforceTor or false) ''
${netnsIptables} -w -P OUTPUT DROP ${netnsIptables} -w -P OUTPUT DROP
${netnsIptables} -w -A OUTPUT -d 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT ${netnsIptables} -w -A OUTPUT -d 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT
'' + concatMapStrings (otherNetns: let '' + optionalString (v.availableNetns != []) ''
other = netns.${otherNetns}; ${netnsIptables} -w -A INPUT -s ${allowedAddresses} -j ACCEPT
in '' ${netnsIptables} -w -A OUTPUT -d ${allowedAddresses} -j ACCEPT
${netnsIptables} -w -A INPUT -s ${other.address} -j ACCEPT '';
${netnsIptables} -w -A OUTPUT -d ${other.address} -j ACCEPT
'') v.availableNetns;
preStop = '' preStop = ''
${ip} netns delete ${netnsName} ${ip} netns delete ${netnsName}
${ip} link del ${peer}
''; '';
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = "yes"; RemainAfterExit = "yes";
ExecStartPre = "-${ip} netns delete ${netnsName}";
}; };
}; };
}; };
@ -252,18 +250,11 @@ in {
services.bitcoind = { services.bitcoind = {
bind = netns.bitcoind.address; bind = netns.bitcoind.address;
rpcbind = [ rpcbind = netns.bitcoind.address;
"${netns.bitcoind.address}"
"127.0.0.1"
];
rpcallowip = [ rpcallowip = [
"127.0.0.1" bridgeIp # For operator user
] ++ map (n: "${netns.${n}.address}") netns.bitcoind.availableNetns; netns.bitcoind.address
cli = let ] ++ map (n: netns.${n}.address) netns.bitcoind.availableNetns;
inherit (config.services.bitcoind) cliBase;
in pkgs.writeScriptBin cliBase.name ''
exec netns-exec ${netns.bitcoind.netnsName} ${cliBase}/bin/${cliBase.name} "$@"
'';
}; };
systemd.services.bitcoind-import-banlist.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-bitcoind"; systemd.services.bitcoind-import-banlist.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-bitcoind";
@ -271,27 +262,17 @@ in {
services.lnd = { services.lnd = {
listen = netns.lnd.address; listen = netns.lnd.address;
rpclisten = [ rpclisten = netns.lnd.address;
"${netns.lnd.address}" restlisten = netns.lnd.address;
"127.0.0.1"
];
restlisten = [
"${netns.lnd.address}"
"127.0.0.1"
];
cliExec = mkCliExec "lnd";
}; };
services.liquidd = { services.liquidd = {
bind = netns.liquidd.address; bind = netns.liquidd.address;
rpcbind = [ rpcbind = netns.liquidd.address;
"${netns.liquidd.address}"
"127.0.0.1"
];
rpcallowip = [ rpcallowip = [
"127.0.0.1" bridgeIp # For operator user
] ++ map (n: "${netns.${n}.address}") netns.liquidd.availableNetns; netns.liquidd.address
cliExec = mkCliExec "liquidd"; ] ++ map (n: netns.${n}.address) netns.liquidd.availableNetns;
}; };
services.electrs.address = netns.electrs.address; services.electrs.address = netns.electrs.address;
@ -308,7 +289,7 @@ in {
host = netns.nanopos.address; host = netns.nanopos.address;
}; };
services.lightning-loop.cliExec = mkCliExec "lightning-loop"; services.lightning-loop.rpcAddress = netns.lightning-loop.address;
services.nbxplorer.bind = netns.nbxplorer.address; services.nbxplorer.bind = netns.nbxplorer.address;
services.btcpayserver.bind = netns.btcpayserver.address; services.btcpayserver.bind = netns.btcpayserver.address;

View File

@ -49,17 +49,11 @@ in {
hiddenServices.sshd = mkHiddenService { port = 22; }; hiddenServices.sshd = mkHiddenService { port = 22; };
}; };
# netns-isolation
nix-bitcoin.netns-isolation = {
addressblock = 1;
};
# bitcoind # bitcoind
services.bitcoind = { services.bitcoind = {
enable = true; enable = true;
listen = true; listen = true;
dataDirReadableByGroup = mkIf cfg.electrs.high-memory true; dataDirReadableByGroup = mkIf cfg.electrs.high-memory true;
proxy = cfg.tor.client.socksListenAddress;
enforceTor = true; enforceTor = true;
port = 8333; port = 8333;
assumevalid = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6"; assumevalid = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6";
@ -74,11 +68,7 @@ in {
services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; }; services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; };
# clightning # clightning
services.clightning = { services.clightning.enforceTor = true;
proxy = cfg.tor.client.socksListenAddress;
enforceTor = true;
always-use-proxy = true;
};
services.tor.hiddenServices.clightning = mkIf cfg.clightning.enable (mkHiddenService { services.tor.hiddenServices.clightning = mkIf cfg.clightning.enable (mkHiddenService {
port = cfg.clightning.onionport; port = cfg.clightning.onionport;
toHost = cfg.clightning.bind-addr; toHost = cfg.clightning.bind-addr;
@ -86,17 +76,11 @@ in {
}); });
# lnd # lnd
services.lnd = { services.lnd.enforceTor = true;
tor-socks = cfg.tor.client.socksListenAddress;
enforceTor = true;
};
services.tor.hiddenServices.lnd = mkIf cfg.lnd.enable (mkHiddenService { port = cfg.lnd.onionport; toHost = cfg.lnd.listen; toPort = cfg.lnd.listenPort; }); services.tor.hiddenServices.lnd = mkIf cfg.lnd.enable (mkHiddenService { port = cfg.lnd.onionport; toHost = cfg.lnd.listen; toPort = cfg.lnd.listenPort; });
# lightning-loop # lightning-loop
services.lightning-loop = { services.lightning-loop.enforceTor = true;
proxy = cfg.tor.client.socksListenAddress;
enforceTor = true;
};
# liquidd # liquidd
services.liquidd = { services.liquidd = {
@ -104,7 +88,6 @@ in {
prune = 1000; prune = 1000;
validatepegin = true; validatepegin = true;
listen = true; listen = true;
proxy = cfg.tor.client.socksListenAddress;
enforceTor = true; enforceTor = true;
port = 7042; port = 7042;
}; };

View File

@ -10,10 +10,6 @@
#include <sys/capability.h> #include <sys/capability.h>
static char *allowed_netns[] = { static char *allowed_netns[] = {
"nb-lnd",
"nb-lightning-loop",
"nb-bitcoind",
"nb-liquidd",
"nb-joinmarket" "nb-joinmarket"
}; };

View File

@ -169,12 +169,12 @@ EOF
} }
# A basic subset of tests to keep the total runtime within # A basic subset of tests to keep the total runtime within
# manageable bounds (<3 min on desktop systems). # manageable bounds (<4 min on desktop systems).
# These are also run on the CI server. # These are also run on the CI server.
basic() { basic() {
scenario=default buildTest "$@" scenario=default buildTest "$@"
scenario=netns buildTest "$@" scenario=netns buildTest "$@"
scenario=full evalTest "$@" scenario=netnsRegtest buildTest "$@"
} }
all() { all() {
@ -182,6 +182,7 @@ all() {
scenario=netns buildTest "$@" scenario=netns buildTest "$@"
scenario=full buildTest "$@" scenario=full buildTest "$@"
scenario=regtest buildTest "$@" scenario=regtest buildTest "$@"
scenario=netnsRegtest buildTest "$@"
} }
build() { build() {

View File

@ -111,11 +111,7 @@ let testEnv = rec {
}; };
netns = { netns = {
imports = [ scenarios.secureNode ]; imports = with scenarios; [ netnsBase secureNode ];
nix-bitcoin.netns-isolation.enable = true;
test.data.netns = config.nix-bitcoin.netns-isolation.netns;
tests.netns-isolation = true;
# This test is rather slow and unaffected by netns settings # This test is rather slow and unaffected by netns settings
tests.backups = mkForce false; tests.backups = mkForce false;
}; };
@ -132,6 +128,18 @@ let testEnv = rec {
services.joinmarket.enable = true; services.joinmarket.enable = true;
}; };
# netns and regtest, without secure-node.nix
netnsRegtest = {
imports = with scenarios; [ netnsBase regtest ];
};
netnsBase = {
nix-bitcoin.netns-isolation.enable = true;
test.data.netns = config.nix-bitcoin.netns-isolation.netns;
tests.netns-isolation = true;
environment.systemPackages = [ pkgs.fping ];
};
regtestBase = { regtestBase = {
tests.regtest = true; tests.regtest = true;

View File

@ -237,49 +237,45 @@ def _():
# (and their corresponding network namespaces). # (and their corresponding network namespaces).
@test("netns-isolation") @test("netns-isolation")
def _(): def _():
ping_bitcoind = "ip netns exec nb-bitcoind ping -c 1 -w 1" def get_ips(services):
ping_nanopos = "ip netns exec nb-nanopos ping -c 1 -w 1" enabled = enabled_tests.intersection(services)
ping_nbxplorer = "ip netns exec nb-nbxplorer ping -c 1 -w 1" return " ".join(ip(service) for service in enabled)
# Positive ping tests (non-exhaustive) def assert_reachable(src, dests):
machine.succeed( dest_ips = get_ips(dests)
"%s %s &&" % (ping_bitcoind, ip("bitcoind")) if src in enabled_tests and dest_ips:
+ "%s %s &&" % (ping_bitcoind, ip("clightning")) machine.succeed(f"ip netns exec nb-{src} fping -c1 -t100 {dest_ips}")
+ "%s %s &&" % (ping_bitcoind, ip("lnd"))
+ "%s %s &&" % (ping_bitcoind, ip("liquidd"))
+ "%s %s &&" % (ping_bitcoind, ip("nbxplorer"))
+ "%s %s &&" % (ping_nbxplorer, ip("btcpayserver"))
+ "%s %s &&" % (ping_nanopos, ip("lightning-charge"))
+ "%s %s &&" % (ping_nanopos, ip("nanopos"))
+ "%s %s" % (ping_nanopos, ip("nginx"))
)
# Negative ping tests (non-exhaustive) def assert_unreachable(src, dests):
dest_ips = get_ips(dests)
if src in enabled_tests and dest_ips:
machine.fail( machine.fail(
"%s %s ||" % (ping_bitcoind, ip("spark-wallet")) # This fails when no host is reachable within 100 ms
+ "%s %s ||" % (ping_bitcoind, ip("lightning-loop")) f"ip netns exec nb-{src} fping -c1 -t100 --reachable=1 {dest_ips}"
+ "%s %s ||" % (ping_bitcoind, ip("lightning-charge"))
+ "%s %s ||" % (ping_bitcoind, ip("nanopos"))
+ "%s %s ||" % (ping_bitcoind, ip("nginx"))
+ "%s %s ||" % (ping_nanopos, ip("bitcoind"))
+ "%s %s ||" % (ping_nanopos, ip("clightning"))
+ "%s %s ||" % (ping_nanopos, ip("lnd"))
+ "%s %s ||" % (ping_nanopos, ip("lightning-loop"))
+ "%s %s ||" % (ping_nanopos, ip("liquidd"))
+ "%s %s ||" % (ping_nanopos, ip("electrs"))
+ "%s %s ||" % (ping_nanopos, ip("spark-wallet"))
+ "%s %s" % (ping_nanopos, ip("btcpayserver"))
) )
# test that netns-exec can't be run for unauthorized namespace # These reachability tests are non-exhaustive
machine.fail("netns-exec nb-electrs ip a") assert_reachable("bitcoind", ["clightning", "lnd", "liquidd"])
assert_unreachable("bitcoind", ["btcpayserver", "spark-wallet", "lightning-loop"])
assert_unreachable("btcpayserver", ["bitcoind", "lightning-loop", "liquidd"])
# test that netns-exec drops capabilities # netns addresses can not be bound to in the main netns.
# This prevents processes in the main netns from impersonating nix-bitcoin services.
assert_matches(
f"nc -l {ip('bitcoind')} 1080 2>&1 || true", "nc: Cannot assign requested address"
)
if "joinmarket" in enabled_tests:
# netns-exec should drop capabilities
assert_full_match( assert_full_match(
"su operator -c 'netns-exec nb-bitcoind capsh --print | grep Current '", "Current: =\n" "su operator -c 'netns-exec nb-joinmarket capsh --print | grep Current'", "Current: =\n"
) )
# test that netns-exec can not be executed by users that are not operator if "clightning" in enabled_tests:
# netns-exec should fail for unauthorized namespaces
machine.fail("netns-exec nb-clightning ip a")
# netns-exec should only be executable by the operator user
machine.fail("sudo -u clightning netns-exec nb-bitcoind ip a") machine.fail("sudo -u clightning netns-exec nb-bitcoind ip a")
@ -347,7 +343,7 @@ def _():
machine.wait_until_succeeds( machine.wait_until_succeeds(
log_has_string("lightning-loop", "Starting event loop at height 10") log_has_string("lightning-loop", "Starting event loop at height 10")
) )
succeed("sudo -u operator loop getparams | jq -e '.rules'") succeed("sudo -u operator loop getparams")
if "netns-isolation" in enabled_tests: if "netns-isolation" in enabled_tests: