mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 10:11:18 +01:00 
			
		
		
		
	Updates #7123 Updates #5309 Change-Id: I90bcd87a2fb85a91834a0dd4be6e03db08438672 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			125 lines
		
	
	
		
			3.4 KiB
		
	
	
	
		
			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))
 | 
						|
}
 |