diff --git a/README.md b/README.md index 4e6267f..0c5f2d8 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,9 @@ NixOS modules ([src](modules/modules.nix)) * [Lightning Loop](https://github.com/lightninglabs/loop) * [Lightning Pool](https://github.com/lightninglabs/pool) * [charge-lnd](https://github.com/accumulator/charge-lnd): policy-based channel fee manager - * [lndconnect](https://github.com/LN-Zap/lndconnect): connect your wallet to lnd or clightning via a REST onion service + * [lndconnect](https://github.com/LN-Zap/lndconnect): connect your wallet to lnd or + clightning [via WireGuard](./docs/services.md#use-zeus-mobile-lightning-wallet-via-wireguard) or + [Tor](./docs/services.md#use-zeus-mobile-lightning-wallet-via-tor) * [Ride The Lightning](https://github.com/Ride-The-Lightning/RTL): web interface for `lnd` and `clightning` * [spark-wallet](https://github.com/shesek/spark-wallet) * [electrs](https://github.com/romanz/electrs): Electrum server diff --git a/dev/dev-scenarios.nix b/dev/dev-scenarios.nix index cd70754..9091e65 100644 --- a/dev/dev-scenarios.nix +++ b/dev/dev-scenarios.nix @@ -45,4 +45,34 @@ with lib; nix-bitcoin.nodeinfo.enable = true; # test.container.enableWAN = true; }; + + wireguard-lndconnect-online = { config, pkgs, lib, ... }: { + imports = [ + ../modules/presets/wireguard.nix + scenarios.regtestBase + ]; + + # 51820 (default wg port) + 1 + networking.wireguard.interfaces.wg-nb.listenPort = 51821; + test.container.enableWAN = true; + # test.container.exposeLocalhost = true; + + services.clightning.extraConfig = "disable-dns"; + + services.lnd = { + enable = true; + lndconnect = { + enable = true; + onion = true; + }; + }; + services.clightning-rest = { + enable = true; + lndconnect = { + enable = true; + onion = true; + }; + }; + nix-bitcoin.nodeinfo.enable = true; + }; } diff --git a/dev/topics/lndconnect-and-wireguard.sh b/dev/topics/lndconnect-and-wireguard.sh new file mode 100644 index 0000000..7f40ff0 --- /dev/null +++ b/dev/topics/lndconnect-and-wireguard.sh @@ -0,0 +1,64 @@ +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Test Tor and WireGuard connections on a mobile device + +# 1. Run container +run-tests.sh -s wireguard-lndconnect-online container + +# 2. Test connecting via Tor +# Print QR codes for lnd, clightning-rest connections via Tor +c lndconnect +c lndconnect-clightning +# Add these to Zeus >= 0.7.1. +# To explicitly check if the connection is successful, press the node logo in the top +# left corner, and then "Node Info". + +# Debug +c lndconnect --url +c lndconnect-clightning --url + +# 3. Test connecting via WireGuard + +# 3.1 Forward WireGuard port from the container host to the container +iptables -t nat -A PREROUTING -p udp --dport 51821 -j DNAT --to-destination 10.225.255.2 + +# 3.2. Optional: When your container host has an external firewall, +# forward the WireGuard port to the container host: +# - Port: 51821 +# - Protocol: UDP +# - Destination: IPv4 of the container host + +# 3.2 Print QR code and setup wireguard on the mobile device +c nix-bitcoin-wg-connect +c nix-bitcoin-wg-connect --text + +# Print QR codes for lnd, clightning-rest connections via WireGuard +c lndconnect-wg +c lndconnect-clightning-wg +# Add these to Zeus >= 0.7.1. +# To explicitly check if the connection is successful, press the node logo in the top +# left corner, and then "Node Info". + +# Debug +c lndconnect-wg --url +c lndconnect-clightning-wg --url + +# 3.3.remove external firewall port forward, remove local port forward: +iptables -t nat -D PREROUTING -p udp --dport 51821 -j DNAT --to-destination 10.225.255.2 +# Now exit the container shell + +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Debug lndconnect + +run-tests.sh -s wireguard-lndconnect-online container + +c nodeinfo + +c lndconnect --url +c lndconnect-wg --url +c lndconnect-clightning --url +c lndconnect-clightning-wg --url + +c lndconnect +c lndconnect-wg +c lndconnect-clightning +c lndconnect-clightning-wg diff --git a/docs/services.md b/docs/services.md index 27c9698..d677b57 100644 --- a/docs/services.md +++ b/docs/services.md @@ -142,60 +142,154 @@ You can find the `` with command `nodeinfo`. The default password location is `$secretsDir/rtl-password`. See: [Secrets dir](./configuration.md#secrets-dir) -# Use LND or clightning with Zeus (mobile wallet) via Tor -1. Install [Zeus](https://zeusln.app) +# Use Zeus (mobile lightning wallet) via Tor +1. Install [Zeus](https://zeusln.app) (version ≥ 0.7.1) 2. Edit your `configuration.nix` ##### For lnd Add the following config: - ``` - services.lnd.lndconnectOnion.enable = true; + ```nix + services.lnd.lndconnect = { + enable = true; + onion = true; + }; ``` ##### For clightning Add the following config: - ``` + ```nix services.clightning-rest = { enable = true; - lndconnectOnion.enable = true; + lndconnect = { + enable = true; + onion = true; + }; }; ``` 3. Deploy your configuration -3. Run the following command on your node (as user `operator`) to create a QR code +4. Run the following command on your node (as user `operator`) to create a QR code with address and authentication information: ##### For lnd ``` - lndconnect-onion + lndconnect ``` ##### For clightning ``` - lndconnect-onion-clightning + lndconnect-clightning ``` -4. Configure Zeus - - Add a new node - - Select `Scan lndconnect config` (at the bottom) and scan the QR code - - For clightning: Set `Node interface` to `c-lightning-REST` +5. Configure Zeus + - Add a new node and scan the QR code - Click `Save node config` - Start sending and stacking sats privately ### Additional lndconnect features -Create plain text URLs or QR code images: -``` -lndconnect-onion --url -lndconnect-onion --image +- Create a plain text URL: + ```bash + lndconnect --url + ``` +- Set a custom host. By default, `lndconnect` detects the system's external IP and uses it as the host. + ```bash + lndconnect --host myhost + ``` + +# Use Zeus (mobile lightning wallet) via WireGuard + +Connecting Zeus directly to your node is much faster than using Tor, but a bit more complex to setup. + +There are two ways to establish a secure, direct connection: + +- Connecting via TLS. This requires installing your lightning app's + TLS Certificate on your mobile device. + +- Connecting via WireGuard. This approach is simpler and more versatile, and is + described in this guide. + +1. Install [Zeus](https://zeusln.app) (version ≥ 0.7.1) and + [WireGuard](https://www.wireguard.com/install/) on your mobile device. + +2. Add the following to your `configuration.nix`: + ```nix + imports = [ + # Use this line when using the default deployment method + + + # Use this line when using Flakes + (nix-bitcoin + /modules/presets/wireguard.nix) + ] + + # For lnd + services.lnd.lndconnect.enable = true; + + # For clightning + services.clightning-rest = { + enable = true; + lndconnect.enable = true; + }; + ``` +3. Deploy your configuration. + +4. If your node is behind an external firewall or NAT, add the following port forwarding + rule to the external device: + - Port: 51820 (the default value of option `networking.wireguard.interfaces.wg-nb.listenPort`) + - Protocol: UDP + - Destination: IP of your node + +5. Setup WireGuard on your mobile device. + + Run the following command on your node (as user `operator`) to create a QR code + for WireGuard: + ```bash + nix-bitcoin-wg-connect + + # For debugging: Show the WireGuard config as text + nix-bitcoin-wg-connect --text + ``` + The above commands automatically detect your node's external IP.\ + To set a custom IP or hostname, run the following: + ``` + nix-bitcoin-wg-connect 93.184.216.34 + nix-bitcoin-wg-connect mynode.org + ``` + + Configure WireGuard: + - Press the `+` button in the bottom right corner + - Scan the QR code + - Add the tunnel + +6. Setup Zeus + + Run the following command on your node (as user `operator`) to create a QR code for Zeus: + + ##### For lnd + ``` + lndconnect-wg + ``` + + ##### For clightning + ``` + lndconnect-clightning-wg + ``` + + Configure Zeus: + - Add a new node and scan the QR code + - Click `Save node config` + - On the certificate warning screen, click `I understand, save node config`.\ + Certificates are not needed when connecting via WireGuard. + - Start sending and stacking sats privately + +### Additional lndconnect features +Create a plain text URL: +```bash +lndconnect-wg --url `````` -Create a QR code for a custom hostname: -``` -lndconnect-onion --host=mynode.org -``` # Connect to spark-wallet ### Requirements diff --git a/examples/configuration.nix b/examples/configuration.nix index 0305b1c..1cc8122 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -56,13 +56,18 @@ # # == REST server # Set this to create a clightning REST onion service. - # This also adds binary `lndconnect-onion-clightning` to the system environment. + # This also adds binary `lndconnect-clightning` to the system environment. # This binary creates QR codes or URLs for connecting applications to clightning - # via the REST onion service (see ../docs/services.md). + # via the REST onion service. + # You can also connect via WireGuard instead of Tor. + # See ../docs/services.md for details. # # services.clightning-rest = { # enable = true; - # lndconnectOnion.enable = true; + # lndconnect = { + # enable = true; + # onion = true; + # }; # }; ### LND @@ -78,11 +83,17 @@ # The onion service is automatically announced to peers. # nix-bitcoin.onionServices.lnd.public = true; # - # Set this to create an lnd REST onion service. - # This also adds binary `lndconnect-onion` to the system environment. + # Set this to create a lnd REST onion service. + # This also adds binary `lndconnect` to the system environment. # This binary generates QR codes or URLs for connecting applications to lnd via the - # REST onion service (see ../docs/services.md). - # services.lnd.lndconnectOnion.enable = true; + # REST onion service. + # You can also connect via WireGuard instead of Tor. + # See ../docs/services.md for details. + # + # services.lnd.lndconnect = { + # enable = true; + # onion = true; + # }; # ## WARNING # If you use lnd, you should manually backup your wallet mnemonic diff --git a/modules/lndconnect-onion.nix b/modules/lndconnect-onion.nix deleted file mode 100644 index 43b044e..0000000 --- a/modules/lndconnect-onion.nix +++ /dev/null @@ -1,126 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; -let - options = { - services.lnd.lndconnectOnion.enable = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - Create an onion service for the lnd REST server. - Add a `lndconnect-onion` binary to the system environment. - See: https://github.com/LN-Zap/lndconnect - - Usage: - ```bash - # Print QR code - lndconnect-onion - - # Print URL - lndconnect-onion --url - ``` - ''; - }; - - services.clightning-rest.lndconnectOnion.enable = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - Create an onion service for clightning-rest. - Add a `lndconnect-onion-clightning` binary to the system environment. - See: https://github.com/LN-Zap/lndconnect - - Usage: - ```bash - # Print QR code - lndconnect-onion-clightning - - # Print URL - lndconnect-onion-clightning --url - ``` - ''; - }; - }; - - nbLib = config.nix-bitcoin.lib; - runAsUser = config.nix-bitcoin.runAsUserCmd; - - inherit (config.services) - lnd - clightning - clightning-rest; - - mkLndconnect = { - name, - shebang ? "#!${pkgs.stdenv.shell} -e", - onionService, - port, - certPath, - macaroonPath - }: - # TODO-EXTERNAL: - # lndconnect requires a --configfile argument, although it's unused - # https://github.com/LN-Zap/lndconnect/issues/25 - pkgs.writeScriptBin name '' - ${shebang} - exec ${config.nix-bitcoin.pkgs.lndconnect}/bin/lndconnect \ - --host=$(cat ${config.nix-bitcoin.onionAddresses.dataDir}/${onionService}) \ - --port=${toString port} \ - --tlscertpath='${certPath}' \ - --adminmacaroonpath='${macaroonPath}' \ - --configfile=/dev/null "$@" - ''; - - operatorName = config.nix-bitcoin.operator.name; -in { - inherit options; - - config = mkMerge [ - (mkIf (lnd.enable && lnd.lndconnectOnion.enable) { - services.tor = { - enable = true; - relay.onionServices.lnd-rest = nbLib.mkOnionService { - target.addr = nbLib.address lnd.restAddress; - target.port = lnd.restPort; - port = lnd.restPort; - }; - }; - nix-bitcoin.onionAddresses.access.${lnd.user} = [ "lnd-rest" ]; - - environment.systemPackages = [( - mkLndconnect { - name = "lndconnect-onion"; - # Run as lnd user because the macaroon and cert are not group-readable - shebang = "#!/usr/bin/env -S ${runAsUser} ${lnd.user} ${pkgs.bash}/bin/bash"; - onionService = "${lnd.user}/lnd-rest"; - port = lnd.restPort; - certPath = lnd.certPath; - macaroonPath = "${lnd.networkDir}/admin.macaroon"; - } - )]; - }) - - (mkIf (clightning-rest.enable && clightning-rest.lndconnectOnion.enable) { - services.tor = { - enable = true; - relay.onionServices.clightning-rest = nbLib.mkOnionService { - target.addr = nbLib.address clightning-rest.address; - target.port = clightning-rest.port; - port = clightning-rest.port; - }; - }; - # This also allows nodeinfo to show the clightning-rest onion address - nix-bitcoin.onionAddresses.access.${operatorName} = [ "clightning-rest" ]; - - environment.systemPackages = [( - mkLndconnect { - name = "lndconnect-onion-clightning"; - onionService = "${operatorName}/clightning-rest"; - port = clightning-rest.port; - certPath = "${clightning-rest.dataDir}/certs/certificate.pem"; - macaroonPath = "${clightning-rest.dataDir}/certs/access.macaroon"; - } - )]; - }) - ]; -} diff --git a/modules/lndconnect.nix b/modules/lndconnect.nix new file mode 100644 index 0000000..bc39ab8 --- /dev/null +++ b/modules/lndconnect.nix @@ -0,0 +1,205 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options = { + services.lnd.lndconnect = { + enable = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Add a `lndconnect` binary to the system environment which prints + connection info for lnd clients. + See: https://github.com/LN-Zap/lndconnect + + Usage: + ```bash + # Print QR code + lndconnect + + # Print URL + lndconnect --url + ``` + ''; + }; + onion = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Create an onion service for the lnd REST server, + which is used by lndconnect. + ''; + }; + }; + + + services.clightning-rest.lndconnect = { + enable = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Add a `lndconnect-clightning` binary to the system environment which prints + connection info for clightning clients. + See: https://github.com/LN-Zap/lndconnect + + Usage: + ```bash + # Print QR code + lndconnect-clightning + + # Print URL + lndconnect-clightning --url + ``` + ''; + }; + onion = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Create an onion service for the clightning REST server, + which is used by lndconnect. + ''; + }; + }; + + nix-bitcoin.mkLndconnect = mkOption { + readOnly = true; + default = mkLndconnect; + description = mdDoc '' + A function to create a lndconnect binary. + See the source for further details. + ''; + }; + }; + + nbLib = config.nix-bitcoin.lib; + runAsUser = config.nix-bitcoin.runAsUserCmd; + + inherit (config.services) + lnd + clightning-rest; + + mkLndconnect = { + name, + shebang ? "#!${pkgs.stdenv.shell} -e", + isClightning ? false, + port, + macaroonPath, + enableOnion, + onionService ? null, + certPath ? null + }: + # TODO-EXTERNAL: + # lndconnect requires a --configfile argument, although it's unused + # https://github.com/LN-Zap/lndconnect/issues/25 + pkgs.hiPrio (pkgs.writeScriptBin name '' + ${shebang} + url=$( + ${getExe config.nix-bitcoin.pkgs.lndconnect} --url \ + ${optionalString enableOnion "--host=$(cat ${config.nix-bitcoin.onionAddresses.dataDir}/${onionService})"} \ + --port=${toString port} \ + ${if enableOnion || certPath == null then "--nocert" else "--tlscertpath='${certPath}'"} \ + --adminmacaroonpath='${macaroonPath}' \ + --configfile=/dev/null "$@" + ) + + ${optionalString isClightning + # - Change URL procotcol to c-lightning-rest + # - Encode macaroon as hex (in uppercase) instead of base 64. + # Because `macaroon` is always the last URL fragment, the + # sed replacement below works correctly. + '' + macaroonHex=$(${getExe pkgs.xxd} -p -u -c 99999 '${macaroonPath}') + url=$( + echo "$url" | ${getExe pkgs.gnused} " + s|^lndconnect|c-lightning-rest| + s|macaroon=.*|macaroon=$macaroonHex| + "; + ) + '' + } + + # If --url is in args + if [[ " $* " =~ " --url " ]]; then + echo "$url" + else + # This UTF-8 encoding yields a smaller, more convenient output format + # compared to the native lndconnect output + echo -n "$url" | ${getExe pkgs.qrencode} -t UTF8 -o - + fi + ''); + + operatorName = config.nix-bitcoin.operator.name; +in { + inherit options; + + config = mkMerge [ + (mkIf (lnd.enable && lnd.lndconnect.enable) + (mkMerge [ + { + environment.systemPackages = [( + mkLndconnect { + name = "lndconnect"; + # Run as lnd user because the macaroon and cert are not group-readable + shebang = "#!/usr/bin/env -S ${runAsUser} ${lnd.user} ${pkgs.bash}/bin/bash"; + enableOnion = lnd.lndconnect.onion; + onionService = "${lnd.user}/lnd-rest"; + port = lnd.restPort; + certPath = lnd.certPath; + macaroonPath = "${lnd.networkDir}/admin.macaroon"; + } + )]; + + services.lnd.restAddress = mkIf (!lnd.lndconnect.onion) "0.0.0.0"; + } + + (mkIf lnd.lndconnect.onion { + services.tor = { + enable = true; + relay.onionServices.lnd-rest = nbLib.mkOnionService { + target.addr = nbLib.address lnd.restAddress; + target.port = lnd.restPort; + port = lnd.restPort; + }; + }; + nix-bitcoin.onionAddresses.access = { + ${lnd.user} = [ "lnd-rest" ]; + ${operatorName} = [ "lnd-rest" ]; + }; + }) + ])) + + (mkIf (clightning-rest.enable && clightning-rest.lndconnect.enable) + (mkMerge [ + { + environment.systemPackages = [( + mkLndconnect { + name = "lndconnect-clightning"; + isClightning = true; + enableOnion = clightning-rest.lndconnect.onion; + onionService = "${operatorName}/clightning-rest"; + port = clightning-rest.port; + certPath = "${clightning-rest.dataDir}/certs/certificate.pem"; + macaroonPath = "${clightning-rest.dataDir}/certs/access.macaroon"; + } + )]; + + # clightning-rest always binds to all interfaces + } + + (mkIf clightning-rest.lndconnect.onion { + services.tor = { + enable = true; + relay.onionServices.clightning-rest = nbLib.mkOnionService { + target.addr = nbLib.address clightning-rest.address; + target.port = clightning-rest.port; + port = clightning-rest.port; + }; + }; + # This also allows nodeinfo to show the clightning-rest onion address + nix-bitcoin.onionAddresses.access.${operatorName} = [ "clightning-rest" ]; + }) + ]) + ) + ]; +} diff --git a/modules/modules.nix b/modules/modules.nix index fd39077..71d4b88 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -19,7 +19,7 @@ ./lightning-loop.nix ./lightning-pool.nix ./charge-lnd.nix - ./lndconnect-onion.nix # Requires onion-addresses.nix + ./lndconnect.nix # Requires onion-addresses.nix ./rtl.nix ./electrs.nix ./fulcrum.nix diff --git a/modules/nodeinfo.nix b/modules/nodeinfo.nix index 8af59d9..3f906b8 100644 --- a/modules/nodeinfo.nix +++ b/modules/nodeinfo.nix @@ -63,7 +63,7 @@ let infos = OrderedDict() operator = "${config.nix-bitcoin.operator.name}" - def set_onion_address(info, name, port): + def get_onion_address(name, port): path = f"/var/lib/onion-addresses/{operator}/{name}" try: with open(path, "r") as f: @@ -71,7 +71,7 @@ let except OSError: print(f"error reading file {path}", file=sys.stderr) return - info["onion_address"] = f"{onion_address}:{port}" + return f"{onion_address}:{port}" def add_service(service, make_info, systemd_service = None): systemd_service = systemd_service or service @@ -106,7 +106,7 @@ let add_service("${name}", """ info["local_address"] = "${nbLib.addressWithPort cfg.address cfg.port}" '' + mkIfOnionPort name (onionPort: '' - set_onion_address(info, "${name}", ${onionPort}) + info["onion_address"] = get_onion_address("${name}", ${onionPort}) '') + extraCode + '' """, "${systemdServiceName}") @@ -123,8 +123,10 @@ let in { inherit options; - config = { - environment.systemPackages = optional cfg.enable script; + config = mkIf cfg.enable { + environment.systemPackages = [ script ]; + + nix-bitcoin.operator.enable = true; nix-bitcoin.nodeinfo.services = with nodeinfoLib; { bitcoind = mkInfo ""; @@ -133,9 +135,13 @@ in { if 'onion_address' in info: info["id"] = f"{info['nodeid']}@{info['onion_address']}" ''; - lnd = mkInfo '' + lnd = name: cfg: mkInfo ('' + info["rest_address"] = "${nbLib.addressWithPort cfg.restAddress cfg.restPort}" + '' + mkIfOnionPort "lnd-rest" (onionPort: '' + info["onion_rest_address"] = get_onion_address("lnd-rest", ${onionPort}) + '') + '' info["nodeid"] = shell("lncli getinfo | jq -r '.identity_pubkey'") - ''; + '') name cfg; clightning-rest = mkInfo ""; electrs = mkInfo ""; fulcrum = mkInfo ""; @@ -146,7 +152,7 @@ in { rtl = mkInfo ""; # Only add sshd when it has an onion service sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: '' - add_service("sshd", """set_onion_address(info, "sshd", ${onionPort})""") + add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""") ''); }; }; diff --git a/modules/obsolete-options.nix b/modules/obsolete-options.nix index 1465963..f568e1a 100644 --- a/modules/obsolete-options.nix +++ b/modules/obsolete-options.nix @@ -33,7 +33,6 @@ in { (mkRenamedOptionModule [ "services" "liquidd" "rpcbind" ] [ "services" "liquidd" "rpc" "address" ]) # 0.0.70 (mkRenamedOptionModule [ "services" "rtl" "cl-rest" ] [ "services" "clightning-rest" ]) - (mkRenamedOptionModule [ "services" "lnd" "restOnionService" "enable" ] [ "services" "lnd" "lndconnectOnion" "enable" ]) (mkRenamedOptionModule [ "nix-bitcoin" "setup-secrets" ] [ "nix-bitcoin" "setupSecrets" ]) @@ -46,6 +45,28 @@ in { bitcoin peer connections for syncing blocks. This performs well on low and high memory systems. '') + # 0.0.86 + (mkRemovedOptionModule [ "services" "lnd" "restOnionService" "enable" ] '' + Set the following options instead: + services.lnd.lndconnect = { + enable = true; + onion = true; + } + '') + (mkRemovedOptionModule [ "services" "lnd" "lndconnect-onion" ] '' + Set the following options instead: + services.lnd.lndconnect = { + enable = true; + onion = true; + } + '') + (mkRemovedOptionModule [ "services" "clightning-rest" "lndconnect-onion" ] '' + Set the following options instead: + services.clightning-rest.lndconnect = { + enable = true; + onion = true; + } + '') ] ++ # 0.0.59 (map mkSplitEnforceTorOption [ diff --git a/modules/presets/wireguard.nix b/modules/presets/wireguard.nix new file mode 100644 index 0000000..6121ba0 --- /dev/null +++ b/modules/presets/wireguard.nix @@ -0,0 +1,214 @@ +{ config, pkgs, lib, ... }: + +# Create a WireGuard server with a single peer. +# Private/public keys are created via the secrets system. +# Add helper binaries `nix-bitcoin-wg-connect` and optionally `lndconnect-wg`, `lndconnect-clightning-wg`. + +# See ../../docs/services.md ("Use Zeus (mobile lightning wallet) via WireGuard") +# for usage instructions. + +# This is a rather opinionated implementation that lacks the flexibility offered by +# other nix-bitcoin modules, so ship this as a `preset`. +# Some users will prefer to use `lndconnect` with their existing WireGuard or Tailscale setup. + +with lib; +let + options.nix-bitcoin.wireguard = { + subnet = mkOption { + type = types.str; + default = "10.10.0"; + description = mdDoc "The /24 subnet of the wireguard network."; + }; + restrictPeer = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Prevent the peer from connecting to any addresses except for the WireGuard server address. + ''; + }; + }; + + cfg = config.nix-bitcoin.wireguard; + wgSubnet = cfg.subnet; + inherit (config.networking.wireguard.interfaces) wg-nb; + inherit (config.services) + lnd + clightning-rest; + + lndconnect = lnd.enable && lnd.lndconnect.enable; + lndconnect-clightning = clightning-rest.enable && clightning-rest.lndconnect.enable; + + serverAddress = "${wgSubnet}.1"; + peerAddress = "${wgSubnet}.2"; + + secretsDir = config.nix-bitcoin.secretsDir; + + wgConnectUser = if config.nix-bitcoin.operator.enable + then config.nix-bitcoin.operator.name + else "root"; + + # A script that prints a QR code to connect a peer to the server. + # The QR code encodes a wg-quick config that can be imported by the wireguard + # mobile app. + wgConnect = pkgs.writers.writeBashBin "nix-bitcoin-wg-connect" '' + set -euo pipefail + text= + host= + for arg in "$@"; do + case $arg in + --text) + text=1 + ;; + *) + host=$arg + ;; + esac + done + + if [[ ! $host ]]; then + # Use lndconnect to fetch the external ip. + # This internally uses https://github.com/GlenDC/go-external-ip, which + # queries a set of external ip providers. + host=$( + ${getExe config.nix-bitcoin.pkgs.lndconnect} --url --nocert \ + --configfile=/dev/null --adminmacaroonpath=/dev/null \ + | sed -nE 's|.*?/(.*?):.*|\1|p' + ) + fi + + config="[Interface] + PrivateKey = $(cat ${secretsDir}/wg-peer-private-key) + Address = ${peerAddress}/24 + + [Peer] + PublicKey = $(cat ${secretsDir}/wg-server-public-key) + AllowedIPs = ${wgSubnet}.0/24 + Endpoint = $host:${toString wg-nb.listenPort} + PersistentKeepalive = 25 + " + + if [[ $text ]]; then + echo "$config" + else + echo "$config" | ${getExe pkgs.qrencode} -t UTF8 -o - + fi + ''; +in { + inherit options; + + config = { + assertions = [ + { + # Don't support `netns-isolation` for now to keep things simple + assertion = !(config.nix-bitcoin.netns-isolation.enable or false); + message = "`nix-bitcoin.wireguard` is not compatible with `netns-isolation`."; + } + ]; + + networking.wireguard.interfaces.wg-nb = { + ips = [ "${serverAddress}/24" ]; + listenPort = mkDefault 51820; + privateKeyFile = "${secretsDir}/wg-server-private-key"; + allowedIPsAsRoutes = false; + peers = [ + { + # To use the actual public key from the secrets file, use dummy pubkey + # `peer0` and replace it via `getPubkeyFromFile` (see further below) + # at peer service runtime. + publicKey = "peer0"; + allowedIPs = [ "${peerAddress}/32" ]; + } + ]; + }; + + systemd.services = { + wireguard-wg-nb = rec { + wants = [ "nix-bitcoin-secrets.target" ]; + after = wants; + }; + + # HACK: Modify start/stop scripts of the peer setup service to read + # the pubkey from a secrets file. + wireguard-wg-nb-peer-peer0 = let + getPubkeyFromFile = mkBefore '' + if [[ ! -v inPatchedSrc ]]; then + export inPatchedSrc=1 + publicKey=$(cat "${secretsDir}/wg-peer-public-key") + <"''${BASH_SOURCE[0]}" sed "s|\bpeer0\b|$publicKey|g" | ${pkgs.bash}/bin/bash -s + exit + fi + ''; + in { + script = getPubkeyFromFile; + postStop = getPubkeyFromFile; + }; + }; + + environment.systemPackages = [ + wgConnect + ] ++ (optional lndconnect + (pkgs.writers.writeBashBin "lndconnect-wg" '' + exec lndconnect --host "${serverAddress}" --nocert "$@" + '') + ) ++ (optional lndconnect-clightning + (pkgs.writers.writeBashBin "lndconnect-clightning-wg" '' + exec lndconnect-clightning --host "${serverAddress}" --nocert "$@" + '') + ); + + networking.firewall = let + restrictPeerRule = "-s ${peerAddress} ! -d ${serverAddress} -j REJECT"; + in { + allowedUDPPorts = [ wg-nb.listenPort ]; + + extraCommands = + optionalString lndconnect '' + iptables -w -A nixos-fw -p tcp -s ${wgSubnet}.0/24 --dport ${toString lnd.restPort} -j nixos-fw-accept + '' + + optionalString lndconnect-clightning '' + iptables -w -A nixos-fw -p tcp -s ${wgSubnet}.0/24 --dport ${toString clightning-rest.port} -j nixos-fw-accept + '' + + optionalString cfg.restrictPeer '' + iptables -w -A nixos-fw ${restrictPeerRule} + iptables -w -A FORWARD ${restrictPeerRule} + ''; + + extraStopCommands = + # Rules added to chain `nixos-fw` are automatically removed when restarting + # the NixOS firewall service. + mkIf cfg.restrictPeer '' + iptables -w -D FORWARD ${restrictPeerRule} || : + ''; + }; + + # Listen on all addresses, including `serverAddress`. + # This is safe because the listen ports are secured by the firewall. + services.lnd.restAddress = mkIf lndconnect "0.0.0.0"; + # clightning-rest always listens on "0.0.0.0" + + nix-bitcoin.secrets = { + wg-server-private-key = {}; + wg-server-public-key = { user = wgConnectUser; group = "root"; }; + wg-peer-private-key = { user = wgConnectUser; group = "root"; }; + wg-peer-public-key = {}; + }; + + nix-bitcoin.generateSecretsCmds.wireguard = let + wg = "${pkgs.wireguard-tools}/bin/wg"; + in '' + makeWireguardKey() { + local name=$1 + local priv=wg-$name-private-key + local pub=wg-$name-public-key + if [[ ! -e $priv ]]; then + ${wg} genkey > $priv + fi + if [[ $priv -nt $pub ]]; then + ${wg} pubkey < $priv > $pub + fi + } + makeWireguardKey server + makeWireguardKey peer + ''; + }; +} diff --git a/modules/versioning.nix b/modules/versioning.nix index 8254132..5bd2769 100644 --- a/modules/versioning.nix +++ b/modules/versioning.nix @@ -228,7 +228,7 @@ let version = "0.0.70"; condition = config.services.lnd.lndconnectOnion.enable; message = '' - The `lndconnect-rest-onion` binary has been renamed to `lndconnect-onion`. + The `lndconnect-rest-onion` binary has been renamed to `lndconnect`. ''; } { diff --git a/test/run-tests.sh b/test/run-tests.sh index 97957e3..7c0b67a 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -274,6 +274,7 @@ buildable=( hardened clightning-replication lndPruned + wireguard-lndconnect ) buildable() { buildTests buildable "$@"; } diff --git a/test/tests.nix b/test/tests.nix index 36d192e..2501ef6 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -86,8 +86,8 @@ let nix-bitcoin.onionServices.lnd.public = true; - tests.lndconnect-onion-lnd = cfg.lnd.lndconnectOnion.enable; - tests.lndconnect-onion-clightning = cfg.clightning-rest.lndconnectOnion.enable; + tests.lndconnect-onion-lnd = with cfg.lnd.lndconnect; enable && onion; + tests.lndconnect-onion-clightning = with cfg.clightning-rest.lndconnect; enable && onion; tests.lightning-loop = cfg.lightning-loop.enable; services.lightning-loop.certificate.extraIPs = [ "20.0.0.1" ]; @@ -187,9 +187,9 @@ let services.rtl.enable = true; services.spark-wallet.enable = true; services.clightning-rest.enable = true; - services.clightning-rest.lndconnectOnion.enable = true; + services.clightning-rest.lndconnect = { enable = true; onion = true; }; services.lnd.enable = true; - services.lnd.lndconnectOnion.enable = true; + services.lnd.lndconnect = { enable = true; onion = true; }; services.lightning-loop.enable = true; services.lightning-pool.enable = true; services.charge-lnd.enable = true; @@ -405,6 +405,7 @@ in { in { clightning-replication = import ./clightning-replication.nix makeTestVM pkgs; + wireguard-lndconnect = import ./wireguard-lndconnect.nix makeTestVM pkgs; } // mainTests; tests = makeTests scenarios; diff --git a/test/tests.py b/test/tests.py index d43f801..1959c7d 100644 --- a/test/tests.py +++ b/test/tests.py @@ -177,12 +177,12 @@ def _(): @test("lndconnect-onion-lnd") def _(): assert_running("lnd") - assert_matches("runuser -u operator -- lndconnect-onion --url", ".onion") + assert_matches("runuser -u operator -- lndconnect --url", ".onion") @test("lndconnect-onion-clightning") def _(): assert_running("clightning-rest") - assert_matches("runuser -u operator -- lndconnect-onion-clightning --url", ".onion") + assert_matches("runuser -u operator -- lndconnect-clightning --url", ".onion") @test("lightning-loop") def _(): diff --git a/test/wireguard-lndconnect.nix b/test/wireguard-lndconnect.nix new file mode 100644 index 0000000..1feb79c --- /dev/null +++ b/test/wireguard-lndconnect.nix @@ -0,0 +1,103 @@ +# You can run this test via `run-tests.sh -s wireguard-lndconnect` + +makeTestVM: pkgs: +with pkgs.lib; + +makeTestVM { + name = "wireguard-lndconnect"; + + nodes = { + server = { + imports = [ + ../modules/modules.nix + ../modules/presets/wireguard.nix + ]; + + nixpkgs.pkgs = pkgs; + + nix-bitcoin.generateSecrets = true; + nix-bitcoin.operator.enable = true; + + services.clightning-rest = { + enable = true; + lndconnect.enable = true; + }; + # TODO-EXTERNAL: + # When WAN is disabled, DNS bootstrapping slows down service startup by ~15 s. + services.clightning.extraConfig = "disable-dns"; + + services.lnd = { + enable = true; + lndconnect.enable = true; + port = 9736; + }; + }; + + client = { + nixpkgs.pkgs = pkgs; + + environment.systemPackages = with pkgs; [ + wireguard-tools + ]; + }; + }; + + testScript = '' + import base64 + import urllib.parse as Url + from types import SimpleNamespace + + def parse_lndconnect_url(url): + u = Url.urlparse(url) + queries = Url.parse_qs(u.query) + macaroon = queries['macaroon'][0] + is_clightning = url.startswith("c-lightning-rest") + + return SimpleNamespace( + host = u.hostname, + port = u.port, + macaroon_hex = + macaroon if is_clightning else base64.urlsafe_b64decode(macaroon + '===').hex().upper() + ) + + client.start() + server.connect() + + if not "is_interactive" in vars(): + + with subtest("connect client to server via WireGuard"): + server.wait_for_unit("wireguard-wg-nb-peer-peer0.service") + + # Get WireGuard config from server and save it to `/tmp/wireguard.conf` on the client + wg_config = server.succeed("runuser -u operator -- nix-bitcoin-wg-connect server --text") + # Encode to base64 + b64 = base64.b64encode(wg_config.encode('utf-8')).decode() + client.succeed(f"install -m 400 <(echo -n {b64} | base64 -d) /tmp/wireguard.conf") + + # Connect to server via WireGuard + client.succeed("wg-quick up /tmp/wireguard.conf") + + # Ping server from client + print(client.succeed("ping -c 1 -W 0.5 10.10.0.1")) + + with subtest("lndconnect-wg"): + server.wait_for_unit("lnd.service") + lndconnect_url = server.succeed("runuser -u operator -- lndconnect-wg --url") + api = parse_lndconnect_url(lndconnect_url) + # Make lnd REST API call + client.succeed( + f"curl -fsS --max-time 3 --insecure --header 'Grpc-Metadata-macaroon: {api.macaroon_hex}' " + f"-X GET https://{api.host}:{api.port}/v1/getinfo" + ) + + with subtest("lndconnect-clightning-wg"): + server.wait_for_unit("clightning-rest.service") + lndconnect_url = server.succeed("runuser -u operator -- lndconnect-clightning-wg --url") + api = parse_lndconnect_url(lndconnect_url) + # Make clightning-rest API call + client.succeed( + f"curl -fsS --max-time 3 --insecure --header 'macaroon: {api.macaroon_hex}' " + f"--header 'encodingtype: hex' -X GET https://{api.host}:{api.port}/v1/getinfo" + ) + ''; +}