initial commit
This commit is contained in:
1 parent
5b935301e5
commit
85453852d5
105 files changed
+24846
No files matched your search
Vendored
+209
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user