diff --git a/.travis.yml b/.travis.yml index 2514c0a..1942ce7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,8 @@ env: # CACHIX_SIGNING_KEY - secure: "xXCFZ7g+k5YmCGm8R8l3bZElVmt+RD1KscG3kGr5w4HyyDPTzFetPo+sT8bUpysDU0u3HWhfVhHtpog2mhNhwVl3tQwKXea3dHKC1i6ypBg3gjDngmJRR5wo++ocYDpK8qPaU7m/jHQTNFnTA4CbmMcc05GcYx/1Ai/ZGkNwWFjdIcVeOUoiol33gykMOXIGDg2qlXudt33wP53FHbX8L4fxzodWfAuxKK4AoGprxy5eSnU7LCaXxxJmu4HwuV+Ux2U1NfE/E33cvhlUvTQCswVSZFG06mg8rwhMG1ozsDvlL2itZlu/BeUQH5y3XMMlnJIUXUazkRBibf1w/ebVjpOF+anqkqmq8tcbFEa7T+RJeVTIsvP+L8rE8fcmuZtdg9hNmgRnLmaeT0vVwD1L2UqW9HdRyujdoS0jPYuoc1W7f1JQWfAPhBPQ1SrtKyNNqcbVJ34aN7b+4vCzRpQL1JTbmjzQIWhkiKN1qMo1v/wbIydW8yka4hc4JOfdQLaAJEPI1eAC1MLotSAegMnwKWE1dzm66MuPSipksYjZrvsB28cV4aCVUffIuRhrSr1i2afRHwTpNbK9U4/576hah15ftUdR79Sfkcoi1ekSQTFGRvkRIPYtkKLYwFa3jVA41qz7+IIZCf4TsApy3XDdFx91cRub7yPq9BeZ83A+qYQ=" jobs: - - TestModules=1 STABLE=1 + - TestModules=1 STABLE=1 SCENARIO=default + - TestModules=1 STABLE=1 SCENARIO=withnetns - PKG=hwi STABLE=1 - PKG=hwi STABLE=0 - PKG=lightning-charge STABLE=1 @@ -38,7 +39,7 @@ script: exit 1 fi sudo chmod go+rw /dev/kvm - test/run-tests.sh exprForCI + test/run-tests.sh --scenario $SCENARIO exprForCI else echo "(import ./. {}).$PKG" fi diff --git a/examples/configuration.nix b/examples/configuration.nix index 7d4ff87..da23ca2 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -38,14 +38,19 @@ # Enable this module to use clightning, a Lightning Network implementation # in C. services.clightning.enable = true; - # Enable this option to listen for incoming lightning connections. By - # default nix-bitcoin nodes offer outgoing connectivity. - # services.clightning.autolisten = true; + # 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; ### LND # Disable clightning and uncomment the following line in order to enable lnd, # a lightning implementation written in Go. # 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; ## 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 @@ -117,6 +122,13 @@ # `docs/usage.md`. # services.hardware-wallets.trezor = true; + ### netns-isolation (EXPERIMENTAL) + # Enable this module to use Network Namespace Isolation. This feature places + # every service in its own network namespace and only allows truly necessary + # connections between network namespaces, making sure services are isolated on + # a network-level as much as possible. + # nix-bitcoin.netns-isolation.enable = true; + # FIXME: Define your hostname. networking.hostName = "nix-bitcoin"; time.timeZone = "UTC"; diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 653c52b..fd15040 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -18,6 +18,7 @@ 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.proxy != null) "proxy=${cfg.proxy}"} listen=${if cfg.listen then "1" else "0"} @@ -30,6 +31,8 @@ let (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (attrValues cfg.rpc.users) } + ${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} + ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} ${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"} ${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"} @@ -68,6 +71,13 @@ 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"; @@ -117,6 +127,20 @@ in { ''; }; }; + rpcbind = mkOption { + type = types.listOf 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. + ''; + }; rpcuser = mkOption { type = types.nullOr types.str; default = "bitcoinrpc"; @@ -233,11 +257,20 @@ in { }; cli = mkOption { type = types.package; + default = cfg.cli-nonetns-exec; + description = "Binary to connect with the bitcoind instance."; + }; + # Needed because bitcoind-import-banlist already executes inside + # nb-bitcoind, hence it doesn't need netns-exec prefixed. + cli-nonetns-exec = mkOption { readOnly = true; + type = types.package; default = pkgs.writeScriptBin "bitcoin-cli" '' exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@" ''; - description = "Binary to connect with the bitcoind instance."; + description = '' + Binary to connect with the bitcoind instance without netns-exec. + ''; }; enforceTor = nix-bitcoin-services.enforceTor; }; @@ -297,7 +330,7 @@ in { bindsTo = [ "bitcoind.service" ]; after = [ "bitcoind.service" ]; script = '' - cd ${cfg.cli}/bin + cd ${cfg.cli-nonetns-exec}/bin # Poll until bitcoind accepts commands. This can take a long time. while ! ./bitcoin-cli getnetworkinfo &> /dev/null; do sleep 1 diff --git a/modules/clightning.nix b/modules/clightning.nix index 1b6ffff..16b94b3 100644 --- a/modules/clightning.nix +++ b/modules/clightning.nix @@ -5,14 +5,15 @@ with lib; let cfg = config.services.clightning; inherit (config) nix-bitcoin-services; + onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []); configFile = pkgs.writeText "config" '' - autolisten=${if cfg.autolisten then "true" else "false"} network=bitcoin 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"} ${optionalString (cfg.bind-addr != null) "bind-addr=${cfg.bind-addr}"} - bitcoin-rpcuser=${cfg.bitcoin-rpcuser} + ${optionalString (cfg.bitcoin-rpcconnect != null) "bitcoin-rpcconnect=${cfg.bitcoin-rpcconnect}"} + bitcoin-rpcuser=${config.services.bitcoind.rpcuser} rpc-file-mode=0660 ''; in { @@ -28,7 +29,8 @@ in { type = types.bool; default = false; description = '' - If enabled, the clightning service will listen. + Bind (and maybe announce) on IPv4 and IPv6 interfaces if no addr, + bind-addr or announce-addr options are specified. ''; }; proxy = mkOption { @@ -48,11 +50,15 @@ in { default = null; description = "Set an IP address or UNIX domain socket to listen to"; }; - bitcoin-rpcuser = mkOption { - type = types.str; - description = '' - Bitcoin RPC user - ''; + announce-tor = mkOption { + type = types.bool; + default = false; + description = "Announce clightning Tor Hidden Service"; + }; + bitcoin-rpcconnect = mkOption { + type = types.nullOr types.str; + default = null; + description = "The bitcoind RPC host to connect to."; }; dataDir = mkOption { type = types.path; @@ -93,12 +99,13 @@ 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 = [ pkgs.nix-bitcoin.bitcoind ]; wantedBy = [ "multi-user.target" ]; - requires = [ "bitcoind.service" ]; - after = [ "bitcoind.service" ]; + requires = [ "bitcoind.service" ] ++ onion-chef-service; + after = [ "bitcoind.service" ] ++ onion-chef-service; preStart = '' cp ${configFile} ${cfg.dataDir}/config chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' @@ -106,6 +113,7 @@ in { rm -f ${cfg.dataDir}/bitcoin/lightning-rpc chmod 600 ${cfg.dataDir}/config echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/config' + ${optionalString cfg.announce-tor "echo announce-addr=$(cat /var/lib/onion-chef/clightning/clightning) >> '${cfg.dataDir}/config'"} ''; serviceConfig = nix-bitcoin-services.defaultHardening // { ExecStart = "${pkgs.nix-bitcoin.clightning}/bin/lightningd --lightning-dir=${cfg.dataDir}"; diff --git a/modules/electrs.nix b/modules/electrs.nix index 13b886a..a8773a6 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -17,6 +17,14 @@ in { default = "/var/lib/electrs"; description = "The data directory for electrs."; }; + # Needed until electrs tls proxy is removed + host = mkOption { + type = types.str; + default = "localhost"; + description = '' + The host on which incoming connections arrive. + ''; + }; user = mkOption { type = types.str; default = "electrs"; @@ -44,6 +52,13 @@ in { default = 50001; description = "RPC port."; }; + daemonrpc = mkOption { + type = types.str; + default = "127.0.0.1:8332"; + description = '' + Bitcoin daemon JSONRPC 'addr:port' to connect + ''; + }; extraArgs = mkOption { type = types.separatedString " "; default = ""; @@ -97,7 +112,8 @@ in { "--jsonrpc-import --index-batch-size=10" } \ --db-dir '${cfg.dataDir}' --daemon-dir '${config.services.bitcoind.dataDir}' \ - --electrum-rpc-addr=${toString cfg.address}:${toString cfg.port} ${cfg.extraArgs} + --electrum-rpc-addr=${toString cfg.address}:${toString cfg.port} \ + --daemon-rpc-addr=${toString cfg.daemonrpc} ${cfg.extraArgs} ''; User = cfg.user; Group = cfg.group; diff --git a/modules/lightning-charge.nix b/modules/lightning-charge.nix index e2d4ce5..18e27ba 100644 --- a/modules/lightning-charge.nix +++ b/modules/lightning-charge.nix @@ -21,6 +21,17 @@ in { default = "/var/lib/lightning-charge"; description = "The data directory for lightning-charge."; }; + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "http server listen address"; + }; + extraArgs = mkOption { + type = types.separatedString " "; + default = ""; + description = "Extra command line arguments passed to lightning-charge."; + }; + enforceTor = nix-bitcoin-services.enforceTor; }; config = mkIf cfg.enable { @@ -60,13 +71,15 @@ in { # Needed to access clightning.dataDir in preStart PermissionsStartOnly = "true"; EnvironmentFile = "${config.nix-bitcoin.secretsDir}/lightning-charge-env"; - ExecStart = "${pkgs.nix-bitcoin.lightning-charge}/bin/charged -l ${config.services.clightning.dataDir}/bitcoin -d ${cfg.dataDir}/lightning-charge.db"; + ExecStart = "${pkgs.nix-bitcoin.lightning-charge}/bin/charged -l ${config.services.clightning.dataDir}/bitcoin -d ${cfg.dataDir}/lightning-charge.db -i ${cfg.host} ${cfg.extraArgs}"; User = user; Restart = "on-failure"; RestartSec = "10s"; ReadWritePaths = "${cfg.dataDir}"; - } // nix-bitcoin-services.nodejs - // nix-bitcoin-services.allowTor; + } // (if cfg.enforceTor + then nix-bitcoin-services.allowTor + else nix-bitcoin-services.allowAnyIP) + // nix-bitcoin-services.nodejs; }; nix-bitcoin.secrets.lightning-charge-env.user = user; }; diff --git a/modules/liquid.nix b/modules/liquid.nix index 1e58cbb..ea15e31 100644 --- a/modules/liquid.nix +++ b/modules/liquid.nix @@ -15,6 +15,7 @@ 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.proxy != null) "proxy=${cfg.proxy}"} listen=${if cfg.listen then "1" else "0"} @@ -25,8 +26,11 @@ let (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (attrValues cfg.rpc.users) } + ${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} + ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} ${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"} ${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"} + ${optionalString (cfg.mainchainrpchost != null) "mainchainrpchost=${cfg.mainchainrpchost}"} # Extra config options (from liquidd nixos service) ${cfg.extraConfig} @@ -80,6 +84,13 @@ 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; @@ -111,6 +122,20 @@ in { }; }; + rpcbind = mkOption { + type = types.listOf 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. + ''; + }; rpcuser = mkOption { type = types.nullOr types.str; default = null; @@ -121,6 +146,14 @@ in { default = null; description = "Password for JSON-RPC connections"; }; + mainchainrpchost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The address which the daemon will try to connect to the trusted + mainchain daemon to validate peg-ins. + ''; + }; testnet = mkOption { type = types.bool; @@ -177,14 +210,12 @@ in { ''; }; cli = mkOption { - readOnly = true; default = pkgs.writeScriptBin "elements-cli" '' exec ${pkgs.nix-bitcoin.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@" ''; description = "Binary to connect with the liquidd instance."; }; swap-cli = mkOption { - readOnly = true; default = pkgs.writeScriptBin "liquidswap-cli" '' exec ${pkgs.nix-bitcoin.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@" ''; diff --git a/modules/lnd.nix b/modules/lnd.nix index ef60dcb..8e5ccc3 100644 --- a/modules/lnd.nix +++ b/modules/lnd.nix @@ -5,6 +5,7 @@ with lib; let cfg = config.services.lnd; inherit (config) nix-bitcoin-services; + onion-chef-service = (if cfg.announce-tor then [ "onion-chef.service" ] else []); secretsDir = config.nix-bitcoin.secretsDir; configFile = pkgs.writeText "lnd.conf" '' datadir=${cfg.dataDir} @@ -13,17 +14,17 @@ let tlscertpath=${secretsDir}/lnd-cert tlskeypath=${secretsDir}/lnd-key - rpclisten=localhost:${toString cfg.rpcPort} - restlisten=localhost:${toString cfg.restPort} + listen=${toString cfg.listen} + ${lib.concatMapStrings (rpclisten: "rpclisten=${rpclisten}:${toString cfg.rpcPort}\n") cfg.rpclisten} + ${lib.concatMapStrings (restlisten: "restlisten=${restlisten}:${toString cfg.restPort}\n") cfg.restlisten} bitcoin.active=1 bitcoin.node=bitcoind tor.active=true - tor.v3=true - tor.streamisolation=true - tor.privatekeypath=${cfg.dataDir}/v3_onion_private_key + ${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"} + bitcoind.rpchost=${cfg.bitcoind-host} bitcoind.rpcuser=${config.services.bitcoind.rpcuser} bitcoind.zmqpubrawblock=${config.services.bitcoind.zmqpubrawblock} bitcoind.zmqpubrawtx=${config.services.bitcoind.zmqpubrawtx} @@ -45,6 +46,25 @@ in { default = "/var/lib/lnd"; description = "The data directory for LND."; }; + listen = mkOption { + type = types.str; + default = "localhost"; + description = "Bind to given address to listen to peer connections"; + }; + rpclisten = mkOption { + type = types.listOf types.str; + default = [ "localhost" ]; + description = '' + Bind to given address to listen to RPC connections. + ''; + }; + restlisten = mkOption { + type = types.listOf types.str; + default = [ "localhost" ]; + description = '' + Bind to given address to listen to REST connections. + ''; + }; rpcPort = mkOption { type = types.port; default = 10009; @@ -55,6 +75,23 @@ in { default = 8080; description = "Port on which to listen for REST connections."; }; + bitcoind-host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + The host that your local bitcoind daemon is listening on. + ''; + }; + tor-socks = mkOption { + type = types.nullOr types.str; + default = 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"; + }; extraConfig = mkOption { type = types.lines; default = ""; @@ -70,7 +107,6 @@ in { description = "The package providing lnd binaries."; }; cli = mkOption { - readOnly = true; default = pkgs.writeScriptBin "lncli" # Switch user because lnd makes datadir contents readable by user only '' @@ -96,19 +132,21 @@ in { ]; services.bitcoind = { - zmqpubrawblock = "tcp://127.0.0.1:28332"; - zmqpubrawtx = "tcp://127.0.0.1:28333"; + zmqpubrawblock = "tcp://${cfg.bitcoind-host}:28332"; + zmqpubrawtx = "tcp://${cfg.bitcoind-host}:28333"; }; + services.onion-chef.access.lnd = if cfg.announce-tor then [ "lnd" ] else []; systemd.services.lnd = { description = "Run LND"; path = [ pkgs.nix-bitcoin.bitcoind ]; wantedBy = [ "multi-user.target" ]; - requires = [ "bitcoind.service" ]; - after = [ "bitcoind.service" ]; + requires = [ "bitcoind.service" ] ++ onion-chef-service; + after = [ "bitcoind.service" ] ++ onion-chef-service; preStart = '' install -m600 ${configFile} '${cfg.dataDir}/lnd.conf' echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/lnd.conf' + ${optionalString cfg.announce-tor "echo externalip=$(cat /var/lib/onion-chef/lnd/lnd) >> '${cfg.dataDir}/lnd.conf'"} ''; serviceConfig = nix-bitcoin-services.defaultHardening // { ExecStart = "${cfg.package}/bin/lnd --configfile=${cfg.dataDir}/lnd.conf"; diff --git a/modules/modules.nix b/modules/modules.nix index 93774c4..6eb1d04 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -14,6 +14,7 @@ ./hardware-wallets.nix ./lnd.nix ./secrets/secrets.nix + ./netns-isolation.nix ]; disabledModules = [ "services/networking/bitcoind.nix" ]; diff --git a/modules/nanopos.nix b/modules/nanopos.nix index 91af50c..eb410d0 100644 --- a/modules/nanopos.nix +++ b/modules/nanopos.nix @@ -49,6 +49,26 @@ in { "The items file (see nanopos README)."; ''; }; + charged-url = mkOption { + type = types.str; + default = "http://localhost:9112"; + description = '' + "The lightning charge server url."; + ''; + }; + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + "http server listen address."; + ''; + }; + extraArgs = mkOption { + type = types.separatedString " "; + default = ""; + description = "Extra command line arguments passed to nanopos."; + }; + enforceTor = nix-bitcoin-services.enforceTor; }; config = mkIf cfg.enable { @@ -59,6 +79,20 @@ in { ]; environment.systemPackages = [ pkgs.nix-bitcoin.nanopos ]; + + services.nginx = { + enable = true; + virtualHosts."_" = { + root = "/var/www"; + extraConfig = '' + location /store/ { + proxy_pass http://${toString cfg.host}:${toString cfg.port}; + rewrite /store/(.*) /$1 break; + } + ''; + }; + }; + systemd.services.nanopos = { description = "Run nanopos"; wantedBy = [ "multi-user.target" ]; @@ -66,12 +100,14 @@ in { after = [ "lightning-charge.service" ]; serviceConfig = nix-bitcoin-services.defaultHardening // { EnvironmentFile = "${config.nix-bitcoin.secretsDir}/nanopos-env"; - ExecStart = "${pkgs.nix-bitcoin.nanopos}/bin/nanopos -y ${cfg.itemsFile} -p ${toString cfg.port} --show-bolt11"; + ExecStart = "${pkgs.nix-bitcoin.nanopos}/bin/nanopos -y ${cfg.itemsFile} -i ${toString cfg.host} -p ${toString cfg.port} -c ${toString cfg.charged-url} --show-bolt11 ${cfg.extraArgs}"; User = "nanopos"; Restart = "on-failure"; RestartSec = "10s"; - } // nix-bitcoin-services.nodejs - // nix-bitcoin-services.allowTor; + } // (if cfg.enforceTor + then nix-bitcoin-services.allowTor + else nix-bitcoin-services.allowAnyIP) + // nix-bitcoin-services.nodejs; }; users.users.nanopos = { description = "nanopos User"; diff --git a/modules/netns-isolation.nix b/modules/netns-isolation.nix new file mode 100644 index 0000000..22eaf12 --- /dev/null +++ b/modules/netns-isolation.nix @@ -0,0 +1,303 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.nix-bitcoin.netns-isolation; + + netns = builtins.mapAttrs (n: v: { + inherit (v) id; + address = "169.254.${toString cfg.addressblock}.${toString v.id}"; + availableNetns = builtins.filter isEnabled availableNetns.${n}; + }) enabledServices; + + # Symmetric netns connection matrix + # if clightning.connections = [ "bitcoind" ]; then + # availableNetns.bitcoind = [ "clighting" ]; + # and + # availableNetns.clighting = [ "bitcoind" ]; + availableNetns = let + # base = { clightning = [ "bitcoind" ]; ... } + base = builtins.mapAttrs (n: v: + builtins.filter isEnabled v.connections + ) enabledServices; + in + foldl (xs: s1: + foldl (xs: s2: + xs // { "${s2}" = xs.${s2} ++ [ s1 ]; } + ) xs cfg.services.${s1}.connections + ) base (builtins.attrNames base); + + enabledServices = filterAttrs (n: v: isEnabled n) cfg.services; + isEnabled = x: config.services.${x}.enable; + + ip = "${pkgs.iproute}/bin/ip"; + iptables = "${config.networking.firewall.package}/bin/iptables"; + + bridgeIp = "169.254.${toString cfg.addressblock}.10"; + +in { + options.nix-bitcoin.netns-isolation = { + enable = mkEnableOption "netns isolation"; + + addressblock = mkOption { + type = types.ints.u8; + default = "1"; + description = '' + Specify the N address block in 169.254.N.0/24. + ''; + }; + + services = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + id = mkOption { + # TODO: Exclude 10 + # TODO: Assert uniqueness + type = types.int; + description = '' + id for the netns, that is used for the IP address host part and + naming the interfaces. Must be unique. Must not be 10. + ''; + }; + connections = mkOption { + type = with types; listOf str; + default = []; + }; + }; + }); + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + # Prerequisites + networking.dhcpcd.denyInterfaces = [ "br0" "br-nb*" "nb-veth*" ]; + services.tor.client.socksListenAddress = "${bridgeIp}:9050"; + networking.firewall.interfaces.br0.allowedTCPPorts = [ 9050 ]; + boot.kernel.sysctl."net.ipv4.ip_forward" = true; + security.wrappers.netns-exec = { + source = "${pkgs.nix-bitcoin.netns-exec}/netns-exec"; + capabilities = "cap_sys_admin=ep"; + owner = "${config.nix-bitcoin.operatorName}"; + permissions = "u+rx,g+rx,o-rwx"; + }; + + nix-bitcoin.netns-isolation.services = { + bitcoind = { + id = 12; + }; + clightning = { + id = 13; + connections = [ "bitcoind" ]; + }; + lnd = { + id = 14; + connections = [ "bitcoind" ]; + }; + liquidd = { + id = 15; + connections = [ "bitcoind" ]; + }; + electrs = { + id = 16; + connections = [ "bitcoind" ] + ++ ( optionals config.services.electrs.TLSProxy.enable [ "nginx" ]); + }; + spark-wallet = { + id = 17; + # communicates with clightning over lightning-rpc socket + connections = []; + }; + lightning-charge = { + id = 18; + # communicates with clightning over lightning-rpc socket + connections = []; + }; + nanopos = { + id = 19; + connections = [ "nginx" "lightning-charge" ]; + }; + recurring-donations = { + id = 20; + # communicates with clightning over lightning-rpc socket + connections = []; + }; + nginx = { + id = 21; + connections = []; + }; + }; + + 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 = { + bind = netns.bitcoind.address; + rpcbind = [ + "${netns.bitcoind.address}" + "127.0.0.1" + ]; + rpcallowip = [ + "127.0.0.1" + ] ++ lib.lists.concatMap (s: [ + "${netns.${s}.address}" + ]) netns.bitcoind.availableNetns; + cli = pkgs.writeScriptBin "bitcoin-cli" '' + netns-exec nb-bitcoind ${config.services.bitcoind.package}/bin/bitcoin-cli -datadir='${config.services.bitcoind.dataDir}' "$@" + ''; + }; + + # clightning: Custom netns configs + services.clightning = mkIf config.services.clightning.enable { + bitcoin-rpcconnect = netns.bitcoind.address; + bind-addr = "${netns.clightning.address}:${toString config.services.clightning.onionport}"; + }; + + # lnd: Custom netns configs + services.lnd = mkIf config.services.lnd.enable { + listen = netns.lnd.address; + rpclisten = [ + "${netns.lnd.address}" + "127.0.0.1" + ]; + restlisten = [ + "${netns.lnd.address}" + "127.0.0.1" + ]; + bitcoind-host = netns.bitcoind.address; + cli = pkgs.writeScriptBin "lncli" + # 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 = mkIf config.services.liquidd.enable { + bind = netns.liquidd.address; + rpcbind = [ + "${netns.liquidd.address}" + "127.0.0.1" + ]; + rpcallowip = [ + "127.0.0.1" + ] ++ lib.lists.concatMap (s: [ + "${netns.${s}.address}" + ]) netns.liquidd.availableNetns; + mainchainrpchost = netns.bitcoind.address; + cli = pkgs.writeScriptBin "elements-cli" '' + 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 = mkIf config.services.electrs.enable { + host = if config.services.electrs.TLSProxy.enable then netns.nginx.address else netns.electrs.address; + address = netns.electrs.address; + daemonrpc = "${netns.bitcoind.address}:${toString config.services.bitcoind.rpc.port}"; + }; + + # spark-wallet: Custom netns configs + services.spark-wallet = mkIf config.services.spark-wallet.enable { + host = netns.spark-wallet.address; + extraArgs = "--no-tls"; + }; + + # lightning-charge: Custom netns configs + services.lightning-charge.host = mkIf config.services.lightning-charge.enable netns.lightning-charge.address; + + # nanopos: Custom netns configs + services.nanopos = mkIf config.services.nanopos.enable { + charged-url = "http://${netns.lightning-charge.address}:9112"; + host = netns.nanopos.address; + }; + + # nginx: Custom netns configs + services.nix-bitcoin-webindex.host = mkIf config.services.nix-bitcoin-webindex.enable netns.nginx.address; + + }) + # Custom netns config option values if netns-isolation not enabled + (mkIf (!cfg.enable) { + # clightning + services.clightning.bind-addr = "127.0.0.1:${toString config.services.clightning.onionport}"; + }) + ]; +} diff --git a/modules/nix-bitcoin-services.nix b/modules/nix-bitcoin-services.nix index daf1355..097567e 100644 --- a/modules/nix-bitcoin-services.nix +++ b/modules/nix-bitcoin-services.nix @@ -36,7 +36,7 @@ with lib; nodejs = { MemoryDenyWriteExecute = "false"; }; # Allow tor traffic. Allow takes precedence over Deny. allowTor = { - IPAddressAllow = "127.0.0.1/32 ::1/128"; + IPAddressAllow = "127.0.0.1/32 ::1/128 169.254.0.0/16"; }; # Allow any traffic allowAnyIP = { IPAddressAllow = "any"; }; diff --git a/modules/nix-bitcoin-webindex.nix b/modules/nix-bitcoin-webindex.nix index 39ba9b9..b75ab2e 100644 --- a/modules/nix-bitcoin-webindex.nix +++ b/modules/nix-bitcoin-webindex.nix @@ -13,11 +13,7 @@ let nix-bitcoin
--