create_lnurl_from_baseurl: allow HTTP for RFC1918 / loopback hosts (extend the localhost exception) #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Follow-up to #1 (
e9d911e). The newcreate_lnurl_from_baseurlhelper composes an LNURL fromsettings.lnbits_baseurland runs it throughpython-lnurl'slnurl_encode, which validates the URL against the LNURL spec — HTTPS is required, withlocalhost/127.0.0.1accepted as a documented dev exception.This is correct for production: a customer wallet on the public internet won't redeem a plain-HTTP LNURL, so producing one would just generate broken QRs.
But it breaks a real and intentional dev workflow: regtest LNbits running plain HTTP on a LAN IP (e.g.,
http://192.168.0.32:5001) for cross-device smoke tests where the customer's phone wallet is on the same LAN.lnurl_encoderaisesInvalidUrl,_populate_lnurlswallows theValueError,link.lnurlstaysNone, and downstream consumers (aiolabs/lamassu-nextATM) fail loudly.Reproduce:
The principled extension
python-lnurl'slocalhost/127.0.0.1exception is exactly right for the underlying invariant: HTTP is only acceptable when the URL is intrinsically unreachable from the public internet, because that's where wallet leniency about the scheme actually matters. Loopback addresses fit that. So do all of RFC1918:10.0.0.0/8172.16.0.0/12192.168.0.0/16Plus loopback (
127.0.0.0/8) andlocalhost.A customer wallet on the public internet can't reach any of these by IP. The only customers that can are LAN-local — which is, by definition, a dev/internal-test scenario. Extending the existing
localhostexception to cover all of these:lnurl_encodestill rejects)lnbits_allow_local_http_lnurlor similar)Proposed patch
In
helpers.py, add a private-network detector and branchcreate_lnurl_from_baseurlaccordingly. When the URL matches the dev rule, manually bech32-encode (bypassinglnurl_encode); otherwise letlnurl_encodeenforce the spec.(The
Lnurl(...)constructor in_manual_bech32_lnurlmay need to be replaced with a duck-typed object if the library's constructor itself validates —_populate_lnurlintransport_rpcs.pyonly reads.bech32and.url, so adataclass-style minimal object suffices.)What changes downstream
For
aiolabs/lamassu-next(the surfaced consumer): the cash-in flow against a regtest LNbits running plain HTTP on a LAN IP now works end-to-end without TLS termination. Customer phone wallet on the same LAN scans the QR, dereferences the HTTP callback, redeems.Production with HTTPS LNbits: unchanged.
_is_private_network_httpreturnsFalse, the validatedlnurl_encodepath runs.Production with HTTP + public IP/hostname: still rejected. The detector returns
Falsefor public IPs,lnurl_encoderaises,_populate_lnurlswallows,link.lnurlstaysNone, consumers fail loudly. The misconfig case stays caught.Why not the other shapes
lnbits_allow_local_http_lnurlflag — explicit opt-in is clearer than the implicit-IP-detector, but introduces a new audit surface that can be turned on by mistake in production. The RFC1918 rule self-documents.VITE_LNBITS_HTTP_URLenv var that aiolabs/withdraw#1 /e9d911ejust deleted. Defeats the source-of-truth alignment.Tests
test_create_lnurl_from_baseurl_http_localhost_succeedstest_create_lnurl_from_baseurl_http_127_0_0_1_succeedstest_create_lnurl_from_baseurl_http_rfc1918_succeeds(parametrized over10.0.0.5,172.16.0.5,192.168.0.5)test_create_lnurl_from_baseurl_http_public_ip_raises_value_errortest_create_lnurl_from_baseurl_https_public_succeedstest_create_lnurl_from_baseurl_http_unresolvable_hostname_raisesCross-refs
e9d911e) — the original nostr-transportlink.lnurl=nullfixhttp://192.168.0.32:5001showed the python-lnurl validation rejecting the LAN-IP HTTP URLaiolabs/lamassu-next:apps/machine/src/services/lightning.ts:692-707(the newlink.lnurlconsumption path)Closing as won't-fix — going with TLS termination (self-signed cert) on the dev LNbits instead.
The carve-out shipped as
66026ab+40dce4dgot the producer side generating LAN-IP HTTP LNURLs, but the symmetric problem on the consumer side (lnbits-core/api/v1/lnurlscan→lnurl.handle()→Lnurl(...)→valid_lnurl_hostrejects non-HTTPS non-localhost) meant LNbits couldn't redeem its own output. Carving out the consumer side too would require an lnbits-core fork patch on_handle+check_callback_url, and at that point the diff is wider than just running TLS.Reverted in:
2877cf6— Revert "fix: allow HTTP LNURL for RFC1918/loopback baseurls (#2)"0e06ab2— Revert "fix: extend RFC1918 LNURL carve-out to the HTTP-views path"The fix for #1 (
e9d911e, populatelnurl/lnurl_urlfromsettings.lnbits_baseurlin the nostr-transport handlers) is kept — it's orthogonal to the transport scheme and works the same whether LNbits is on HTTP or HTTPS.