From 90d80cf81e21cc0ff47829d78e4d44f8e0028a6c Mon Sep 17 00:00:00 2001
From: Paul Holzinger <pholzing@redhat.com>
Date: Thu, 12 May 2022 14:34:37 +0200
Subject: use resolvconf package from c/common/libnetwork

Podman and Buildah should use the same code the generate the resolv.conf
file. This mostly moved the podman code into c/common and created a
better API for it so buildah can use it as well.

[NO NEW TESTS NEEDED] All existing tests should continue to pass.

Fixes #13599 (There is no way to test this in CI without breaking the
hosts resolv.conf)

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
---
 .../common/libnetwork/resolvconf/resolv.go         | 182 +++++++++++++++++++++
 .../common/libnetwork/resolvconf/resolvconf.go     | 156 ++++++++++++++++++
 vendor/modules.txt                                 |   1 +
 3 files changed, 339 insertions(+)
 create mode 100644 vendor/github.com/containers/common/libnetwork/resolvconf/resolv.go
 create mode 100644 vendor/github.com/containers/common/libnetwork/resolvconf/resolvconf.go

(limited to 'vendor')

diff --git a/vendor/github.com/containers/common/libnetwork/resolvconf/resolv.go b/vendor/github.com/containers/common/libnetwork/resolvconf/resolv.go
new file mode 100644
index 000000000..c451d3b49
--- /dev/null
+++ b/vendor/github.com/containers/common/libnetwork/resolvconf/resolv.go
@@ -0,0 +1,182 @@
+package resolvconf
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/containers/common/pkg/util"
+	"github.com/opencontainers/runtime-spec/specs-go"
+	"github.com/sirupsen/logrus"
+)
+
+const (
+	localhost         = "127.0.0.1"
+	systemdResolvedIP = "127.0.0.53"
+)
+
+// Params for the New() function.
+type Params struct {
+	// Path is the path to new resolv.conf file which should be created.
+	Path string
+	// Namespaces is the list of container namespaces.
+	// This is required to fist check for a resolv.conf under /etc/netns,
+	// created by "ip netns". Also used to check if the container has a
+	// netns in which case localhost nameserver must be filtered.
+	Namespaces []specs.LinuxNamespace
+	// IPv6Enabled will filter ipv6 nameservers when not set to true.
+	IPv6Enabled bool
+	// KeepHostServers can be set when it is required to still keep the
+	// original resolv.conf content even when custom Nameserver/Searches/Options
+	// are set. In this case they will be appended to the given values.
+	KeepHostServers bool
+	// Nameservers is a list of nameservers the container should use,
+	// instead of the default ones from the host.
+	Nameservers []string
+	// Searches is a list of dns search domains the container should use,
+	// instead of the default ones from the host.
+	Searches []string
+	// Options is a list of dns options the container should use,
+	// instead of the default ones from the host.
+	Options []string
+
+	// resolvConfPath is the path which should be used as base to get the dns
+	// options. This should only be used for testing purposes. For all other
+	// callers this defaults to /etc/resolv.conf.
+	resolvConfPath string
+}
+
+func getDefaultResolvConf(params *Params) ([]byte, bool, error) {
+	resolveConf := DefaultResolvConf
+	// this is only used by testing
+	if params.resolvConfPath != "" {
+		resolveConf = params.resolvConfPath
+	}
+	hostNS := true
+	for _, ns := range params.Namespaces {
+		if ns.Type == specs.NetworkNamespace {
+			hostNS = false
+			if ns.Path != "" && !strings.HasPrefix(ns.Path, "/proc/") {
+				// check for netns created by "ip netns"
+				path := filepath.Join("/etc/netns", filepath.Base(ns.Path), "resolv.conf")
+				_, err := os.Stat(path)
+				if err == nil {
+					resolveConf = path
+				}
+			}
+			break
+		}
+	}
+
+	contents, err := os.ReadFile(resolveConf)
+	if err != nil && !errors.Is(err, os.ErrNotExist) {
+		return nil, false, err
+	}
+	if hostNS {
+		return contents, hostNS, nil
+	}
+
+	ns := getNameservers(contents)
+	// Check for local only resolver, in this case we want to get the real nameservers
+	// since localhost is not reachable from the netns.
+	if len(ns) == 1 {
+		var path string
+		switch ns[0] {
+		case systemdResolvedIP:
+			// used by systemd-resolved
+			path = "/run/systemd/resolve/resolv.conf"
+		case localhost:
+			// used by NetworkManager https://github.com/containers/podman/issues/13599
+			path = "/run/NetworkManager/no-stub-resolv.conf"
+		}
+		if path != "" {
+			// read the actual resolv.conf file for
+			resolvedContents, err := os.ReadFile(path)
+			if err != nil {
+				// do not error when the file does not exists, the detection logic is not perfect
+				if !errors.Is(err, os.ErrNotExist) {
+					return nil, false, fmt.Errorf("local resolver detected, but could not read real resolv.conf at %q: %w", path, err)
+				}
+			} else {
+				logrus.Debugf("found local resolver, using %q to get the nameservers", path)
+				contents = resolvedContents
+			}
+		}
+	}
+
+	return contents, hostNS, nil
+}
+
+// unsetSearchDomainsIfNeeded removes the search domain when they contain a single dot as element.
+func unsetSearchDomainsIfNeeded(searches []string) []string {
+	if util.StringInSlice(".", searches) {
+		return nil
+	}
+	return searches
+}
+
+// New creates a new resolv.conf file with the given params.
+func New(params *Params) error {
+	// short path, if everything is given there is no need to actually read the hosts /etc/resolv.conf
+	if len(params.Nameservers) > 0 && len(params.Options) > 0 && len(params.Searches) > 0 && !params.KeepHostServers {
+		return build(params.Path, params.Nameservers, unsetSearchDomainsIfNeeded(params.Searches), params.Options)
+	}
+
+	content, hostNS, err := getDefaultResolvConf(params)
+	if err != nil {
+		return fmt.Errorf("failed to get the default /etc/resolv.conf content: %w", err)
+	}
+
+	content = filterResolvDNS(content, params.IPv6Enabled, !hostNS)
+
+	nameservers := params.Nameservers
+	if len(nameservers) == 0 || params.KeepHostServers {
+		nameservers = append(nameservers, getNameservers(content)...)
+	}
+
+	searches := unsetSearchDomainsIfNeeded(params.Searches)
+	// if no params.Searches then use host ones
+	// otherwise make sure that they were no explicitly unset before adding host ones
+	if len(params.Searches) == 0 || (params.KeepHostServers && len(searches) > 0) {
+		searches = append(searches, getSearchDomains(content)...)
+	}
+
+	options := params.Options
+	if len(options) == 0 || params.KeepHostServers {
+		options = append(options, getOptions(content)...)
+	}
+
+	return build(params.Path, nameservers, searches, options)
+}
+
+// Add will add the given nameservers to the given resolv.conf file.
+// It will add the nameserver in front of the existing ones.
+func Add(path string, nameservers []string) error {
+	contents, err := os.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	nameservers = append(nameservers, getNameservers(contents)...)
+	return build(path, nameservers, getSearchDomains(contents), getOptions(contents))
+}
+
+// Remove the given nameserver from the given resolv.conf file.
+func Remove(path string, nameservers []string) error {
+	contents, err := os.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	oldNameservers := getNameservers(contents)
+	newNameserver := make([]string, 0, len(oldNameservers))
+	for _, ns := range oldNameservers {
+		if !util.StringInSlice(ns, nameservers) {
+			newNameserver = append(newNameserver, ns)
+		}
+	}
+
+	return build(path, newNameserver, getSearchDomains(contents), getOptions(contents))
+}
diff --git a/vendor/github.com/containers/common/libnetwork/resolvconf/resolvconf.go b/vendor/github.com/containers/common/libnetwork/resolvconf/resolvconf.go
new file mode 100644
index 000000000..54b8c3227
--- /dev/null
+++ b/vendor/github.com/containers/common/libnetwork/resolvconf/resolvconf.go
@@ -0,0 +1,156 @@
+// Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf.
+// Originally from github.com/docker/libnetwork/resolvconf but heavily modified to better work with podman.
+package resolvconf
+
+import (
+	"bytes"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+)
+
+const (
+	// DefaultResolvConf points to the default file used for dns configuration on a linux machine.
+	DefaultResolvConf = "/etc/resolv.conf"
+)
+
+var (
+	// Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS.
+	defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
+	defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
+	ipv4NumBlock   = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`
+	ipv4Address    = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
+	// This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also
+	// will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants
+	// -- e.g. other link-local types -- either won't work in containers or are unnecessary.
+	// For readability and sufficiency for Docker purposes this seemed more reasonable than a
+	// 1000+ character regexp with exact and complete IPv6 validation.
+	ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})(%\w+)?`
+
+	// ipLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
+	ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
+
+	localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipLocalhost + `\s*\n*`)
+	nsIPv6Regexp      = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`)
+	nsRegexp          = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`)
+	searchRegexp      = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
+	optionsRegexp     = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`)
+)
+
+// filterResolvDNS cleans up the config in resolvConf.  It has two main jobs:
+// 1. If a netns is enabled, it looks for localhost (127.*|::1) entries in the provided
+//    resolv.conf, removing local nameserver entries, and, if the resulting
+//    cleaned config has no defined nameservers left, adds default DNS entries
+// 2. Given the caller provides the enable/disable state of IPv6, the filter
+//    code will remove all IPv6 nameservers if it is not enabled for containers
+//
+func filterResolvDNS(resolvConf []byte, ipv6Enabled bool, netnsEnabled bool) []byte {
+	// If we're using the host netns, we have nothing to do besides hash the file.
+	if !netnsEnabled {
+		return resolvConf
+	}
+	cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{})
+	// if IPv6 is not enabled, also clean out any IPv6 address nameserver
+	if !ipv6Enabled {
+		cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
+	}
+	// if the resulting resolvConf has no more nameservers defined, add appropriate
+	// default DNS servers for IPv4 and (optionally) IPv6
+	if len(getNameservers(cleanedResolvConf)) == 0 {
+		logrus.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns)
+		dns := defaultIPv4Dns
+		if ipv6Enabled {
+			logrus.Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns)
+			dns = append(dns, defaultIPv6Dns...)
+		}
+		cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
+	}
+	return cleanedResolvConf
+}
+
+// getLines parses input into lines and strips away comments.
+func getLines(input []byte) [][]byte {
+	lines := bytes.Split(input, []byte("\n"))
+	var output [][]byte
+	for _, currentLine := range lines {
+		commentIndex := bytes.Index(currentLine, []byte("#"))
+		if commentIndex == -1 {
+			output = append(output, currentLine)
+		} else {
+			output = append(output, currentLine[:commentIndex])
+		}
+	}
+	return output
+}
+
+// getNameservers returns nameservers (if any) listed in /etc/resolv.conf.
+func getNameservers(resolvConf []byte) []string {
+	nameservers := []string{}
+	for _, line := range getLines(resolvConf) {
+		ns := nsRegexp.FindSubmatch(line)
+		if len(ns) > 0 {
+			nameservers = append(nameservers, string(ns[1]))
+		}
+	}
+	return nameservers
+}
+
+// getSearchDomains returns search domains (if any) listed in /etc/resolv.conf
+// If more than one search line is encountered, only the contents of the last
+// one is returned.
+func getSearchDomains(resolvConf []byte) []string {
+	domains := []string{}
+	for _, line := range getLines(resolvConf) {
+		match := searchRegexp.FindSubmatch(line)
+		if match == nil {
+			continue
+		}
+		domains = strings.Fields(string(match[1]))
+	}
+	return domains
+}
+
+// getOptions returns options (if any) listed in /etc/resolv.conf
+// If more than one options line is encountered, only the contents of the last
+// one is returned.
+func getOptions(resolvConf []byte) []string {
+	options := []string{}
+	for _, line := range getLines(resolvConf) {
+		match := optionsRegexp.FindSubmatch(line)
+		if match == nil {
+			continue
+		}
+		options = strings.Fields(string(match[1]))
+	}
+	return options
+}
+
+// build writes a configuration file to path containing a "nameserver" entry
+// for every element in dns, a "search" entry for every element in
+// dnsSearch, and an "options" entry for every element in dnsOptions.
+func build(path string, dns, dnsSearch, dnsOptions []string) error {
+	content := new(bytes.Buffer)
+	if len(dnsSearch) > 0 {
+		if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." {
+			if _, err := content.WriteString("search " + searchString + "\n"); err != nil {
+				return err
+			}
+		}
+	}
+	for _, dns := range dns {
+		if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil {
+			return err
+		}
+	}
+	if len(dnsOptions) > 0 {
+		if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" {
+			if _, err := content.WriteString("options " + optsString + "\n"); err != nil {
+				return err
+			}
+		}
+	}
+
+	return os.WriteFile(path, content.Bytes(), 0o644)
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a42943eef..a4ba3043e 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -119,6 +119,7 @@ github.com/containers/common/libnetwork/etchosts
 github.com/containers/common/libnetwork/internal/util
 github.com/containers/common/libnetwork/netavark
 github.com/containers/common/libnetwork/network
+github.com/containers/common/libnetwork/resolvconf
 github.com/containers/common/libnetwork/types
 github.com/containers/common/libnetwork/util
 github.com/containers/common/pkg/apparmor
-- 
cgit v1.2.3-54-g00ecf