From 7fd35ab968acd3334afc94d291fb0fdbdc8d5e97 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Tue, 19 Apr 2022 13:58:35 +0200 Subject: [PATCH] use etchosts package from c/common Use the new logic from c/common to create the hosts file. This will help to better allign the hosts files between buildah and podman. Also this fixes several bugs: - remove host entries when container is stopped and has a netNsCtr - add entries for containers in a pod - do not duplicate entries in the hosts file - use the correct slirp ip when an userns is used Features: - configure host.containers.internal entry in containers.conf - configure base hosts file in containers.conf Fixes #12003 Fixes #13224 Signed-off-by: Paul Holzinger --- libpod/container_internal.go | 80 ++--- libpod/container_internal_linux.go | 228 ++++-------- libpod/container_internal_linux_test.go | 28 -- test/e2e/run_dns_test.go | 3 +- test/system/030-run.bats | 2 +- test/system/200-pod.bats | 2 +- test/system/500-networking.bats | 34 +- .../common/libnetwork/etchosts/hosts.go | 339 ++++++++++++++++++ .../common/libnetwork/etchosts/ip.go | 91 +++++ .../common/libnetwork/etchosts/util.go | 30 ++ vendor/modules.txt | 1 + 11 files changed, 597 insertions(+), 241 deletions(-) create mode 100644 vendor/github.com/containers/common/libnetwork/etchosts/hosts.go create mode 100644 vendor/github.com/containers/common/libnetwork/etchosts/ip.go create mode 100644 vendor/github.com/containers/common/libnetwork/etchosts/util.go diff --git a/libpod/container_internal.go b/libpod/container_internal.go index c7567a55e24c..149e342dafbc 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -1,7 +1,6 @@ package libpod import ( - "bufio" "bytes" "context" "fmt" @@ -17,8 +16,10 @@ import ( "github.com/containers/buildah/copier" "github.com/containers/buildah/pkg/overlay" butil "github.com/containers/buildah/util" + "github.com/containers/common/libnetwork/etchosts" "github.com/containers/common/pkg/cgroups" "github.com/containers/common/pkg/chown" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/pkg/ctime" @@ -31,6 +32,7 @@ import ( "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/pkg/lockfile" "github.com/containers/storage/pkg/mount" "github.com/coreos/go-systemd/v22/daemon" securejoin "github.com/cyphar/filepath-securejoin" @@ -1001,17 +1003,14 @@ func (c *Container) completeNetworkSetup() error { } } // check if we have a bindmount for /etc/hosts - if hostsBindMount, ok := state.BindMounts["/etc/hosts"]; ok && len(c.cniHosts()) > 0 { - ctrHostPath := filepath.Join(c.state.RunDir, "hosts") - if hostsBindMount == ctrHostPath { - // read the existing hosts - b, err := ioutil.ReadFile(hostsBindMount) - if err != nil { - return err - } - if err := ioutil.WriteFile(hostsBindMount, append(b, []byte(c.cniHosts())...), 0644); err != nil { - return err - } + if hostsBindMount, ok := state.BindMounts[config.DefaultHostsFile]; ok { + entries, err := c.getHostsEntries() + if err != nil { + return err + } + // add new container ips to the hosts file + if err := etchosts.Add(hostsBindMount, entries); err != nil { + return err } } @@ -1036,18 +1035,6 @@ func (c *Container) completeNetworkSetup() error { return ioutil.WriteFile(resolvBindMount, []byte(strings.Join(outResolvConf, "\n")), 0644) } -func (c *Container) cniHosts() string { - var hosts string - for _, status := range c.getNetworkStatus() { - for _, netInt := range status.Interfaces { - for _, netAddress := range netInt.Subnets { - hosts += fmt.Sprintf("%s\t%s %s\n", netAddress.IPNet.IP.String(), c.Hostname(), c.config.Name) - } - } - } - return hosts -} - // Initialize a container, creating it in the runtime func (c *Container) init(ctx context.Context, retainRetries bool) error { // Unconditionally remove conmon temporary files. @@ -1889,6 +1876,24 @@ func (c *Container) cleanup(ctx context.Context) error { lastError = errors.Wrapf(err, "error removing container %s network", c.ID()) } + // cleanup host entry if it is shared + if c.config.NetNsCtr != "" { + if hoststFile, ok := c.state.BindMounts[config.DefaultHostsFile]; ok { + if _, err := os.Stat(hoststFile); err == nil { + // we cannot use the dependency container lock due ABBA deadlocks + if lock, err := lockfile.GetLockfile(hoststFile); err == nil { + lock.Lock() + // make sure to ignore ENOENT error in case the netns container was cleanup before this one + if err := etchosts.Remove(hoststFile, getLocalhostHostEntry(c)); err != nil && !errors.Is(err, os.ErrNotExist) { + // this error is not fatal we still want to do proper cleanup + logrus.Errorf("failed to remove hosts entry from the netns containers /etc/hosts: %v", err) + } + lock.Unlock() + } + } + } + } + // Remove the container from the runtime, if necessary. // Do this *before* unmounting storage - some runtimes (e.g. Kata) // apparently object to having storage removed while the container still @@ -2025,33 +2030,6 @@ func (c *Container) writeStringToStaticDir(filename, contents string) (string, e return destFileName, nil } -// appendStringToRunDir appends the provided string to the runtimedir file -func (c *Container) appendStringToRunDir(destFile, output string) (string, error) { - destFileName := filepath.Join(c.state.RunDir, destFile) - - f, err := os.OpenFile(destFileName, os.O_APPEND|os.O_RDWR, 0600) - if err != nil { - return "", err - } - defer f.Close() - - compareStr := strings.TrimRight(output, "\n") - scanner := bufio.NewScanner(f) - scanner.Split(bufio.ScanLines) - - for scanner.Scan() { - if strings.Compare(scanner.Text(), compareStr) == 0 { - return filepath.Join(c.state.RunDir, destFile), nil - } - } - - if _, err := f.WriteString(output); err != nil { - return "", errors.Wrapf(err, "unable to write %s", destFileName) - } - - return filepath.Join(c.state.RunDir, destFile), nil -} - // saveSpec saves the OCI spec to disk, replacing any existing specs for the container func (c *Container) saveSpec(spec *spec.Spec) error { // If the OCI spec already exists, we need to replace it diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 9991003d6661..e702f680308a 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -27,6 +27,7 @@ import ( "github.com/containers/buildah/pkg/chrootuser" "github.com/containers/buildah/pkg/overlay" butil "github.com/containers/buildah/util" + "github.com/containers/common/libnetwork/etchosts" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/apparmor" "github.com/containers/common/pkg/cgroups" @@ -47,6 +48,7 @@ import ( "github.com/containers/podman/v4/version" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/pkg/lockfile" securejoin "github.com/cyphar/filepath-securejoin" runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -1894,23 +1896,25 @@ func (c *Container) makeBindMounts() error { // check if dependency container has an /etc/hosts file. // It may not have one, so only use it if it does. - hostsPath, exists := bindMounts["/etc/hosts"] + hostsPath, exists := bindMounts[config.DefaultHostsFile] if !c.config.UseImageHosts && exists { - depCtr.lock.Lock() - // generate a hosts file for the dependency container, - // based on either its old hosts file, or the default, - // and add the relevant information from the new container (hosts and IP) - hostsPath, err = depCtr.appendHosts(hostsPath, c) + // we cannot use the dependency container lock due ABBA deadlocks in cleanup() + lock, err := lockfile.GetLockfile(hostsPath) + if err != nil { + return fmt.Errorf("failed to lock hosts file: %w", err) + } + lock.Lock() + // add the newly added container to the hosts file + // we always use 127.0.0.1 as ip since they have the same netns + err = etchosts.Add(hostsPath, getLocalhostHostEntry(c)) + lock.Unlock() if err != nil { - depCtr.lock.Unlock() return errors.Wrapf(err, "error creating hosts file for container %s which depends on container %s", c.ID(), depCtr.ID()) } - depCtr.lock.Unlock() // finally, save it in the new container - err := c.mountIntoRootDirs("/etc/hosts", hostsPath) - + err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath) if err != nil { return errors.Wrapf(err, "error assigning mounts to container %s", c.ID()) } @@ -1938,7 +1942,7 @@ func (c *Container) makeBindMounts() error { } if !c.config.UseImageHosts { - if err := c.updateHosts("/etc/hosts"); err != nil { + if err := c.createHosts(); err != nil { return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) } } @@ -1957,7 +1961,7 @@ func (c *Container) makeBindMounts() error { } } else { if !c.config.UseImageHosts && c.state.BindMounts["/etc/hosts"] == "" { - if err := c.updateHosts("/etc/hosts"); err != nil { + if err := c.createHosts(); err != nil { return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) } } @@ -2343,169 +2347,79 @@ func (c *Container) removeNameserver(ips []string) error { return nil } -// updateHosts updates the container's hosts file -func (c *Container) updateHosts(path string) error { - var hosts string - - orig, err := ioutil.ReadFile(path) - if err != nil { - // Ignore if the path does not exist - if !os.IsNotExist(err) { - return err - } - } else { - hosts = string(orig) - } - - hosts += c.getHosts() - hosts = c.appendLocalhost(hosts) - - newHosts, err := c.writeStringToRundir("hosts", hosts) - if err != nil { - return err - } - - if err = c.mountIntoRootDirs("/etc/hosts", newHosts); err != nil { - return err - } - - return nil +func getLocalhostHostEntry(c *Container) etchosts.HostEntries { + return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}} } -// based on networking mode we may want to append the localhost -// if there isn't any record for it and also this should happen -// in slirp4netns and similar network modes. -func (c *Container) appendLocalhost(hosts string) string { - if !strings.Contains(hosts, "localhost") && - !c.config.NetMode.IsHost() { - hosts += "127.0.0.1\tlocalhost\n::1\tlocalhost\n" - } - - return hosts -} - -// appendHosts appends a container's config and state pertaining to hosts to a container's -// local hosts file. netCtr is the container from which the netNS information is -// taken. -// path is the basis of the hosts file, into which netCtr's netNS information will be appended. -// FIXME. Path should be used by this function,but I am not sure what is correct; remove //lint -// once this is fixed -func (c *Container) appendHosts(path string, netCtr *Container) (string, error) { //nolint - return c.appendStringToRunDir("hosts", netCtr.getHosts()) -} - -// getHosts finds the pertinent information for a container's host file in its config and state -// and returns a string in a format that can be written to the host file -func (c *Container) getHosts() string { - var hosts string - - if len(c.config.HostAdd) > 0 { - for _, host := range c.config.HostAdd { - // the host format has already been verified at this point - fields := strings.SplitN(host, ":", 2) - hosts += fmt.Sprintf("%s %s\n", fields[1], fields[0]) - } - } - - hosts += c.cniHosts() - - // Add hostname for slirp4netns - if c.Hostname() != "" { - if c.config.NetMode.IsSlirp4netns() { - // When using slirp4netns, the interface gets a static IP - slirp4netnsIP, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) - if err != nil { - logrus.Warnf("Failed to determine slirp4netnsIP: %v", err.Error()) - } else { - hosts += fmt.Sprintf("# used by slirp4netns\n%s\t%s %s\n", slirp4netnsIP.String(), c.Hostname(), c.config.Name) - } +// getHostsEntries returns the container ip host entries for the correct netmode +func (c *Container) getHostsEntries() (etchosts.HostEntries, error) { + var entries etchosts.HostEntries + names := []string{c.Hostname(), c.config.Name} + switch { + case c.config.NetMode.IsBridge(): + entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...) + case c.config.NetMode.IsSlirp4netns(): + ip, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) + if err != nil { + return nil, err } - - // Do we have a network namespace? - netNone := false - if c.config.NetNsCtr == "" && !c.config.CreateNetNS { + entries = etchosts.HostEntries{{IP: ip.String(), Names: names}} + default: + // check for net=none + if !c.config.CreateNetNS { for _, ns := range c.config.Spec.Linux.Namespaces { if ns.Type == spec.NetworkNamespace { if ns.Path == "" { - netNone = true + entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}} } break } } } - // If we are net=none (have a network namespace, but not connected to - // anything) add the container's name and hostname to localhost. - if netNone { - hosts += fmt.Sprintf("127.0.0.1 %s %s\n", c.Hostname(), c.config.Name) - } } + return entries, nil +} - // Add gateway entry if we are not in a machine. If we use podman machine - // the gvproxy dns server will take care of host.containers.internal. - // https://github.com/containers/gvisor-tap-vsock/commit/1108ea45162281046d239047a6db9bc187e64b08 - if !c.runtime.config.Engine.MachineEnabled { - var depCtr *Container - netStatus := c.getNetworkStatus() - if c.config.NetNsCtr != "" { - // ignoring the error because there isn't anything to do - depCtr, _ = c.getRootNetNsDepCtr() - } else if len(netStatus) != 0 { - depCtr = c +func (c *Container) createHosts() error { + var containerIPsEntries etchosts.HostEntries + var err error + // if we configure the netns after the container create we should not add + // the hosts here since we have no information about the actual ips + // instead we will add them in c.completeNetworkSetup() + if !c.config.PostConfigureNetNS { + containerIPsEntries, err = c.getHostsEntries() + if err != nil { + return fmt.Errorf("failed to get container ip host entries: %w", err) } + } + baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint) + if err != nil { + return err + } - // getLocalIP returns the non loopback local IP of the host - getLocalIP := func() string { - addrs, err := net.InterfaceAddrs() - if err != nil { - return "" - } - for _, address := range addrs { - // check the address type and if it is not a loopback the display it - if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - return ipnet.IP.String() - } - } - } - return "" - } + targetFile := filepath.Join(c.state.RunDir, "hosts") + err = etchosts.New(&etchosts.Params{ + BaseFile: baseHostFile, + ExtraHosts: c.config.HostAdd, + ContainerIPs: containerIPsEntries, + HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network), + TargetFile: targetFile, + }) + if err != nil { + return err + } - if depCtr != nil { - host := "" - outer: - for net, status := range depCtr.getNetworkStatus() { - network, err := c.runtime.network.NetworkInspect(net) - // only add the host entry for bridge networks - // ip/macvlan gateway is normally not on the host - if err != nil || network.Driver != types.BridgeNetworkDriver { - continue - } - for _, netInt := range status.Interfaces { - for _, netAddress := range netInt.Subnets { - if netAddress.Gateway != nil { - host = fmt.Sprintf("%s host.containers.internal\n", netAddress.Gateway.String()) - break outer - } - } - } - } - // if no bridge gw was found try to use a local ip - if host == "" { - if ip := getLocalIP(); ip != "" { - host = fmt.Sprintf("%s\t%s\n", ip, "host.containers.internal") - } - } - hosts += host - } else if c.config.NetMode.IsSlirp4netns() { - if ip := getLocalIP(); ip != "" { - hosts += fmt.Sprintf("%s\t%s\n", ip, "host.containers.internal") - } - } else { - logrus.Debug("Network configuration does not support host.containers.internal address") - } + if err := os.Chown(targetFile, c.RootUID(), c.RootGID()); err != nil { + return err + } + if err := label.Relabel(targetFile, c.MountLabel(), false); err != nil { + return err } - return hosts + if err = c.mountIntoRootDirs(config.DefaultHostsFile, targetFile); err != nil { + return err + } + return nil } // generateGroupEntry generates an entry or entries into /etc/group as diff --git a/libpod/container_internal_linux_test.go b/libpod/container_internal_linux_test.go index a7dd0fc31920..2c1193445ee5 100644 --- a/libpod/container_internal_linux_test.go +++ b/libpod/container_internal_linux_test.go @@ -8,7 +8,6 @@ import ( "os" "testing" - "github.com/containers/podman/v4/pkg/namespaces" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" ) @@ -70,30 +69,3 @@ func TestGenerateUserGroupEntry(t *testing.T) { } assert.Equal(t, group, "567:x:567:567\n") } - -func TestAppendLocalhost(t *testing.T) { - { - c := Container{ - config: &ContainerConfig{ - ContainerNetworkConfig: ContainerNetworkConfig{ - NetMode: namespaces.NetworkMode("slirp4netns"), - }, - }, - } - - assert.Equal(t, "127.0.0.1\tlocalhost\n::1\tlocalhost\n", c.appendLocalhost("")) - assert.Equal(t, "127.0.0.1\tlocalhost", c.appendLocalhost("127.0.0.1\tlocalhost")) - } - { - c := Container{ - config: &ContainerConfig{ - ContainerNetworkConfig: ContainerNetworkConfig{ - NetMode: namespaces.NetworkMode("host"), - }, - }, - } - - assert.Equal(t, "", c.appendLocalhost("")) - assert.Equal(t, "127.0.0.1\tlocalhost", c.appendLocalhost("127.0.0.1\tlocalhost")) - } -} diff --git a/test/e2e/run_dns_test.go b/test/e2e/run_dns_test.go index a313e8d40b53..7561a2e851ba 100644 --- a/test/e2e/run_dns_test.go +++ b/test/e2e/run_dns_test.go @@ -78,8 +78,7 @@ var _ = Describe("Podman run dns", func() { session := podmanTest.Podman([]string{"run", "--add-host=foobar:1.1.1.1", "--add-host=foobaz:2001:db8::68", ALPINE, "cat", "/etc/hosts"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - Expect(session.OutputToStringArray()).To(ContainElement(HavePrefix("1.1.1.1 foobar"))) - Expect(session.OutputToStringArray()).To(ContainElement(HavePrefix("2001:db8::68 foobaz"))) + Expect(session.OutputToStringArray()).To(ContainElements(HavePrefix("1.1.1.1\tfoobar"), HavePrefix("2001:db8::68\tfoobaz"))) }) It("podman run add hostname", func() { diff --git a/test/system/030-run.bats b/test/system/030-run.bats index aba18badb80b..b435254f14fe 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -780,7 +780,7 @@ EOF run_podman '?' run --rm --add-host "foo.com:1.2.3.4" $IMAGE cat "/etc/hosts" mv $tmpfile /etc/hosts is "$status" 0 "podman run without /etc/hosts file should work" - is "$output" "1.2.3.4 foo.com.*" "users can add hosts even without /etc/hosts" + is "$output" "1.2.3.4[[:blank:]]foo.com.*" "users can add hosts even without /etc/hosts" } # rhbz#1854566 : $IMAGE has incorrect permission 555 on the root '/' filesystem diff --git a/test/system/200-pod.bats b/test/system/200-pod.bats index e6f4ecdbc2f6..591c2dbbf0e4 100644 --- a/test/system/200-pod.bats +++ b/test/system/200-pod.bats @@ -252,7 +252,7 @@ EOF is "$output" ".*invalid config provided: cannot set hostname when joining the pod UTS namespace: invalid configuration" "--hostname should not be allowed in share UTS pod" run_podman run --rm --pod $pod_id $IMAGE cat /etc/hosts - is "$output" ".*$add_host_ip $add_host_n" "--add-host was added" + is "$output" ".*$add_host_ip[[:blank:]]$add_host_n" "--add-host was added" is "$output" ".* $hostname" "--hostname is in /etc/hosts" # ^^^^ this must be a tab, not a space diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index 78ad3fe04977..87dc0984ec14 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -137,14 +137,46 @@ load helpers done } +@test "podman pod manages /etc/hosts correctly" { + local pod_name=pod-$(random_string 10) + local infra_name=infra-$(random_string 10) + local con1_name=con1-$(random_string 10) + local con2_name=con2-$(random_string 10) + run_podman pod create --name $pod_name --infra-name $infra_name + pid="$output" + run_podman run --pod $pod_name --name $con1_name $IMAGE cat /etc/hosts + is "$output" ".*\s$pod_name $infra_name.*" "Pod hostname in /etc/hosts" + is "$output" ".*127.0.0.1\s.*$con1_name.*" "Container1 name in /etc/hosts" + # get the length of the hosts file + old_lines=${#lines[@]} + + # since the first container should be cleaned up now we should only see the + # new host entry and the old one should be removed (lines check) + run_podman run --pod $pod_name --name $con2_name $IMAGE cat /etc/hosts + is "$output" ".*\s$pod_name $infra_name.*" "Pod hostname in /etc/hosts" + is "$output" ".*127.0.0.1\s.*$con2_name.*" "Container2 name in /etc/hosts" + is "${#lines[@]}" "$old_lines" "Number of hosts lines is equal" + + run_podman pod rm $pod_name + is "$output" "$pid" "Only ID in output (no extra errors)" +} + @test "podman run with slirp4ns assigns correct addresses to /etc/hosts" { CIDR="$(random_rfc1918_subnet)" IP=$(hostname -I | cut -f 1 -d " ") local conname=con-$(random_string 10) run_podman run --rm --network slirp4netns:cidr="${CIDR}.0/24" \ --name $conname --hostname $conname $IMAGE cat /etc/hosts - is "$output" ".*${IP} host.containers.internal" "host.containers.internal should be the cidr+2 address" + is "$output" ".*${IP} host.containers.internal" "host.containers.internal should be host address" is "$output" ".*${CIDR}.100 $conname $conname" "$conname should be the cidr+100 address" + + if is_rootless; then + # check the slirp ip also works correct with userns + run_podman run --rm --userns keep-id --network slirp4netns:cidr="${CIDR}.0/24" \ + --name $conname --hostname $conname $IMAGE cat /etc/hosts + is "$output" ".*${IP} host.containers.internal" "host.containers.internal should be host address" + is "$output" ".*${CIDR}.100 $conname $conname" "$conname should be the cidr+100 address" + fi } @test "podman run with slirp4ns adds correct dns address to resolv.conf" { diff --git a/vendor/github.com/containers/common/libnetwork/etchosts/hosts.go b/vendor/github.com/containers/common/libnetwork/etchosts/hosts.go new file mode 100644 index 000000000000..ce248a181ba9 --- /dev/null +++ b/vendor/github.com/containers/common/libnetwork/etchosts/hosts.go @@ -0,0 +1,339 @@ +package etchosts + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/util" +) + +const ( + hostContainersInternal = "host.containers.internal" + localhost = "localhost" +) + +type HostEntries []HostEntry + +type HostEntry struct { + IP string + Names []string +} + +// Params for the New() function call +type Params struct { + // BaseFile is the file where we read entries from and add entries to + // the target hosts file. If the name is empty it will not read any entries. + BaseFile string + // ExtraHosts is a slice of entries in the "hostname:ip" format. + // Optional. + ExtraHosts []string + // ContainerIPs should contain the main container ipv4 and ipv6 if available + // with the container name and host name as names set. + // Optional. + ContainerIPs HostEntries + // HostContainersInternalIP is the IP for the host.containers.internal entry. + // Optional. + HostContainersInternalIP string + // TargetFile where the hosts are written to. + TargetFile string +} + +// New will create a new hosts file and write this to the target file. +// This function does not prevent any kind of concurrency problems, it is +// the callers responsibility to avoid concurrent writes to this file. +// The extraHosts are written first, then the hosts from the file baseFile and the +// containerIps. The container ip entry is only added when the name was not already +// added before. +func New(params *Params) error { + if err := new(params); err != nil { + return fmt.Errorf("failed to create new hosts file: %w", err) + } + return nil +} + +// Add adds the given entries to the hosts file, entries are only added if +// they are not already present. +// Add is not atomic because it will keep the current file inode. This is +// required to keep bind mounts for containers working. +func Add(file string, entries HostEntries) error { + if err := add(file, entries); err != nil { + return fmt.Errorf("failed to add entries to hosts file: %w", err) + } + return nil +} + +// AddIfExists will add the given entries only if one of the existsEntries +// is in the hosts file. This API is required for podman network connect. +// Since we want to add the same host name for each network ip we want to +// add duplicates and the normal Add() call prevents us from doing so. +// However since we also do not want to overwrite potential entries that +// were added by users manually we first have to check if there are the +// current expected entries in the file. Note that this will only check +// for one match not all. It will also only check that the ip and one of +// the hostnames match like Remove(). +func AddIfExists(file string, existsEntries, newEntries HostEntries) error { + if err := addIfExists(file, existsEntries, newEntries); err != nil { + return fmt.Errorf("failed to add entries to hosts file: %w", err) + } + return nil +} + +// Remove will remove the given entries from the file. An entry will be +// removed when the ip and at least one name matches. Not all names have +// to match. If the given entries are not present in the file no error is +// returned. +// Remove is not atomic because it will keep the current file inode. This is +// required to keep bind mounts for containers working. +func Remove(file string, entries HostEntries) error { + if err := remove(file, entries); err != nil { + return fmt.Errorf("failed to remove entries from hosts file: %w", err) + } + return nil +} + +// new see comment on New() +func new(params *Params) error { + entries, err := parseExtraHosts(params.ExtraHosts) + if err != nil { + return err + } + entries2, err := parseHostsFile(params.BaseFile) + if err != nil { + return err + } + entries = append(entries, entries2...) + + // preallocate the slice with enough space for the 3 special entries below + containerIPs := make(HostEntries, 0, len(params.ContainerIPs)+3) + + // if localhost was not added we add it + // https://github.com/containers/podman/issues/11411 + lh := []string{localhost} + l1 := HostEntry{IP: "127.0.0.1", Names: lh} + l2 := HostEntry{IP: "::1", Names: lh} + containerIPs = append(containerIPs, l1, l2) + if params.HostContainersInternalIP != "" { + e := HostEntry{IP: params.HostContainersInternalIP, Names: []string{hostContainersInternal}} + containerIPs = append(containerIPs, e) + } + containerIPs = append(containerIPs, params.ContainerIPs...) + + if err := writeHostFile(params.TargetFile, entries, containerIPs); err != nil { + return err + } + return nil +} + +// add see comment on Add() +func add(file string, entries HostEntries) error { + currentEntries, err := parseHostsFile(file) + if err != nil { + return err + } + + names := make(map[string]struct{}) + for _, entry := range currentEntries { + for _, name := range entry.Names { + names[name] = struct{}{} + } + } + + // open file in append mode since we only add, we do not have to write existing entries again + f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return err + } + defer f.Close() + + return addEntriesIfNotExists(f, entries, names) +} + +// addIfExists see comment on AddIfExists() +func addIfExists(file string, existsEntries, newEntries HostEntries) error { + // special case when there are no existing entries do a normal add + // this can happen when we connect a network which was not connected + // to any other networks before + if len(existsEntries) == 0 { + return add(file, newEntries) + } + + currentEntries, err := parseHostsFile(file) + if err != nil { + return err + } + + for _, entry := range currentEntries { + if !checkIfEntryExists(entry, existsEntries) { + // keep looking for existing entries + continue + } + // if we have a matching existing entry add the new entries + // open file in append mode since we only add, we do not have to write existing entries again + f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return err + } + defer f.Close() + + for _, e := range newEntries { + if _, err = f.WriteString(formatLine(e.IP, e.Names)); err != nil { + return err + } + } + return nil + } + // no match found is no error + return nil +} + +// remove see comment on Remove() +func remove(file string, entries HostEntries) error { + currentEntries, err := parseHostsFile(file) + if err != nil { + return err + } + + f, err := os.Create(file) + if err != nil { + return err + } + defer f.Close() + + for _, entry := range currentEntries { + if checkIfEntryExists(entry, entries) { + continue + } + if _, err = f.WriteString(formatLine(entry.IP, entry.Names)); err != nil { + return err + } + } + return nil +} + +func checkIfEntryExists(current HostEntry, entries HostEntries) bool { + // check if the current entry equals one of the given entries + for _, rm := range entries { + if current.IP == rm.IP { + // it is enough if one of the names match, in this case we remove the full entry + for _, name := range current.Names { + if util.StringInSlice(name, rm.Names) { + return true + } + } + } + } + return false +} + +// parseExtraHosts converts a slice of "name:ip" string to entries. +// Because podman and buildah both store the extra hosts in this format +// we convert it here instead of having to this on the caller side. +func parseExtraHosts(extraHosts []string) (HostEntries, error) { + entries := make(HostEntries, 0, len(extraHosts)) + for _, entry := range extraHosts { + values := strings.SplitN(entry, ":", 2) + if len(values) != 2 { + return nil, fmt.Errorf("unable to parse host entry %q: incorrect format", entry) + } + if values[0] == "" { + return nil, fmt.Errorf("hostname in host entry %q is empty", entry) + } + if values[1] == "" { + return nil, fmt.Errorf("IP address in host entry %q is empty", entry) + } + e := HostEntry{IP: values[1], Names: []string{values[0]}} + entries = append(entries, e) + } + return entries, nil +} + +// parseHostsFile parses a given host file and returns all entries in it. +// Note that this will remove all comments and spaces. +func parseHostsFile(file string) (HostEntries, error) { + // empty file is valid, in this case we skip adding entries from the file + if file == "" { + return nil, nil + } + + f, err := os.Open(file) + if err != nil { + // do not error when the default hosts file does not exists + // https://github.com/containers/podman/issues/12667 + if errors.Is(err, os.ErrNotExist) && file == config.DefaultHostsFile { + return nil, nil + } + return nil, err + } + defer f.Close() + + entries := HostEntries{} + scanner := bufio.NewScanner(f) + for scanner.Scan() { + // split of the comments + line := scanner.Text() + if c := strings.IndexByte(line, '#'); c != -1 { + line = line[:c] + } + fields := strings.Fields(line) + // if we only have a ip without names we skip it + if len(fields) < 2 { + continue + } + + e := HostEntry{IP: fields[0], Names: fields[1:]} + entries = append(entries, e) + } + + return entries, scanner.Err() +} + +// writeHostFile write the entries to the given file +func writeHostFile(file string, userEntries, containerIPs HostEntries) error { + f, err := os.Create(file) + if err != nil { + return err + } + defer f.Close() + + names := make(map[string]struct{}) + for _, entry := range userEntries { + for _, name := range entry.Names { + names[name] = struct{}{} + } + if _, err = f.WriteString(formatLine(entry.IP, entry.Names)); err != nil { + return err + } + } + + return addEntriesIfNotExists(f, containerIPs, names) +} + +// addEntriesIfNotExists only adds the entries for names that are not already +// in the hosts file, otherwise we start overwriting user entries +func addEntriesIfNotExists(f io.StringWriter, containerIPs HostEntries, names map[string]struct{}) error { + for _, entry := range containerIPs { + freeNames := make([]string, 0, len(entry.Names)) + for _, name := range entry.Names { + if _, ok := names[name]; !ok { + freeNames = append(freeNames, name) + } + } + if len(freeNames) > 0 { + if _, err := f.WriteString(formatLine(entry.IP, freeNames)); err != nil { + return err + } + } + } + return nil +} + +// formatLine converts the given ip and names to a valid hosts line. +// The returned string includes the newline. +func formatLine(ip string, names []string) string { + return ip + "\t" + strings.Join(names, " ") + "\n" +} diff --git a/vendor/github.com/containers/common/libnetwork/etchosts/ip.go b/vendor/github.com/containers/common/libnetwork/etchosts/ip.go new file mode 100644 index 000000000000..3d14b714737e --- /dev/null +++ b/vendor/github.com/containers/common/libnetwork/etchosts/ip.go @@ -0,0 +1,91 @@ +package etchosts + +import ( + "net" + + "github.com/containers/common/libnetwork/types" + "github.com/containers/common/libnetwork/util" + "github.com/containers/common/pkg/config" + "github.com/containers/storage/pkg/unshare" +) + +// GetHostContainersInternalIP return the host.containers.internal ip +// if netStatus is not nil then networkInterface also must be non nil otherwise this function panics +func GetHostContainersInternalIP(conf *config.Config, netStatus map[string]types.StatusBlock, networkInterface types.ContainerNetwork) string { + switch conf.Containers.HostContainersInternalIP { + case "": + // if empty (default) we will automatically choose one below + // if machine we let the gvproxy dns server handle the dns name so do not add it + if conf.Engine.MachineEnabled { + return "" + } + case "none": + return "" + default: + return conf.Containers.HostContainersInternalIP + } + ip := "" + // Only use the bridge ip when root, as rootless the interfaces are created + // inside the special netns and not the host so we cannot use them. + if unshare.IsRootless() { + return getLocalIP() + } + for net, status := range netStatus { + network, err := networkInterface.NetworkInspect(net) + // only add the host entry for bridge networks + // ip/macvlan gateway is normally not on the host + if err != nil || network.Driver != types.BridgeNetworkDriver { + continue + } + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Subnets { + if netAddress.Gateway != nil { + if util.IsIPv4(netAddress.Gateway) { + return netAddress.Gateway.String() + } + // ipv6 address but keep looking since we prefer to use ipv4 + ip = netAddress.Gateway.String() + } + } + } + } + if ip != "" { + return ip + } + return getLocalIP() +} + +// getLocalIP returns the non loopback local IP of the host +func getLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + ip := "" + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && ipnet.IP.IsGlobalUnicast() { + if util.IsIPv4(ipnet.IP) { + return ipnet.IP.String() + } + // if ipv6 we keep looking for an ipv4 address + ip = ipnet.IP.String() + } + } + return ip +} + +// GetNetworkHostEntries returns HostEntries for all ips in the network status +// with the given hostnames. +func GetNetworkHostEntries(netStatus map[string]types.StatusBlock, names ...string) HostEntries { + hostEntries := make(HostEntries, 0, len(netStatus)) + for _, status := range netStatus { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Subnets { + e := HostEntry{IP: netAddress.IPNet.IP.String(), Names: names} + hostEntries = append(hostEntries, e) + } + } + } + return hostEntries +} diff --git a/vendor/github.com/containers/common/libnetwork/etchosts/util.go b/vendor/github.com/containers/common/libnetwork/etchosts/util.go new file mode 100644 index 000000000000..d78284594bb6 --- /dev/null +++ b/vendor/github.com/containers/common/libnetwork/etchosts/util.go @@ -0,0 +1,30 @@ +package etchosts + +import ( + "fmt" + + "github.com/containers/common/pkg/config" + securejoin "github.com/cyphar/filepath-securejoin" +) + +// GetBaseHostFile return the hosts file which should be used as base. +// The first param should be the config value config.Containers.BaseHostsFile +// The second param should be the root path to the mounted image. This is +// required when the user conf value is set to "image". +func GetBaseHostFile(confValue, imageRoot string) (string, error) { + switch confValue { + case "": + return config.DefaultHostsFile, nil + case "none": + return "", nil + case "image": + // use secure join to prevent problems with symlinks + path, err := securejoin.SecureJoin(imageRoot, config.DefaultHostsFile) + if err != nil { + return "", fmt.Errorf("failed to get /etc/hosts path in image: %w", err) + } + return path, nil + default: + return confValue, nil + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1e0d8a7e3ced..219dc2415987 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -114,6 +114,7 @@ github.com/containers/buildah/util github.com/containers/common/libimage github.com/containers/common/libimage/manifests github.com/containers/common/libnetwork/cni +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