Merge #226: Improve netns-isolation and tests

e5fb3f6a7f run-tests: document how to pass extra build args (Erik Arvstedt)
df790f6766 run-tests: allow linking test build results for all scenarios (Erik Arvstedt)
91697b1427 test: allow for testing all scenarios (Erik Arvstedt)
28236691aa test: rename scenarios/lib.py -> base.py (Erik Arvstedt)
80da0a41bc test: load complete test environment in debug mode (Erik Arvstedt)
9b4cd7bd1c test: simplify scenario handling (Erik Arvstedt)
0f56ea6ad1 test: include scenario in test name (Erik Arvstedt)
9237e5dc3d test: use pydoc docstring (Erik Arvstedt)
ed73627e02 netns-exec: minor style fixes (Erik Arvstedt)
91ebc2d517 netns-exec: simplify installation (Erik Arvstedt)
809e754851 netns: improve bridge setup (Erik Arvstedt)
b7450877a0 netns: rename bridge peer devices br-nb-veth* -> nb-veth-br* (Erik Arvstedt)
8bfb7bb2f8 netns: rename bridge br0 -> nb-br (Erik Arvstedt)
32e70a7516 netns: move webindex config for modules-only usage (Erik Arvstedt)
121301337b netns: add option 'allowedUser' for modules-only usage (Erik Arvstedt)
9715134f06 netns: don't repeat cli definitions (Erik Arvstedt)
e385c73256 netns: separate implementation and service configs (Erik Arvstedt)
d0b8d77de2 netns: remove conditionals for service settings (Erik Arvstedt)
0f0f6ddbb9 netns: add comment about undesirable algorithmic complexity (Erik Arvstedt)
a3ae8668e6 netns: use map instead of concatMap (Erik Arvstedt)
b7fc819be5 netns: consistent var naming (Erik Arvstedt)
5a81693ef3 netns: add range check for netns ids (Erik Arvstedt)
74f1610668 netns: clarify addressblock description (Erik Arvstedt)
4eb92df08c netns: remove redundant filter (Erik Arvstedt)
50de54aef1 netns: remove empty connections defs (Erik Arvstedt)

Pull request description:

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

Tree-SHA512: e2accf7b5ab5d4c4c07a8f9307409021809326648139424ff7ebaa7be3e628f21d5be8dafabe19b9659d09537a5b3976e2513bc287e79027376b5271006bc214
This commit is contained in:
Jonas Nick 2020-08-25 13:29:21 +00:00
commit 5c99656cce
No known key found for this signature in database
GPG Key ID: 4861DBF262123605
13 changed files with 251 additions and 226 deletions

View File

@ -265,20 +265,16 @@ in {
}; };
cli = mkOption { cli = mkOption {
type = types.package; type = types.package;
default = cfg.cli-nonetns-exec; # Overriden on netns-isolation
default = cfg.cliBase;
description = "Binary to connect with the bitcoind instance."; description = "Binary to connect with the bitcoind instance.";
}; };
# Needed because bitcoin-cli commands executed through systemd already cliBase = mkOption {
# run inside nb-bitcoind, hence they don't need netns-exec prefixed.
cli-nonetns-exec = 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 without netns-exec.
'';
}; };
enforceTor = nix-bitcoin-services.enforceTor; enforceTor = nix-bitcoin-services.enforceTor;
}; };
@ -315,7 +311,7 @@ in {
fi fi
''; '';
postStart = '' postStart = ''
cd ${cfg.cli-nonetns-exec}/bin 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 ! ./bitcoin-cli getnetworkinfo &> /dev/null; do
sleep 1 sleep 1
@ -342,7 +338,7 @@ in {
bindsTo = [ "bitcoind.service" ]; bindsTo = [ "bitcoind.service" ];
after = [ "bitcoind.service" ]; after = [ "bitcoind.service" ];
script = '' script = ''
cd ${cfg.cli-nonetns-exec}/bin cd ${cfg.cliBase}/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

@ -30,10 +30,11 @@ in {
default = pkgs.writeScriptBin "loop" default = pkgs.writeScriptBin "loop"
# Switch user because lnd makes datadir contents readable by user only # Switch user because lnd makes datadir contents readable by user only
'' ''
exec sudo -u lnd ${cfg.package}/bin/loop "$@" ${cfg.cliExec} sudo -u lnd ${cfg.package}/bin/loop "$@"
''; '';
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;
}; };

View File

@ -210,17 +210,19 @@ in {
''; '';
}; };
cli = mkOption { cli = mkOption {
readOnly = true;
default = pkgs.writeScriptBin "elements-cli" '' default = pkgs.writeScriptBin "elements-cli" ''
exec ${pkgs.nix-bitcoin.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@" ${cfg.cliExec} ${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.";
}; };
swap-cli = mkOption { swapCli = mkOption {
default = pkgs.writeScriptBin "liquidswap-cli" '' default = pkgs.writeScriptBin "liquidswap-cli" ''
exec ${pkgs.nix-bitcoin.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@" ${cfg.cliExec} ${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;
}; };
}; };
@ -229,7 +231,7 @@ in {
environment.systemPackages = [ environment.systemPackages = [
pkgs.nix-bitcoin.elementsd pkgs.nix-bitcoin.elementsd
(hiPrio cfg.cli) (hiPrio cfg.cli)
(hiPrio cfg.swap-cli) (hiPrio cfg.swapCli)
]; ];
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [

View File

@ -115,11 +115,12 @@ 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
'' ''
exec sudo -u lnd ${cfg.package}/bin/lncli --tlscertpath ${secretsDir}/lnd-cert \ ${cfg.cliExec} sudo -u lnd ${cfg.package}/bin/lncli --tlscertpath ${secretsDir}/lnd-cert \
--macaroonpath '${cfg.dataDir}/chain/bitcoin/mainnet/admin.macaroon' "$@" --macaroonpath '${cfg.dataDir}/chain/bitcoin/mainnet/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;
}; };

View File

@ -8,7 +8,8 @@ let
netns = builtins.mapAttrs (n: v: { netns = builtins.mapAttrs (n: v: {
inherit (v) id; inherit (v) id;
address = "169.254.${toString cfg.addressblock}.${toString v.id}"; address = "169.254.${toString cfg.addressblock}.${toString v.id}";
availableNetns = builtins.filter isEnabled availableNetns.${n}; availableNetns = availableNetns.${n};
netnsName = "nb-${n}";
}) enabledServices; }) enabledServices;
# Symmetric netns connection matrix # Symmetric netns connection matrix
@ -16,6 +17,12 @@ let
# availableNetns.bitcoind = [ "clighting" ]; # availableNetns.bitcoind = [ "clighting" ];
# and # and
# availableNetns.clighting = [ "bitcoind" ]; # availableNetns.clighting = [ "bitcoind" ];
#
# FIXME: Although negligible for our purposes, this calculation's runtime
# is in the order of (number of connections * number of services),
# because attrsets and lists are fully copied on each update with '//' or '++'.
# This can only be improved with an update in the nix language.
#
availableNetns = let availableNetns = let
# base = { clightning = [ "bitcoind" ]; ... } # base = { clightning = [ "bitcoind" ]; ... }
base = builtins.mapAttrs (n: v: base = builtins.mapAttrs (n: v:
@ -36,6 +43,7 @@ let
bridgeIp = "169.254.${toString cfg.addressblock}.10"; bridgeIp = "169.254.${toString cfg.addressblock}.10";
mkCliExec = service: "exec netns-exec ${netns.${service}.netnsName}";
in { in {
options.nix-bitcoin.netns-isolation = { options.nix-bitcoin.netns-isolation = {
enable = mkEnableOption "netns isolation"; enable = mkEnableOption "netns isolation";
@ -44,7 +52,7 @@ in {
type = types.ints.u8; type = types.ints.u8;
default = "1"; default = "1";
description = '' description = ''
Specify the N address block in 169.254.N.0/24. The address block N in 169.254.N.0/24, used as the prefix for netns addresses.
''; '';
}; };
@ -53,12 +61,11 @@ in {
type = types.attrsOf (types.submodule { type = types.attrsOf (types.submodule {
options = { options = {
id = mkOption { id = mkOption {
# TODO: Exclude 10
# TODO: Assert uniqueness # TODO: Assert uniqueness
type = types.int; type = types.ints.between 11 255;
description = '' description = ''
id for the netns, that is used for the IP address host part and id for the netns, used for the IP address host part and
naming the interfaces. Must be unique. Must not be 10. for naming the interfaces. Must be unique. Must be greater than 10.
''; '';
}; };
connections = mkOption { connections = mkOption {
@ -68,21 +75,118 @@ in {
}; };
}); });
}; };
allowedUser = mkOption {
type = types.str;
description = ''
User that is allowed to execute commands in the service network namespaces.
The user's group is also authorized.
'';
};
netns = mkOption {
default = netns;
readOnly = true;
description = "Exposes netns parameters.";
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable (mkMerge [
# Prerequisites
networking.dhcpcd.denyInterfaces = [ "br0" "br-nb*" "nb-veth*" ]; # Base infrastructure
{
networking.dhcpcd.denyInterfaces = [ "nb-br" "nb-veth*" ];
services.tor.client.socksListenAddress = "${bridgeIp}:9050"; services.tor.client.socksListenAddress = "${bridgeIp}:9050";
networking.firewall.interfaces.br0.allowedTCPPorts = [ 9050 ]; networking.firewall.interfaces.nb-br.allowedTCPPorts = [ 9050 ];
boot.kernel.sysctl."net.ipv4.ip_forward" = true; boot.kernel.sysctl."net.ipv4.ip_forward" = true;
security.wrappers.netns-exec = { security.wrappers.netns-exec = {
source = "${pkgs.nix-bitcoin.netns-exec}/netns-exec"; source = pkgs.nix-bitcoin.netns-exec;
capabilities = "cap_sys_admin=ep"; capabilities = "cap_sys_admin=ep";
owner = "${config.nix-bitcoin.operatorName}"; owner = cfg.allowedUser;
permissions = "u+rx,g+rx,o-rwx"; permissions = "u+rx,g+rx,o-rwx";
}; };
systemd.services = {
# Due to a NixOS bug we can't currently use option `networking.bridges` to
# setup the bridge while `networking.useDHCP` is enabled.
nb-netns-bridge = {
description = "nix-bitcoin netns bridge";
wantedBy = [ "network-setup.service" ];
partOf = [ "network-setup.service" ];
before = [ "network-setup.service" ];
after = [ "network-pre.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = "yes";
};
script = ''
${ip} link add name nb-br type bridge
${ip} link set nb-br up
${ip} addr add ${bridgeIp}/24 brd + dev nb-br
${iptables} -w -t nat -A POSTROUTING -s 169.254.${toString cfg.addressblock}.0/24 -j MASQUERADE
'';
preStop = ''
${iptables} -w -t nat -D POSTROUTING -s 169.254.${toString cfg.addressblock}.0/24 -j MASQUERADE
${ip} link del nb-br
'';
};
} //
(let
makeNetnsServices = n: v: let
veth = "nb-veth-${toString v.id}";
peer = "nb-veth-br-${toString v.id}";
inherit (v) netnsName;
ipNetns = "${ip} -n ${netnsName}";
netnsIptables = "${ip} netns exec ${netnsName} ${config.networking.firewall.package}/bin/iptables";
in {
"${n}".serviceConfig.NetworkNamespacePath = "/var/run/netns/${netnsName}";
"netns-${n}" = rec {
requires = [ "nb-netns-bridge.service" ];
after = [ "nb-netns-bridge.service" ];
bindsTo = [ "${n}.service" ];
requiredBy = bindsTo;
before = bindsTo;
script = ''
${ip} netns add ${netnsName}
${ipNetns} link set lo up
${ip} link add ${veth} type veth peer name ${peer}
${ip} link set ${veth} netns ${netnsName}
${ipNetns} addr add ${v.address}/24 dev ${veth}
${ip} link set ${peer} up
${ipNetns} link set ${veth} up
${ip} link set ${peer} master nb-br
${ipNetns} route add default via ${bridgeIp}
${netnsIptables} -w -P INPUT DROP
${netnsIptables} -w -A INPUT -s 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT
'' + (optionalString (config.services.${n}.enforceTor or false)) ''
${netnsIptables} -w -P OUTPUT DROP
${netnsIptables} -w -A OUTPUT -d 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT
'' + concatMapStrings (otherNetns: let
other = netns.${otherNetns};
in ''
${netnsIptables} -w -A INPUT -s ${other.address} -j ACCEPT
${netnsIptables} -w -A OUTPUT -d ${other.address} -j ACCEPT
'') v.availableNetns;
preStop = ''
${ip} netns delete ${netnsName}
${ip} link del ${peer}
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = "yes";
ExecStartPre = "-${ip} netns delete ${netnsName}";
};
};
};
in foldl (services: n:
services // (makeNetnsServices n netns.${n})
) {} (builtins.attrNames netns));
}
# Service-specific config
{
nix-bitcoin.netns-isolation.services = { nix-bitcoin.netns-isolation.services = {
bitcoind = { bitcoind = {
id = 12; id = 12;
@ -106,12 +210,10 @@ in {
spark-wallet = { spark-wallet = {
id = 17; id = 17;
# communicates with clightning over lightning-rpc socket # communicates with clightning over lightning-rpc socket
connections = [];
}; };
lightning-charge = { lightning-charge = {
id = 18; id = 18;
# communicates with clightning over lightning-rpc socket # communicates with clightning over lightning-rpc socket
connections = [];
}; };
nanopos = { nanopos = {
id = 19; id = 19;
@ -120,11 +222,9 @@ in {
recurring-donations = { recurring-donations = {
id = 20; id = 20;
# communicates with clightning over lightning-rpc socket # communicates with clightning over lightning-rpc socket
connections = [];
}; };
nginx = { nginx = {
id = 21; id = 21;
connections = [];
}; };
lightning-loop = { lightning-loop = {
id = 22; id = 22;
@ -132,81 +232,6 @@ in {
}; };
}; };
systemd.services = {
netns-bridge = {
description = "Create bridge";
requiredBy = [ "tor.service" ];
before = [ "tor.service" ];
script = ''
${ip} link add name br0 type bridge
${ip} link set br0 up
${ip} addr add ${bridgeIp}/24 brd + dev br0
${iptables} -w -t nat -A POSTROUTING -s 169.254.${toString cfg.addressblock}.0/24 -j MASQUERADE
'';
preStop = ''
${iptables} -w -t nat -D POSTROUTING -s 169.254.${toString cfg.addressblock}.0/24 -j MASQUERADE
${ip} link del br0
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = "yes";
};
};
bitcoind-import-banlist.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-bitcoind";
} //
(let
makeNetnsServices = n: v: let
vethName = "nb-veth-${toString v.id}";
netnsName = "nb-${n}";
ipNetns = "${ip} -n ${netnsName}";
netnsIptables = "${ip} netns exec ${netnsName} ${config.networking.firewall.package}/bin/iptables";
in {
"${n}".serviceConfig.NetworkNamespacePath = "/var/run/netns/${netnsName}";
"netns-${n}" = rec {
requires = [ "netns-bridge.service" ];
after = [ "netns-bridge.service" ];
bindsTo = [ "${n}.service" ];
requiredBy = bindsTo;
before = bindsTo;
script = ''
${ip} netns add ${netnsName}
${ipNetns} link set lo up
${ip} link add ${vethName} type veth peer name br-${vethName}
${ip} link set ${vethName} netns ${netnsName}
${ipNetns} addr add ${v.address}/24 dev ${vethName}
${ip} link set br-${vethName} up
${ipNetns} link set ${vethName} up
${ip} link set br-${vethName} master br0
${ipNetns} route add default via ${bridgeIp}
${netnsIptables} -w -P INPUT DROP
${netnsIptables} -w -A INPUT -s 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT
'' + (optionalString (config.services.${n}.enforceTor or false)) ''
${netnsIptables} -w -P OUTPUT DROP
${netnsIptables} -w -A OUTPUT -d 127.0.0.1,${bridgeIp},${v.address} -j ACCEPT
'' + concatMapStrings (otherNetns: let
other = netns.${otherNetns};
in ''
${netnsIptables} -w -A INPUT -s ${other.address} -j ACCEPT
${netnsIptables} -w -A OUTPUT -d ${other.address} -j ACCEPT
'') v.availableNetns;
preStop = ''
${ip} netns delete ${netnsName}
${ip} link del br-${vethName}
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = "yes";
ExecStartPre = "-${ip} netns delete ${netnsName}";
};
};
};
in foldl (services: n:
services // (makeNetnsServices n netns.${n})
) {} (builtins.attrNames netns));
# bitcoin: Custom netns configs
services.bitcoind = { services.bitcoind = {
bind = netns.bitcoind.address; bind = netns.bitcoind.address;
rpcbind = [ rpcbind = [
@ -215,22 +240,21 @@ in {
]; ];
rpcallowip = [ rpcallowip = [
"127.0.0.1" "127.0.0.1"
] ++ lib.lists.concatMap (s: [ ] ++ map (n: "${netns.${n}.address}") netns.bitcoind.availableNetns;
"${netns.${s}.address}" cli = let
]) netns.bitcoind.availableNetns; inherit (config.services.bitcoind) cliBase;
cli = pkgs.writeScriptBin "bitcoin-cli" '' in pkgs.writeScriptBin cliBase.name ''
netns-exec nb-bitcoind ${config.services.bitcoind.package}/bin/bitcoin-cli -datadir='${config.services.bitcoind.dataDir}' "$@" exec netns-exec ${netns.bitcoind.netnsName} ${cliBase}/bin/${cliBase.name} "$@"
''; '';
}; };
systemd.services.bitcoind-import-banlist.serviceConfig.NetworkNamespacePath = "/var/run/netns/nb-bitcoind";
# clightning: Custom netns configs services.clightning = {
services.clightning = mkIf config.services.clightning.enable {
bitcoin-rpcconnect = netns.bitcoind.address; bitcoin-rpcconnect = netns.bitcoind.address;
bind-addr = netns.clightning.address; bind-addr = netns.clightning.address;
}; };
# lnd: Custom netns configs services.lnd = {
services.lnd = mkIf config.services.lnd.enable {
listen = netns.lnd.address; listen = netns.lnd.address;
rpclisten = [ rpclisten = [
"${netns.lnd.address}" "${netns.lnd.address}"
@ -241,16 +265,10 @@ in {
"127.0.0.1" "127.0.0.1"
]; ];
bitcoind-host = netns.bitcoind.address; bitcoind-host = netns.bitcoind.address;
cli = pkgs.writeScriptBin "lncli" cliExec = mkCliExec "lnd";
# Switch user because lnd makes datadir contents readable by user only
''
netns-exec nb-lnd sudo -u lnd ${config.services.lnd.package}/bin/lncli --tlscertpath ${config.nix-bitcoin.secretsDir}/lnd-cert \
--macaroonpath '${config.services.lnd.dataDir}/chain/bitcoin/mainnet/admin.macaroon' "$@"
'';
}; };
# liquidd: Custom netns configs services.liquidd = {
services.liquidd = mkIf config.services.liquidd.enable {
bind = netns.liquidd.address; bind = netns.liquidd.address;
rpcbind = [ rpcbind = [
"${netns.liquidd.address}" "${netns.liquidd.address}"
@ -258,49 +276,29 @@ in {
]; ];
rpcallowip = [ rpcallowip = [
"127.0.0.1" "127.0.0.1"
] ++ lib.lists.concatMap (s: [ ] ++ map (n: "${netns.${n}.address}") netns.liquidd.availableNetns;
"${netns.${s}.address}"
]) netns.liquidd.availableNetns;
mainchainrpchost = netns.bitcoind.address; mainchainrpchost = netns.bitcoind.address;
cli = pkgs.writeScriptBin "elements-cli" '' cliExec = mkCliExec "liquidd";
netns-exec nb-liquidd ${pkgs.nix-bitcoin.elementsd}/bin/elements-cli -datadir='${config.services.liquidd.dataDir}' "$@"
'';
swap-cli = pkgs.writeScriptBin "liquidswap-cli" ''
netns-exec nb-liquidd ${pkgs.nix-bitcoin.liquid-swap}/bin/liquidswap-cli -c '${config.services.liquidd.dataDir}/elements.conf' "$@"
'';
}; };
# electrs: Custom netns configs services.electrs = {
services.electrs = mkIf config.services.electrs.enable {
address = netns.electrs.address; address = netns.electrs.address;
daemonrpc = "${netns.bitcoind.address}:${toString config.services.bitcoind.rpc.port}"; daemonrpc = "${netns.bitcoind.address}:${toString config.services.bitcoind.rpc.port}";
}; };
# spark-wallet: Custom netns configs services.spark-wallet = {
services.spark-wallet = mkIf config.services.spark-wallet.enable {
host = netns.spark-wallet.address; host = netns.spark-wallet.address;
extraArgs = "--no-tls"; extraArgs = "--no-tls";
}; };
# lightning-charge: Custom netns configs services.lightning-charge.host = netns.lightning-charge.address;
services.lightning-charge.host = mkIf config.services.lightning-charge.enable netns.lightning-charge.address;
# nanopos: Custom netns configs services.nanopos = {
services.nanopos = mkIf config.services.nanopos.enable {
charged-url = "http://${netns.lightning-charge.address}:9112"; charged-url = "http://${netns.lightning-charge.address}:9112";
host = netns.nanopos.address; host = netns.nanopos.address;
}; };
# nginx: Custom netns configs services.lightning-loop.cliExec = mkCliExec "lightning-loop";
services.nix-bitcoin-webindex.host = mkIf config.services.nix-bitcoin-webindex.enable netns.nginx.address; }
]);
# loop: Custom netns configs
services.lightning-loop = mkIf config.services.lightning-loop.enable {
cli = pkgs.writeScriptBin "loop"
# Switch user because lnd makes datadir contents readable by user only
''
netns-exec nb-lightning-loop sudo -u lnd ${config.services.lightning-loop.package}/bin/loop "$@"
'';
};
};
} }

View File

@ -55,4 +55,11 @@ with lib;
set -eo pipefail set -eo pipefail
${src} ${src}
''; '';
cliExec = mkOption {
# Used by netns-isolation to execute the cli in the service's private netns
internal = true;
type = types.str;
default = "exec";
};
} }

View File

@ -41,7 +41,10 @@ in {
}; };
host = mkOption { host = mkOption {
type = types.str; type = types.str;
default = "localhost"; default = if config.nix-bitcoin.netns-isolation.enable then
config.nix-bitcoin.netns-isolation.netns.nginx.address
else
"localhost";
description = "HTTP server listen address."; description = "HTTP server listen address.";
}; };
enforceTor = nix-bitcoin-services.enforceTor; enforceTor = nix-bitcoin-services.enforceTor;
@ -77,13 +80,12 @@ in {
systemd.services.create-web-index = { systemd.services.create-web-index = {
description = "Get node info"; description = "Get node info";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
path = with pkgs; [ path = with pkgs; [
config.programs.nodeinfo config.programs.nodeinfo
config.services.clightning.cli
config.services.lnd.cli
jq jq
sudo sudo
]; ] ++ optional config.services.lnd.enable config.services.lnd.cli
++ optional config.services.clightning.enable config.services.clightning.cli;
serviceConfig = nix-bitcoin-services.defaultHardening // { serviceConfig = nix-bitcoin-services.defaultHardening // {
ExecStart="${pkgs.bash}/bin/bash ${createWebIndex}"; ExecStart="${pkgs.bash}/bin/bash ${createWebIndex}";
User = "root"; User = "root";

View File

@ -194,7 +194,9 @@ in {
port = 50001; port = 50001;
enforceTor = true; enforceTor = true;
}; };
services.tor.hiddenServices.electrs = mkHiddenService { port = cfg.electrs.port; toHost = cfg.electrs.address; }; services.tor.hiddenServices.electrs = mkIf cfg.electrs.enable (mkHiddenService {
port = cfg.electrs.port; toHost = cfg.electrs.address;
});
services.spark-wallet = { services.spark-wallet = {
onion-service = true; onion-service = true;
@ -236,6 +238,7 @@ in {
[ cfg.hardware-wallets.group ]); [ cfg.hardware-wallets.group ]);
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys; openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
}; };
nix-bitcoin.netns-isolation.allowedUser = operatorName;
# Give operator access to onion hostnames # Give operator access to onion hostnames
services.onion-chef.enable = true; services.onion-chef.enable = true;
services.onion-chef.access.${operatorName} = [ "bitcoind" "clightning" "nginx" "liquidd" "spark-wallet" "electrs" "sshd" ]; services.onion-chef.access.${operatorName} = [ "bitcoind" "clightning" "nginx" "liquidd" "spark-wallet" "electrs" "sshd" ];

View File

@ -5,7 +5,6 @@ stdenv.mkDerivation {
buildInputs = [ pkgs.libcap ]; buildInputs = [ pkgs.libcap ];
src = ./src; src = ./src;
installPhase = '' installPhase = ''
mkdir -p $out cp main $out
cp main $out/netns-exec
''; '';
} }

View File

@ -1,7 +1,4 @@
/* This program must be run with CAP_SYS_ADMIN. This can be achieved for example /* This program requires CAP_SYS_ADMIN */
* with
* # setcap CAP_SYS_ADMIN+ep ./main
*/
#define _GNU_SOURCE #define _GNU_SOURCE
#include <sched.h> #include <sched.h>
@ -12,18 +9,17 @@
#include <fcntl.h> #include <fcntl.h>
#include <sys/capability.h> #include <sys/capability.h>
static char *available_netns[] = { static char *allowed_netns[] = {
"nb-lnd", "nb-lnd",
"nb-lightning-loop", "nb-lightning-loop",
"nb-bitcoind", "nb-bitcoind",
"nb-liquidd" "nb-liquidd"
}; };
int check_netns(char *netns) { int is_netns_allowed(char *netns) {
int i; int n_allowed_netns = sizeof(allowed_netns) / sizeof(allowed_netns[0]);
int n_available_netns = sizeof(available_netns) / sizeof(available_netns[0]); for (int i = 0; i < n_allowed_netns; i++) {
for (i = 0; i < n_available_netns; i++) { if (strcmp(allowed_netns[i], netns) == 0) {
if (strcmp(available_netns[i], netns) == 0) {
return 1; return 1;
} }
} }
@ -35,6 +31,7 @@ void print_capabilities() {
printf("Capabilities: %s\n", cap_to_text(caps, NULL)); printf("Capabilities: %s\n", cap_to_text(caps, NULL));
cap_free(caps); cap_free(caps);
} }
void drop_capabilities() { void drop_capabilities() {
cap_t caps = cap_get_proc(); cap_t caps = cap_get_proc();
cap_clear(caps); cap_clear(caps);
@ -43,25 +40,24 @@ void drop_capabilities() {
} }
int main(int argc, char **argv) { int main(int argc, char **argv) {
int fd;
char netns_path[256]; char netns_path[256];
if (argc < 3) { if (argc < 3) {
printf("usage: %s <netns> <command to execute>\n", argv[0]); printf("usage: %s <netns> <command>\n", argv[0]);
return 1; return 1;
} }
if (!check_netns(argv[1])) { if (!is_netns_allowed(argv[1])) {
printf("Failed checking %s against available netns.\n", argv[1]); printf("%s is not an allowed netns.\n", argv[1]);
return 1; return 1;
} }
if(snprintf(netns_path, sizeof(netns_path), "/var/run/netns/%s", argv[1]) < 0) { if(snprintf(netns_path, sizeof(netns_path), "/var/run/netns/%s", argv[1]) < 0) {
printf("Failed concatenating %s to the netns path.\n", argv[1]); printf("Path length exceeded for netns %s.\n", argv[1]);
return 1; return 1;
} }
fd = open(netns_path, O_RDONLY); int fd = open(netns_path, O_RDONLY);
if (fd < 0) { if (fd < 0) {
printf("Failed opening netns %s: %d, %s \n", netns_path, errno, strerror(errno)); printf("Failed opening netns %s: %d, %s \n", netns_path, errno, strerror(errno));
return 1; return 1;
@ -84,4 +80,3 @@ int main(int argc, char **argv) {
execvp(argv[2], &argv[2]); execvp(argv[2], &argv[2]);
return 0; return 0;
} }

View File

@ -1,3 +1,6 @@
is_interactive = "is_interactive" in vars()
def succeed(*cmds): def succeed(*cmds):
"""Returns the concatenated output of all cmds""" """Returns the concatenated output of all cmds"""
return machine.succeed(*cmds) return machine.succeed(*cmds)
@ -29,15 +32,15 @@ def assert_running(unit):
assert_no_failure(unit) assert_no_failure(unit)
# Don't execute the following test suite when this script is running in interactive mode
if "is_interactive" in vars():
raise Exception()
### Tests
# The argument extra_tests is a dictionary from strings to functions. The string
# determines at which point of run_tests the corresponding function is executed.
def run_tests(extra_tests): def run_tests(extra_tests):
"""
:param extra_tests: Test functions that hook into the testing code below
:type extra_tests: Dict[str, Callable[]]
"""
# Don't execute the following test suite when this script is running in interactive mode
if is_interactive:
raise Exception()
test_security() test_security()
assert_running("bitcoind") assert_running("bitcoind")

View File

@ -4,53 +4,55 @@
# The test (./test.nix) uses the NixOS testing framework and is executed in a VM. # The test (./test.nix) uses the NixOS testing framework and is executed in a VM.
# #
# Usage: # Usage:
# Run test # Run all tests
# ./run-tests.sh
#
# Test specific scenario
# ./run-tests.sh --scenario <scenario> # ./run-tests.sh --scenario <scenario>
# #
# Run test and save result to avoid garbage collection # Run test and link results to avoid garbage collection
# ./run-tests.sh --scenario <scenario> build --out-link /tmp/nix-bitcoin-test # ./run-tests.sh [--scenario <scenario>] --out-link-prefix /tmp/nix-bitcoin-test build
#
# Pass extra args to nix-build
# ./run-tests.sh build --builders 'ssh://mybuildhost - - 15'
# #
# Run interactive test debugging # Run interactive test debugging
# ./run-tests.sh --scenario <scenario> debug # ./run-tests.sh [--scenario <scenario>] debug
# #
# This starts the testing VM and drops you into a Python REPL where you can # This starts the testing VM and drops you into a Python REPL where you can
# manually execute the tests from ./test-script.py # manually execute the tests from ./test-script.py
set -eo pipefail set -eo pipefail
die() {
printf '%s\n' "$1" >&2
exit 1
}
# Initialize all the option variables.
# This ensures we are not contaminated by variables from the environment.
scenario= scenario=
outLinkPrefix=
while :; do while :; do
case $1 in case $1 in
--scenario) --scenario|-s)
if [ "$2" ]; then if [[ $2 ]]; then
scenario=$2 scenario=$2
shift shift
else shift
die 'ERROR: "--scenario" requires a non-empty option argument.' else
fi >&2 echo 'Error: "$1" requires an argument.'
;; exit 1
-?*) fi
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 ;;
;; --out-link-prefix|-o)
*) if [[ $2 ]]; then
break outLinkPrefix=$2
shift
shift
else
>&2 echo 'Error: "$1" requires an argument.'
exit 1
fi
;;
*)
break
esac esac
shift
done done
if [[ -z $scenario ]]; then
die 'ERROR: "--scenario" is required'
fi
numCPUs=${numCPUs:-$(nproc)} numCPUs=${numCPUs:-$(nproc)}
# Min. 800 MiB needed to avoid 'out of memory' errors # Min. 800 MiB needed to avoid 'out of memory' errors
memoryMiB=${memoryMiB:-2048} memoryMiB=${memoryMiB:-2048}
@ -108,8 +110,13 @@ debug() {
} }
# Run the test by building the test derivation # Run the test by building the test derivation
build() { buildTest() {
vmTestNixExpr | nix-build --no-out-link "$@" - if [[ $outLinkPrefix ]]; then
buildArgs="--out-link $outLinkPrefix-$scenario"
else
buildArgs=--no-out-link
fi
vmTestNixExpr | nix-build $buildArgs "$@" -
} }
# On continuous integration nodes there are few other processes running alongside the # On continuous integration nodes there are few other processes running alongside the
@ -137,4 +144,18 @@ vmTestNixExpr() {
EOF EOF
} }
build() {
if [[ $scenario ]]; then
buildTest "$@"
else
scenario=default buildTest "$@"
scenario=withnetns buildTest "$@"
fi
}
# Set default scenario for all actions other than 'build'
if [[ $1 && $1 != build ]]; then
: ${scenario:=default}
fi
eval "${@:-build}" eval "${@:-build}"

View File

@ -1,14 +1,10 @@
# Integration test, can be run without internet access. # Integration test, can be run without internet access.
# Make sure to update build() in ./run-tests.sh when adding new scenarios
{ scenario ? "default" }: { scenario ? "default" }:
let
netns-isolation = builtins.getAttr scenario { default = false; withnetns = true; };
testScriptFilename = builtins.getAttr scenario { default = ./scenarios/default.py; withnetns = ./scenarios/withnetns.py; };
in
import ./make-test.nix rec { import ./make-test.nix rec {
name = "nix-bitcoin"; name = "nix-bitcoin-${scenario}";
hardened = { hardened = {
imports = [ <nixpkgs/nixos/modules/profiles/hardened.nix> ]; imports = [ <nixpkgs/nixos/modules/profiles/hardened.nix> ];
@ -23,7 +19,7 @@ import ./make-test.nix rec {
# hardened # hardened
]; ];
nix-bitcoin.netns-isolation.enable = mkForce netns-isolation; nix-bitcoin.netns-isolation.enable = (scenario == "withnetns");
services.bitcoind.extraConfig = mkForce "connect=0"; services.bitcoind.extraConfig = mkForce "connect=0";
@ -61,5 +57,6 @@ import ./make-test.nix rec {
install -o nobody -g nogroup -m777 <(:) /secrets/dummy install -o nobody -g nogroup -m777 <(:) /secrets/dummy
''; '';
}; };
testScript = builtins.readFile ./scenarios/lib.py + "\n\n" + builtins.readFile testScriptFilename; testScript =
builtins.readFile ./base.py + "\n\n" + builtins.readFile "${./.}/scenarios/${scenario}.py";
} }