v0.3.0 8f98fd3

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close