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

+209
View File
@@ -0,0 +1,209 @@
// Package cache implements the hot answer cache: a sharded LRU over packed
// wire-format DNS messages, backed by a bbolt snapshot so a restart does not
// start cold.
//
// Design notes that matter for performance:
//
// - Entries are stored as packed wire bytes, not as *dns.Msg. A hit costs
// one Unpack and yields a private copy, so serving never mutates shared
// state and TTL ageing is free. This is the same trade dnsmasq makes.
// - Keys are Go strings built from the already-allocated question name, so
// the lookup path does not allocate on a hit.
// - The LRU is sharded by name hash: a hot domain only contends with other
// names in its own shard.
// - Expired-but-useful entries are kept for a serve-stale window so a dead
// upstream degrades latency instead of availability.
package cache
import (
"encoding/binary"
"strings"
"time"
"github.com/miekg/dns"
)
// Key identifies a cache entry. Scope lets several logically distinct
// resolvers share one cache (for example per-upstream-group ACL overrides)
// without colliding.
type Key struct {
Name string
Type uint16
Class uint16
// DO mirrors the request's DNSSEC OK bit. Since we pass AD/DO through
// without validating, a DO query legitimately gets a different answer.
DO bool
// Scope is a small caller-defined namespace, usually a group/ACL key.
Scope uint8
// CD (checking disabled) is not part of the key: RFC 4035 says a CD
// query is answered from a non-CD cached entry, which is what we do.
}
// QueryKey builds a Key from a DNS question.
func QueryKey(name string, qtype, qclass uint16, do bool, scope uint8) Key {
return Key{
Name: canonical(name),
Type: qtype,
Class: qclass,
DO: do,
Scope: scope,
}
}
// FromMsg builds a Key from the first question of a message.
func FromMsg(m *dns.Msg, scope uint8) (Key, bool) {
if len(m.Question) == 0 {
return Key{}, false
}
q := m.Question[0]
return QueryKey(q.Name, q.Qtype, q.Qclass, Do(m), scope), true
}
// Do reports whether the message carries the DNSSEC OK bit.
func Do(m *dns.Msg) bool {
if o := m.IsEdns0(); o != nil {
return o.Do()
}
return false
}
// canonical lowercases and strips the trailing dot of a name so that
// "Example.COM." and "example.com" share one entry.
func canonical(name string) string {
if n := len(name); n > 0 && name[n-1] == '.' {
name = name[:n-1]
}
// A fast path for the overwhelmingly common all-lowercase case keeps this
// allocation-free.
for i := 0; i < len(name); i++ {
c := name[i]
if c >= 'A' && c <= 'Z' {
return strings.ToLower(name)
}
}
return name
}
// packKey serialises a key for the bbolt snapshot. The wire format is
// length-prefixed so it is unambiguous and compact:
//
// varint len | wire name | uint16 type | uint16 class | uint8 flags
func packKey(buf []byte, k Key) ([]byte, error) {
name := k.Name
if n := len(name); n > 255*4 {
return nil, errKeyTooLong
}
var tmp [256]byte
wireLen, err := dns.PackDomainName(name, tmp[:], 0, nil, false)
if err != nil {
return nil, err
}
buf = binary.AppendUvarint(buf, uint64(wireLen))
buf = append(buf, tmp[:wireLen]...)
buf = binary.LittleEndian.AppendUint16(buf, k.Type)
buf = binary.LittleEndian.AppendUint16(buf, k.Class)
var flags uint8
if k.DO {
flags |= 1
}
flags |= k.Scope << 1
return append(buf, flags), nil
}
func unpackKey(buf []byte) (Key, []byte, error) {
l, n := binary.Uvarint(buf)
if n <= 0 || l > uint64(len(buf)-n) {
return Key{}, nil, errBadKey
}
buf = buf[n:]
wire := buf[:int(l)]
buf = buf[l:]
if len(buf) < 5 {
return Key{}, nil, errBadKey
}
name, _, err := dns.UnpackDomainName(wire, 0)
if err != nil {
return Key{}, nil, err
}
var k Key
k.Name = canonical(name)
k.Type = binary.LittleEndian.Uint16(buf[0:])
k.Class = binary.LittleEndian.Uint16(buf[2:])
flags := buf[4]
k.DO = flags&1 != 0
k.Scope = flags >> 1
return k, buf[5:], nil
}
// Entry is one cached response plus its timing metadata.
//
// Wire is the packed response exactly as it will be served (minus ID and TTL
// ageing, both applied on unpack). Origin records where the answer came from
// for the UI.
type Entry struct {
Wire []byte
// TTL is the value the entry was stored with, used to age the answer on
// every serve so clients see a monotonically decreasing TTL.
TTL uint32
// Age is the logical age at store time (0 for a fresh answer).
Age time.Duration
// Origin is a small enum: "upstream", "prewarm", "local", "blocked".
Origin string
// Upstream is the address the answer came from, shown in the UI.
Upstream string
// CreatedAt is the wall clock when the entry was stored.
CreatedAt time.Time
// ExpiresAt is when the entry stops being fresh.
ExpiresAt time.Time
// StaleUntil is when the entry is finally evicted. It is ExpiresAt plus
// the serve-stale window.
StaleUntil time.Time
// Bytes is the accounted size, used by the LRU and the size gauges.
Bytes int
// Hits counts serves since it was stored.
Hits uint32
// Neg marks a cached NXDOMAIN or NODATA answer.
Neg bool
// Truncated records that the upstream answer did not fit in 512 bytes,
// so a cache hit must re-truncate for a small client buffer.
Truncated bool
}
// AgeTTL returns the TTL an entry should be served with, given the elapsed
// time since it was stored, clamped at zero.
func (e *Entry) AgeTTL(now time.Time) uint32 {
if e.CreatedAt.IsZero() {
return e.TTL
}
elapsed := uint32(now.Sub(e.CreatedAt).Seconds())
if elapsed >= e.TTL {
return 0
}
return e.TTL - elapsed
}
// Remaining returns how long the entry stays fresh.
func (e *Entry) Remaining(now time.Time) time.Duration {
if !e.ExpiresAt.After(now) {
return 0
}
return e.ExpiresAt.Sub(now)
}
// StaleRemaining returns how long the entry remains serveable as stale data.
func (e *Entry) StaleRemaining(now time.Time) time.Duration {
if !e.StaleUntil.After(now) {
return 0
}
return e.StaleUntil.Sub(now)
}
// Expired reports whether the entry is past its freshness window.
func (e *Entry) Expired(now time.Time) bool { return !e.ExpiresAt.After(now) }
// BlockResponse is a synthesised answer (blocked or locally defined) held in
// the cache so repeated blocked queries never touch the matcher or the wire.
type BlockResponse struct {
Wire []byte
TTL uint32
}