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 be62fa5..d677b57 100644 --- a/docs/services.md +++ b/docs/services.md @@ -200,6 +200,97 @@ See: [Secrets dir](./configuration.md#secrets-dir) 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 +`````` + # Connect to spark-wallet ### Requirements * Android phone diff --git a/examples/configuration.nix b/examples/configuration.nix index 8cf8f93..1cc8122 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -58,7 +58,9 @@ # Set this to create a clightning REST onion service. # 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; @@ -84,7 +86,10 @@ # 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). + # 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; 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/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 ed513d6..2501ef6 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -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/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" + ) + ''; +}