Skip to content

Improve /etc/hosts generation#48823

Merged
thaJeztah merged 7 commits intomoby:masterfrom
robmry:simplify_etchosts
Nov 7, 2024
Merged

Improve /etc/hosts generation#48823
thaJeztah merged 7 commits intomoby:masterfrom
robmry:simplify_etchosts

Conversation

@robmry
Copy link
Contributor

@robmry robmry commented Nov 5, 2024

- What I did

Follow up on:

- How I did it

Don't update /etc/hosts separately for each initial network

Collect endpoint addresses, then add them to the /etc/hosts file in a single update

Use netip.Addr instead of string when building /etc/hosts

Use netip.Addr instead of string for /etc/hosts generation

And (from @thaJeztah) - libnetwork: Sandbox.buildHostsFile: rename var that shadowed type

libnetwork/etchosts: don't panic on invalid regex

This regex is constructed using user-input, which could technically
produce an invalid regex.

Given that we have an error-return to our availability, let's return
any error we get, instead of panicking.

libnetwork/etchosts: Add: combine with "mergeRecords()"

The mergeRecords function wasn't actually merging anything, but only
appended records to the existing /etc/hosts content. However, doing so
was split across two functions; Add and mergeRecords();

  • Add() obtains a lock for the given path
  • then calls mergeRecords which reads the file-content and appends the
    new records to the content.
  • Closes the file and returns the new content
  • Then Add does a os.WriteFile to ... the same file

Given that we're appending, we won't have to read the file's content, and
we can append to the file itself.

libnetwork/etchosts: Delete: truncate file instead of close and write

We already have the filehandle open, so we could just truncate, and
overwrite the content.

libnetwork: Sandbox.buildHostsFile: remove intermediate var

Call the respective (etchosts.BuildNoIPv6 or etchosts.Build) functions
directly instead of using the intermediate buildf variable.

Add a comment explaining host-networking hosts file generation

- How to verify it

Existing tests.

- Description for the changelog

n/a

@robmry robmry force-pushed the simplify_etchosts branch from 90b7949 to fb4ee42 Compare November 6, 2024 09:35
@robmry robmry changed the title Simplify etchosts Improve /etc/hosts generation Nov 6, 2024
@robmry robmry force-pushed the simplify_etchosts branch from fb4ee42 to cdae8b1 Compare November 6, 2024 11:39
@robmry robmry marked this pull request as ready for review November 6, 2024 14:27
@thaJeztah thaJeztah added status/2-code-review area/networking Networking kind/refactor PR's that refactor, or clean-up code labels Nov 7, 2024
@thaJeztah thaJeztah added this to the 28.0.0 milestone Nov 7, 2024
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes LGTM; I left some comments with suggestions; I opened a PR against your branch (happy for input!); also feel free to squash those changes with your commit(s) where applicable;

defer pathLock(path)()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit intrigued by the mergeRecords() (which, actually doesn't merge, but append), followed by os.WriteFile steps;

  • Add() obtains a lock for the given path
  • then calls mergeRecords which reads the file, and appends the new records to the content
  • Closes the file and returns the new content
  • Then Add does a os.WriteFile to ... the same file

From the above; given that we're appending, we wouldn't even need to read the file, and we could just construct the "to be added" content, and .. write to the file?

// Add adds an arbitrary number of Records to an already existing /etc/hosts file
func Add(path string, recs []Record) error {
	if len(recs) == 0 {
		return nil
	}

	defer pathLock(path)()

	content := bytes.NewBuffer(nil)
	for _, r := range recs {
		if _, err := r.WriteTo(content); err != nil {
			return err
		}
	}

	f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	_, err = f.Write(content.Bytes())
	_ = f.Close()
	return err
}


extraContent := make([]etchosts.Record, 0, len(sb.config.extraHosts))
extraContent := make([]etchosts.Record, 0, len(sb.config.extraHosts)+len(ifaceIPs))
for _, extraHost := range sb.config.extraHosts {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not new, but I noticed while looking at this code; the extraHost is shadowing the extraHost type defined in this package; perhaps rename it to host (can be separate commit)

edit: looks like the second commit is touching this loop, so might as well change it as part of that one 👍

extraContent = append(extraContent, sb.makeHostsRecs(ifaceIPs)...)

// Assume IPv6 support, unless it's definitely disabled.
buildf := etchosts.Build
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not new, but wondering why we used the buildF indirect; this could probably be written as;

// Assume IPv6 support, unless it's definitely disabled.
if en, ok := sb.IPv6Enabled(); ok && !en {
	return etchosts.BuildNoIPv6(sb.config.hostsPath, extraContent)
}
return etchosts.Build(sb.config.hostsPath, extraContent)

☝️ was also curious (because it mentions "definitely disabled"), should it also skip if !ok, or only if !en ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ was also curious (because it mentions "definitely disabled"), should it also skip if !ok, or only if !en ?

If !ok, maybe there is IPv6 in the container, but it can't be sure. So, it includes the IPv6 records by calling Build instead of BuildNoIPv6 ... I think it's alright.

@thaJeztah
Copy link
Member

thaJeztah commented Nov 7, 2024

I think I just spotted a bug in our logic (unrelated to this PR) here;

if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 {
// We are working under the assumption that the origin file option had been properly expressed by the upper layer
// if not here we are going to error out
if err := copyFile(sb.config.originHostsPath, sb.config.hostsPath); err != nil && !os.IsNotExist(err) {
return types.InternalErrorf("could not copy source hosts file %s to %s: %v", sb.config.originHostsPath, sb.config.hostsPath, err)
}
return nil
}

In the above, we copy the hosts's /etc/hosts (sb.config.originHostsPath) for the container, but ONLY if there's no extra-hosts. If extra hosts ARE provided, we fall through, and create a fresh /etc/hosts for the container, which means that we're not preserving entries from /etc/hosts in that case?

if err := buildf(sb.config.hostsPath, extraContent); err != nil {
return err
}

☝️ I think we need to always do the copyFile, but we can return early if we don't have any extraHosts to add 🤔

i.e., something like;

if sb.config.useDefaultSandBox {
	// We are working under the assumption that the origin file option had been properly expressed by the upper layer
	// if not here we are going to error out
	if err := copyFile(sb.config.originHostsPath, sb.config.hostsPath); err != nil && !os.IsNotExist(err) {
		return types.InternalErrorf("could not copy source hosts file %s to %s: %v", sb.config.originHostsPath, sb.config.hostsPath, err)
	}
	if len(sb.config.extraHosts) == 0 {
		// nothing to add; we're done.
		return nil
	}
}

⚠️ ☝️ maybe some additional changes are needed to make sure we append the extraHosts (not replace the content)

@akerouanton
Copy link
Member

akerouanton commented Nov 7, 2024

I think I just spotted a bug in our logic (unrelated to this PR) here;

I'm not convinced that's an unintentional bug. This was introduced in:

This PR doesn't justify why this condition was tighten, but it references this issue:

Which has no mentions of /etc/hosts except this comment: moby/libnetwork#2187 (comment).

Documentation ref:
https://docs.docker.com/config/containers/container-networking/#dns-services
"By default, a container inherits the DNS settings of the Docker daemon, including the /etc/hosts and /etc/resolv.conf. You can override these settings on a per-container basis"

Currently the docs says (see here):

Your container will have lines in /etc/hosts which define the hostname of the container itself, as well as localhost and a few other common things. Custom hosts, defined in /etc/hosts on the host machine, aren't inherited by containers.

Now let's imagine the following situation:

  1. You have foo.bar pointing to 10.0.0.10 in your host's /etc/hosts ;
  2. And you want to change that in your --net=host container to point to 192.168.0.10 ;

With your proposal, the container would end up with two entries, one for each IP address. libresolv would then pick up either one randomly. So, there would be no way to override what's set in the host's /etc/hosts (at least not without an entrypoint mangling /etc/hosts). I'm wrong actually, libresolv will only take the first entry into consideration. BUT Go returns both addresses:

❯ /bin/cat main.go
package main

import (
	"fmt"
	"net"
)

func main() {
	addrs, err := net.LookupHost("foo.bar")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", addrs)
}

❯ go run main.go
[10.0.0.10 192.168.0.10]

I'm afraid that if we implement what you're suggesting, we're going to break someone, somewhere.

That's way to late, but I think the correct UX would have been to disallow --add-host on --net=host containers -- /etc/hosts being part of the mount namespace instead of the netns is mostly an implementation details that should have been properly hidden from users.

EDIT: I think we should add a proper comment stating why it's done that way. I always find this condition confusing and need a bit of time to find out a good reason to keep it that way.

@robmry robmry force-pushed the simplify_etchosts branch from cdae8b1 to 5e5cd8c Compare November 7, 2024 10:43
@robmry
Copy link
Contributor Author

robmry commented Nov 7, 2024

Thank you @thaJeztah ... I cherry-picked your commits to this branch (wasn't quite sure what github would do if I merged your PR) - they all look good, I didn't see any reason to squash or modify anything.

While we're in the area, it's tempting to deal with this FIXME, but there's no need to mix a functional change with the refactoring in this PR so I'll try to look at it separately ...

// FIXME(robmry) - this only matches on hostname, not address. So, if a container
// is connected to two networks then disconnected from one of them, the hosts
// entries for both networks are deleted.
func Delete(path string, recs []Record) error {

@thaJeztah
Copy link
Member

That's way to late, but I think the correct UX would have been to disallow --add-host on --net=host containers -- /etc/hosts being part of the mount namespace instead of the netns is mostly an implementation details that should have been properly hidden from users.

Yes, that was my other thinking as well; when running with --net=host, the container is basicaly a "guess" of the host's networking namespace, but (likely?) should have no control over it (i.e., joining the host's network should make the container act as close as possible to not being containerised from a networking perspective).

Currently we're doing something in between and we either use /etc/hosts from the host, OR "extra-hosts" defined by the user (and drop the ones from the host). The only thing I'm wondering is if the host.docker.internal case is something that would require an exception, but maybe not.

EDIT: I think we should add a proper comment stating why it's done that way. I always find this condition confusing and need a bit of time to find out a good reason to keep it that way.

At least 💯 on that. I also think we should still consider the "produce an error" idea; ISTR we did something for that when trying to use -p (port mapping) when using --net=host, but don't recall if we chose to use an error or print a warning ("mapping discarded")

@thaJeztah
Copy link
Member

wasn't quite sure what github would do if I merged your PR

Merging works as well (but you'd get a "merge" commit in your branch, so usually a "rebase" after to get rid of those). It's pretty neat actually, as it's an easy way to bring in suggestions.

@thaJeztah
Copy link
Member

Perhaps (but not a "strong" blocker sqash the "rename var that shadowed type" commit with your second commit; that would make git-blame slightly easier to find back the actually relevant changes (instead of seeing "rename var")

robmry and others added 5 commits November 7, 2024 11:05
Also, libnetwork: Sandbox.buildHostsFile: rename var that shadowed type

Co-authored-by: Sebastiaan van Stijn <[email protected]>
Signed-off-by: Rob Murray <[email protected]>
This regex is constructed using user-input, which could technically
produce an invalid regex.

Given that we have an error-return to our availability, let's return
any error we get, instead of panicking.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
The `mergeRecords` function wasn't actually _merging_ anything, but only
appended records to the existing `/etc/hosts` content. However, doing so
was split across two functions; `Add` and `mergeRecords()`;

- `Add()` obtains a lock for the given path
- then calls `mergeRecords` which reads the file-content and appends the
  new records to the content.
- Closes the file and returns the new content
- Then `Add` does a `os.WriteFile` to ... the same file

Given that we're appending, we won't have to read the file's content, and
we can append to the file itself.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
We already have the filehandle open, so we could just truncate, and
overwrite the content.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
Call the respective (`etchosts.BuildNoIPv6` or `etchosts.Build`) functions
directly instead of using the intermediate `buildf` variable.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
@robmry robmry force-pushed the simplify_etchosts branch from 5e5cd8c to 7c1e41a Compare November 7, 2024 11:09
@robmry
Copy link
Contributor Author

robmry commented Nov 7, 2024

Perhaps (but not a "strong" blocker sqash the "rename var that shadowed type" commit with your second commit; that would make git-blame slightly easier to find back the actually relevant changes (instead of seeing "rename var")

Done.

@robmry
Copy link
Contributor Author

robmry commented Nov 7, 2024

Merging works as well (but you'd get a "merge" commit in your branch, so usually a "rebase" after to get rid of those). It's pretty neat actually, as it's an easy way to bring in suggestions.

Ah, thank you - it was the merge commit I wanted to avoid. Will do that next time.

@robmry
Copy link
Contributor Author

robmry commented Nov 7, 2024

EDIT: I think we should add a proper comment stating why it's done that way. I always find this condition confusing and need a bit of time to find out a good reason to keep it that way.

I've been around that loop before too - so, added a comment pointing at @akerouanton's analysis above.

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Comment on lines +22 to 23
func (sb *Sandbox) addHostsEntries(_ context.Context, ifaceIP []netip.Addr) error {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still curious if this cannot be implemented on Windows, or if we just never did.

@thaJeztah thaJeztah merged commit 66d45fa into moby:master Nov 7, 2024
@robmry robmry deleted the simplify_etchosts branch November 7, 2024 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Networking kind/refactor PR's that refactor, or clean-up code status/2-code-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants