Files
mrambossekandClaude Opus 5 35baf70cdb
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 26s
server-release / release (push) Successful in 27s
server: canary DNS — authoritative zone with frozen §6.1 reference records
Stdlib DNS responder (no external deps): parses single-question queries
with EDNS OPT (bufsize, DO, ECS), serves the spec's frozen reference
records (ttl-{5,60,3600,86400} A/AAAA/TXT, many-rr 8×A in order, big-txt
~1800B), and per-query <nonce>.<session>.<zone> answers in 192.0.2.0/24.
UDP truncation sets TC past 512 (or the EDNS bufsize); TCP never
truncates — the EDNS-bufsize / TCP-fallback test. Every query is logged
(qname, resolver, transport, EDNS, ECS, case) and surfaced per session
prefix in GET /v1/sessions/{id}/observations as dns_canary. Profile gains
canary_zone + the canary-dns capability when configured.

Wire format validated against an independent client (correct rcodes,
answer counts, TC behavior, full EDNS response); unit tests cover
references, truncation-vs-EDNS, logging, NXDOMAIN.

Versioning: patch-first convention recorded in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:08:30 +02:00

79 lines
3.0 KiB
Go

// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
// Package canarydns serves the authoritative canary zone (spec §6.1). The
// reference records below are FROZEN by the protocol spec — names, TTLs, and
// RDATA are ground truth the client compares against, so they must never
// change without a spec revision and a matching update in the app. All
// addresses are documentation-range (RFC 5737 192.0.2.0/24, RFC 3849
// 2001:db8::/32).
package canarydns
import "net/netip"
// refRecord is one frozen reference name (relative to the zone) with its
// per-type answers. A zero value in a field means "no record of that type".
type refRecord struct {
label string
ttl uint32
a []netip.Addr // A / AAAA answers (order preserved)
txt []string // one string per TXT record
}
// referenceRecords are the spec §6.1 fixed records. The RDATA constants are
// frozen HERE (the spec calls this file the source of truth) and mirrored in
// the app. Order within many-rr is part of the test (order/stripping check).
var referenceRecords = []refRecord{
{label: "ttl-5", ttl: 5,
a: []netip.Addr{netip.MustParseAddr("192.0.2.5"), netip.MustParseAddr("2001:db8::5")},
txt: []string{"echolot-ref ttl=5"}},
{label: "ttl-60", ttl: 60,
a: []netip.Addr{netip.MustParseAddr("192.0.2.60"), netip.MustParseAddr("2001:db8::60")},
txt: []string{"echolot-ref ttl=60"}},
{label: "ttl-3600", ttl: 3600,
a: []netip.Addr{netip.MustParseAddr("192.0.2.36"), netip.MustParseAddr("2001:db8::3600")},
txt: []string{"echolot-ref ttl=3600"}},
{label: "ttl-86400", ttl: 86400,
a: []netip.Addr{netip.MustParseAddr("192.0.2.86"), netip.MustParseAddr("2001:db8::8640")},
txt: []string{"echolot-ref ttl=86400"}},
// many-rr: exactly 8 A records in defined order.
{label: "many-rr", ttl: 300, a: []netip.Addr{
netip.MustParseAddr("192.0.2.101"), netip.MustParseAddr("192.0.2.102"),
netip.MustParseAddr("192.0.2.103"), netip.MustParseAddr("192.0.2.104"),
netip.MustParseAddr("192.0.2.105"), netip.MustParseAddr("192.0.2.106"),
netip.MustParseAddr("192.0.2.107"), netip.MustParseAddr("192.0.2.108"),
}},
// big-txt: ~1800 bytes, exercises EDNS bufsize / TCP fallback.
{label: "big-txt", ttl: 300, txt: bigTxt()},
}
// bigTxt builds a deterministic ~1800-byte TXT payload as a sequence of
// 255-byte character-strings (the DNS TXT chunk limit). The content is fixed
// so the client can verify integrity, not just length.
func bigTxt() []string {
const total = 1800
const pattern = "echolot-big-txt-reference-0123456789abcdef-"
buf := make([]byte, 0, total)
for len(buf) < total {
buf = append(buf, pattern...)
}
buf = buf[:total]
var out []string
for len(buf) > 0 {
n := min(255, len(buf))
out = append(out, string(buf[:n]))
buf = buf[n:]
}
return out
}
// findReference returns the reference record for a label, or nil.
func findReference(label string) *refRecord {
for i := range referenceRecords {
if referenceRecords[i].label == label {
return &referenceRecords[i]
}
}
return nil
}