Skip to content

Respond to TXT record queries with peer addresses#12

Open
arya2 wants to merge 2 commits intoZcashFoundation:mainfrom
arya2:support-txt-records
Open

Respond to TXT record queries with peer addresses#12
arya2 wants to merge 2 commits intoZcashFoundation:mainfrom
arya2:support-txt-records

Conversation

@arya2
Copy link
Copy Markdown
Contributor

@arya2 arya2 commented Jan 21, 2026

Based on #11

Motivation

We want to eventually replace use TXT records instead of A or AAAA records so that browsers pointed at the domain won't send requests to random IPs.

arya2 added 2 commits January 20, 2026 15:07
…ts and spawning background tasks to update/prune them from server logic.
@alchemydc
Copy link
Copy Markdown
Collaborator

Thanks @arya2 . Looks like we're on the right track, and agree that we should really be using TXT instead of A.

(automated) Review (opus 4.6)

This PR does two things:

  1. Refactoring: Extracts address cache logic into server/address_cache.rs and rate limiter into server/rate_limiter.rs (from PR Splits up rate limiting and response caching logic into their own modules #11).
  2. Feature: Adds RecordType::TXT handling so peer addresses can be queried via TXT records (to prevent browsers from connecting to random node IPs).

The refactoring is clean — moved code is essentially identical. However, the TXT feature has a critical correctness bug and a few other issues.


Blocking Issue: TXT responses use A/AAAA RData instead of TXT RData

The core feature of this PR does not work correctly. When a TXT query arrives, the handler chains IPv4 and IPv6 addresses together but then creates A/AAAA RData records, not TXT records:

// In handle_request_inner — the TXT arm produces PeerSocketAddr values...
RecordType::TXT => {
    let AddressRecords { ipv4, ipv6 } = self.latest_addresses.borrow().clone();
    Some(("TXT", ipv4.into_iter().chain(ipv6.into_iter()).collect()))
}

// ...which are then turned into A/AAAA RData:
let rdata = match addr.ip() {
    std::net::IpAddr::V4(ipv4) => RData::A(...),
    std::net::IpAddr::V6(ipv6) => RData::AAAA(...),
};

Most resolvers will silently drop records whose rdata type doesn't match the query type. The fix is to produce RData::TXT records with the IP serialized as a string, e.g.:

RecordType::TXT => {
    // Collect all addresses, serialize as TXT rdata
    let AddressRecords { ipv4, ipv6 } = self.latest_addresses.borrow().clone();
    for addr in ipv4.into_iter().chain(ipv6) {
        let txt = hickory_proto::rr::rdata::TXT::new(vec![addr.ip().to_string()]);
        records.push(Record::from_rdata(name.clone().into(), self.dns_ttl, RData::TXT(txt)));
    }
    // record metric + send response (needs its own path, not the shared one below)
}

This needs a separate code path from A/AAAA since the rdata construction is different. Consider whether each address should be a separate TXT record or all addresses should be packed into a single TXT record with multiple character-strings — the DNS spec allows both, but one TXT record with multiple character-strings is more compact and conventional for multi-value responses.


Non-blocking issues

0. No test for TXT queries

The PR adds the headline feature (TXT support) but has no test for it. There are existing tests for A and AAAA queries (test_dns_server_starts_and_responds, test_seeder_authority_handles_aaaa_queries) — please add an analogous test for TXT queries to prove the feature works.

1. Spurious log message in rate_limiter::spawn

pub fn spawn(config: SeederConfig) -> (Option<Arc<RateLimiter>>, JoinHandle<()>) {
    tracing::info!("Initializing zebra-network...");  // ← copy-paste error

This function initializes the rate limiter, not zebra-network. Remove or fix this log line.

2. rate_limiter::spawn takes full SeederConfig by value

The function only reads config.rate_limit. Prefer accepting Option<&RateLimitConfig> (or Option<RateLimitConfig>) to narrow the interface and avoid the config.clone() at the call site.

3. TXT response has no peer count bound

A/AAAA responses are already bounded by MAX_DNS_RESPONSE_PEERS (25 per family) from the cache updater. But TXT chains both pools together, yielding up to 50 addresses. This can produce large UDP responses that get truncated or require TCP fallback. Consider capping TXT responses as well, or document the intentional choice.

4. Test in address_cache.rs not wrapped in #[cfg(test)]

The relocated test_dns_response_constants test is a bare #[test] fn at module level, not inside a #[cfg(test)] mod tests {} block. This is inconsistent with the convention in server.rs and compiles test code into the production binary.

5. else branch is a no-op with stale TODO comments

} else {
    // For NS, SOA, etc, we might want to return something else or Refused.
    // Returning empty NOERROR or NXDOMAIN?
    // Let's return NOERROR empty for now.
}

The block is empty — either remove the else or turn the comments into a tracking issue so they don't rot.


Refactoring (commit 1) looks good

The extraction of address_cache and rate_limiter into submodules is clean, preserves identical logic, and improves the navigability of server.rs. No concerns there.

@arya2
Copy link
Copy Markdown
Contributor Author

arya2 commented Mar 2, 2026

Blocking Issue: TXT responses use A/AAAA RData instead of TXT RData

Zebra can use these fns to get the records:
https://docs.rs/hickory-resolver/latest/hickory_resolver/struct.Resolver.html#method.lookup
https://docs.rs/hickory-resolver/latest/hickory_resolver/lookup/struct.Lookup.html#method.records

Non-blocking issues
3. TXT response has no peer count bound

It's implicitly bound, perhaps should be documented.

All the non-blocking issues are good callouts.

@arya2
Copy link
Copy Markdown
Contributor Author

arya2 commented Mar 2, 2026

SRV records, if they work, might be more appropriate than TXT records.

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