Skip to content

libnet: add support for custom interface names#49155

Merged
vvoland merged 4 commits intomoby:masterfrom
akerouanton:custom-ifname
Feb 6, 2025
Merged

libnet: add support for custom interface names#49155
vvoland merged 4 commits intomoby:masterfrom
akerouanton:custom-ifname

Conversation

@akerouanton
Copy link
Member

@akerouanton akerouanton commented Dec 20, 2024

Fixes:

- What I did

To support this a new netlabel is added: com.docker.network.endpoint.ifname.

It gives the ability to specify the interface name to be set by netdrivers when the interface is added / moved into the container's network namespace.

All builtin netdrivers support it.

- How I did it

First commit refactor libnetwork/osl to add a new field dstPrefix. So far, its field dstName was used to store both the name prefix and the auto-generated interface name. When an interface was created, dstName would take the prefix and later on that field would be mutated to take the final name. Now, the dstPrefix is stored separately and the dstName is the final interface name (either auto-generated, or statically defined).

The second commit is the actual feature implementation.

The third commit refactors how iface names are generated. We now scan all the interfaces tied to a Namespace to find the first gap in numeric suffixes. This allows users to statically define ifnames that match generated names. For instance, a user can set eth2 for a specific endpoint, and libnetwork will use [0, 1] and [3, ...] for dynamic ifname assignment.

- How to verify it

  • All netdrivers have a new integration test.
  • A new integration test makes sure custom ifnames are preserved on live restore.
  • Mixing static and dynamic ifname assignments with the same prefix is also tested through a new integration test.
$ docker run --rm -it --network=name=bridge,driver-opt=com.docker.network.endpoint.ifname=foobar alpine ip link show | grep foobar
11: foobar@if20: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP

- Description for the changelog

- Add a new netlabel `com.docker.network.endpoint.ifname` to customize the interface name used when connecting a container to a network. It's supported by all built-in network drivers on Linux.
  - When a container is created with multiple networks specified, there's no guarantee on the order networks will be connected to the container. So, if a custom interface name uses the same prefix as the auto-generated names (eg. `eth`), the container might fail to start.
  - The recommended practice is to use a different prefix (eg. `en0`), or a numerical suffix high enough to never collide (eg. `eth100`).
  - This label can be specified on `docker network connect` via the `--driver-opt` flag, eg. `docker network connect --driver-opt=com.docker.network.endpoint.ifname=foobar …`.
  - Or via the long-form `--network` flag on `docker run`, eg. `docker run --network=name=bridge,driver-opt=com.docker.network.endpoint.ifname=foobar …`

@akerouanton akerouanton added status/2-code-review kind/feature Functionality or other elements that the project doesn't currently have. Features are new and shiny area/networking Networking impact/changelog impact/documentation area/networking/d/overlay Networking area/networking/d/bridge Networking area/networking/d/ipvlan Networking area/networking/d/macvlan Networking labels Dec 20, 2024
@akerouanton akerouanton added this to the 28.0.0 milestone Dec 20, 2024
@akerouanton akerouanton self-assigned this Dec 20, 2024
@akerouanton akerouanton force-pushed the custom-ifname branch 9 times, most recently from 32cc271 to 115f4f1 Compare January 7, 2025 09:01
@akerouanton akerouanton marked this pull request as ready for review January 7, 2025 09:01
@akerouanton akerouanton force-pushed the custom-ifname branch 3 times, most recently from 8fb2d3e to 8042945 Compare January 7, 2025 10:53
@akerouanton akerouanton force-pushed the custom-ifname branch 2 times, most recently from 27e6740 to 8e26804 Compare January 13, 2025 16:45
@thaJeztah
Copy link
Member

CI failures look legit

@akerouanton akerouanton force-pushed the custom-ifname branch 3 times, most recently from d05cb6e to 59498e2 Compare January 30, 2025 13:22
ctx := setupTest(t)
apiClient := testEnv.APIClient()

master := "dm-dummy0"
Copy link
Member

Choose a reason for hiding this comment

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

The comment at the top should probably go here, as this is where that part of the test happens?

Suggested change
master := "dm-dummy0"
// verify the driver automatically provisions the 802.1q link (dm-dummy0.60)
master := "dm-dummy0"

Copy link
Member

Choose a reason for hiding this comment

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

Also for fixed values, I started to use const more; I think it sometimes help glancing over a test to identify "dynamic" values (may change during the test) and fixtures;

Suggested change
master := "dm-dummy0"
// verify the driver automatically provisions the 802.1q link (dm-dummy0.60)
const master = "dm-dummy0"

Copy link
Member Author

@akerouanton akerouanton Jan 31, 2025

Choose a reason for hiding this comment

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

Ah, yeah I had these changes parked yesterday while I was trying to fix unobvious concurrency issues in the osl package. I should have put this PR back in draft mode.

I removed these comments as they don't say anything useful. It was copy/pasted from other tests -- we should probably remove them in those other tests too.

@akerouanton akerouanton force-pushed the custom-ifname branch 3 times, most recently from 4838111 to 7e6b766 Compare January 31, 2025 09:22
// append it to dstPrefix.
//
// It's safe to call concurrently.
func (n *Namespace) createInterface(ctx context.Context, targetNs netns.NsHandle, srcName, dstPrefix, dstName string, options ...IfaceOption) (*Interface, netlink.Link, error) {
Copy link
Member Author

@akerouanton akerouanton Jan 31, 2025

Choose a reason for hiding this comment

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

This new method is introduced to make sure the Namespace is locked between the call to generateIfaceName and append(n.iFaces, i), because generateIfaceName uses n.iFaces to generate a unique dstName.

However, the underlying network link represented by Interface need to have its final dstName set before appending because Namespace.RemoveInterface() references it and could be run concurrently with AddInterface.

waitForBridgePort and n.advertiseAddrs could introduce a lot of delay if things go wrong. So instead of locking for the whole lifespan of AddInterface, this method will append earlier than before.

iface, err := nlh.LinkByName(i.DstName())


path := n.path
isDefault := n.isDefault
nlh := n.nlHandle
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure if we're all going to agree on this code style.

I think that by not making copies of Namespace properties, we clearly signal that: 1. unaltered versions of Namespace properties are used; 2. they're safe to access concurrently.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good - I think we can take it further (not in this PR), to end up with comments on struct members that clearly state which need mutex protection.

Copy link
Contributor

@robmry robmry left a comment

Choose a reason for hiding this comment

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

I don't think there's a test that shows what's expected to happen when an explicit interface name clashes with an already allocated name ... perhaps worth adding?

# docker network create n0
# docker run --rm -ti --network n0 --name c1 busybox
# docker network create n1
# docker network connect --driver-opt com.docker.network.endpoint.ifname=eth0 n1 c1
Error response from daemon: failed to add interface vethbdb03af to sandbox: error renaming interface "vethbdb03af" to "eth0": file exists

Because we don't have a "join order" for networks, a command like this will sometimes work, and sometimes won't ... just depending on which network connection happens to handled first.

# docker run --rm -ti --network name=n0,driver-opt=com.docker.network.endpoint.ifname=eth0 --network name=n1 --name c1 alpine

So, if I want the "n0" endpoint to be called "eth0" ... I either have to connect it first, then separately connect others (without specifying multiple networks in the run), or explicitly name all the interfaces, or use a different default prefix for the network. Maybe that's a limitation we need to document?

Perhaps it's also worth adding examples like the ones above of how to use the new endpoint option in the changelog note (or, just a mention that it goes in the driver-opt option)?


path := n.path
isDefault := n.isDefault
nlh := n.nlHandle
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good - I think we can take it further (not in this PR), to end up with comments on struct members that clearly state which need mutex protection.

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.

Just leaving as comment; I tried digging in, but it's a lot of complexity and I'm wondering if we (also see some of my comments) can get this done without changing so many signatures (especially because it appears some of those already have the information available in some form or another)

Comment on lines +219 to +223
// AddInterface adds an existing Interface to the sandbox. The operation will
// rename from the Interface SrcName to DstName as it moves, and reconfigure
// the interface according to the specified settings. If dstPrefix is provided,
// but not dstName, AddInterface will auto-generate a unique suffix and append
// it to dstPrefix.
Copy link
Member

@thaJeztah thaJeztah Feb 4, 2025

Choose a reason for hiding this comment

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

Nit: we should try to avoid future tense more for these; this part confused me, because dstName is not an argument here; is this because it's passed through options?;

if dstPrefix is provided, but not dstName, AddInterface will auto-generate a unique suffix and append it to dstPrefix.

Perhaps something like this would work (Ideally we'd mention SomeOption here to set the name, instead of referring to IfaceOption.dstName 🤔);

// AddInterface adds an existing Interface to the sandbox. The operation renames
// the Interface SrcName to DstName as it moves, and reconfigures the interface
// according to the specified settings. By default, AddInterface auto-generates
// a unique interface using the given dstPrefix as a prefix, unless a custom
// interface-name is passed as an option and set through [IfaceOption.dstName].

Some confusing logic in the code as well, but I probably need to look closer at that to understand what exactly it does, because it looks like for the default network, dstName is ignored?

if n.isDefault {
	i.dstName = i.srcName
} else {
	i.dstName = fmt.Sprintf("%s%d", dstPrefix, n.nextIfIndex[dstPrefix])
	n.nextIfIndex[dstPrefix]++
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Woops looks like this comment slipped in the wrong commit - that's why you're getting confused by dstName being mentioned here.

I rewrote the comment to use present tense, but since this function is left untouched until the next (2nd) commit I rather removed that change from the 1st one.

Some confusing logic in the code as well, but I probably need to look closer at that to understand what exactly it does, because it looks like for the default network, dstName is ignored?

Yes, this function is confusing... isDefault here isn't "the default network" but the "default network namespace" - in other words, the "host" netns / network. There might be good reasons for that but I don't know tbh.

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha! More stuff to untangle in our backlog 😂

}

if err := d.Join(ctx, nid, epid, sb.Key(), ep, sb.Labels()); err != nil {
if err := d.Join(ctx, nid, epid, sb.Key(), ep, ep.generic, sb.Labels()); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

See my other comment as well; it looks like the information needed is all captured in the endpoint itself (which is passed and must implement the driverapi.JoinInfo interface; that interface already has a InterfaceNames() method (but not sure if that's related); just wondering if instead of passing 2 separate properties from the same ep, we could just pass ep and have it expose the information that's needed (either through the existing JoinInfo interface method, or perhaps that needs to either be expanded, or a separate GenericInfoProvider (whatever) interface that can be implemented optionally?

(I notice sandbox has a Labels() method, although I don't think it's currently part of an interface)

// JoinInfo represents a set of resources that the driver has the ability to provide during
// join time.
type JoinInfo interface {
// InterfaceName returns an InterfaceNameInfo go interface to facilitate
// setting the names for the interface.
InterfaceName() InterfaceNameInfo

Copy link
Member Author

Choose a reason for hiding this comment

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

it looks like the information needed is all captured in the endpoint itself (which is passed and must implement the driverapi.JoinInfo interface; that interface already has a InterfaceNames() method (but not sure if that's related)

It definitely is related, but data flows the other way around. That is, netdrivers call InterfacesNames().SetNames(srcName, dstPrefix, dstName) on driverapi.JoinInfo to set the interface names.

Libnetwork's sbJoin method does the following:

  • Call the netdriver Join method with the Endpoint passed as 2nd argument (implementor of driverapi.JoinInfo). The driver sets a bunch of values on the libnet's EndpointInterface through the setters defined in driverapi.JoinInfo.
  • Then call populateNetworkResources which builds a list of osl options based on the values set on EndpointInterface at the previous step, and then call the AddInterface() method defined in the osl package with those options.

So, to implement your suggestion, I'd have to either break the unidirectional nature of driverapi.JoinInfo, or add yet another interface. I think both are equally bad and undesirable - and long term plan is to get rid of those interfaces.

(I notice sandbox has a Labels() method, although I don't think it's currently part of an interface)

The custom interface name is an endpoint option, not a sandbox option.

Copy link
Member

Choose a reason for hiding this comment

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

The custom interface name is an endpoint option, not a sandbox option.

Ah, sorry, I meant "add a similar accessor to the Endpoint interface, like the Sandbox interface has, but as discussed in our call, we should consider all these internal, and have the flexibility of changing things / shuffle things around later.

Comment on lines +80 to +83
func GetIfname(opts map[string]interface{}) string {
if opts == nil {
return ""
}
opt, ok := opts[Ifname]
if !ok {
return ""
}
ifname, ok := opt.(string)
if !ok {
return ""
}
return ifname
}
Copy link
Member

Choose a reason for hiding this comment

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

This can be a one-liner;

Suggested change
func GetIfname(opts map[string]interface{}) string {
if opts == nil {
return ""
}
opt, ok := opts[Ifname]
if !ok {
return ""
}
ifname, ok := opt.(string)
if !ok {
return ""
}
return ifname
}
func GetIfname(opts map[string]interface{}) string {
ifName, _ := opts[Ifname].(string)
return ifName
}

e.g.; https://go.dev/play/p/9QdxgApB7qE

package main

import "testing"

func GetIfname(opts map[string]interface{}) string {
	opt, _ := opts["com.docker.network.endpoint.ifname"].(string)
	return opt
}

func TestMaybeNil(t *testing.T) {
	ifName := GetIfname(nil)
	t.Log(ifName)

	ifName = GetIfname(map[string]interface{}{"com.docker.network.endpoint.ifname": true})
	t.Log(ifName)

	ifName = GetIfname(map[string]interface{}{"com.docker.network.endpoint.ifname": "eth0"})
	t.Log(ifName)
}

Copy link
Member

Choose a reason for hiding this comment

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

But with the above taken into account, I'm even wondering if we should just inline it into where it's used 🙈

Copy link
Member Author

Choose a reason for hiding this comment

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

Right 🤦

As for inlining, I don't have strong opinions but I feel like having a dedicated accessor that takes care of casting the option somewhat hides the ugliness of these map[string]interface{} and make callers' code slightly easier to read.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, it's fine for now to keep a Method / Function for this. We need to have a close look at all the fuzzy genericopts in a wider scope, and see if we can reduce the back-and-forth changing of strongly typed arguments to a generic one (and back! in some cases)

Comment on lines 405 to +404
// restore interfaces
for iface, opts := range interfaces {
i, err := newInterface(n, iface.SrcName, iface.DstPrefix, opts...)
i, err := newInterface(n, iface.SrcName, iface.DstPrefix, iface.DstName, opts...)
Copy link
Member

Choose a reason for hiding this comment

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

This commit doesn't compile, because newInterface signature was not modified (yet)?

I'm actually wondering; dstName is an optional option, and because of that probably should not be a positional argument (which more commonly are used for required arguments); is there a reason we cannot pass interface name as part of opts ... ?

Copy link
Member Author

Choose a reason for hiding this comment

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

This commit doesn't compile, because newInterface signature was not modified (yet)?

Rebase issue - this should be fixed now.

I'm actually wondering; dstName is an optional option, and because of that probably should not be a positional argument (which more commonly are used for required arguments); is there a reason we cannot pass interface name as part of opts ... ?

Both dstPrefix and dstName are now optional params - either one has to be provided.


for _, i := range tbox.Interfaces() {
err = s.AddInterface(context.Background(), i.SrcName(), i.DstName(),
err = s.AddInterface(context.Background(), i.SrcName(), i.dstPrefix, i.DstName(),
Copy link
Member

Choose a reason for hiding this comment

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

These don't compile in this commit, because AddInterface signature was not (yet) changed

Perhaps an idea; could we change newInterface() to accept a name only, not a prefix ? It looks like the current logic is;

  • call newInterface(), which constructs the interface, but only "partially"
  • after it's created, we get the prefix (or name), and based on that, we patch it up

It feels more organic to feed the interface-name into it when constructing (which could even be i.DstName() to take an argument, to enumerate suffixes), e.g. i.DstName(n.SuffixEnumerator) (where SuffixEnumerator would give the next suffix to use

Copy link
Member Author

Choose a reason for hiding this comment

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

These don't compile in this commit, because AddInterface signature was not (yet) changed

Rebase issue - this should be fixed now.

It feels more organic to feed the interface-name into it when constructing (which could even be i.DstName() to take an argument, to enumerate suffixes), e.g. i.DstName(n.SuffixEnumerator) (where SuffixEnumerator would give the next suffix to use

Namespace.RestoreInterfaces(), which is the other function calling newInterface, does something wildly different with the output of newInterface(). That code is fragile IMO, so I don't think this PR is the right place to change what newInterface() does, and I think your suggestion for SuffixEnumerator would actually bring more unnecessary complexity.

Comment on lines +383 to +392
if strings.HasPrefix(i.DstName(), prefix) {
s := i.DstName()[len(prefix):]
if v, err := strconv.Atoi(s); err == nil {
suffixes = append(suffixes, v)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This can use CutPrefix to do it all at once;

Suggested change
if strings.HasPrefix(i.DstName(), prefix) {
s := i.DstName()[len(prefix):]
if v, err := strconv.Atoi(s); err == nil {
suffixes = append(suffixes, v)
}
}
if s, ok := strings.CutPrefix(i.DstName(), prefix); ok {
if v, err := strconv.Atoi(s); err == nil {
suffixes = append(suffixes, v)
}
}

⚠️ ❓ do we need to account for someone using a prefix with a - at the end? i.e.;

  • use eth as prefix
  • then use eth- as prefix

Which, when checking for eth as prefix would also contain interfaces prefixed with eth-, but now we'd be parsing, e.g. eth-1 as strconv.Atoi(-1), so -1 ?

The "pick first available slot" further down would then pick 0;

	suffixes := []int{-1, 0, 1}

	for i := 0; i < len(suffixes); i++ {
		if i != suffixes[i] {
			return prefix + strconv.Itoa(i)
		}
	}

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, negative suffixes are now ignored.

Comment on lines +391 to +408
if len(suffixes) == 0 {
return prefix + "0"
}

sort.Ints(suffixes)

for i := 0; i < len(suffixes); i++ {
if i != suffixes[i] {
return prefix + strconv.Itoa(i)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This last bit of code looks to be to inject a suffix in the first possible slot; is it important to have consecutive numbers, or would it be fine to just pick "max + 1" ?

I was also playing to see if we could skip this part, which I think we could if we know the first loop already produced only consecutive numbers; basically, if the highest number matches the number of interfaces, I think we can tell "they must've been consecutive"

⚠️ 🤔 actually this would fail with the negative numbers (eth-1 -> suffix=-1)

func (n *Namespace) generateIfaceName(prefix string) string {
	var suffixes []int
	var maxSuffix int
	for _, i := range n.iFaces {
		if s, ok := strings.CutPrefix(i.DstName(), prefix); ok {
			if v, err := strconv.Atoi(s); err == nil {
				if v > maxSuffix {
					maxSuffix = v
				}
				suffixes = append(suffixes, v)
			}
		}
	}

	if len(suffixes) == 0 {
		return prefix + "0"
	}

	// maximum suffix matches length of interfaces, which means that
	// all interfaces with the given prefix were numbered without
	// gaps; pick the next one.
	if len(suffixes) == maxSuffix {
		return prefix + strconv.Itoa(maxSuffix+1)
	}

	sort.Ints(suffixes)

	for i := 0; i < len(suffixes); i++ {
		if i != suffixes[i] {
			return prefix + strconv.Itoa(i)
		}
	}

	return prefix + strconv.Itoa(len(suffixes))
}

Copy link
Member Author

Choose a reason for hiding this comment

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

This last bit of code looks to be to inject a suffix in the first possible slot; is it important to have consecutive numbers, or would it be fine to just pick "max + 1" ?

That could become an issue if "max + 1" overflows the maximum number of characters for interface names (IFNAMSIZ = 16 chars), eg. prefix = 'mylonglongif' / highest suffix = '9999'.

By looking for the lowest slot, this would be handled correctly (granted there's no 9999 networks attached to that container, in which case the issue isn't on our side).

I added a comment to clarify this.

I was also playing to see if we could skip this part, which I think we could if we know the first loop already produced only consecutive numbers; basically, if the highest number matches the number of interfaces, I think we can tell "they must've been consecutive"

Actually that code doesn't test if there's no gaps in suffixes. Doing that would be slightly more involved, at which point I'm not sure this gives any benefit given the added complexity. I'd prefer to keep this code simple, stupid.

}{
{prefix: "test", names: []string{"test0", "test1"}, want: "test2"},
{prefix: "test", names: []string{"test0", "test2"}, want: "test1"},
{prefix: "test", names: []string{"test2"}, want: "test0"},
Copy link
Member

Choose a reason for hiding this comment

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

See my other comment; not sure how realistic it is to happen, but we should probably have test-cases with mixed prefixes, and one that has a - in the prefix; for example, this will fail, and pick eth0;

Suggested change
{prefix: "test", names: []string{"test2"}, want: "test0"},
{prefix: "test", names: []string{"test2"}, want: "test0"},
{prefix: "eth", names: []string{"eth-0", "eth-1", "eth0"}, want: "eth1"},

Copy link
Member Author

Choose a reason for hiding this comment

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

Done - added a test case for negative numbers.

There was an edge case around -0 being treated as 0 - it's now handled correctly.

Comment on lines 1465 to +1478
containerVethPrefix = network.config.ContainerIfacePrefix
}
if err := iNames.SetNames(endpoint.srcName, containerVethPrefix); err != nil {
if err := iNames.SetNames(endpoint.srcName, containerVethPrefix, netlabel.GetIfname(epOpts)); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Overall, I'm a bit on the fence on the approach; I know we have the labels as "fuzzy options", but making it part of the option-type seems to be digging in even deeper (map[string]interface{} being the API), and I'm wondering if we can have a more targeted approach (WithInterfaceName(name), or WithInterfaceName(prefix, name) (if we want that option to either set prefix or name), where we parse the labels early on 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure to see where this hypothetical option would be placed / passed around as that doesn't seem to fit anywhere in the current design.

Basically, epOpts being a set of (mostly) user-defined options passed to the netdriver isn't that terrible since these are handled exclusively by the netdriver (or should be), and libnetwork should assume no knowledge about their semantics and how / if they're going to be handled.

If you're suggesting that this option is passed by libnetwork to the netdriver, then libnetwork has to know about that label and that would break that ^. Plus, it'd go against your suggestion to change less signatures.

If you're suggesting that it should be passed by the netdriver to libnetwork, that's probably overkill for passing just a single option. I'm not a big fan of coming up with a new design for the sake of a single option with no clear plans to reuse the same mechanism for other options. We can look into this in the future but I doubt it'd be used by anything else.

@akerouanton
Copy link
Member Author

Force pushing some changes to check if the integration test TestInspectNetwork fails on the CI since I see it failing locally but can't pinpoint the root cause. For the record, here's the only log that seems relevant during the test run:

time="2025-02-05T17:07:06.678119088Z" level=error msg="fatal task error" error="link /go/src/github.com/docker/docker/bundles/test-integration/TestInspectNetwork/d4a82747d8cf8/root/vfs/dir/881dc556aa2e8939b005e433f2cb2a79a8e60385c7b23c3ae5a5226991f70180-init/bin/[ /go/src/github.com/docker/docker/bundles/test-integration/TestInspectNetwork/d4a82747d8cf8/root/vfs/dir/881dc556aa2e8939b005e433f2cb2a79a8e60385c7b23c3ae5a5226991f70180-init/bin/adduser: file exists" module=node/agent/taskmanager node.id=lxewhfhiufgh3mpz9iam0ru9d service.id=sy93xyxaj46ikxtfvowh53whe task.id=tqlaop75asy7uzt3m5ajupryq

@akerouanton
Copy link
Member Author

akerouanton commented Feb 6, 2025

Okay, so the issue I was seeing locally was caused by 'Docker VMM'. Tests should be green now.

I don't think there's a test that shows what's expected to happen when an explicit interface name clashes with an already allocated name ... perhaps worth adding?

Done.

Perhaps it's also worth adding examples like the ones above of how to use the new endpoint option in the changelog note

Done. I added a recommendation regarding colliding prefixes.

@akerouanton akerouanton requested a review from thaJeztah February 6, 2025 06:28
Before this commit, `Interface.dstName` was used to store the dest
ifname prefix, and then the final ifname.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
To support this, a new netlabel is added: `com.docker.network.endpoint.ifname`.

It gives the ability to specify the interface name to be set by
netdrivers when the interface is added / moved into the container's
network namespace.

All builtin netdrivers support it.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
At first glance, it seemed like the Namespace lock was held to make
local copies of Namespace properties, but all those properties all safe
to access concurrently. So, un-alias those props and reduce the scope
of the mutex lock.

This helps understanding where locking is really needed.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
// can be added dynamically.
type Namespace struct {
path string
path string // path is the absolute path to the network namespace. It is safe to access it concurrently.
Copy link
Member

Choose a reason for hiding this comment

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

As none of these are exported, it would be possible to shuffle things round, and perhaps we should look if we can split fields into immutable fields (set when constructing), and mutable fields (requiring the my mutex)

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 after rebasing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking/d/bridge Networking area/networking/d/ipvlan Networking area/networking/d/macvlan Networking area/networking/d/overlay Networking area/networking Networking impact/changelog impact/documentation kind/feature Functionality or other elements that the project doesn't currently have. Features are new and shiny status/2-code-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants