tailscale/net/dns/resolvconffile/resolvconffile.go

125 lines
3.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package resolvconffile parses & serializes /etc/resolv.conf-style files.
//
// It's a leaf package so both net/dns and net/dns/resolver can depend
// on it and we can unify a handful of implementations.
//
// The package is verbosely named to disambiguate it from resolvconf
// the daemon, which Tailscale also supports.
package resolvconffile
import (
"bufio"
"bytes"
"fmt"
"io"
"net/netip"
"os"
"strings"
"tailscale.com/util/dnsname"
)
// Path is the canonical location of resolv.conf.
const Path = "/etc/resolv.conf"
// Config represents a resolv.conf(5) file.
type Config struct {
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netip.Addr
// SearchDomains are the domain suffixes to use when expanding
// single-label name queries. SearchDomains is additive to
// whatever non-Tailscale search domains the OS has.
SearchDomains []dnsname.FQDN
}
// Write writes c to w. It does so in one Write call.
func (c *Config) Write(w io.Writer) error {
buf := new(bytes.Buffer)
io.WriteString(buf, "# resolv.conf(5) file generated by tailscale\n")
io.WriteString(buf, "# For more info, see https://tailscale.com/s/resolvconf-overwrite\n")
io.WriteString(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
for _, ns := range c.Nameservers {
io.WriteString(buf, "nameserver ")
io.WriteString(buf, ns.String())
io.WriteString(buf, "\n")
}
if len(c.SearchDomains) > 0 {
io.WriteString(buf, "search")
for _, domain := range c.SearchDomains {
io.WriteString(buf, " ")
io.WriteString(buf, domain.WithoutTrailingDot())
}
io.WriteString(buf, "\n")
}
_, err := w.Write(buf.Bytes())
return err
}
// Parse parses a resolv.conf file from r.
func Parse(r io.Reader) (*Config, error) {
config := new(Config)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
line, _, _ = strings.Cut(line, "#") // remove any comments
line = strings.TrimSpace(line)
if s, ok := strings.CutPrefix(line, "nameserver"); ok {
nameserver := strings.TrimSpace(s)
if len(nameserver) == len(s) {
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
}
ip, err := netip.ParseAddr(nameserver)
if err != nil {
return nil, err
}
config.Nameservers = append(config.Nameservers, ip)
continue
}
if s, ok := strings.CutPrefix(line, "search"); ok {
domains := strings.TrimSpace(s)
if len(domains) == len(s) {
// No leading space?!
return nil, fmt.Errorf("missing space after \"search\" in %q", line)
}
for len(domains) > 0 {
domain := domains
i := strings.IndexAny(domain, " \t")
if i != -1 {
domain = domain[:i]
domains = strings.TrimSpace(domains[i+1:])
} else {
domains = ""
}
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return nil, fmt.Errorf("parsing search domain %q in %q: %w", domain, line, err)
}
config.SearchDomains = append(config.SearchDomains, fqdn)
}
}
}
return config, nil
}
// ParseFile parses the named resolv.conf file.
func ParseFile(name string) (*Config, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
if n := fi.Size(); n > 10<<10 {
return nil, fmt.Errorf("unexpectedly large %q file: %d bytes", name, n)
}
all, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return Parse(bytes.NewReader(all))
}