a recursive dns resolver

Compare changes

Choose any two refs to compare.

+1 -1
Justfile
···
build: format
#!/usr/bin/env sh
VERSION=$(just version)
-
go build -ldflags "-X code.kiri.systems/kiri/alky/pkg/metrics.version=$VERSION" .
+
go build -ldflags "-X tangled.sh/seiso.moe/alky/pkg/metrics.version=$VERSION" .
+1 -1
docker-compose.yml
···
soft: 262144
hard: 262144
healthcheck:
-
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
+
test: curl http://localhost:8123/ping || exit 1
interval: 30s
timeout: 5s
retries: 3
+60
migrations/00002_modified_metrics.sql
···
+
-- +goose Up
+
ALTER TABLE alky_dns_queries
+
MODIFY COLUMN timestamp DateTime CODEC(Delta, ZSTD(1)),
+
MODIFY COLUMN instance_id String CODEC(ZSTD(1)),
+
MODIFY COLUMN query_name String CODEC(ZSTD(1)),
+
MODIFY COLUMN query_type LowCardinality(String) CODEC(ZSTD(1)),
+
MODIFY COLUMN query_class LowCardinality(String) CODEC(ZSTD(1)),
+
MODIFY COLUMN remote_addr String CODEC(ZSTD(1)),
+
MODIFY COLUMN response_code LowCardinality(String) CODEC(ZSTD(1)),
+
MODIFY COLUMN duration Int64 CODEC(T64, ZSTD(1)),
+
MODIFY COLUMN cache_hit Bool CODEC(ZSTD(1));
+
+
ALTER TABLE alky_dns_queries MODIFY TTL timestamp + INTERVAL 30 DAY;
+
+
ALTER TABLE alky_dns_cache_metrics
+
DROP COLUMN IF EXISTS total_queries,
+
MODIFY COLUMN timestamp DateTime CODEC(Delta, ZSTD(1)),
+
MODIFY COLUMN instance_id String CODEC(ZSTD(1)),
+
MODIFY COLUMN cache_hits Int64 CODEC(T64, ZSTD(1)),
+
MODIFY COLUMN cache_misses Int64 CODEC(T64, ZSTD(1)),
+
MODIFY COLUMN negative_hits Int64 CODEC(T64, ZSTD(1)),
+
MODIFY COLUMN positive_hits Int64 CODEC(T64, ZSTD(1)),
+
MODIFY COLUMN evictions Int64 CODEC(T64, ZSTD(1)),
+
MODIFY COLUMN size Int64 CODEC(T64, ZSTD(1));
+
+
ALTER TABLE alky_dns_cache_metrics
+
ADD COLUMN IF NOT EXISTS expired_count Int64 CODEC(T64, ZSTD(1));
+
+
ALTER TABLE alky_dns_cache_metrics MODIFY TTL timestamp + INTERVAL 30 DAY;
+
+
-- +goose Down
+
ALTER TABLE alky_dns_queries
+
MODIFY COLUMN timestamp DateTime,
+
MODIFY COLUMN instance_id String,
+
MODIFY COLUMN query_name String,
+
MODIFY COLUMN query_type String,
+
MODIFY COLUMN query_class String,
+
MODIFY COLUMN remote_addr String,
+
MODIFY COLUMN response_code String,
+
MODIFY COLUMN duration Int64,
+
MODIFY COLUMN cache_hit Bool;
+
+
ALTER TABLE alky_dns_queries MODIFY TTL timestamp + toIntervalDay(30);
+
+
ALTER TABLE alky_dns_cache_metrics
+
ADD COLUMN IF NOT EXISTS total_queries Int64 AFTER instance_id;
+
+
ALTER TABLE alky_dns_cache_metrics
+
MODIFY COLUMN timestamp DateTime,
+
MODIFY COLUMN instance_id String,
+
MODIFY COLUMN cache_hits Int64,
+
MODIFY COLUMN cache_misses Int64,
+
MODIFY COLUMN negative_hits Int64,
+
MODIFY COLUMN positive_hits Int64,
+
MODIFY COLUMN evictions Int64,
+
MODIFY COLUMN size Int;
+
+
ALTER TABLE alky_dns_cache_metrics DROP COLUMN IF EXISTS expired_count;
+
+
ALTER TABLE alky_dns_cache_metrics MODIFY TTL timestamp + toIntervalDay(30);
-388
pkg/dns/cache.go
···
-
package dns
-
-
import (
-
"container/list"
-
"fmt"
-
"log/slog"
-
"strings"
-
"sync"
-
"sync/atomic"
-
"time"
-
-
"tangled.sh/seiso.moe/magna"
-
)
-
-
const (
-
defaultNegativeTTL = 5 * time.Minute
-
maxNegativeTTL = 3 * time.Hour
-
)
-
-
type CacheEntry struct {
-
Key string
-
RCode magna.RCode
-
IsNegative bool
-
-
Answer []magna.ResourceRecord
-
Authority []magna.ResourceRecord
-
Additional []magna.ResourceRecord
-
-
CacheTime time.Time
-
ExpireAt time.Time
-
}
-
-
type CacheStats struct {
-
Hits atomic.Int64
-
Misses atomic.Int64
-
Evictions atomic.Int64
-
Expired atomic.Int64
-
Size atomic.Int64
-
NegativeHits atomic.Int64
-
PositiveHits atomic.Int64
-
}
-
-
type LRUCache struct {
-
mu sync.RWMutex
-
maxSize int
-
cleanupInterval time.Duration
-
lruList *list.List
-
cacheMap map[string]*list.Element
-
stats CacheStats
-
stopCleanup chan struct{}
-
logger *slog.Logger
-
}
-
-
type lruItem struct {
-
key string
-
entry *CacheEntry
-
}
-
-
type Cache interface {
-
Get(key string) (*CacheEntry, bool)
-
Set(key string, entry *CacheEntry)
-
GetStats() CacheStats
-
Stop()
-
}
-
-
func NewLRUCache(maxSize int, cleanupInterval time.Duration, logger *slog.Logger) *LRUCache {
-
if maxSize <= 0 {
-
maxSize = 5000
-
}
-
-
if cleanupInterval <= 0 {
-
cleanupInterval = 5 * time.Minute
-
}
-
-
cache := &LRUCache{
-
maxSize: maxSize,
-
cleanupInterval: cleanupInterval,
-
lruList: list.New(),
-
cacheMap: make(map[string]*list.Element, maxSize),
-
stopCleanup: make(chan struct{}),
-
logger: logger.With("component", "cache"),
-
}
-
-
cache.logger.Info("starting LRU cache", "max_size", maxSize, "cleanup_interval", cleanupInterval)
-
go cache.periodicCleanup()
-
return cache
-
}
-
-
func GenerateCacheKey(q magna.Question) string {
-
name := strings.ToLower(q.QName)
-
if !strings.HasSuffix(name, ".") {
-
name += "."
-
}
-
return fmt.Sprintf("%s:%s:%s", name, q.QType.String(), q.QClass.String())
-
}
-
-
func (c *LRUCache) Get(key string) (*CacheEntry, bool) {
-
c.mu.RLock()
-
element, exists := c.cacheMap[key]
-
c.mu.RUnlock()
-
-
if !exists {
-
c.stats.Misses.Add(1)
-
return nil, false
-
}
-
-
c.mu.Lock()
-
defer c.mu.Unlock()
-
-
element, exists = c.cacheMap[key]
-
if !exists {
-
c.stats.Misses.Add(1)
-
return nil, false
-
}
-
-
item := element.Value.(*lruItem)
-
entry := item.entry
-
-
now := time.Now()
-
if now.After(entry.ExpireAt) {
-
c.stats.Misses.Add(1)
-
c.stats.Expired.Add(1)
-
c.removeItem(element)
-
return nil, false
-
}
-
-
c.stats.Hits.Add(1)
-
if entry.IsNegative {
-
c.stats.NegativeHits.Add(1)
-
} else {
-
c.stats.PositiveHits.Add(1)
-
}
-
-
c.lruList.MoveToFront(element)
-
respEntry := c.adjustTTLs(entry, now)
-
-
return respEntry, true
-
}
-
-
func (c *LRUCache) adjustTTLs(original *CacheEntry, now time.Time) *CacheEntry {
-
adjustedEntry := &CacheEntry{
-
Key: original.Key,
-
RCode: original.RCode,
-
IsNegative: original.IsNegative,
-
CacheTime: original.CacheTime,
-
ExpireAt: original.ExpireAt,
-
Answer: make([]magna.ResourceRecord, len(original.Answer)),
-
Authority: make([]magna.ResourceRecord, len(original.Authority)),
-
Additional: make([]magna.ResourceRecord, len(original.Additional)),
-
}
-
-
remainingDuration := max(original.ExpireAt.Sub(now), 0)
-
remainingTTL := uint32(remainingDuration.Seconds())
-
-
copyAndAdjust := func(dest *[]magna.ResourceRecord, src []magna.ResourceRecord) {
-
for i, rr := range src {
-
(*dest)[i] = rr
-
rrExpireAt := original.CacheTime.Add(time.Duration(rr.TTL) * time.Second)
-
rrRemainingDuration := max(rrExpireAt.Sub(now), 0)
-
rrRemainingTTL := uint32(rrRemainingDuration.Seconds())
-
-
finalTTL := min(rrRemainingTTL, remainingTTL)
-
(*dest)[i].TTL = finalTTL
-
}
-
}
-
-
copyAndAdjust(&adjustedEntry.Answer, original.Answer)
-
copyAndAdjust(&adjustedEntry.Authority, original.Authority)
-
copyAndAdjust(&adjustedEntry.Additional, original.Additional)
-
-
return adjustedEntry
-
}
-
-
func (c *LRUCache) Set(key string, entry *CacheEntry) {
-
c.logger.Info("setting key", "key", key)
-
if entry == nil {
-
c.logger.Warn("attempted to set nil entry in cache", "key", key)
-
return
-
}
-
-
if entry.ExpireAt.Before(time.Now().Add(1 * time.Second)) {
-
return
-
}
-
-
c.mu.Lock()
-
defer c.mu.Unlock()
-
-
if element, exists := c.cacheMap[key]; exists {
-
c.lruList.MoveToFront(element)
-
element.Value.(*lruItem).entry = entry
-
return
-
}
-
-
newItem := &lruItem{
-
key: key,
-
entry: entry,
-
}
-
element := c.lruList.PushFront(newItem)
-
c.cacheMap[key] = element
-
c.stats.Size.Store(int64(c.lruList.Len()))
-
-
for int64(c.lruList.Len()) > int64(c.maxSize) {
-
c.evictLRU()
-
}
-
}
-
-
func (c *LRUCache) evictLRU() {
-
element := c.lruList.Back()
-
if element != nil {
-
c.removeItem(element)
-
c.stats.Evictions.Add(1)
-
}
-
}
-
-
func (c *LRUCache) removeItem(element *list.Element) {
-
item := element.Value.(*lruItem)
-
delete(c.cacheMap, item.key)
-
c.lruList.Remove(element)
-
c.stats.Size.Store(int64(c.lruList.Len()))
-
}
-
-
func (c *LRUCache) GetStats() CacheStats {
-
statsSnapshot := CacheStats{}
-
statsSnapshot.Hits.Store(c.stats.Hits.Load())
-
statsSnapshot.Misses.Store(c.stats.Misses.Load())
-
statsSnapshot.Evictions.Store(c.stats.Evictions.Load())
-
statsSnapshot.Expired.Store(c.stats.Expired.Load())
-
statsSnapshot.Size.Store(c.stats.Size.Load())
-
statsSnapshot.NegativeHits.Store(c.stats.NegativeHits.Load())
-
statsSnapshot.PositiveHits.Store(c.stats.PositiveHits.Load())
-
return statsSnapshot
-
}
-
-
func (c *LRUCache) periodicCleanup() {
-
ticker := time.NewTicker(c.cleanupInterval)
-
defer ticker.Stop()
-
-
for {
-
select {
-
case <-ticker.C:
-
c.cleanupExpired()
-
case <-c.stopCleanup:
-
return
-
}
-
}
-
}
-
-
func (c *LRUCache) cleanupExpired() {
-
c.mu.Lock()
-
defer c.mu.Unlock()
-
-
now := time.Now()
-
cleanedCount := 0
-
element := c.lruList.Back()
-
-
for element != nil {
-
item := element.Value.(*lruItem)
-
prevElement := element.Prev()
-
-
if now.After(item.entry.ExpireAt) {
-
c.removeItem(element)
-
c.stats.Expired.Add(1)
-
cleanedCount++
-
}
-
-
element = prevElement
-
}
-
if cleanedCount > 0 {
-
c.logger.Info("Cache cleanup finished", "items_removed", cleanedCount, "current_size", c.stats.Size.Load())
-
}
-
}
-
-
func (c *LRUCache) Stop() {
-
close(c.stopCleanup)
-
}
-
-
func CreateCacheEntry(query magna.Question, response *magna.Message) *CacheEntry {
-
now := time.Now()
-
entry := &CacheEntry{
-
Key: GenerateCacheKey(query),
-
RCode: response.Header.RCode,
-
Answer: response.Answer,
-
Authority: response.Authority,
-
Additional: response.Additional,
-
CacheTime: now,
-
}
-
-
var minTTL time.Duration = -1
-
-
if response.Header.RCode == magna.NXDOMAIN || (response.Header.RCode == magna.NOERROR && len(response.Answer) == 0) {
-
entry.IsNegative = true
-
negativeTTL := defaultNegativeTTL
-
-
for _, rr := range response.Authority {
-
if rr.RType == magna.SOAType {
-
soa, ok := rr.RData.(*magna.SOA)
-
if ok && soa != nil {
-
ttl := time.Duration(rr.TTL) * time.Second
-
minimum := time.Duration(soa.Minimum) * time.Second
-
-
if minimum > maxNegativeTTL {
-
negativeTTL = ttl
-
} else {
-
negativeTTL = minDuration(ttl, minimum)
-
}
-
-
} else {
-
negativeTTL = time.Duration(rr.TTL) * time.Second
-
}
-
break
-
}
-
}
-
-
if negativeTTL < 0 {
-
negativeTTL = defaultNegativeTTL
-
}
-
if negativeTTL > maxNegativeTTL {
-
negativeTTL = maxNegativeTTL
-
}
-
-
minTTL = negativeTTL
-
} else if response.Header.RCode == magna.NOERROR {
-
entry.IsNegative = false
-
-
if len(response.Answer) > 0 {
-
minTTL = time.Duration(response.Answer[0].TTL) * time.Second
-
-
for _, rr := range response.Answer[1:] {
-
currentTTL := time.Duration(rr.TTL) * time.Second
-
if currentTTL < minTTL {
-
minTTL = currentTTL
-
}
-
}
-
} else {
-
entry.IsNegative = true
-
negativeTTL := defaultNegativeTTL
-
for _, rr := range response.Authority {
-
if rr.RType == magna.SOAType {
-
soa, ok := rr.RData.(*magna.SOA)
-
if ok && soa != nil {
-
ttl := time.Duration(rr.TTL) * time.Second
-
minimum := time.Duration(soa.Minimum) * time.Second
-
if minimum > maxNegativeTTL {
-
negativeTTL = ttl
-
} else {
-
negativeTTL = minDuration(ttl, minimum)
-
}
-
-
} else {
-
negativeTTL = time.Duration(rr.TTL) * time.Second
-
}
-
break
-
}
-
}
-
if negativeTTL < 0 {
-
negativeTTL = defaultNegativeTTL
-
}
-
if negativeTTL > maxNegativeTTL {
-
negativeTTL = maxNegativeTTL
-
}
-
minTTL = negativeTTL
-
}
-
-
} else {
-
return nil
-
}
-
-
if minTTL < 0 {
-
return nil
-
}
-
-
entry.ExpireAt = now.Add(minTTL)
-
return entry
-
}
-
-
func minDuration(a, b time.Duration) time.Duration {
-
if a < b {
-
return a
-
}
-
return b
-
}
-
-
func min(a, b uint32) uint32 {
-
if a < b {
-
return a
-
}
-
return b
-
}
-43
main.go
···
"os/signal"
"strings"
"syscall"
-
"time"
"tangled.sh/seiso.moe/alky/pkg/config"
"tangled.sh/seiso.moe/alky/pkg/dns"
···
}
logger.Info("Root hints loaded", "count", len(rootServers), "path", cfg.Server.RootHintsFile)
-
cache := dns.NewLRUCache(cfg.Cache.MaxItems, cfg.Cache.CleanupInterval.Duration, logger.With("component", "cache"))
-
defer cache.Stop()
-
logger.Info("DNS cache initialized")
-
queryHandler := &dns.QueryHandler{
RootServers: rootServers,
Timeout: cfg.Advanced.QueryTimeout.Duration,
-
Cache: cache,
Logger: logger.With("component", "resolver"),
}
-
go monitorCacheMetrics(cache, metricsClient, logger.With("component", "cache-monitor"))
-
var currentHandler dns.Handler = queryHandler
if cfg.Ratelimit.Rate > 0 {
···
sig := <-sigChan
logger.Info("Received signal, shutting down gracefully...", "signal", sig.String())
-
logger.Info("Stopping cache...")
-
cache.Stop()
-
logger.Info("Closing metrics client...")
metricsClient.Close()
···
}
}
-
func monitorCacheMetrics(cache dns.Cache, metricsClient *metrics.ClickHouseMetrics, logger *slog.Logger) {
-
interval := 1 * time.Minute
-
logger.Info("Starting cache metrics monitoring", "interval", interval)
-
ticker := time.NewTicker(interval)
-
defer ticker.Stop()
-
-
for {
-
select {
-
case <-ticker.C:
-
stats := cache.GetStats()
-
logger.Debug("Recording cache metrics",
-
"hits", stats.Hits.Load(),
-
"misses", stats.Misses.Load(),
-
"size", stats.Size.Load(),
-
"evictions", stats.Evictions.Load(),
-
"expired", stats.Expired.Load(),
-
"pos_hits", stats.PositiveHits.Load(),
-
"neg_hits", stats.NegativeHits.Load(),
-
)
-
metricsClient.RecordCacheStats(metrics.CacheMetric{
-
Timestamp: time.Now(),
-
CacheHits: stats.Hits.Load(),
-
CacheMisses: stats.Misses.Load(),
-
NegativeHits: stats.NegativeHits.Load(),
-
PositiveHits: stats.PositiveHits.Load(),
-
Evictions: stats.Evictions.Load() + stats.Expired.Load(),
-
Size: stats.Size.Load(),
-
})
-
}
-
}
-
}
-
func setupLogger(cfg *config.Config) *slog.Logger {
var logLevel slog.Level
switch strings.ToLower(cfg.Logging.Level) {
+4
pkg/rootservers/loader.go
···
}
for _, line := range bytes.Split(data, []byte{'\n'}) {
+
if len(line) == 0 {
+
continue
+
}
+
// skip comments
if line[0] == ';' {
continue
-1
pkg/config/config.go
···
"fmt"
"os"
"slices"
-
"strings"
"time"
"github.com/BurntSushi/toml"
+1 -1
go.mod
···
github.com/ClickHouse/clickhouse-go/v2 v2.34.0
github.com/stretchr/testify v1.10.0
golang.org/x/time v0.11.0
-
tangled.sh/seiso.moe/magna v0.0.1
+
tangled.sh/seiso.moe/magna v0.0.2
)
require (
+2
go.sum
···
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tangled.sh/seiso.moe/magna v0.0.1 h1:v8GM2y3xEinc0jGVxYf/33xtWJ74ES9EuTaMxXL8zxo=
tangled.sh/seiso.moe/magna v0.0.1/go.mod h1:bqm+DTo2Pv4ITT0EnR079l++BJgoChBswSB/3KeijUk=
+
tangled.sh/seiso.moe/magna v0.0.2 h1:4VGPlqv/7tVyTtsR4Qkk8ZNypuNbmaeLogWzkpbHrRs=
+
tangled.sh/seiso.moe/magna v0.0.2/go.mod h1:bqm+DTo2Pv4ITT0EnR079l++BJgoChBswSB/3KeijUk=
+7 -7
pkg/dns/ratelimit.go
···
}
type rateLimiter struct {
-
config RateLimitConfig
-
limiters map[string]*ipRateLimiterEntry
-
mu sync.RWMutex
+
config RateLimitConfig
+
limiters map[string]*ipRateLimiterEntry
+
mu sync.RWMutex
stopCleanup chan struct{}
}
···
func newRateLimiter(config RateLimitConfig) *rateLimiter {
rl := &rateLimiter{
-
config: config,
-
limiters: make(map[string]*ipRateLimiterEntry),
+
config: config,
+
limiters: make(map[string]*ipRateLimiterEntry),
stopCleanup: make(chan struct{}),
}
···
}
func (rl *rateLimiter) allow(ip string) bool {
-
rl.mu.Lock()
+
rl.mu.Lock()
defer rl.mu.Unlock()
entry, exists := rl.limiters[ip]
···
if !exists {
limiter := rate.NewLimiter(rate.Limit(rl.config.Rate), rl.config.Burst)
entry := &ipRateLimiterEntry{
-
limiter: limiter,
+
limiter: limiter,
lastAccess: now,
}
+65 -11
pkg/dns/resolve.go
···
"tangled.sh/seiso.moe/magna"
)
-
var errNXDOMAIN = fmt.Errorf("nxdomain")
+
var (
+
errNXDOMAIN = fmt.Errorf("alky: domain does not exist")
+
errOnlySOA = fmt.Errorf("alky: only an soa was reffered")
+
errNoServers = fmt.Errorf("alky: no servers responded")
+
errMaxDepth = fmt.Errorf("alky: maximum recursion depth exceeded")
+
errNonMatchingID = fmt.Errorf("alky: response ID mismatch")
+
errNonMatchingQuestion = fmt.Errorf("alky: response question mismatch")
+
errQueryTimeout = fmt.Errorf("alky: query timeout")
+
)
const (
depthKey contextKey = "dns_recursion_depth"
···
func withIncrementedDepth(ctx context.Context, maxDepth int) (context.Context, error) {
depth := getDepth(ctx)
if depth >= maxDepth {
-
return nil, fmt.Errorf("maximum recursion depth (%d) exceeded", maxDepth)
+
return nil, errMaxDepth
}
return context.WithValue(ctx, depthKey, depth+1), nil
}
···
msg.Header.NSCount = uint16(len(records))
msg.Header.ANCount = 0
msg.Answer = nil
+
} else if err == errOnlySOA {
+
msg = msg.SetRCode(magna.NOERROR)
+
msg.Authority = records
+
msg.Header.NSCount = uint16(len(records))
+
msg.Header.ANCount = 0
+
msg.Header.ARCount = 0
+
msg.Answer = nil
} else if err != nil {
+
h.Logger.Warn("error", "error", err)
msg = msg.SetRCode(magna.SERVFAIL)
} else {
msg.Answer = records
···
for _, s := range servers {
msg, err := queryServer(ctx, question, s, h.Timeout)
if err != nil {
-
h.Logger.Warn("unable to resolve question", "server", s)
+
h.Logger.Warn("unable to resolve question", "server", s, "error", err)
continue
}
+
if msg.Header.RCode == magna.NXDOMAIN {
+
_, authority := ExtractSOA(msg)
+
return authority, errNXDOMAIN
+
}
+
if ok, answers := ExtractAnswer(question, msg); ok {
if msg.Answer[0].RType == magna.CNAMEType {
cnameQuestion := magna.Question{QName: msg.Answer[0].RData.String(), QType: question.QType, QClass: question.QClass}
···
if ok, answers := h.HandleReferral(ctx, question, msg); ok {
return h.resolveQuestion(ctx, question, answers)
}
+
+
if ok, answers := ExtractSOA(msg); ok {
+
return answers, errOnlySOA
+
}
}
-
return []magna.ResourceRecord{}, nil
+
return []magna.ResourceRecord{}, errNoServers
}
func queryServer(ctx context.Context, question magna.Question, server string, timeout time.Duration) (magna.Message, error) {
+
ctx, cancel := context.WithTimeout(ctx, timeout)
+
defer cancel()
+
var d net.Dialer
conn, err := d.DialContext(ctx, "udp", fmt.Sprintf("%s:53", server))
if err != nil {
···
}
defer conn.Close()
-
go func() {
-
<-ctx.Done()
-
conn.Close()
-
}()
-
conn.SetDeadline(time.Now().Add(timeout))
-
query := magna.CreateRequest(0, false)
+
query := magna.CreateRequest(magna.QUERY, false)
query = query.AddQuestion(question)
msg, err := query.Encode()
if err != nil {
···
}
var response magna.Message
-
err = response.Decode(p)
+
if err := response.Decode(p); err != nil {
+
return magna.Message{}, err
+
}
+
+
if err := validateResponse(*query, response, question); err != nil {
+
return magna.Message{}, err
+
}
return response, err
}
+
func validateResponse(query magna.Message, response magna.Message, question magna.Question) error {
+
if response.Header.ID != query.Header.ID {
+
return errNonMatchingID
+
}
+
if len(response.Question) < 1 || response.Question[0] != question {
+
return errNonMatchingQuestion
+
}
+
return nil
+
}
+
func ExtractAnswer(q magna.Question, r magna.Message) (bool, []magna.ResourceRecord) {
answers := make([]magna.ResourceRecord, 0, r.Header.ANCount)
for _, a := range r.Answer {
···
return true, answers
}
+
func ExtractSOA(r magna.Message) (bool, []magna.ResourceRecord) {
+
answers := make([]magna.ResourceRecord, 0, r.Header.NSCount)
+
for _, a := range r.Authority {
+
if a.RType == magna.SOAType {
+
answers = append(answers, a)
+
}
+
}
+
+
if len(answers) <= 0 {
+
return false, []magna.ResourceRecord{}
+
}
+
+
return true, answers
+
}
+
func HandleGlueRecords(q magna.Question, r magna.Message) (bool, []string) {
answers := make([]string, 0, r.Header.ARCount)
for _, a := range r.Authority {