Skip to content

fix: resolve BIP353 via the system resolver; widen LNURL fallback gate - #7

Merged
Alex71btc merged 1 commit into
Alex71btc:mainfrom
helix-nine:fix/system-resolver-and-lnurl-fallback
Jul 27, 2026
Merged

fix: resolve BIP353 via the system resolver; widen LNURL fallback gate#7
Alex71btc merged 1 commit into
Alex71btc:mainfrom
helix-nine:fix/system-resolver-and-lnurl-fallback

Conversation

@helix-nine

Copy link
Copy Markdown
Contributor

Fixes the two issues traced in Start9-Community/bolt12-pay-startos#32. Two small, independent changes; neither adds DNSSEC validation or changes which addresses are payable.

1. Resolve BIP353 through the system resolver

_new_resolver() pinned ["1.1.1.1", "8.8.8.8"] and spoke plain UDP/53 to them, ignoring /etc/resolv.conf. StartOS 0.4.0 requires container DNS to go through the resolver it configures and drops queries aimed at a public resolver, so every BIP353 lookup times out there (dns.exception.Timeout → the 504 in _resolve_bip353_address).

Now the resolver defaults to whatever the runtime configured, with DNS_RESOLVER_NAMESERVERS available to pin an explicit list.

This is a no-op for your Docker deployments. app/docker-compose.yml already sets dns: [1.1.1.1, 8.8.8.8], and that key writes exactly those servers into the container's /etc/resolv.conf — so the new code reads back the same pair the old code hardcoded. Verified by running the patched module in python:3.11-slim:

docker run --dns 1.1.1.1 --dns 8.8.8.8 ...
  /etc/resolv.conf nameservers : ['1.1.1.1', '8.8.8.8']
  _new_resolver() nameservers  : ['1.1.1.1', '8.8.8.8']

docker run -e DNS_RESOLVER_NAMESERVERS=9.9.9.9,1.0.0.1 ...
  /etc/resolv.conf nameservers : ['10.0.0.1']
  _new_resolver() nameservers  : ['9.9.9.9', '1.0.0.1']

docker run (no dns pin) ...
  /etc/resolv.conf nameservers : ['10.0.0.1']
  _new_resolver() nameservers  : ['10.0.0.1']

If you'd rather keep the public resolvers as a hedge against ISP resolvers mangling underscore-label TXT lookups, the cleaner form is a fallback on NXDOMAIN/NoAnswer rather than an unconditional override — happy to add that instead if you prefer.

2. Gate the LNURL fallback on status code, not error text

pay_address decided whether to fall through to LNURL by string-matching the BIP353 error:

if "No BIP353 TXT record found" not in message:
    raise

Every user@domain reaches _resolve_bip353_address first — HRN_RE can't tell a Lightning Address from a BIP353 address — and only the 404 branch produces that string. So a transport failure suppressed an LNURL payment that would have succeeded. Now:

if exc.status_code not in BIP353_LNURL_FALLBACK_STATUSES:   # {404, 502, 504}
    raise

Worth being explicit, since it came up: this is latent, not a live regression. With a reachable resolver, sats@example.com returns NOERROR/no-answer at both candidate FQDNs → NoAnswer → 404 → fallback fires. That's why Lightning Addresses have always worked; the gate only bites once the lookup can't complete at all. Confirmed against your own records — sats.user._bitcoin-payment.alex71btc.com and sats._bitcoin-payment.alex71btc.com both return Status=0, Answers=0.

I deliberately left 422 (TXT present but no usable lno) propagating rather than falling through. Adding it would let a domain with a malformed BIP353 record still get paid over LNURL, which may well be what you want — say the word and I'll include it.

Verification

Ran the patched module in the app's own base image (python:3.11-slim, pip install -r app/requirements.txt), importing backend.app for real rather than testing a copy of the logic:

=== fix A: resolver selection ===
  PASS  defaults to the system resolver
  PASS  does not hardcode public resolvers
  PASS  lifetime/timeout still applied
  PASS  env override is honored

=== fix B: pay_address falls through to LNURL ===
  PASS  404 -> LNURL fallback
  PASS  504 -> LNURL fallback
  PASS  502 -> LNURL fallback
  PASS  400 -> propagates to caller
  PASS  422 -> propagates to caller

=== regression ===
  PASS  pre-existing 404 behavior preserved

The 400/422 cases confirm caller errors still surface instead of being silently retried as LNURL.

Not covered: I haven't run a real payment end to end, since that needs a funded LND. Worth exercising one BIP353 and one Lightning Address payment before merging.

On DNSSEC

Nothing here validates DNSSEC or requires it, so the interop you described — paying offers on domains without DNSSEC, which Phoenix refuses — is unchanged. The DNSSEC point in the issue was only about what StartOS's own resolver can't currently supply if a client wanted to validate; it isn't a behaviour I'm proposing for this app, and I've filed it on our side (Start9Labs/start-technologies#3591) rather than pushing it here.

_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.
@Alex71btc
Alex71btc merged commit b7874db into Alex71btc:main Jul 27, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants