Skip to content

Commit 0fce9de

Browse files
committed
Fix cycles detection in cloaking rules
Detect actual cycles in the name-target graph instead of rejecting every target that matches any cloak rule. Fixes #3238
1 parent f28f0b3 commit 0fce9de

1 file changed

Lines changed: 39 additions & 8 deletions

File tree

dnscrypt-proxy/plugin_cloak.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ type CloakedName struct {
2727
PTR []string
2828
}
2929

30+
// followsTarget returns true when resolving this rule requires following its
31+
// name target. Rules mapped to at least one IP address never follow it.
32+
func (cloakedName *CloakedName) followsTarget() bool {
33+
return !cloakedName.isIP && cloakedName.target != ""
34+
}
35+
3036
type PluginCloak struct {
3137
sync.RWMutex
3238
patternMatcher *PatternMatcher
@@ -148,25 +154,50 @@ func (plugin *PluginCloak) loadRules(lines string, patternMatcher *PatternMatche
148154
}
149155
}
150156

157+
return detectCloakingLoops(cloakedNames, patternMatcher)
158+
}
159+
160+
// detectCloakingLoops rejects rules whose name targets form a resolution loop.
161+
// A target that matches another cloaking rule is fine as long as the chain
162+
// terminates: rules mapped to IP addresses never follow their name target at
163+
// runtime, so they end the chain. Only chains that revisit a rule can loop
164+
// forever. The offender with the lowest line number is reported so the error
165+
// does not depend on map iteration order.
166+
func detectCloakingLoops(cloakedNames map[string]*CloakedName, patternMatcher *PatternMatcher) error {
151167
var firstRecursive *CloakedName
152168
var firstRecursivePattern string
153169
for _, cloakedName := range cloakedNames {
154-
if cloakedName.isIP || cloakedName.target == "" {
170+
if !cloakedName.followsTarget() {
155171
continue
156172
}
157-
matched, pattern, _ := patternMatcher.Eval(cloakedName.target)
158-
if matched && (firstRecursive == nil || cloakedName.lineNo < firstRecursive.lineNo) {
159-
firstRecursive = cloakedName
160-
firstRecursivePattern = pattern
173+
visited := map[*CloakedName]bool{cloakedName: true}
174+
current := cloakedName
175+
for {
176+
matched, pattern, xNext := patternMatcher.Eval(current.target)
177+
if !matched {
178+
break
179+
}
180+
next := xNext.(*CloakedName)
181+
if !next.followsTarget() {
182+
break
183+
}
184+
if visited[next] {
185+
if firstRecursive == nil || cloakedName.lineNo < firstRecursive.lineNo {
186+
firstRecursive = cloakedName
187+
firstRecursivePattern = pattern
188+
}
189+
break
190+
}
191+
visited[next] = true
192+
current = next
161193
}
162194
}
163195
if firstRecursive != nil {
164196
return fmt.Errorf(
165-
"recursive cloaking rule at line %d: target [%s] matches cloak pattern [%s]",
197+
"recursive cloaking rule at line %d: target [%s] loops back to cloak pattern [%s]",
166198
firstRecursive.lineNo, firstRecursive.target, firstRecursivePattern,
167199
)
168200
}
169-
170201
return nil
171202
}
172203

@@ -294,7 +325,7 @@ func (plugin *PluginCloak) Eval(pluginsState *PluginsState, msg *dns.Msg) error
294325
}
295326
}
296327
synth := EmptyResponseFromMessage(msg)
297-
if !cloakedName.isIP && ((qtype == dns.TypeA && cloakedName.ipv4 == nil) ||
328+
if cloakedName.followsTarget() && ((qtype == dns.TypeA && cloakedName.ipv4 == nil) ||
298329
(qtype == dns.TypeAAAA && cloakedName.ipv6 == nil) || expired) {
299330
target := cloakedName.target
300331
plugin.RUnlock()

0 commit comments

Comments
 (0)