# smartdns - Implementation Hints

## Architecture Overview

The smartdns library is structured into three main modules:

1. **Client Module** (`ts_client/`) - DNS client functionality
2. **Server Module** (`ts_server/`) - DNS server with Rust backend
3. **Main Module** (`ts/`) - Re-exports both client and server
4. **Rust Module** (`rust/`) - High-performance DNS server binary

## Rust Backend Architecture (v8.0+)

The DNS server's network I/O, packet parsing/encoding, and DNSSEC signing run in Rust.
TypeScript retains handler registration, ACME orchestration, and the public API.

### Rust Crate Structure
- `rustdns` - Main binary with IPC management loop (`--management` flag)
- `rustdns-protocol` - DNS wire format parsing/encoding, record types
- `rustdns-server` - Async UDP + HTTPS DoH servers (tokio, hyper, rustls)
- `rustdns-dnssec` - ECDSA P-256 / ED25519 key generation and RRset signing

### IPC Flow
```
DNS Query -> Rust (UDP/HTTPS) -> Parse packet
  -> Try local resolution (localhost, DNSKEY)
  -> If handler needed: emit "dnsQuery" event to TypeScript
  -> TypeScript runs minimatch handlers, sends "dnsQueryResult" back
  -> Rust builds response, signs DNSSEC if requested, sends packet
```

### Key Files
- `ts_server/classes.rustdnsbridge.ts` - TypeScript IPC bridge wrapping smartrust.RustBridge
- `ts_server/classes.dnsserver.ts` - DnsServer class (public API, delegates to Rust bridge)
- `rust/crates/rustdns/src/management.rs` - IPC management loop
- `rust/crates/rustdns/src/resolver.rs` - DNS resolver with callback support

## Client Module (Smartdns class)

### Key Features:
- DNS record queries (A, AAAA, TXT, MX, etc.)
- Support for multiple DNS providers (Google DNS, Cloudflare)
- DNS propagation checking with retry logic
- DNSSEC verification support
- Both HTTP-based (DoH) and Node.js DNS resolver fallback

### Implementation Details:
- Uses Cloudflare's DNS-over-HTTPS API as primary resolver
- Falls back to Node.js DNS module for local resolution
- Implements automatic retry logic with configurable intervals
- Properly handles quoted TXT records and trailing dots in domain names

### Key Methods:
- `getRecordsA()`, `getRecordsAAAA()`, `getRecordsTxt()` - Type-specific queries
- `getRecords()` - Generic record query with retry support
- `checkUntilAvailable()` - DNS propagation verification
- `getNameServers()` - NS record lookup
- `makeNodeProcessUseDnsProvider()` - Configure system DNS resolver

## Server Module (DnsServer class)

### Key Features:
- Full DNS server supporting UDP and HTTPS (DoH) protocols
- DNSSEC implementation with multiple algorithms
- Dynamic handler registration for custom responses
- Let's Encrypt integration for automatic SSL certificates
- Wildcard domain support with pattern matching

### DNSSEC Implementation:
- Supports ECDSA (algorithm 13), ED25519 (algorithm 15), and RSA (algorithm 8)
- Automatic DNSKEY and DS record generation
- RRSIG signature generation for authenticated responses
- Key tag computation following RFC 4034

### Let's Encrypt Integration:
- Automatic SSL certificate retrieval using DNS-01 challenges
- Dynamic TXT record handler registration for ACME validation
- Certificate renewal and HTTPS server restart capability
- Domain authorization filtering for security

### Handler System:
- Pattern-based domain matching using minimatch
- Support for all common record types
- **Multiple Handler Support**: As of v7.4.2+, multiple handlers can contribute records of the same type
- Handler chaining for complex scenarios
- Automatic SOA response for unhandled queries

### Multiple Records Support (v7.4.2+):
- Server now processes ALL matching handlers for a query (previously stopped after first match)
- Enables proper multi-NS record support for domain registration
- Supports round-robin DNS with multiple A/AAAA records
- Allows multiple TXT records (SPF, DKIM, domain verification)
- Each handler contributes its record to the response

## Key Dependencies

- `dns-packet`: DNS packet encoding/decoding (wire format, used by TS fallback path)
- `acme-client`: Let's Encrypt certificate automation
- `minimatch`: Glob pattern matching for domains
- `@push.rocks/smartrust`: TypeScript-to-Rust IPC bridge
- `@tsclass/tsclass`: Type definitions for DNS records

## Testing Insights

The test suite demonstrates:
- Mock ACME client for testing Let's Encrypt integration
- Self-signed certificate generation for HTTPS testing
- Unique port allocation to avoid conflicts
- Proper server cleanup between tests
- Both UDP and HTTPS query validation

## Common Patterns

1. **DNS Record Types**: Internally mapped to numeric values (A=1, AAAA=28, etc.)
2. **Error Handling**: Graceful fallback and retry mechanisms
3. **DNSSEC Workflow**: Zone → Key Generation → Signing → Verification
4. **Certificate Flow**: Domain validation → Challenge setup → Verification → Certificate retrieval

## Performance Considerations

- Client implements caching via DNS-over-HTTPS responses
- Server can handle concurrent UDP and HTTPS requests
- DNSSEC signing is performed on-demand for efficiency
- Handler registration is O(n) lookup but uses pattern caching

## Security Notes

- DNSSEC provides authentication but not encryption
- DoH (DNS-over-HTTPS) provides both privacy and integrity
- Let's Encrypt integration requires proper domain authorization
- Handler patterns should be carefully designed to avoid open resolvers

## Recent Improvements (v7.4.3)

1. **DNSSEC RRset Signing**: Fixed to properly sign entire RRsets together instead of individual records
2. **SOA Record Serialization**: Implemented proper SOA record encoding for DNSSEC compatibility
3. **Configurable Primary Nameserver**: Added `primaryNameserver` option to customize SOA mname field

## Recent Improvements (v7.4.8)

1. **MX Record DNSSEC Support**: Implemented MX record serialization for DNSSEC signing
   - MX records consist of a 16-bit preference value followed by the exchange domain name
   - Properly serializes both components for DNSSEC signature generation
   - Enables mail exchange records to be authenticated with DNSSEC

## Authority Model (server)

Authority comes from **configuration**, never from the presence of a handler.

- `authoritativeZones` (falling back to `[dnssecZone]`) is the single source of
  truth for the `aa` bit. An answer only carries `aa` when the queried name
  falls inside one of those zones.
- `registerHandler(pattern, types, handler, options?)` takes
  `options.authority`:
  - `'authoritative'` (default) — the handler only contributes for names inside
    a configured zone. A match outside every zone is **suppressed**, not
    answered, and reported on the `refused` event. A registration that cannot
    match any configured zone also logs a warning once, at registration time.
  - `'non-authoritative'` — the handler may answer any matching name, but the
    response never carries `aa` and is never DNSSEC-signed. This is the mode
    for split-horizon / private-route overlays.
- `aa` is a per-message flag, so a single non-authoritative contribution
  downgrades the whole response.

Why this matters: before v8, registering a handler *was* the public claim of
authority. A private-route overlay registered for a domain delegated elsewhere
made the public nameserver answer with `aa` and an RFC1918 address, while
refusing every other query type for the same name. Both halves came from the
same defect — two disagreeing sources of truth.

### Response outcomes

`TDnsQueryResponseKind` maps one-to-one onto the wire:

| kind | rcode | `aa` | authority section |
| --- | --- | --- | --- |
| `answer` | 0 | yes | — |
| `nonAuthoritativeAnswer` | 0 | no | — |
| `authoritativeNegative` (NODATA) | 0 | yes | zone SOA |
| `authoritativeNameError` (NXDOMAIN) | 3 | yes | zone SOA |
| `refused` | 5 | no | — |
| `servfail` | 2 | no | — |

NXDOMAIN is only emitted when the name provably does not exist: no handler
pattern matches it for any type, it is not the apex of a configured zone, no
configured zone sits below it, and no handler pattern is anchored strictly
below it (literal `.name` suffix, or a wildcard that matches a probe label —
mid-label wildcards such as `x.*.example.com` are not fully inverted by this
check and can still leave an ancestor at NXDOMAIN). The apex always exists
(it answers its own SOA), and a name with covered descendants is an empty
non-terminal — both get NODATA, because NXDOMAIN denies the entire subtree
(RFC 8020) and a wrong one lets aggressive negative caching darken the zone.
A catch-all pattern such as `'*'` therefore suppresses NXDOMAIN entirely —
that is deliberate.

The zone apex answers its own SOA even with no registered SOA handler; an
authoritative server that cannot answer SOA for its own zone breaks negative
caching and resolver heuristics.

One deliberate exception to the table: RFC 6761 localhost handling (when
`enableLocalhostHandling` is on) answers `localhost` A/AAAA with `aa` set even
though `localhost` is outside every configured zone — special-use names are
authoritative by definition, not by delegation.

## Handler Lifecycle

- `registerHandler` returns an `IDnsHandlerRegistration` with a unique `id` and
  an `unregister()` that removes **only** that registration.
- `unregisterHandlerById(id)` and `unregisterHandlersByOwner(owner)` are the
  precise teardown primitives; `listHandlers()` exposes the current set for
  leak checks.
- `unregisterHandler(pattern, types)` is retained and unchanged: it still
  removes *every* handler matching that pattern/type pair. It cannot distinguish
  two consumers sharing a key — prefer the handle.
- ACME authorization follows the same rule: `filterAuthorizedDomains` /
  `retrieveSslCertificate` admit exactly the domains inside a configured
  authoritative zone. Handler registrations play no part — a DNS-01 challenge
  for an out-of-zone domain could never validate, because its challenge TXT
  handler would be suppressed.

## Transport And Protocol Notes

- **EDNS0**: a request carrying an OPT record always gets an OPT back
  (RFC 6891 6.1.1), advertising `DNS_ADVERTISED_UDP_PAYLOAD` (1232). Requests
  without OPT get no OPT. Client-advertised payload sizes are clamped up to 512
  so a hostile OPT cannot force pathological truncation.
- **Truncation**: UDP responses are truncated to the client's advertised payload
  size with the TC bit set, so resolvers retry over TCP. Without this, oversized
  datagrams were emitted and silently dropped by real networks. Truncation is
  size arithmetic, never a re-encode per dropped record — the encoder does not
  compress names, so record sizes are additive (`DnsPacket::encoded_len`).
  `processRawDnsPacketAsync(packet, 'udp')` declares the same UDP bound for the
  manual path (`handleUdpMessage` passes it automatically); without the hint no
  size bound is applied, which is correct for TCP and DoH framing. Note the
  manual-path deadlock under Known Limitations before relying on this.
- **RA** is cleared on every path — this server never recurses.
- **RDATA validation** fails closed: a handler returning an unencodable value
  (for example a non-IPv4 string for an A record) produces SERVFAIL and an error
  log, rather than a malformed record on the wire.
- **SOA serial** is fixed for the lifetime of the server process and passed to
  Rust via `soaSerial`, so apex answers and negative authorities agree.

## Observability

- `query` events carry `responseKind`, `authorityZone` and `suppressedHandlers`.
- `refused` events fire for every refusal and every suppressed handler match,
  carrying the handler `id`, `domainPattern` and `owner`. Wire this up: the
  original incident was invisible for weeks because nothing surfaced it.

## Known Limitations

1. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records (this may be desired behavior for some use cases)
2. **NXDOMAIN under catch-all patterns**: see the authority model above.
3. **SOA serial does not track zone changes** — it is a process-lifetime
   constant, not a change counter. Secondaries driven by AXFR/IXFR are not
   supported.
4. **Manual-path (`processPacket`) queries deadlock on handler resolution**
   — pre-existing, not introduced by the v8 hardening. The IPC select loop in
   `rust/crates/rustdns/src/management.rs` awaits `handle_request` inline, so
   while `handle_process_packet` waits for the TypeScript `dnsQuery` callback
   the loop can neither emit the query event nor read its result; every
   handler-backed query through `processRawDnsPacketAsync` /
   `handleUdpMessage` times out to SERVFAIL after 10 s. The built-in UDP, TCP
   and DoH listeners are unaffected (they resolve outside the IPC request
   loop). The `transport: 'udp'` truncation contract on this path is wired
   and unit-tested but cannot be observed end-to-end until the loop spawns
   `processPacket` handling instead of awaiting it inline. No test exercises
   `processRawDnsPacketAsync`; fixing this needs its own release.
## Static Rust Binaries

- The Rust binaries in `dist_rust/` are statically linked (static-pie) via `"static": true` in the `@git.zone/tsrust` block of `.smartconfig.json` (tsrust >= 1.4.1). They run on both glibc (Debian/Ubuntu) and musl (Alpine) systems.
- tsrust injects `RUSTFLAGS="-C target-feature=+crt-static"` only into its own per-target cargo invocations and verifies the result (no `PT_INTERP` ELF header). Keep `rust/.cargo/config.toml` free of `rustflags` entries: the injected env variable would replace them, and a repo-wide `rustflags` would also break plain `cargo test`/`cargo check` (proc-macros cannot build with `+crt-static` on linux-gnu without an explicit `--target`).
- `pnpm run test:rust` runs the Rust unit tests; plain `cargo test` in `rust/` works as well.
- Verify linkage manually with `ldd dist_rust/<binary>_linux_amd64` → "statically linked".
