Skip to content

egress: tighten DNS answer filtering and TTL clamping#71

Merged
JAORMX merged 3 commits intomainfrom
jaosorior/egress-dns-correctness
Apr 17, 2026
Merged

egress: tighten DNS answer filtering and TTL clamping#71
JAORMX merged 3 commits intomainfrom
jaosorior/egress-dns-correctness

Conversation

@JAORMX
Copy link
Copy Markdown
Contributor

@JAORMX JAORMX commented Apr 17, 2026

Summary

Phase 3 of the hardening series, scoped to net/egress/. Two
correctness improvements to the DNS interceptor that populates dynamic
firewall rules from snooped DNS responses:

  1. Answer filter. ParseDNSResponse previously collected every A
    record in the Answer section and associated it with the question
    name. An allow-listed zone returning additional A records with
    unrelated owner names would silently install those IPs into dynamic
    egress rules. The parser now only accepts A records whose owner is
    reachable from the question via the response's own CNAME chain —
    preserving legitimate CDN redirects while dropping out-of-bailiwick
    answers. A multi-pass walk bounded by the Answer count handles
    CNAME loops without infinite iteration.

  2. TTL cap. Existing minTTL (60 s) kept short-TTL rules alive
    long enough to avoid churn. Added a symmetric maxTTL (5 min
    default) so a very long advertised TTL does not leave a
    dynamically-allowed IP in the rule set indefinitely. Both clamps
    are exposed via functional options (WithMinTTL, WithMaxTTL)
    on NewDNSInterceptor; constructor gains a variadic opts
    parameter so existing callers compile unchanged.

Also adds a small internal normalizeDNSName helper (lowercase +
trailing-dot strip) used for both filter comparison and the returned
qname, since DNS is case-insensitive and wire-format names typically
carry a trailing dot that caller-supplied policy names do not.

Commits are bisectable; each lands with its own tests and task verify is green at every commit.

Test plan

  • task verify green at every commit
  • New unit tests in dns_test.go: out-of-bailiwick A record
    dropped, legitimate CNAME chain followed, mixed response with
    both valid CNAME-A and injected unrelated-A dropped
  • New unit test in interceptor_test.go: TTL clamp verified
    with a 5 ms WithMaxTTL — rule goes live immediately and
    expires past the clamp
  • End-to-end via brood-box with local replace: VM boots,
    allowed hosts (api.anthropic.com, github.com) resolve
    correctly, unknown hosts get NXDOMAIN as expected

🤖 Generated with Claude Code

JAORMX and others added 3 commits April 17, 2026 09:09
Introduces a small internal helper that lower-cases and strips a
single trailing dot. DNS names are case-insensitive per RFC 1035
and wire-format names typically carry a trailing dot that
caller-supplied policy names typically do not.

No call sites yet — the next commit wires it into the Answer-name
filter inside ParseDNSResponse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, ParseDNSResponse collected every A record in the
Answer section and associated them with the question name. An
attacker controlling an allowed zone could return additional A
records with different owner names — for internal IPs, cloud
metadata endpoints, or any IP — and those would be installed
into dynamic egress rules under the question's qname, silently
broadening the allowlist.

Collect only A records whose owner name is reachable from qname
via the response's own CNAME chain. A multi-pass walk bounded by
the Answer count handles legitimate CNAME chains while rejecting
injected out-of-bailiwick records and detecting CNAME loops
without infinite iteration.

Also normalize the returned qname (lower-cased, trailing-dot
stripped) via normalizeDNSName so downstream policy matching
sees a canonical form; the policy matcher was already normalizing
internally so behavior is preserved.

Tests cover: legitimate multi-A response, out-of-bailiwick A
record dropped, CNAME chain followed, and mixed response with
both valid CNAME-A and injected unrelated-A dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing minTTL clamp prevents rule churn from very short
DNS TTLs. Add a symmetric maxTTL clamp (default 5 min) so a very
long advertised TTL does not leave a dynamically-allowed IP in
the rule set for hours or days. Bounds exposure if an upstream
zone returns rogue long-lived answers, and keeps the working set
of dynamic rules pegged to recent resolutions.

Exposed via functional options (WithMinTTL, WithMaxTTL) on
NewDNSInterceptor. The constructor signature gains a variadic
opts parameter — backwards compatible for existing callers.

WithMaxTTL(0) disables the cap; WithMinTTL(0) preserves the
default. Test uses a 5ms max to keep the clamp provable in a
real-time unit test without making the suite slow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JAORMX JAORMX merged commit 0c203e9 into main Apr 17, 2026
7 checks passed
@JAORMX JAORMX deleted the jaosorior/egress-dns-correctness branch April 17, 2026 06:16
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