Skip to content

Commit dcdd952

Browse files
authored
fix(resolver): restore fallbackUpstream for rewritten queries (#2223)
1 parent 74f10f6 commit dcdd952

5 files changed

Lines changed: 192 additions & 4 deletions

resolver/conditional_upstream_resolver.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,9 @@ func (r *ConditionalUpstreamResolver) Resolve(ctx context.Context, request *mode
9494
resolved := false
9595
if len(r.mapping) > 0 {
9696
resolved, response, err = r.processRequest(ctx, request)
97-
if err != nil {
98-
return nil, err
99-
}
10097
}
10198

102-
if !resolved {
99+
if !resolved && err == nil {
103100
logger.WithField("next_resolver", Name(r.next)).Trace("go to next resolver")
104101
response, err = r.next.Resolve(ctx, request)
105102
if err != nil {
@@ -110,6 +107,18 @@ func (r *ConditionalUpstreamResolver) Resolve(ctx context.Context, request *mode
110107
// Revert the request
111108
request.Req = original
112109

110+
// The mapped resolver failed or had nothing: ask the rest of the chain,
111+
// using the original name (`fallbackUpstream`).
112+
if shouldFallbackUpstream(&r.cfg.RewriterConfig, resolved, response, err) {
113+
logger.WithField("next_resolver", Name(r.next)).Trace("fallback to next resolver")
114+
115+
return r.next.Resolve(ctx, request)
116+
}
117+
118+
if err != nil {
119+
return nil, err
120+
}
121+
113122
// Revert rewrites in the response
114123
if rewritten != nil && response != NoResponse && response != nil && response.Res != nil {
115124
revertRewritesInResponse(response.Res, originalNames)

resolver/conditional_upstream_resolver_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,121 @@ var _ = Describe("ConditionalUpstreamResolver", Label("conditionalResolver"), fu
257257
})
258258
})
259259

260+
When("the mapped upstream has no answer and fallbackUpstream is set", func() {
261+
BeforeEach(func() {
262+
emptyUpstream := NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) {
263+
response = new(dns.Msg)
264+
response.SetReply(request)
265+
266+
return response
267+
})
268+
269+
sutConfig.FallbackUpstream = true
270+
sutConfig.Mapping.Upstreams["empty.box"] = []config.Upstream{emptyUpstream.Start()}
271+
sutConfig.Rewrite = map[string]string{"source.test": "empty.box"}
272+
})
273+
274+
It("should ask the next resolver with the original name", func() {
275+
var seen string
276+
277+
m = &mockResolver{}
278+
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
279+
m.ResolveFn = func(_ context.Context, req *Request) (*Response, error) {
280+
seen = req.Req.Question[0].Name
281+
resp, err := util.NewMsgWithAnswer(seen, 250, A, "192.192.192.192")
282+
Expect(err).Should(Succeed())
283+
284+
return &Response{Res: resp, RType: ResponseTypeRESOLVED, Reason: "RESOLVED"}, nil
285+
}
286+
sut.Next(m)
287+
288+
Expect(sut.Resolve(ctx, newRequest("www.source.test.", A))).
289+
Should(
290+
SatisfyAll(
291+
BeDNSRecord("www.source.test.", A, "192.192.192.192"),
292+
HaveResponseType(ResponseTypeRESOLVED),
293+
HaveReturnCode(dns.RcodeSuccess),
294+
))
295+
296+
Expect(seen).Should(Equal("www.source.test."))
297+
})
298+
})
299+
300+
When("the mapped upstream fails and fallbackUpstream is set", func() {
301+
BeforeEach(func() {
302+
deadUpstream := NewMockUDPUpstreamServer().WithAnswerRR("dead.box. 123 IN A 1.2.3.4")
303+
upstream := deadUpstream.Start()
304+
deadUpstream.Close()
305+
306+
sutConfig.FallbackUpstream = true
307+
sutConfig.Mapping.Upstreams["dead.box"] = []config.Upstream{upstream}
308+
sutConfig.Rewrite = map[string]string{"source.test": "dead.box"}
309+
})
310+
311+
It("should ask the next resolver with the original name", func() {
312+
var seen string
313+
314+
m = &mockResolver{}
315+
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
316+
m.ResolveFn = func(_ context.Context, req *Request) (*Response, error) {
317+
seen = req.Req.Question[0].Name
318+
resp, err := util.NewMsgWithAnswer(seen, 250, A, "192.192.192.192")
319+
Expect(err).Should(Succeed())
320+
321+
return &Response{Res: resp, RType: ResponseTypeRESOLVED, Reason: "RESOLVED"}, nil
322+
}
323+
sut.Next(m)
324+
325+
Expect(sut.Resolve(ctx, newRequest("www.source.test.", A))).
326+
Should(HaveResponseType(ResponseTypeRESOLVED))
327+
328+
Expect(seen).Should(Equal("www.source.test."))
329+
})
330+
})
331+
332+
When("the mapped upstream fails and fallbackUpstream is not set", func() {
333+
BeforeEach(func() {
334+
deadUpstream := NewMockUDPUpstreamServer().WithAnswerRR("dead.box. 123 IN A 1.2.3.4")
335+
upstream := deadUpstream.Start()
336+
deadUpstream.Close()
337+
338+
sutConfig.Mapping.Upstreams["dead.box"] = []config.Upstream{upstream}
339+
sutConfig.Rewrite = map[string]string{"source.test": "dead.box"}
340+
})
341+
342+
It("should return the error", func() {
343+
_, err := sut.Resolve(ctx, newRequest("www.source.test.", A))
344+
Expect(err).Should(HaveOccurred())
345+
Expect(m.Calls).Should(BeEmpty())
346+
})
347+
})
348+
349+
When("the mapped upstream has no answer and fallbackUpstream is not set", func() {
350+
BeforeEach(func() {
351+
emptyUpstream := NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) {
352+
response = new(dns.Msg)
353+
response.SetReply(request)
354+
355+
return response
356+
})
357+
358+
sutConfig.Mapping.Upstreams["empty.box"] = []config.Upstream{emptyUpstream.Start()}
359+
sutConfig.Rewrite = map[string]string{"source.test": "empty.box"}
360+
})
361+
362+
It("should return the empty answer and not ask the next resolver", func() {
363+
Expect(sut.Resolve(ctx, newRequest("www.source.test.", A))).
364+
Should(
365+
SatisfyAll(
366+
HaveNoAnswer(),
367+
HaveResponseType(ResponseTypeCONDITIONAL),
368+
HaveReturnCode(dns.RcodeSuccess),
369+
))
370+
371+
Expect(m.Calls).Should(BeEmpty())
372+
})
373+
})
374+
260375
When("request does not match rewrite rule but matches conditional mapping", func() {
261376
It("should not rewrite and resolve via conditional upstream", func() {
262377
// Direct request to fritz.box (no rewrite)

resolver/custom_dns_resolver.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,16 @@ func (r *CustomDNSResolver) Resolve(ctx context.Context, request *model.Request)
230230
// Revert the request
231231
request.Req = original
232232

233+
// A response we produced ourselves without an answer means the mapping had
234+
// nothing for this query: ask the rest of the chain, using the original
235+
// name (`fallbackUpstream`).
236+
answered := err == nil && response != nil && response.RType == model.ResponseTypeCUSTOMDNS
237+
if shouldFallbackUpstream(&r.cfg.RewriterConfig, answered, response, nil) {
238+
logger.WithField("next_resolver", Name(r.next)).Trace("fallback to next resolver")
239+
240+
return r.next.Resolve(ctx, request)
241+
}
242+
233243
if err != nil {
234244
return response, err
235245
}

resolver/custom_dns_resolver_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
. "github.com/0xERR0R/blocky/helpertest"
1111
"github.com/0xERR0R/blocky/log"
1212
. "github.com/0xERR0R/blocky/model"
13+
"github.com/0xERR0R/blocky/util"
1314
"github.com/miekg/dns"
1415
. "github.com/onsi/ginkgo/v2"
1516
. "github.com/onsi/gomega"
@@ -592,6 +593,39 @@ var _ = Describe("CustomDNSResolver", func() {
592593
sut.Next(m)
593594
})
594595

596+
When("the mapping has no answer of the requested type and fallbackUpstream is set", func() {
597+
BeforeEach(func() {
598+
cfg.FallbackUpstream = true
599+
sut = NewCustomDNSResolver(cfg)
600+
})
601+
602+
It("should ask the next resolver with the original name", func() {
603+
var seen string
604+
605+
m = &mockResolver{}
606+
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
607+
m.ResolveFn = func(_ context.Context, req *Request) (*Response, error) {
608+
seen = req.Req.Question[0].Name
609+
resp, err := util.NewMsgWithAnswer(seen, uint(TTL), AAAA, "2001:db8::1")
610+
Expect(err).Should(Succeed())
611+
612+
return &Response{Res: resp, RType: ResponseTypeRESOLVED, Reason: "RESOLVED"}, nil
613+
}
614+
sut.Next(m)
615+
616+
// custom.domain has an A record only, so the AAAA query ends up
617+
// as an empty CUSTOM DNS answer
618+
Expect(sut.Resolve(ctx, newRequest("www.source.test.", AAAA))).
619+
Should(
620+
SatisfyAll(
621+
HaveResponseType(ResponseTypeRESOLVED),
622+
HaveReturnCode(dns.RcodeSuccess),
623+
))
624+
625+
Expect(seen).Should(Equal("www.source.test."))
626+
})
627+
})
628+
595629
When("request matches rewrite rule", func() {
596630
It("should rewrite subdomain and resolve from mapping", func() {
597631
// Request for www.source.test should be rewritten to www.custom.domain

resolver/rewrite_helper.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package resolver
33
import (
44
"strings"
55

6+
"github.com/0xERR0R/blocky/config"
7+
"github.com/0xERR0R/blocky/model"
68
"github.com/0xERR0R/blocky/util"
79

810
"github.com/miekg/dns"
@@ -64,6 +66,24 @@ func rewriteDomain(domain string, rewriteMap map[string]string) (string, string)
6466
return domain, ""
6567
}
6668

69+
// shouldFallbackUpstream reports whether a rewritten query that ended without an
70+
// answer should be retried, with its original name, on the rest of the chain.
71+
// See `fallbackUpstream` in the documentation.
72+
//
73+
// answered tells whether the resolver produced the response itself: a response
74+
// it only passed through from the next resolver must not be retried there.
75+
func shouldFallbackUpstream(cfg *config.RewriterConfig, answered bool, response *model.Response, err error) bool {
76+
if !cfg.FallbackUpstream || len(cfg.Rewrite) == 0 || !answered {
77+
return false
78+
}
79+
80+
if err != nil {
81+
return true
82+
}
83+
84+
return response != nil && response != NoResponse && response.Res != nil && len(response.Res.Answer) == 0
85+
}
86+
6787
// revertRewritesInResponse reverts domain rewrites in the DNS response
6888
func revertRewritesInResponse(response *dns.Msg, originalNames map[string]string) {
6989
if len(originalNames) == 0 {

0 commit comments

Comments
 (0)