From 3ab3e32e7fcb15ba1b0c4868b4bc9d7323a42468 Mon Sep 17 00:00:00 2001 From: Helix <267227783+helix-nine@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:39:19 +0000 Subject: [PATCH] fix: resolve BIP353 via the system resolver; widen LNURL fallback _new_resolver() pinned 1.1.1.1/8.8.8.8 and spoke to them directly, ignoring /etc/resolv.conf. Runtimes that require DNS to go through the resolver they configure drop those packets, so every BIP353 lookup times out. Default to the system resolver and keep an explicit list available via DNS_RESOLVER_NAMESERVERS. Deployments that pin resolvers with compose's dns: key are unaffected -- that key writes the same servers into resolv.conf. pay_address gated its LNURL fallback on a substring of the BIP353 error message, so only a 404 could reach it. A Lightning Address is indistinguishable from a BIP353 address at that point, so a transport failure (502/504) suppressed an LNURL payment that would have succeeded. Gate on status code instead. Neither change adds DNSSEC validation or alters which addresses are payable. --- app/backend/app.py | 22 ++++++++++++++++++---- app/docker-compose.example.yml | 3 +++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/backend/app.py b/app/backend/app.py index d186e76..05b7805 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -310,6 +310,15 @@ def get_cloudflare_config(): DNS_RESOLVER_LIFETIME = float(os.environ.get("DNS_RESOLVER_LIFETIME", "10")) DNS_RESOLVER_TIMEOUT = float(os.environ.get("DNS_RESOLVER_TIMEOUT", "10")) +DNS_RESOLVER_NAMESERVERS = [ + server + for server in os.environ.get("DNS_RESOLVER_NAMESERVERS", "").replace(",", " ").split() + if server +] + +# BIP353 lookup outcomes that should fall through to the LNURL path in pay_address: +# no record (404), lookup failed (502), lookup timed out (504). +BIP353_LNURL_FALLBACK_STATUSES = frozenset({404, 502, 504}) LNURL_MIN_SENDABLE_MSAT = int(os.environ.get("LNURL_MIN_SENDABLE_MSAT", "1000")) LNURL_MAX_SENDABLE_MSAT = int(os.environ.get("LNURL_MAX_SENDABLE_MSAT", "1000000000")) @@ -1376,8 +1385,11 @@ def _extract_offer_from_txt_record(txt_value: str) -> Optional[str]: def _new_resolver() -> dns.resolver.Resolver: + # Default to the host's own resolver (/etc/resolv.conf). Some runtimes only + # permit DNS via the resolver they configure, so pinning one here fails there. resolver = dns.resolver.Resolver() - resolver.nameservers = ["1.1.1.1", "8.8.8.8"] + if DNS_RESOLVER_NAMESERVERS: + resolver.nameservers = DNS_RESOLVER_NAMESERVERS resolver.lifetime = DNS_RESOLVER_LIFETIME resolver.timeout = DNS_RESOLVER_TIMEOUT return resolver @@ -2836,9 +2848,11 @@ async def pay_address(payload: PayAddressRequest, request: StarletteRequest) -> return PayOfferResponse(resolved_offer=normalized_offer, raw_output=raw_output) except HTTPException as exc: - message = str(exc.detail) - - if "No BIP353 TXT record found" not in message: + # A Lightning Address is indistinguishable from a BIP353 address here, so + # fall through to LNURL whenever the offer lookup found nothing to pay -- + # whether the record was absent (404) or the lookup never completed + # (502/504). Anything else is the caller's error and still propagates. + if exc.status_code not in BIP353_LNURL_FALLBACK_STATUSES: raise lnurl_result = await _resolve_lnurl_invoice( diff --git a/app/docker-compose.example.yml b/app/docker-compose.example.yml index ecf0f10..4cb937d 100644 --- a/app/docker-compose.example.yml +++ b/app/docker-compose.example.yml @@ -38,6 +38,9 @@ services: CONFIG_JSON_PATH: /data/config.json SECRETS_JSON_PATH: /data/config/secrets.json CORS_ORIGINS: "*" + # Optional. BIP353 lookups use the container's own resolver (the `dns:` + # entries below). Set this to override it with an explicit list instead. + # DNS_RESOLVER_NAMESERVERS: "1.1.1.1 8.8.8.8" dns: - 1.1.1.1 - 8.8.8.8