Skip to content

Commit

Permalink
Pass container hostname to netavark
Browse files Browse the repository at this point in the history
Passing the hostname allows netavark to include it in DHCP lease
requests which, in an environment where DDNS is used, can cause
DNS entries to be created automatically.

* The current Hostname() function in container.go was updated to
check the new `container_name_as_hostname` option in the
CONTAINERS table of containers.conf.  If set and no hostname
was configured for the container, it causes the hostname to be
set to a version of the container's name with the characters not
valid for a hostname removed.  If not set (the default), the original
behavior of setting the hostname to the short container ID is
preserved.

* Because the Hostname() function can return the host's hostname
if the container isn't running in a private UTS namespace, and we'd
NEVER want to send _that_ in a DHCP request for a container, a new
function NetworkHostname() was added which functions like Hostname()
except that it will return an empty string instead of the host's
hostname if the container is not running in a private UTS namespace.

* networking_common.getNetworkOptions() now uses NetworkHostname()
to set the ContainerHostname member of the NetworkOptions structure.
That member was added to the structure in a corresponding commit in
common/libnetwork/types/network.go.

* Added test to containers_conf_test.go

Signed-off-by: George Joseph <[email protected]>
  • Loading branch information
gtjoseph committed Nov 29, 2024
1 parent 575a65b commit 44d3c3b
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 7 deletions.
53 changes: 49 additions & 4 deletions libpod/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,8 +666,12 @@ func (c *Container) RuntimeName() string {
// Runtime spec accessors
// Unlocked

// Hostname gets the container's hostname
func (c *Container) Hostname() string {
// hostname determines the container's hostname.
// If 'network' is true and the container isn't running in a
// private UTS namespoace, an emoty string will be returned
// instead of the host's hostname because we never want to
// send the host's hostname to a DHCP or DNS server.
func (c *Container) hostname(network bool) string {
if c.config.UTSNsCtr != "" {
utsNsCtr, err := c.runtime.GetContainer(c.config.UTSNsCtr)
if err != nil {
Expand All @@ -677,25 +681,66 @@ func (c *Container) Hostname() string {
}
return utsNsCtr.Hostname()
}

if c.config.Spec.Hostname != "" {
return c.config.Spec.Hostname
}

// if the container is not running in a private UTS namespace,
// return the host's hostname.
// If the container is not running in a private UTS namespace,
// return the host's hostname unless 'network' is true in which
// case we return an empty string.
privateUTS := c.hasPrivateUTS()
if !privateUTS {
hostname, err := os.Hostname()
if err == nil {
if network {
return ""
}
return hostname
}
logrus.Errorf("unable to get host's hostname for container %s: %v", c.ID(), err)
return ""
}

// If container_name_as_hostname is set in the CONTAINERS table in
// containers.conf, use a sanitized version of the container's name
// as the hostname. Since the container name must already match
// the set '[a-zA-Z0-9][a-zA-Z0-9_.-]*', we can just remove any
// underscores and limit it to 253 characters to make it a valid
// hostname.
if c.runtime.config.Containers.ContainerNameAsHostName {
sanitizedHostname := strings.ReplaceAll(c.Name(), "_", "");
if (len(sanitizedHostname) <= 253) {
return sanitizedHostname
}
return sanitizedHostname[:253]
}

// Otherwise use the container's short ID as the hostname.
if len(c.ID()) < 11 {
return c.ID()
}
return c.ID()[:12]
}

// Hostname gets the container's hostname
func (c *Container) Hostname() string {
return c.hostname(false)
}

// If the container isn't running in a private UTS namespace, Hostname()
// will return the host's hostname as the container's hostname. If netavark
// were to try and obtain a DHCP lease with the host's hostname in an environment
// where DDNS was active, bad things could happen. NetworkHostname() on the
// other hand, will return an empty string if the container isn't running
// in a private UTS namespace.
//
// This function should only be used to populate the ContainerHostname member
// of the common.libnetwork.types.NetworkOptions struct.
func (c *Container) NetworkHostname() string {
return c.hostname(true)
}

// WorkingDir returns the containers working dir
func (c *Container) WorkingDir() string {
if c.config.Spec.Process != nil {
Expand Down
7 changes: 4 additions & 3 deletions libpod/networking_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ func (c *Container) getNetworkOptions(networkOpts map[string]types.PerNetworkOpt
nameservers = append(nameservers, ip.String())
}
opts := types.NetworkOptions{
ContainerID: c.config.ID,
ContainerName: getNetworkPodName(c),
DNSServers: nameservers,
ContainerID: c.config.ID,
ContainerName: getNetworkPodName(c),
DNSServers: nameservers,
ContainerHostname: c.NetworkHostname(),
}
opts.PortMappings = c.convertPortMappings()

Expand Down
102 changes: 102 additions & 0 deletions test/e2e/containers_conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -763,4 +763,106 @@ var _ = Describe("Verify podman containers.conf usage", func() {
Expect(inspect.OutputToString()).Should(Equal(mode))
}
})

startContainer := func(params ...string) string {
args := []string{"create"}
for _, param := range params {
if param == "--name" {
args = append(args, "--replace")
break
}
}
args = append(args, params...)
args = append(args, ALPINE, "true")

result := podmanTest.Podman(args)
result.WaitWithDefaultTimeout()
Expect(result).Should(ExitCleanly())
containerID := result.OutputToString()

return containerID
}

getContainerConfig := func(containerID string, formatParam string) string {
inspect := podmanTest.Podman([]string{"inspect", "--format", formatParam, containerID})
inspect.WaitWithDefaultTimeout()
value := inspect.OutputToString()
return value
}

It("podman containers.conf container_name_as_hostname", func() {

// With default containers.conf

// Start container with no options
containerID := startContainer()
hostname := getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should be the first 12 characters of the containerID
Expect(hostname).To(Equal(containerID[:12]))

// Start container with name
containerID = startContainer("--name", "cname1")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should still be the first 12 characters of the containerID
Expect(hostname).To(Equal(containerID[:12]))

// Start container with just hostname
containerID = startContainer("--hostname", "cname1.dev")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should now be "cname1.dev"
Expect(hostname).To(Equal("cname1.dev"))

// Start container with name and hostname
containerID = startContainer("--name", "cname1", "--hostname", "cname1.dev")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should now be "cname1.dev"
Expect(hostname).To(Equal("cname1.dev"))

// Create containers.conf override with container_name_as_hostname=true
conffile := filepath.Join(podmanTest.TempDir, "container.conf")
err := os.WriteFile(conffile, []byte("[containers]\ncontainer_name_as_hostname=true\n"), 0755)
Expect(err).ToNot(HaveOccurred())
os.Setenv("CONTAINERS_CONF_OVERRIDE", conffile)

// Start container with no options
containerID = startContainer()
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
name := getContainerConfig(containerID, "{{ .Name }}")
// Hostname should be the auto generated container name with '_' removed
Expect(hostname).To(Equal(strings.ReplaceAll(name, "_", "")))

// Start container with name
containerID = startContainer("--name", "cname1")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should be the container name
Expect(hostname).To(Equal("cname1"))

// Start container with name containing '_'
containerID = startContainer("--name", "cname1_2_3")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should be the set container name with all '_' removed
Expect(hostname).To(Equal("cname123"))

// Start container with just hostname
containerID = startContainer("--hostname", "cname1.dev")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should now be "cname1.dev"
Expect(hostname).To(Equal("cname1.dev"))

// Start container with name and hostname
containerID = startContainer("--name", "cname1", "--hostname", "cname1.dev")
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
// Hostname should still be "cname1.dev"
Expect(hostname).To(Equal("cname1.dev"))

// Start container with name = 260 characters
longHostname := "cnabcdefghijklmnopqrstuvwxyz1234567890.abcdefghijklmnopqrstuvwxyz1234567890.abcdefghijklmnopqrstuvwxyz1234567890.abcdefghijklmnopqrstuvwxyz1234567890.abcdefghijklmnopqrstuvwxyz1234567890.abcdefghijklmnopqrstuvwxyz1234567890.abcdefghijklmnopqrstuvwxyz1234567890"
containerID = startContainer("--name", longHostname)
hostname = getContainerConfig(containerID, "{{ .Config.Hostname }}")
name = getContainerConfig(containerID, "{{ .Name }}")
// Double check that name actually got set correctly
Expect(name).To(Equal(longHostname))
// Hostname should be the container name truncated to 253 characters
Expect(hostname).To(Equal(name[:253]))
})
})

0 comments on commit 44d3c3b

Please sign in to comment.