summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libpod/container_internal.go80
-rw-r--r--libpod/container_internal_linux.go228
-rw-r--r--libpod/container_internal_linux_test.go28
-rw-r--r--test/e2e/run_dns_test.go3
-rw-r--r--test/system/030-run.bats2
-rw-r--r--test/system/200-pod.bats2
-rw-r--r--test/system/500-networking.bats34
-rw-r--r--vendor/github.com/containers/common/libnetwork/etchosts/hosts.go339
-rw-r--r--vendor/github.com/containers/common/libnetwork/etchosts/ip.go91
-rw-r--r--vendor/github.com/containers/common/libnetwork/etchosts/util.go30
-rw-r--r--vendor/modules.txt1
11 files changed, 597 insertions, 241 deletions
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index b051b7f2d..6c0d51df3 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"
@@ -1006,17 +1008,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
}
}
@@ -1041,18 +1040,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.
@@ -1894,6 +1881,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
@@ -2030,33 +2035,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 f8d0a124c..d409f7a04 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -28,6 +28,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"
@@ -49,6 +50,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"
@@ -2072,23 +2074,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())
}
@@ -2116,7 +2120,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())
}
}
@@ -2135,7 +2139,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())
}
}
@@ -2521,169 +2525,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 a7dd0fc31..2c1193445 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 a313e8d40..7561a2e85 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 526003c2d..31db5f1c4 100644
--- a/test/system/030-run.bats
+++ b/test/system/030-run.bats
@@ -782,7 +782,7 @@ EOF
mv $hosts_tmp /etc/hosts
assert "$status" = 0 \
"podman run without /etc/hosts file should work"
- assert "$output" =~ "^1\.2\.3\.4 foo.com.*" \
+ assert "$output" =~ "^1\.2\.3\.4[[:blank:]]foo\.com.*" \
"users can add hosts even without /etc/hosts"
}
diff --git a/test/system/200-pod.bats b/test/system/200-pod.bats
index 56449dcad..ef4bf1a6c 100644
--- a/test/system/200-pod.bats
+++ b/test/system/200-pod.bats
@@ -250,7 +250,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 3bfc58a07..f5bb76d9a 100644
--- a/test/system/500-networking.bats
+++ b/test/system/500-networking.bats
@@ -133,14 +133,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 000000000..ce248a181
--- /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 000000000..3d14b7147
--- /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 000000000..d78284594
--- /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 dac0c8fca..f86a8bc26 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