resolvers.go RawHistoryFolder
1// SPDX-License-Identifier: MIT
2
3package dnsfwd
4
5import (
6 "bufio"
7 "net"
8 "net/netip"
9 "os"
10 "strings"
11 "sync"
12 "time"
13)
14
15// Resolvers reads the upstream nameservers from a resolv.conf-format file and
16// re-reads it when it changes. Entries such as the systemd-resolved stub
17// (127.0.0.53) are kept on purpose: the forwarder runs on the host, where the
18// stub is reachable, and that stub is the only resolver that knows the host's
19// split-DNS configuration.
20type Resolvers struct {
21 path string
22
23 mu sync.Mutex
24 mtime time.Time
25 size int64
26 addrs []string
27}
28
29// NewResolvers returns a Resolvers reading path (default /etc/resolv.conf).
30func NewResolvers(path string) *Resolvers {
31 if path == "" {
32 path = "/etc/resolv.conf"
33 }
34 return &Resolvers{path: path}
35}
36
37// Addrs returns the upstream "host:53" addresses, in file order. When the file
38// cannot be read the last known list is returned.
39func (r *Resolvers) Addrs() []string {
40 r.mu.Lock()
41 defer r.mu.Unlock()
42 st, err := os.Stat(r.path)
43 if err != nil {
44 return r.addrs
45 }
46 if st.ModTime().Equal(r.mtime) && st.Size() == r.size && r.addrs != nil {
47 return r.addrs
48 }
49 f, err := os.Open(r.path)
50 if err != nil {
51 return r.addrs
52 }
53 defer func() { _ = f.Close() }()
54 r.addrs = ParseResolvConf(f)
55 r.mtime, r.size = st.ModTime(), st.Size()
56 return r.addrs
57}
58
59// ParseResolvConf extracts "nameserver" entries as "ip:53" strings. Comments,
60// unknown directives and invalid addresses are ignored; duplicates removed.
61// IPv6 addresses with a zone (fe80::1%eth0) are kept with their zone.
62func ParseResolvConf(f interface{ Read([]byte) (int, error) }) []string {
63 var out []string
64 seen := map[string]bool{}
65 sc := bufio.NewScanner(f)
66 for sc.Scan() {
67 line := strings.TrimSpace(sc.Text())
68 if i := strings.IndexAny(line, "#;"); i >= 0 {
69 line = strings.TrimSpace(line[:i])
70 }
71 fields := strings.Fields(line)
72 if len(fields) < 2 || fields[0] != "nameserver" {
73 continue
74 }
75 a, err := netip.ParseAddr(fields[1])
76 if err != nil || a.IsUnspecified() {
77 continue
78 }
79 s := net.JoinHostPort(a.String(), "53")
80 if !seen[s] {
81 seen[s] = true
82 out = append(out, s)
83 }
84 }
85 return out
86}