Skip to content

fix: query DNSBLs with the reversed address form (DEF-52371) - #21

Open
quirky4 wants to merge 1 commit into
mainfrom
def-52371-rbl-reversed-lookup
Open

fix: query DNSBLs with the reversed address form (DEF-52371)#21
quirky4 wants to merge 1 commit into
mainfrom
def-52371-rbl-reversed-lookup

Conversation

@quirky4

@quirky4 quirky4 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes DEF-52371.

@rbl has never matched any real DNSBL. A blocklist publishes an address
written backwards in front of its zone — 1.2.3.4 in example.com is
4.3.2.1.example.com (RFC 5782 §2.1, reversed hex nibbles for IPv6 per §2.4) —
but the operator built the name from the address as-is. Every lookup asked for a
name no zone publishes, came back NXDOMAIN, and Evaluate returned false for
every address; IPv6 operands produced a name that is not legal DNS at all.
Separately, a listed address whose TXT lookup failed was reported as unlisted,
which keeps the operator inert against the many zones that publish an address
record with no TXT.

Both failed open silently, so an @rbl rule looked like enforcement while never
firing once.

What changed

  • Query names are built in the reversed form: decimal octets for IPv4, hex
    nibbles for IPv6. An IPv4-mapped IPv6 address takes the IPv4 form.
  • The address record alone settles the verdict. TXT carries only the reason
    string, so a zone that publishes none — or a TXT lookup that fails on its own
    — no longer flips a listing to "not listed".
  • The answer must be a listing code. A blocklist answers in 127.0.0.0/8 and
    reserves the top of that block (127.255.255.0/24) to complain about the
    query rather than the address — Spamhaus returns 127.255.255.254 for every
    query made through a public resolver. A resolver that invents addresses for
    names it cannot resolve (NXDOMAIN hijacking, wildcard zones) answers outside
    the block entirely. Reading either as a listing would deny all traffic;
    both now warn and report not listed.
  • httpbl_msg and TX:0 describe the current match: a match with no reason no
    longer leaves an earlier @rbl rule's message standing.
  • @rbl with no service hostname is a configuration error instead of a rule
    that loads and can never match.

The lookups stay on net's own resolver rather than a DNSBL library, which
needs a context (to hold the deadline that keeps a slow blocklist from pinning
goroutines, per DEF-51539) and an injectable resolver (so the tests can answer
the queries). LookupHost specifically, not LookupIP/LookupNetIP: those
route through net's shared lookup group, whose call stops being cancellable once
it has a second waiter and then outlives all of them — measured as a
reproducible regression of the DEF-51539 goroutine leak.

⚠️ Rollout

This turns a permanently inert operator active. Any deployed @rbl rule begins
matching, and blocking, for the first time. Worth checking deployed rule sets
and the resolver in use before this ships.

Testing

Fixtures previously encoded the bug — they used unreversed zone names, one of
them palindromic. They now use the reversed form, plus a case whose reversed and
unreversed names differ, so building the name the wrong way round fails the
suite. New coverage: IPv6, IPv4-mapped IPv6, a listed address with no TXT
record, an answer outside the listing range, an answer complaining about the
query, reason isolation between two matches, and the missing-hostname config
error.

Every new assertion was mutation-tested — reverting each production change makes
its test fail. go vet, golangci-lint (0 issues), go test -race -count=2,
and the full suite pass; the DEF-51539 goroutine-leak test was verified
repeatedly in isolation.

A blocklist lists an address written backwards in front of its zone:
1.2.3.4 in example.com is published as 4.3.2.1.example.com (RFC 5782
2.1, and reversed hex nibbles for IPv6 per 2.4). The operator built the
name from the address as-is, so every lookup asked for a name no zone
publishes and came back NXDOMAIN — @rbl matched no address against any
real blocklist, and IPv6 operands produced a name that is not legal DNS
at all.

A listed address whose TXT lookup failed was also reported as unlisted,
which keeps the operator inert against the many zones that publish an
address record with no TXT. The address record alone settles the
verdict; TXT only carries the reason string.

Both defects failed open silently, so an @rbl rule looked like
enforcement while never firing. The test fixtures encoded the first one:
they used unreversed zone names, one of them palindromic. They now use
the reversed form, and a case whose reversed and unreversed names differ
fails if the name is ever built the wrong way round.

Making the lookups reach real zones puts weight on what comes back, so
the answer now has to be a listing code. A blocklist answers in
127.0.0.0/8, reserving the top of that block to complain about the query
itself — a public resolver, an exhausted quota — and a resolver that
invents addresses for names it cannot resolve answers outside the block
altogether. Reading either as a listing would deny every request.

Two smaller gaps in the same operator: a rule whose reason lookup finds
nothing no longer leaves the previous match's message in httpbl_msg, and
@rbl without a service hostname is now a configuration error instead of
a rule that loads and can never match.

The lookups stay on net's own resolver because the DNSBL libraries take
neither a context nor a custom resolver — the first holds the deadline that
keeps a slow blocklist from pinning goroutines, the second lets the tests
answer a query.
@quirky4
quirky4 requested a review from cl-vkuznetsov August 12, 2026 12:03
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.

1 participant