@@ -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
2530type 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
3037type 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+
348446func normalizeIPAndOptionalPort (addr string , defaultPort string ) (string , error ) {
349447 var host , port string
350448 var err error
0 commit comments