Skip to content

Commit 1661a56

Browse files
committed
Add $RESOLVCONF:<file> support for forwarding rules
Originally suggested by Nicol Lintner @nlintn -- Thanks! Fixes #3117
1 parent 4ad7c23 commit 1661a56

2 files changed

Lines changed: 115 additions & 10 deletions

File tree

dnscrypt-proxy/example-forwarding-rules.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
## The following keywords can also be used instead of a server address:
1111
## $BOOTSTRAP to use the default bootstrap resolvers
1212
## $DHCP to use the default DNS resolvers provided by the DHCP server
13+
## $RESOLVCONF:<file> to use the resolvers specified in <file> (with
14+
## resolv.conf syntax); only 'nameserver' lines are parsed, other
15+
## options are ignored; name of <file> mustn't contain any commas (,)
1316

1417
## In order to enable this feature, the "forwarding_rules" property needs to
1518
## be set to this file name inside the main configuration file.
@@ -27,10 +30,14 @@
2730
## Forward *.local to the resolvers provided by the DHCP server
2831
# local $DHCP
2932

33+
## Forward *.localnet to the resolvers specified in '/etc/resolv.conf'
34+
# localnet $RESOLVCONF:/etc/resolv.conf
35+
3036
## Forward *.internal to 192.168.1.1, and if it doesn't work, to the
31-
## DNS from the local DHCP server, and if it still doesn't work, to the
32-
## bootstrap resolvers
33-
# internal 192.168.1.1,$DHCP,$BOOTSTRAP
37+
## DNS from the local DHCP server, and if that doesn't work, to the
38+
## bootstrap resolvers, and if it still doesn't work, to the resolvers
39+
## specified in '/etc/resolv.conf'
40+
# internal 192.168.1.1,$DHCP,$BOOTSTRAP,$RESOLVCONF:/etc/resolv.conf
3441

3542
## Forward queries for example.com and *.example.com to 9.9.9.9 and 8.8.8.8
3643
# example.com 9.9.9.9,8.8.8.8

dnscrypt-proxy/plugin_forward.go

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import (
66
"fmt"
77
"math/rand"
88
"net"
9+
"os"
10+
"path/filepath"
911
"strings"
1012
"sync"
13+
"sync/atomic"
14+
"time"
1115

1216
"codeberg.org/miekg/dns"
1317
"github.com/jedisct1/dlog"
@@ -20,11 +24,14 @@ const (
2024
Explicit SearchSequenceItemType = iota
2125
Bootstrap
2226
DHCP
27+
Resolvconf
2328
)
2429

2530
type SearchSequenceItem struct {
26-
typ SearchSequenceItemType
27-
servers []string
31+
typ SearchSequenceItemType
32+
servers []string
33+
resolvconf string
34+
rcLastFail atomic.Int64 // unix timestamp of last failed resolv.conf read
2835
}
2936

3037
type PluginForwardEntry struct {
@@ -140,6 +147,30 @@ func (plugin *PluginForward) parseForwardFile(lines string) (bool, []PluginForwa
140147
}
141148
requiresDHCP = true
142149
default:
150+
const resolvconfPrefix = "$RESOLVCONF:"
151+
if strings.HasPrefix(server, resolvconfPrefix) {
152+
file := server[len(resolvconfPrefix):]
153+
if len(file) == 0 {
154+
dlog.Criticalf(
155+
"File needs to be specified for $RESOLVCONF in line %d",
156+
1+lineNo,
157+
)
158+
continue
159+
}
160+
file = filepath.Clean(file)
161+
if !filepath.IsAbs(file) {
162+
dlog.Warnf(
163+
"$RESOLVCONF path '%s' at line %d is not absolute; "+
164+
"this may not resolve as expected", file, 1+lineNo,
165+
)
166+
}
167+
sequence = append(sequence, SearchSequenceItem{
168+
typ: Resolvconf,
169+
resolvconf: file,
170+
})
171+
dlog.Infof("Forwarding [%s] to the servers specified in '%s'", domain, file)
172+
continue
173+
}
143174
if strings.HasPrefix(server, "$") {
144175
dlog.Criticalf("Unknown keyword [%s] at line %d", server, 1+lineNo)
145176
continue
@@ -149,8 +180,8 @@ func (plugin *PluginForward) parseForwardFile(lines string) (bool, []PluginForwa
149180
continue
150181
} else {
151182
idxServers := -1
152-
for i, item := range sequence {
153-
if item.typ == Explicit {
183+
for i := range sequence {
184+
if sequence[i].typ == Explicit {
154185
idxServers = i
155186
}
156187
}
@@ -271,11 +302,13 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
271302
var err error
272303
var respMsg *dns.Msg
273304
tries := 4
274-
for _, item := range sequence {
305+
const resolvconfRetryInterval int64 = 30 // seconds
306+
307+
for i := range sequence {
275308
var server string
276-
switch item.typ {
309+
switch sequence[i].typ {
277310
case Explicit:
278-
server = item.servers[rand.Intn(len(item.servers))]
311+
server = sequence[i].servers[rand.Intn(len(sequence[i].servers))]
279312
case Bootstrap:
280313
server = plugin.bootstrapResolvers[rand.Intn(len(plugin.bootstrapResolvers))]
281314
case DHCP:
@@ -295,6 +328,41 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
295328
dlog.Infof("DHCP didn't provide any DNS server to forward [%s]", qName)
296329
continue
297330
}
331+
case Resolvconf:
332+
if lastFail := sequence[i].rcLastFail.Load(); lastFail != 0 &&
333+
time.Now().Unix()-lastFail < resolvconfRetryInterval {
334+
continue
335+
}
336+
servers, warnings, err := parseResolvConf(sequence[i].resolvconf)
337+
if err != nil {
338+
dlog.Warnf(
339+
"Failed to open '%s' while resolving [%s]: %v",
340+
sequence[i].resolvconf, qName, err,
341+
)
342+
sequence[i].rcLastFail.Store(time.Now().Unix())
343+
continue
344+
}
345+
if len(servers) == 0 {
346+
for _, w := range warnings {
347+
dlog.Warn(w)
348+
}
349+
dlog.Warnf(
350+
"No valid nameservers in '%s' while resolving [%s]",
351+
sequence[i].resolvconf, qName,
352+
)
353+
sequence[i].rcLastFail.Store(time.Now().Unix())
354+
continue
355+
}
356+
sequence[i].rcLastFail.Store(0) // clear failure state on successful read
357+
nameserver := servers[rand.Intn(len(servers))]
358+
server, err = normalizeIPAndOptionalPort(nameserver, "53")
359+
if err != nil {
360+
dlog.Warnf(
361+
"Syntax error in address '%s' while resolving [%s]: %v",
362+
nameserver, qName, err,
363+
)
364+
continue
365+
}
298366
}
299367
pluginsState.serverName = server
300368
if len(server) == 0 {
@@ -345,6 +413,36 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
345413
return err
346414
}
347415

416+
func parseResolvConf(filename string) (servers []string, warnings []string, err error) {
417+
data, err := os.ReadFile(filename)
418+
if err != nil {
419+
return nil, nil, err
420+
}
421+
for line := range strings.SplitSeq(string(data), "\n") {
422+
line = strings.TrimSpace(line)
423+
if !strings.HasPrefix(line, "nameserver") {
424+
continue
425+
}
426+
fields := strings.Fields(line)
427+
if len(fields) < 2 {
428+
continue
429+
}
430+
addr := fields[1]
431+
host := addr
432+
if h, _, err := net.SplitHostPort(addr); err == nil {
433+
host = h
434+
}
435+
if net.ParseIP(host) == nil {
436+
warnings = append(warnings, fmt.Sprintf(
437+
"Ignoring invalid nameserver address '%s' in [%s]", addr, filename,
438+
))
439+
continue
440+
}
441+
servers = append(servers, addr)
442+
}
443+
return
444+
}
445+
348446
func normalizeIPAndOptionalPort(addr string, defaultPort string) (string, error) {
349447
var host, port string
350448
var err error

0 commit comments

Comments
 (0)