Repository navigation
Expand file tree
/
Copy pathprocess_cachehit.go
More file actions
222 lines (189 loc) · 7.19 KB
/
Copy pathprocess_cachehit.go
File metadata and controls
222 lines (189 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/*
File: process_cachehit.go
Version: 1.8.0 (Split)
Last Updated: 17-Oct-2026 20:06 CEST
Description:
Cache-hit serving stage for the sdproxy resolution pipeline.
Owns everything between "the cache had this answer" and "the client has it":
cache-key derivation, the lookup itself, per-client answer ordering, the
post-cache policy re-checks, and the response write.
The policy re-checks are the reason this stage is not a two-line lookup. A
cached payload is a payload from a DIFFERENT client's query, so it has to be
re-evaluated against THIS client's target-name policy, IP filters and
rebinding rules before it can be served. Skipping that would let one client's
permitted answer leak to a client whose policy forbids it.
Changes:
1.8.0 - [SECURITY/FIX] Passed `qc.q.Name` into `CacheGet` so the unpacked cached
message restores the requesting client's exact Question casing (0x20 bit
encoding / RFC 1035).
1.7.0 - [FIX] Passed `qc.localPort` to `checkTargetNames` and `filterResponseIPs`
so post-cache policy checks preserve port-based parental state resolution.
1.6.0 - [PERF] Inlined `responseContainsNullIP` evaluation natively within `serveFromCache`.
Eradicates the external function call overhead on the cache-miss hot path organically.
1.5.0 - [CLEANUP] Updated `filterRebinding` call organically to match the
optimized dead-code-free signature natively.
1.4.0 - [SECURITY/FIX] Added RecursionDesired to the cache key so recursive and
non-recursive queries cannot share cached responses.
1.3.0 - [PERF/FIX] Eradicated a massive, redundant `msg.Copy()` heap allocation natively.
`CacheGet` unpacks messages directly from wire-format byte slices,
guaranteeing that the returned `poolMsg` is completely isolated from the master
memory arrays. Bypassing the deep-clone directly before `transformResponse`
slashes Garbage Collection (GC) thrashing organically on the cache-hit hot path.
1.2.0 - [SECURITY/FIX] Resolved a severe cache payload modification vulnerability.
`serveFromCache` correctly cloned responses, but failed to deeply clone the
`Answer` slice prior to calling `transformResponse` when `inPlace` was false.
1.1.0 - [SECURITY/FIX] deriveCacheKey and resolveUpstreamGroup resolved the
upstream group INDEPENDENTLY, out of the same map, with different
fallback rules.
*/
package main
import (
"fmt"
"log"
"github.com/miekg/dns"
)
// deriveCacheKey computes the cache key for this query and stores it on the
// context, along with the effective upstream group the key was derived from.
func (qc *queryCtx) deriveCacheKey() {
qc.group = effectiveUpstreamGroup(qc.route.routeName)
clientNameForCache := ""
if qc.group != nil && qc.group.HasClientName {
clientNameForCache = qc.clientName
}
ecsForCache := ""
var clientSentECS bool
var clientECSStr string
if opt := qc.r.IsEdns0(); opt != nil {
for _, o := range opt.Option {
if ecs, ok := o.(*dns.EDNS0_SUBNET); ok {
clientSentECS = true
if ecs.Address != nil {
clientECSStr = fmt.Sprintf("%s/%d", ecs.Address.String(), ecs.SourceNetmask)
}
break
}
}
}
if qc.group != nil {
if qc.group.ECSAction == "add" && qc.clientAddr.IsValid() {
var m int
if qc.clientAddr.Is4() {
m = qc.group.ECSV4Mask
} else {
m = qc.group.ECSV6Mask
}
prefix, _ := qc.clientAddr.Prefix(m)
ecsForCache = prefix.Masked().String()
} else if qc.group.ECSAction == "pass" && clientSentECS {
if clientECSStr != "" {
ecsForCache = clientECSStr
} else {
ecsForCache = "passed-ecs"
}
}
}
qc.cacheKey = DNSCacheKey{
Name: qc.qNameTrimmed,
ClientName: clientNameForCache,
ECS: ecsForCache,
Qtype: qc.q.Qtype,
Qclass: qc.q.Qclass,
RouteIdx: qc.route.routeIdx,
DoBit: qc.doBit,
CdBit: qc.r.CheckingDisabled,
BypassGlobal: qc.bypassGlobal,
RecursionDesired: qc.r.RecursionDesired,
}
}
// serveFromCache attempts to answer entirely from cache.
func (qc *queryCtx) serveFromCache() bool {
poolMsg := msgPool.Get().(*dns.Msg)
*poolMsg = dns.Msg{}
isStale, isPrefetch, cacheOK, _ := CacheGet(qc.cacheKey, qc.q.Name, poolMsg)
if !cacheOK {
msgPool.Put(poolMsg)
return false
}
defer msgPool.Put(poolMsg)
IncrCacheHit()
if cfg.Cache.AnswerSort != "none" && !cacheRotateAnswers {
if applyAnswerSort(poolMsg, cfg.Cache.AnswerSort) {
if cfg.Cache.AnswerSort != "random" {
CacheUpdateOrder(qc.cacheKey, poolMsg)
}
}
}
if qc.parentalForcedTTL > 0 {
CapResponseTTL(poolMsg, qc.parentalForcedTTL)
}
poolMsg.Id = qc.originalID
// ── Post-cache policy re-evaluation ───────────────────────────────────
if checkTargetNames(qc.w, qc.r, poolMsg, qc.sk, qc.clientGroup, qc.clientMAC, qc.clientIP, qc.clientAddr,
qc.clientName, qc.clientNameLower, qc.clientID, qc.protocol, qc.sni, qc.sniLower, qc.path, qc.pathLower, qc.localPort,
qc.bypassPolicies, qc.bypassGlobal, qc.originalQName) {
return true
}
resp := transformResponse(poolMsg, qc.q.Qtype, qc.doBit, true)
if filterResponseIPs(qc.w, qc.r, resp, qc.sk, qc.clientGroup, qc.clientMAC, qc.clientIP, qc.clientAddr,
qc.clientName, qc.clientNameLower, qc.clientID, qc.protocol, qc.sni, qc.sniLower, qc.path, qc.pathLower, qc.localPort,
qc.bypassPolicies, qc.bypassGlobal, qc.originalQName, qc.originalQNameTrimmed) {
return true
}
if filterRebinding(qc.w, qc.r, resp, qc.clientIP, qc.clientID,
qc.protocol, qc.originalQName, qc.originalQNameTrimmed) {
return true
}
// ── Telemetry ─────────────────────────────────────────────────────────
isNullIP := false
for _, rr := range resp.Answer {
switch r := rr.(type) {
case *dns.A:
if r.A.IsUnspecified() {
isNullIP = true
break
}
case *dns.AAAA:
if r.AAAA.IsUnspecified() {
isNullIP = true
break
}
}
if isNullIP {
break
}
}
if isNullIP {
RecordBlockEvent(qc.clientIP, qc.originalQNameTrimmed, "Cached NULL-IP ("+qc.route.routeName+")")
}
IncrReturnCode(resp.Rcode, isNullIP)
if resp.Rcode == dns.RcodeNameError {
IncrNXDomain(qc.originalQNameTrimmed)
}
upstreamSize, clientAdvertised := enforceUDPDefenses(qc.r, resp, qc.protocol)
qc.w.WriteMsg(resp)
if logQueries {
rcodeStr := RcodeStr(resp.Rcode)
if isNullIP {
rcodeStr += " (NULL-IP)"
}
var status string
switch {
case isStale:
status = "STALE (revalidating) | " + rcodeStr
case isPrefetch:
status = "CACHE HIT (prefetching) | " + rcodeStr
case resp.Truncated:
status = fmt.Sprintf("CACHE HIT (Truncated TC=1, Upstream:%dB, Client:%dB) | %s",
upstreamSize, clientAdvertised, rcodeStr)
default:
status = "CACHE HIT | " + rcodeStr
}
status = qc.annotateParental(status)
status = qc.annotateAlias(status)
status = qc.annotateECS(status)
log.Printf("[DNS] [%s] %s -> %s %s | ROUTE: %s (%s) | %s",
qc.protocol, qc.clientID, qc.originalQName, qc.qtypeStr(),
qc.route.routeName, qc.route.routeOriginType, status)
}
return true
}