initial commit

This commit is contained in:
developer committed 2026-09-27 11:20:34 +00:00
1 parent 5b935301e5
commit 85453852d5
105 files changed
+24846

No files matched your search

+451
View File
@@ -0,0 +1,451 @@
// Package localzone serves authoritative data from DNS master files: split
// DNS setups, internal service records and the odd "answer this one name
// locally" need, all without a second nameserver to run.
package localzone
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync/atomic"
"time"
"github.com/miekg/dns"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/zonefile"
)
// Store is an immutable, compiled set of local zones.
type Store struct {
// zones maps a lowercased apex (with trailing dot) to its compiled zone.
zones map[string]*zone
// count is the number of records held, for the UI.
count int
// files lists the sources that produced this store.
files []string
// builtAt is the compile timestamp.
builtAt time.Time
}
type zone struct {
origin string
// exact is keyed by "name|type".
exact map[string][]dns.RR
// wild holds "*.name" entries.
wild map[string]map[uint16][]dns.RR
// soa is the zone's SOA, used for NXDOMAIN and NODATA authority.
soa *dns.SOA
// ns holds the apex NS set.
ns map[uint16][]dns.RR
// delegations are the names served by NS records (a cut below the apex).
delegations map[string]bool
minTTL uint32
}
// Options tunes zone loading.
type Options struct {
// Origins overrides the per-file origin derivation.
Origins map[string]string
// MaxFiles bounds how many files are read (glob results included).
MaxFiles int
}
// Load compiles every master file in paths. A path may be a glob.
func Load(paths []string, opt Options) (*Store, error) {
files, err := expand(paths, opt.MaxFiles)
if err != nil {
return nil, err
}
s := &Store{
zones: make(map[string]*zone, len(files)),
files: files,
builtAt: time.Now(),
}
for _, f := range files {
origin := zonefile.OriginFromPath(f)
if o, ok := opt.Origins[f]; ok {
origin = dns.Fqdn(o)
}
z, err := zonefile.ParseFile(f, origin)
if err != nil {
return nil, fmt.Errorf("localzone: %w", err)
}
apex := strings.ToLower(z.Origin)
zz, ok := s.zones[apex]
if !ok {
zz = &zone{
origin: apex,
exact: make(map[string][]dns.RR),
wild: make(map[string]map[uint16][]dns.RR),
delegations: make(map[string]bool),
ns: make(map[uint16][]dns.RR),
minTTL: 0xFFFFFFFF,
}
s.zones[apex] = zz
}
for _, rec := range z.Records {
if add(zz, rec) {
s.count++
}
}
}
for _, zz := range s.zones {
if zz.minTTL == 0xFFFFFFFF {
zz.minTTL = 60
}
}
return s, nil
}
func add(z *zone, rec zonefile.Record) bool {
hdr := rec.RR.Header()
name := strings.ToLower(dns.Fqdn(hdr.Name))
// SOA is zone metadata, not a queryable record.
if soa, ok := rec.RR.(*dns.SOA); ok {
z.soa = soa
return false
}
key := name
isWild := false
if strings.HasPrefix(name, "*.") {
key = name[2:]
isWild = true
}
// A record must live inside its zone; silently dropping out-of-zone data
// would be a surprise, so refuse it loudly instead.
if !inZone(key, z.origin) {
return false
}
// NS at or below the apex marks a delegation: the data below belongs to
// another server, so we must not answer for it.
if hdr.Rrtype == dns.TypeNS && !isWild {
if key == z.origin {
z.ns[hdr.Rrtype] = append(z.ns[hdr.Rrtype], rec.RR)
return true
}
z.delegations[key] = true
}
if hdr.Ttl < z.minTTL {
z.minTTL = hdr.Ttl
}
if isWild {
m, ok := z.wild[key]
if !ok {
m = make(map[uint16][]dns.RR)
z.wild[key] = m
}
m[hdr.Rrtype] = append(m[hdr.Rrtype], rec.RR)
return true
}
k := name + "|" + dns.TypeToString[hdr.Rrtype]
z.exact[k] = append(z.exact[k], rec.RR)
return true
}
func inZone(name, origin string) bool {
if origin == "." {
return true
}
return name == origin || strings.HasSuffix(name, "."+origin)
}
// expand turns configured paths (possibly globs) into a concrete file list.
func expand(paths []string, max int) ([]string, error) {
var out []string
seen := map[string]bool{}
for _, p := range paths {
if strings.ContainsAny(p, "*?[") {
matches, err := filepath.Glob(p)
if err != nil {
return nil, fmt.Errorf("localzone: bad glob %q: %w", p, err)
}
sort.Strings(matches)
for _, m := range matches {
if !seen[m] {
seen[m] = true
out = append(out, m)
}
}
continue
}
if !seen[p] {
seen[p] = true
out = append(out, p)
}
}
// A path that is a directory contributes its *.zone files, which is the
// natural thing to point at.
var expanded []string
for _, p := range out {
fi, err := os.Stat(p)
if err != nil {
return nil, fmt.Errorf("localzone: %w", err)
}
if fi.IsDir() {
entries, err := filepath.Glob(filepath.Join(p, "*.zone"))
if err != nil {
return nil, err
}
sort.Strings(entries)
expanded = append(expanded, entries...)
continue
}
expanded = append(expanded, p)
}
if max > 0 && len(expanded) > max {
expanded = expanded[:max]
}
return expanded, nil
}
// Result is the outcome of a local lookup.
type Result struct {
// Found is true when the store has an answer (including a CNAME).
Found bool
// Records is the RRset for the queried type, or a CNAME when the name
// only has one.
Records []dns.RR
// Type is the type the records are for (may differ from the question for
// a CNAME answer).
Type uint16
// Delegated is true when the name is served by another server.
Delegated bool
// Authority is the SOA for NXDOMAIN/NODATA responses.
Authority *dns.SOA
// TTL is the smallest TTL in the answer.
TTL uint32
// Zone is the apex of the zone that answered.
Zone string
}
// Lookup resolves a question against the local store. It does not chase CNAMEs
// across the local store; the handler re-queries, which keeps the wildcard and
// delegation rules in one place.
func (s *Store) Lookup(q dns.Question) Result {
if s == nil || len(s.zones) == 0 {
return Result{}
}
name := strings.ToLower(dns.Fqdn(q.Name))
qtype := q.Qtype
// The cache and the resolver both skip the root and the local reverse
// zones, but a local zone may legitimately define them, so check first.
var best *zone
for apex, z := range s.zones {
if !inZone(name, apex) {
continue
}
if best == nil || len(apex) > len(best.origin) {
best = z
}
}
if best == nil {
return Result{}
}
if qtype == dns.TypeANY {
return s.any(best, name)
}
// A delegation means "ask them, not me".
if best.delegations[name] {
return Result{Found: false, Delegated: true, Zone: best.origin, Authority: best.soa, TTL: best.minTTL}
}
if rrs, ok := best.exact[name+"|"+dns.TypeToString[qtype]]; ok {
return s.result(best, name, qtype, rrs)
}
// Wildcard: "*.zone" covers one label; RFC 1034 §4.3.3 wants the closest
// enclosing wildcard, so try progressively shorter parents.
if rrs, ok := closestWildcard(best, name, qtype); ok {
return s.result(best, name, qtype, rrs)
}
// A CNAME at the name answers any other type.
if qtype != dns.TypeCNAME {
if rrs, ok := best.exact[name+"|CNAME"]; ok {
return s.result(best, name, dns.TypeCNAME, rrs)
}
}
// NODATA: the name exists but has no records of this type.
if nameExists(best, name) {
return Result{Found: true, Authority: best.soa, Zone: best.origin, TTL: best.minTTL}
}
return Result{Found: false, Authority: best.soa, Zone: best.origin, TTL: best.minTTL}
}
func (s *Store) result(z *zone, name string, qtype uint16, rrs []dns.RR) Result {
min := uint32(0xFFFFFFFF)
for _, rr := range rrs {
if t := rr.Header().Ttl; t < min {
min = t
}
}
if min == 0xFFFFFFFF {
min = z.minTTL
}
return Result{Found: true, Records: rrs, Type: qtype, TTL: min, Zone: z.origin, Authority: z.soa}
}
func (s *Store) any(z *zone, name string) Result {
var out []dns.RR
if rrs, ok := z.exact[name+"|A"]; ok {
out = append(out, rrs...)
}
if rrs, ok := z.exact[name+"|AAAA"]; ok {
out = append(out, rrs...)
}
if rrs, ok := z.exact[name+"|CNAME"]; ok {
out = append(out, rrs...)
}
if rrs, ok := z.exact[name+"|MX"]; ok {
out = append(out, rrs...)
}
if rrs, ok := z.exact[name+"|TXT"]; ok {
out = append(out, rrs...)
}
if rrs, ok := z.exact[name+"|SRV"]; ok {
out = append(out, rrs...)
}
if rrs, ok := z.exact[name+"|NS"]; ok {
out = append(out, rrs...)
}
if len(out) == 0 {
if rrs, ok := closestWildcardAny(z, name); ok {
out = append(out, rrs...)
}
}
if len(out) == 0 {
return Result{Found: nameExists(z, name), Authority: z.soa, Zone: z.origin, TTL: z.minTTL}
}
return s.result(z, name, dns.TypeANY, out)
}
func closestWildcard(z *zone, name string, qtype uint16) ([]dns.RR, bool) {
for parent := parentOf(name); parent != ""; parent = parentOf(parent) {
if m, ok := z.wild[parent]; ok {
if rrs, ok := m[qtype]; ok {
return rrs, true
}
}
if z.delegations[parent] {
return nil, false
}
}
return nil, false
}
func closestWildcardAny(z *zone, name string) ([]dns.RR, bool) {
for parent := parentOf(name); parent != ""; parent = parentOf(parent) {
if m, ok := z.wild[parent]; ok {
var out []dns.RR
for _, rrs := range m {
out = append(out, rrs...)
}
if len(out) > 0 {
return out, true
}
}
}
return nil, false
}
func parentOf(name string) string {
i := strings.IndexByte(name, '.')
if i < 0 {
return ""
}
return name[i+1:]
}
func nameExists(z *zone, name string) bool {
prefix := name + "|"
for k := range z.exact {
if strings.HasPrefix(k, prefix) {
return true
}
}
if _, ok := z.wild[name]; ok {
return true
}
return z.delegations[name]
}
// Count is the number of records in the store.
func (s *Store) Count() int {
if s == nil {
return 0
}
return s.count
}
// Zones lists the zone apexes.
func (s *Store) Zones() []string {
if s == nil {
return nil
}
out := make([]string, 0, len(s.zones))
for z := range s.zones {
out = append(out, z)
}
sort.Strings(out)
return out
}
// Files lists the source files.
func (s *Store) Files() []string {
if s == nil {
return nil
}
return s.files
}
// BuiltAt is the compile timestamp.
func (s *Store) BuiltAt() time.Time {
if s == nil {
return time.Time{}
}
return s.builtAt
}
// Records returns every record in a zone as zone-file text, for the API and
// the UI's zone browser.
func (s *Store) Records(origin string) []string {
if s == nil {
return nil
}
z, ok := s.zones[strings.ToLower(dns.Fqdn(origin))]
if !ok {
return nil
}
var out []string
for _, rrs := range z.exact {
for _, rr := range rrs {
out = append(out, rr.String())
}
}
for name, m := range z.wild {
for _, rrs := range m {
for _, rr := range rrs {
out = append(out, strings.Replace(rr.String(), "\t"+rr.Header().Name, "\t*."+name, 1))
}
}
}
sort.Strings(out)
return out
}
// Holder keeps the current Store for the hot path.
type Holder struct{ cur atomic.Pointer[Store] }
// NewHolder wraps a store.
func NewHolder(s *Store) *Holder {
h := &Holder{}
if s != nil {
h.cur.Store(s)
}
return h
}
// Get returns the current store (nil when nothing is configured).
func (h *Holder) Get() *Store { return h.cur.Load() }
// Set installs a new store.
func (h *Holder) Set(s *Store) { h.cur.Store(s) }