Compare commits
6
Commits
v1.1.1
...
67ae53ad3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67ae53ad3f | ||
|
|
db5b9f6b60 | ||
|
|
2ea1427766 | ||
|
|
b35b327a07 | ||
|
|
a9ff168fd6 | ||
|
|
b436dbed95 |
No files matched your search
@@ -43,6 +43,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
manual re-pin.
|
||||
|
||||
### Fixed
|
||||
- Zeus Connect QR in the Hub was invalid: the LND-only `lndconnect`
|
||||
wrapper used flags Zeus/`lndconnect` do not understand (`--cert`,
|
||||
`--macaroon`), read a non-existent onion path (`free/lnd.onion`),
|
||||
and collided with LND's P2P hidden service. The wrapper now matches
|
||||
the nix-bitcoin contract — dedicated `lnd-rest` onion on port 8080,
|
||||
`--nocert` over Tor, admin macaroon in the URI — so scanning the Hub
|
||||
QR in Zeus works again.
|
||||
- The Hub launcher now uses a persistent per-user browser profile
|
||||
(`$XDG_STATE_HOME/sovran-hub-browser`) instead of a throwaway `/tmp`
|
||||
profile that was deleted on exit. The throwaway profile wiped the
|
||||
|
||||
@@ -26,7 +26,7 @@ are ready.
|
||||
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.1.iso.sha256) ·
|
||||
[Build from Source](#build-from-source)
|
||||
|
||||
<img src="assets/desktop-screenshot.png" alt="Sovran_SystemsOS private Bitcoin desktop" width="800" />
|
||||
<img src="assets/desktop-screenshot.webp" alt="Sovran_SystemsOS private Bitcoin desktop" width="800" />
|
||||
|
||||
*Bitcoin sovereignty from the first boot.*
|
||||
|
||||
@@ -281,6 +281,10 @@ From one place, the Hub helps you:
|
||||
- Reach your Bitcoin tools, private cloud, and communications
|
||||
- Perform supported system operations without everyday terminal commands
|
||||
|
||||
<img src="assets/sovran-hub-screenshot.webp" alt="The Sovran Hub dashboard" width="800" />
|
||||
|
||||
*The Sovran Hub: manage your private infrastructure from one place.*
|
||||
|
||||
### Example home setup
|
||||
|
||||
```text
|
||||
|
||||
@@ -487,9 +487,9 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
"Sovran_SystemsOS makes running a production-grade payment gateway as simple as flipping a switch."
|
||||
),
|
||||
"zeus-connect-setup.service": (
|
||||
"Connect the Zeus mobile wallet to your Lightning node via LND REST. Send and receive "
|
||||
"Connect the Zeus mobile wallet to your Lightning node via LND REST over Tor. Send and receive "
|
||||
"Lightning payments from your phone using a direct node connection. "
|
||||
"Scan the QR code to add your node to Zeus — this gives full node admin access."
|
||||
"Scan the QR code to add your node to Zeus, then enable Use Tor — this gives full node admin access."
|
||||
),
|
||||
"mempool.service": (
|
||||
"Your own blockchain explorer and mempool visualizer. Monitor transactions, "
|
||||
@@ -1532,31 +1532,38 @@ def _evaluate_domain_checklist(
|
||||
|
||||
def _generate_qr_png_bytes(data: str, scale: int = 6, margin: int = 2) -> bytes | None:
|
||||
"""Generate a QR code PNG and return the raw bytes.
|
||||
Uses qrencode CLI (available on the system via credentials.nix)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["qrencode", "-o", "-", "-t", "PNG", "-s", str(scale), "-m", str(margin), "-l", "H", data],
|
||||
capture_output=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
return result.stdout
|
||||
except Exception:
|
||||
pass
|
||||
Uses qrencode CLI (available on the system via credentials.nix).
|
||||
|
||||
High error-correction (H) is preferred for short payloads. Long
|
||||
lndconnect URIs can exceed version-40 capacity at H, so fall back to
|
||||
quartile then low ECC — otherwise the Hub shows an empty Zeus QR.
|
||||
"""
|
||||
for ecc in ("H", "Q", "L"):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["qrencode", "-o", "-", "-t", "PNG", "-s", str(scale), "-m", str(margin), "-l", ecc, data],
|
||||
capture_output=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
return result.stdout
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _generate_qr_svg(data: str, scale: int = 10, margin: int = 4) -> str | None:
|
||||
"""Generate a QR code SVG document (resolution-independent, ideal if the
|
||||
user wants to embed the QR in a website or print it at any size)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["qrencode", "-o", "-", "-t", "SVG", "-s", str(scale), "-m", str(margin), "-l", "H", data],
|
||||
capture_output=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
return result.stdout.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
for ecc in ("H", "Q", "L"):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["qrencode", "-o", "-", "-t", "SVG", "-s", str(scale), "-m", str(margin), "-l", ecc, data],
|
||||
capture_output=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
return result.stdout.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@@ -1680,6 +1687,9 @@ def _resolve_credential(cred: dict) -> dict | None:
|
||||
qr_data = _generate_qr_base64(result["value"])
|
||||
if qr_data:
|
||||
result["qrcode"] = qr_data
|
||||
else:
|
||||
# Don't hide the URI if we could not render a scannable QR.
|
||||
qronly = False
|
||||
if qronly:
|
||||
result["qronly"] = True
|
||||
return result
|
||||
@@ -1714,6 +1724,9 @@ def _resolve_credential(cred: dict) -> dict | None:
|
||||
qr_data = _generate_qr_base64(value)
|
||||
if qr_data:
|
||||
result["qrcode"] = qr_data
|
||||
else:
|
||||
# Don't hide the URI if we could not render a scannable QR.
|
||||
qronly = False
|
||||
|
||||
if qronly:
|
||||
result["qronly"] = True
|
||||
|
||||
@@ -11,9 +11,9 @@ function _getZeusConnectGuideHtml() {
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the LND REST QR and fills in the connection details. Review them, then tap <strong>Save Wallet Config</strong>.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the LND REST QR and fills in the connection details. Turn <strong>Use Tor</strong> on (the host is a .onion address), then tap <strong>Save Wallet Config</strong>.</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the NWC pairing QR shown in Lightning Wallet Connections — that gives your wallet sandboxed, limited access for everyday spending. LND REST connects Zeus directly to your node for full admin control.</div>' +
|
||||
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the NWC pairing QR shown in Lightning Wallet Connections — that gives your wallet sandboxed, limited access for everyday spending. LND REST connects Zeus directly to your node for full admin control. The QR uses your dedicated LND REST Tor address (no TLS cert) so Zeus can scan and connect over Tor.</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 442 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 392 KiB |
@@ -151,6 +151,7 @@
|
||||
dig firefox wp-cli axel
|
||||
lk-jwt-service livekit-libwebrtc livekit
|
||||
matrix-synapse age onlyoffice-desktopeditors
|
||||
tor-browser
|
||||
];
|
||||
|
||||
# ── Shell ──────────────────────────────────────────────────
|
||||
|
||||
@@ -264,13 +264,16 @@ in {
|
||||
curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}";
|
||||
restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1";
|
||||
# Setting macaroon permissions for other users needs root permissions
|
||||
# The admin macaroon is passed to curl via a fd because argv is
|
||||
# world-readable through /proc/<pid>/cmdline
|
||||
script = nbLib.rootScript "lnd-create-macaroons" ''
|
||||
umask ug=r,o=
|
||||
${lib.concatMapStrings (macaroon: ''
|
||||
echo "Create custom macaroon ${macaroon}"
|
||||
macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon"
|
||||
adminMacaroonHex=$(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')
|
||||
${curl} \
|
||||
-H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \
|
||||
-H @<(printf 'Grpc-Metadata-macaroon: %s\n' "$adminMacaroonHex") \
|
||||
-X POST \
|
||||
-d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \
|
||||
${restUrl}/macaroon |\
|
||||
|
||||
@@ -1,63 +1,115 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
# LND-only lndconnect wrapper. Restored to the fort-nix/nix-bitcoin contract
|
||||
# after the LND-only rewrite shipped a Zeus QR that Zeus cannot use:
|
||||
# - unknown flags (--cert / --macaroon instead of --tlscertpath / --adminmacaroonpath)
|
||||
# - onion hostname read from /var/lib/tor/onion/free/lnd/hostname (does not exist)
|
||||
# - REST hidden service named "lnd", colliding with the LND P2P onion
|
||||
# - TLS cert embedded in the URI (localhost CN + QR too dense to scan)
|
||||
#
|
||||
# Zeus needs: lndconnect://<lnd-rest-onion>:8080?macaroon=<admin> (no cert over Tor)
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.lnd;
|
||||
operatorName = config.nix-bitcoin.operator.name;
|
||||
nbLib = config.nix-bitcoin.lib;
|
||||
runAsUser = config.nix-bitcoin.runAsUserCmd;
|
||||
|
||||
mkLndconnect = { name, isClightning ? false, enableOnion, onionService, port, certPath, authSecretPath }:
|
||||
let
|
||||
lnd = config.services.lnd;
|
||||
getOnionAddress = "cat ${config.nix-bitcoin.secretsDir}/onion-address-${onionService} 2>/dev/null || echo ${onionService}.onion";
|
||||
in pkgs.writeScriptBin name ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
set -e
|
||||
certPath="${certPath}"
|
||||
authSecretPath="${authSecretPath}"
|
||||
if [ "${toString enableOnion}" = "1" ]; then
|
||||
host=$(cat /var/lib/tor/onion/${onionService}/hostname 2>/dev/null || echo "${onionService}.onion")
|
||||
port="${toString port}"
|
||||
else
|
||||
host="${nbLib.address lnd.restAddress}"
|
||||
port="${toString lnd.restPort}"
|
||||
fi
|
||||
# lndconnect is provided by pkgs.lndconnect
|
||||
${getExe pkgs.lndconnect} --host="$host" --port="$port" --cert="$certPath" --macaroon="$authSecretPath" "$@"
|
||||
'';
|
||||
mkLndconnect = {
|
||||
name,
|
||||
shebang ? "#!${pkgs.stdenv.shell} -e",
|
||||
port,
|
||||
authSecretPath,
|
||||
enableOnion,
|
||||
onionService ? null,
|
||||
certPath ? null
|
||||
}:
|
||||
# lndconnect requires a --configfile argument, although it's unused
|
||||
# https://github.com/LN-Zap/lndconnect/issues/25
|
||||
lib.hiPrio (pkgs.writeScriptBin name ''
|
||||
${shebang}
|
||||
url=$(
|
||||
${getExe 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='${authSecretPath}' \
|
||||
--configfile=/dev/null "$@"
|
||||
)
|
||||
|
||||
# If --url is in args
|
||||
if [[ " $* " =~ " --url " ]]; then
|
||||
echo "$url"
|
||||
else
|
||||
# UTF-8 QR is smaller than lndconnect's native output
|
||||
echo -n "$url" | ${getExe pkgs.qrencode} -t UTF8 -o -
|
||||
fi
|
||||
'');
|
||||
in {
|
||||
options.services.lnd.lndconnect = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable lndconnect for LND";
|
||||
description = ''
|
||||
Add a `lndconnect` binary to the system environment which prints
|
||||
connection info for lnd clients (Zeus).
|
||||
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 = "Expose lndconnect via Tor onion service";
|
||||
description = ''
|
||||
Create an onion service for the lnd REST server,
|
||||
which is used by lndconnect / Zeus.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable (mkMerge [
|
||||
(mkIf cfg.lndconnect.enable {
|
||||
environment.systemPackages = [
|
||||
(mkLndconnect {
|
||||
config = mkIf (cfg.enable && cfg.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} ${cfg.user} ${pkgs.bash}/bin/bash";
|
||||
enableOnion = cfg.lndconnect.onion;
|
||||
onionService = "${operatorName}/lnd";
|
||||
onionService = "${cfg.user}/lnd-rest";
|
||||
port = cfg.restPort;
|
||||
certPath = cfg.certPath;
|
||||
authSecretPath = "${cfg.networkDir}/admin.macaroon";
|
||||
})
|
||||
];
|
||||
})
|
||||
(mkIf (cfg.lndconnect.enable && cfg.lndconnect.onion) {
|
||||
services.tor.relay.onionServices.lnd = nbLib.mkOnionService {
|
||||
port = cfg.restPort;
|
||||
target = { addr = nbLib.address cfg.restAddress; port = cfg.restPort; };
|
||||
}
|
||||
)];
|
||||
|
||||
# LAN / clearnet Zeus needs REST on all interfaces. Tor-only stays on
|
||||
# the existing restAddress (loopback) and is reached via lnd-rest.
|
||||
services.lnd.restAddress = mkIf (!cfg.lndconnect.onion) "0.0.0.0";
|
||||
}
|
||||
|
||||
(mkIf cfg.lndconnect.onion {
|
||||
services.tor = {
|
||||
enable = true;
|
||||
# Dedicated name — must not reuse onionServices.lnd (that's P2P :9735).
|
||||
relay.onionServices.lnd-rest = nbLib.mkOnionService {
|
||||
target.addr = nbLib.address cfg.restAddress;
|
||||
target.port = cfg.restPort;
|
||||
port = cfg.restPort;
|
||||
};
|
||||
};
|
||||
nix-bitcoin.onionAddresses.access = {
|
||||
${cfg.user} = [ "lnd-rest" ];
|
||||
${operatorName} = [ "lnd-rest" ];
|
||||
};
|
||||
nix-bitcoin.onionAddresses.access.${operatorName} = [ "lnd" ];
|
||||
})
|
||||
]);
|
||||
}
|
||||
@@ -33,9 +33,15 @@ lib.mkIf config.sovran_systemsOS.features.element-calling {
|
||||
'';
|
||||
};
|
||||
|
||||
####### ENSURE SERVICES START AFTER KEY EXISTS #######
|
||||
systemd.services.livekit.after = [ "livekit-key-setup.service" "livekit-turn-setup.service" ];
|
||||
systemd.services.livekit.wants = [ "livekit-key-setup.service" "livekit-turn-setup.service" ];
|
||||
####### ENSURE SERVICES START AFTER KEY & NETWORK EXIST #######
|
||||
# Ordering against network-online.target matters: livekit-turn-setup detects
|
||||
# the primary interface from the IPv4 default route. If it runs before the
|
||||
# network is up (no default route yet) it exits 1 and, being a hard
|
||||
# dependency of livekit.service, takes livekit down with it — the Hub then
|
||||
# shows a "failed" red dot until livekit is restarted manually. See the
|
||||
# livekit-turn-setup block for the matching network-online ordering.
|
||||
systemd.services.livekit.after = [ "network-online.target" "livekit-key-setup.service" "livekit-turn-setup.service" ];
|
||||
systemd.services.livekit.wants = [ "network-online.target" "livekit-key-setup.service" "livekit-turn-setup.service" ];
|
||||
systemd.services.lk-jwt-service.after = [ "livekit-key-setup.service" ];
|
||||
systemd.services.lk-jwt-service.wants = [ "livekit-key-setup.service" ];
|
||||
|
||||
@@ -114,7 +120,12 @@ EOF
|
||||
# substituted) that the overridden ExecStart loads.
|
||||
systemd.services.livekit-turn-setup = {
|
||||
description = "Stage TURN cert and generate LiveKit runtime config from domain files";
|
||||
after = [ "caddy.service" "livekit-key-setup.service" ];
|
||||
# Wait for a default IPv4 route before detecting the interface, and for
|
||||
# Caddy to have started (cert generation is async, so also see the retry
|
||||
# loop below). Otherwise on a cold boot this unit can fail / produce empty
|
||||
# certs, which breaks livekit.service (requiredBy) and shows a red dot.
|
||||
after = [ "network-online.target" "caddy.service" "livekit-key-setup.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
before = [ "livekit.service" ];
|
||||
requiredBy = [ "livekit.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
@@ -133,11 +144,30 @@ EOF
|
||||
|
||||
# Copy Caddy's already-issued matrix cert/key into LiveKit's state dir.
|
||||
# The ACME CA hostname directory can vary, so glob for the domain dir.
|
||||
CRT=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.crt" | head -n1)
|
||||
KEY=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.key" | head -n1)
|
||||
cp "$CRT" /var/lib/livekit/turn.crt
|
||||
cp "$KEY" /var/lib/livekit/turn.key
|
||||
chmod 640 /var/lib/livekit/turn.crt /var/lib/livekit/turn.key
|
||||
# Caddy issues ACME certs asynchronously, so on a fresh boot the cert may
|
||||
# not exist yet. Retry (bounded) so we never write an empty turn.crt/key;
|
||||
# otherwise embedded TURN silently breaks until the next livekit restart.
|
||||
CRT=""
|
||||
KEY=""
|
||||
for _ in $(seq 1 30); do
|
||||
CRT=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.crt" | head -n1)
|
||||
KEY=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.key" | head -n1)
|
||||
if [ -n "$CRT" ] && [ -n "$KEY" ] \
|
||||
&& [ -s "$CRT" ] && [ -s "$KEY" ]; then
|
||||
break
|
||||
fi
|
||||
CRT=""
|
||||
KEY=""
|
||||
echo "Waiting for Caddy to issue the $MATRIX ACME certificate..."
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$CRT" ] || [ -z "$KEY" ]; then
|
||||
echo "ERROR: Caddy ACME certificate for $MATRIX not available after retries; TURN will not be enabled for LiveKit." >&2
|
||||
else
|
||||
cp "$CRT" /var/lib/livekit/turn.crt
|
||||
cp "$KEY" /var/lib/livekit/turn.key
|
||||
chmod 640 /var/lib/livekit/turn.crt /var/lib/livekit/turn.key
|
||||
fi
|
||||
|
||||
# Detect the primary network interface from the IPv4 default route.
|
||||
# Restricting LiveKit to this single interface prevents it from
|
||||
|
||||
@@ -47,29 +47,44 @@ EOF
|
||||
systemd.services.zeus-connect-setup = {
|
||||
description = "Save Zeus lndconnect URL";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "lnd.service" ];
|
||||
after = [ "lnd.service" "onion-addresses.service" ];
|
||||
wants = [ "lnd.service" "onion-addresses.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils "/run/current-system/sw" ];
|
||||
# sudo is required: the lndconnect wrapper re-execs as the lnd user so it
|
||||
# can read admin.macaroon (not group-readable).
|
||||
path = [ pkgs.coreutils pkgs.gnugrep pkgs.sudo "/run/current-system/sw" ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/zeus-connect-url"
|
||||
mkdir -p /var/lib/secrets
|
||||
|
||||
# LND may still be creating the wallet / macaroon, and the dedicated
|
||||
# lnd-rest onion hostname is published by onion-addresses.service.
|
||||
URL=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
ATTEMPTS=0
|
||||
while [ "$ATTEMPTS" -lt 60 ]; do
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
URL=$(lndconnect --url 2>/dev/null | tr -d '\r' | tail -n 1 || true)
|
||||
fi
|
||||
# Zeus LND REST over Tor: lndconnect://<v3-onion>:8080?macaroon=...
|
||||
if echo "$URL" | grep -q '^lndconnect://' \
|
||||
&& echo "$URL" | grep -q '\.onion' \
|
||||
&& echo "$URL" | grep -q 'macaroon='; then
|
||||
break
|
||||
fi
|
||||
URL=""
|
||||
ATTEMPTS=$((ATTEMPTS + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ -n "$URL" ]; then
|
||||
echo "$URL" > "$SECRET_FILE"
|
||||
printf '%s\n' "$URL" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Zeus connect URL saved."
|
||||
else
|
||||
echo "No lndconnect URL available yet."
|
||||
echo "No valid lndconnect URL available yet."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -333,6 +333,33 @@ class TestSshPubkeyValidation(unittest.TestCase):
|
||||
_validate_ssh_pubkey("ssh-ed25519")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LND macaroon command-line safety
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLndMacaroonCommandLineSafety(unittest.TestCase):
|
||||
"""The LND admin macaroon must never be exposed in curl's argv."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
path = os.path.join(_REPO_ROOT, "modules", "bitcoin", "lnd.nix")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
cls.lnd_module = f.read()
|
||||
|
||||
def test_admin_macaroon_not_interpolated_into_header_argument(self):
|
||||
self.assertNotIn(
|
||||
'-H "Grpc-Metadata-macaroon: $(',
|
||||
self.lnd_module,
|
||||
)
|
||||
|
||||
def test_admin_macaroon_header_is_passed_via_file_descriptor(self):
|
||||
self.assertIn("adminMacaroonHex=$(", self.lnd_module)
|
||||
self.assertIn(
|
||||
"""-H @<(printf 'Grpc-Metadata-macaroon: %s\\n' "$adminMacaroonHex")""",
|
||||
self.lnd_module,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth-exempt paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Regression tests for the Hub Zeus Connect QR.
|
||||
|
||||
The LND-only rewrite of modules/bitcoin/lndconnect.nix shipped a wrapper
|
||||
that Zeus cannot use. These tests lock the contract the Hub QR depends on
|
||||
without needing lnd / tor / qrencode at test time.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
|
||||
_REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
def _read(relpath: str) -> str:
|
||||
with open(os.path.join(_REPO_ROOT, relpath), encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
class TestLndconnectWrapper(unittest.TestCase):
|
||||
"""The system `lndconnect` wrapper must emit a Zeus-scannable URI."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.src = _read("modules/bitcoin/lndconnect.nix")
|
||||
|
||||
def test_uses_official_lndconnect_flags(self):
|
||||
self.assertIn("--adminmacaroonpath=", self.src)
|
||||
self.assertIn("--configfile=/dev/null", self.src)
|
||||
self.assertIn("--nocert", self.src)
|
||||
self.assertIn("--tlscertpath=", self.src)
|
||||
|
||||
def test_does_not_pass_unknown_short_flags(self):
|
||||
# The broken rewrite called `lndconnect --cert … --macaroon …`.
|
||||
# Those flags do not exist; Zeus then never got a valid URI.
|
||||
self.assertIsNone(re.search(r"--cert=", self.src))
|
||||
self.assertIsNone(re.search(r"--macaroon=", self.src))
|
||||
|
||||
def test_uses_dedicated_lnd_rest_onion(self):
|
||||
self.assertIn("lnd-rest", self.src)
|
||||
self.assertIn('onionServices.lnd-rest', self.src)
|
||||
# Must not collide with the LND P2P onion named `lnd`.
|
||||
self.assertNotIn("onionServices.lnd =", self.src)
|
||||
self.assertNotIn('onionService = "${operatorName}/lnd"', self.src)
|
||||
|
||||
def test_reads_onion_from_onion_addresses_dir(self):
|
||||
self.assertIn("onionAddresses.dataDir", self.src)
|
||||
self.assertNotIn("/var/lib/tor/onion/${onionService}/hostname", self.src)
|
||||
|
||||
def test_omits_tls_cert_over_tor(self):
|
||||
# Onion host + embedded localhost cert = Zeus rejects the QR.
|
||||
self.assertIn('then "--nocert"', self.src)
|
||||
|
||||
|
||||
class TestZeusConnectSetup(unittest.TestCase):
|
||||
"""zeus-connect-setup must wait for the REST onion and validate the URI."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.src = _read("modules/wallet-autoconnect.nix")
|
||||
|
||||
def test_waits_for_onion_addresses(self):
|
||||
self.assertIn("onion-addresses.service", self.src)
|
||||
|
||||
def test_rejects_non_lndconnect_output(self):
|
||||
self.assertIn("^lndconnect://", self.src)
|
||||
self.assertIn("\\.onion", self.src)
|
||||
self.assertIn("macaroon=", self.src)
|
||||
|
||||
def test_no_clightning_fallback(self):
|
||||
self.assertNotIn("lnconnect-clnrest", self.src)
|
||||
|
||||
|
||||
class TestHubQrFallback(unittest.TestCase):
|
||||
"""Hub QR encoding must not die on a payload that is too large for ECC H."""
|
||||
|
||||
def test_qrencode_falls_back_to_lower_ecc(self):
|
||||
src = _read("app/sovran_systemsos_web/server.py")
|
||||
self.assertIn('for ecc in ("H", "Q", "L")', src)
|
||||
|
||||
def test_qronly_does_not_hide_uri_when_qr_fails(self):
|
||||
src = _read("app/sovran_systemsos_web/server.py")
|
||||
self.assertIn("Don't hide the URI if we could not render a scannable QR.", src)
|
||||
|
||||
def test_zeus_guide_mentions_use_tor(self):
|
||||
src = _read("app/sovran_systemsos_web/static/js/service-detail.js")
|
||||
self.assertIn("Use Tor", src)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user