Browse code

libnet: add support for custom interface names

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>

Albin Kerouanton authored on 2024/11/26 22:48:07
Showing 40 changed files
... ...
@@ -16,6 +16,7 @@ import (
16 16
 	"github.com/docker/docker/internal/nlwrap"
17 17
 	"github.com/docker/docker/internal/testutils/networking"
18 18
 	"github.com/docker/docker/libnetwork/drivers/bridge"
19
+	"github.com/docker/docker/libnetwork/netlabel"
19 20
 	"github.com/docker/docker/testutil"
20 21
 	"github.com/docker/docker/testutil/daemon"
21 22
 	"github.com/vishvananda/netlink"
... ...
@@ -404,3 +405,21 @@ func TestIsolated(t *testing.T) {
404 404
 	ping(t, "-4")
405 405
 	ping(t, "-6")
406 406
 }
407
+
408
+func TestEndpointWithCustomIfname(t *testing.T) {
409
+	ctx := setupTest(t)
410
+	apiClient := testEnv.APIClient()
411
+
412
+	ctrID := ctr.Run(ctx, t, apiClient,
413
+		ctr.WithCmd("ip", "-o", "link", "show", "foobar"),
414
+		ctr.WithEndpointSettings("bridge", &networktypes.EndpointSettings{
415
+			DriverOpts: map[string]string{
416
+				netlabel.Ifname: "foobar",
417
+			},
418
+		}))
419
+	defer ctr.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
420
+
421
+	out, err := ctr.Output(ctx, apiClient, ctrID)
422
+	assert.NilError(t, err)
423
+	assert.Assert(t, strings.Contains(out.Stdout, ": foobar@if"), "expected ': foobar@if' in 'ip link show':\n%s", out.Stdout)
424
+}
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"github.com/docker/docker/integration/internal/container"
16 16
 	net "github.com/docker/docker/integration/internal/network"
17 17
 	n "github.com/docker/docker/integration/network"
18
+	"github.com/docker/docker/libnetwork/netlabel"
18 19
 	"github.com/docker/docker/testutil"
19 20
 	"github.com/docker/docker/testutil/daemon"
20 21
 	"gotest.tools/v3/assert"
... ...
@@ -682,3 +683,33 @@ func TestPointToPoint(t *testing.T) {
682 682
 		})
683 683
 	}
684 684
 }
685
+
686
+func TestEndpointWithCustomIfname(t *testing.T) {
687
+	skip.If(t, testEnv.IsRemoteDaemon)
688
+	skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")
689
+
690
+	ctx := setupTest(t)
691
+	apiClient := testEnv.APIClient()
692
+
693
+	// master dummy interface 'di' notation represent 'docker ipvlan'
694
+	const master = "di-dummy0"
695
+	n.CreateMasterDummy(ctx, t, master)
696
+	defer n.DeleteInterface(ctx, t, master)
697
+
698
+	// create a network specifying the desired sub-interface name
699
+	const netName = "ipvlan-custom-ifname"
700
+	net.CreateNoError(ctx, t, apiClient, netName, net.WithIPvlan("di-dummy0.70", ""))
701
+
702
+	ctrID := container.Run(ctx, t, apiClient,
703
+		container.WithCmd("ip", "-o", "link", "show", "foobar"),
704
+		container.WithEndpointSettings(netName, &network.EndpointSettings{
705
+			DriverOpts: map[string]string{
706
+				netlabel.Ifname: "foobar",
707
+			},
708
+		}))
709
+	defer container.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
710
+
711
+	out, err := container.Output(ctx, apiClient, ctrID)
712
+	assert.NilError(t, err)
713
+	assert.Assert(t, strings.Contains(out.Stdout, ": foobar@if"), "expected ': foobar@if' in 'ip link show':\n%s", out.Stdout)
714
+}
... ...
@@ -48,3 +48,10 @@ func TestMain(m *testing.M) {
48 48
 	shutdown(ctx)
49 49
 	os.Exit(code)
50 50
 }
51
+
52
+func setupTest(t *testing.T) context.Context {
53
+	ctx := testutil.StartSpan(baseContext, t)
54
+	environment.ProtectAll(ctx, t, testEnv)
55
+	t.Cleanup(func() { testEnv.Clean(ctx, t) })
56
+	return ctx
57
+}
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/docker/docker/integration/internal/container"
15 15
 	net "github.com/docker/docker/integration/internal/network"
16 16
 	n "github.com/docker/docker/integration/network"
17
+	"github.com/docker/docker/libnetwork/netlabel"
17 18
 	"github.com/docker/docker/testutil"
18 19
 	"github.com/docker/docker/testutil/daemon"
19 20
 	"gotest.tools/v3/assert"
... ...
@@ -640,3 +641,32 @@ func TestPointToPoint(t *testing.T) {
640 640
 	assert.Check(t, is.Equal(res.Stderr.Len(), 0))
641 641
 	assert.Check(t, is.Contains(res.Stdout.String(), "1 packets transmitted, 1 packets received"))
642 642
 }
643
+
644
+func TestEndpointWithCustomIfname(t *testing.T) {
645
+	skip.If(t, testEnv.IsRemoteDaemon)
646
+	skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")
647
+
648
+	ctx := setupTest(t)
649
+	apiClient := testEnv.APIClient()
650
+
651
+	const master = "dm-dummy0"
652
+	n.CreateMasterDummy(ctx, t, master)
653
+	defer n.DeleteInterface(ctx, t, master)
654
+
655
+	// create a network specifying the desired sub-interface name
656
+	const netName = "macvlan-custom-ifname"
657
+	net.CreateNoError(ctx, t, apiClient, netName, net.WithMacvlan("dm-dummy0.60"))
658
+
659
+	ctrID := container.Run(ctx, t, apiClient,
660
+		container.WithCmd("ip", "-o", "link", "show", "foobar"),
661
+		container.WithEndpointSettings(netName, &network.EndpointSettings{
662
+			DriverOpts: map[string]string{
663
+				netlabel.Ifname: "foobar",
664
+			},
665
+		}))
666
+	defer container.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
667
+
668
+	out, err := container.Output(ctx, apiClient, ctrID)
669
+	assert.NilError(t, err)
670
+	assert.Assert(t, strings.Contains(out.Stdout, ": foobar@if"), "expected ': foobar@if' in 'ip link show':\n%s", out.Stdout)
671
+}
... ...
@@ -52,3 +52,10 @@ func TestMain(m *testing.M) {
52 52
 	shutdown(ctx)
53 53
 	os.Exit(code)
54 54
 }
55
+
56
+func setupTest(t *testing.T) context.Context {
57
+	ctx := testutil.StartSpan(baseContext, t)
58
+	environment.ProtectAll(ctx, t, testEnv)
59
+	t.Cleanup(func() { testEnv.Clean(ctx, t) })
60
+	return ctx
61
+}
55 62
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+//go:build !windows
1
+
2
+package overlay // import "github.com/docker/docker/integration/network/overlay"
3
+
4
+import (
5
+	"context"
6
+	"os"
7
+	"testing"
8
+
9
+	"github.com/docker/docker/testutil"
10
+	"github.com/docker/docker/testutil/environment"
11
+	"go.opentelemetry.io/otel"
12
+	"go.opentelemetry.io/otel/attribute"
13
+	"go.opentelemetry.io/otel/codes"
14
+)
15
+
16
+var (
17
+	testEnv     *environment.Execution
18
+	baseContext context.Context
19
+)
20
+
21
+func TestMain(m *testing.M) {
22
+	shutdown := testutil.ConfigureTracing()
23
+
24
+	ctx, span := otel.Tracer("").Start(context.Background(), "integration/network/overlay/TestMain")
25
+	baseContext = ctx
26
+
27
+	var err error
28
+	testEnv, err = environment.New(ctx)
29
+	if err != nil {
30
+		span.SetStatus(codes.Error, err.Error())
31
+		span.End()
32
+		shutdown(ctx)
33
+		panic(err)
34
+	}
35
+
36
+	err = environment.EnsureFrozenImagesLinux(ctx, testEnv)
37
+	if err != nil {
38
+		span.SetStatus(codes.Error, err.Error())
39
+		span.End()
40
+		shutdown(ctx)
41
+		panic(err)
42
+	}
43
+
44
+	testEnv.Print()
45
+	code := m.Run()
46
+	if code != 0 {
47
+		span.SetStatus(codes.Error, "m.Run() returned non-zero exit code")
48
+	}
49
+	span.SetAttributes(attribute.Int("exit", code))
50
+	span.End()
51
+	shutdown(ctx)
52
+	os.Exit(code)
53
+}
54
+
55
+func setupTest(t *testing.T) context.Context {
56
+	ctx := testutil.StartSpan(baseContext, t)
57
+	environment.ProtectAll(ctx, t, testEnv)
58
+	t.Cleanup(func() { testEnv.Clean(ctx, t) })
59
+	return ctx
60
+}
0 61
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+package overlay // import "github.com/docker/docker/integration/network/overlay"
0 1
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+//go:build !windows
1
+
2
+package overlay // import "github.com/docker/docker/integration/network/overlay"
3
+
4
+import (
5
+	"strings"
6
+	"testing"
7
+
8
+	containertypes "github.com/docker/docker/api/types/container"
9
+	"github.com/docker/docker/api/types/network"
10
+	"github.com/docker/docker/integration/internal/container"
11
+	net "github.com/docker/docker/integration/internal/network"
12
+	"github.com/docker/docker/libnetwork/netlabel"
13
+	"github.com/docker/docker/testutil/daemon"
14
+	"gotest.tools/v3/assert"
15
+	"gotest.tools/v3/skip"
16
+)
17
+
18
+func TestEndpointWithCustomIfname(t *testing.T) {
19
+	skip.If(t, testEnv.IsRootless, "rootless mode doesn't support overlay networks")
20
+
21
+	ctx := setupTest(t)
22
+
23
+	d := daemon.New(t)
24
+	d.StartAndSwarmInit(ctx, t)
25
+	defer d.Stop(t)
26
+	defer d.SwarmLeave(ctx, t, true)
27
+
28
+	apiClient := d.NewClientT(t)
29
+
30
+	// create a network specifying the desired sub-interface name
31
+	const netName = "overlay-custom-ifname"
32
+	net.CreateNoError(ctx, t, apiClient, netName,
33
+		net.WithDriver("overlay"),
34
+		net.WithAttachable())
35
+
36
+	ctrID := container.Run(ctx, t, apiClient,
37
+		container.WithCmd("ip", "-o", "link", "show", "foobar"),
38
+		container.WithEndpointSettings(netName, &network.EndpointSettings{
39
+			DriverOpts: map[string]string{
40
+				netlabel.Ifname: "foobar",
41
+			},
42
+		}))
43
+	defer container.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
44
+
45
+	out, err := container.Output(ctx, apiClient, ctrID)
46
+	assert.NilError(t, err)
47
+	assert.Assert(t, strings.Contains(out.Stdout, ": foobar@if"), "expected ': foobar@if' in 'ip link show':\n%s", out.Stdout)
48
+}
... ...
@@ -2,15 +2,19 @@ package network // import "github.com/docker/docker/integration/network"
2 2
 
3 3
 import (
4 4
 	"context"
5
+	"strings"
5 6
 	"testing"
6 7
 	"time"
7 8
 
8 9
 	"github.com/docker/docker/api/types"
10
+	containertypes "github.com/docker/docker/api/types/container"
9 11
 	networktypes "github.com/docker/docker/api/types/network"
10 12
 	swarmtypes "github.com/docker/docker/api/types/swarm"
11 13
 	"github.com/docker/docker/client"
14
+	"github.com/docker/docker/integration/internal/container"
12 15
 	"github.com/docker/docker/integration/internal/network"
13 16
 	"github.com/docker/docker/integration/internal/swarm"
17
+	"github.com/docker/docker/libnetwork/netlabel"
14 18
 	"github.com/docker/docker/testutil"
15 19
 	"github.com/docker/docker/testutil/daemon"
16 20
 	"gotest.tools/v3/assert"
... ...
@@ -465,3 +469,66 @@ func TestServiceWithDefaultAddressPoolInit(t *testing.T) {
465 465
 	err = d.SwarmLeave(ctx, t, true)
466 466
 	assert.NilError(t, err)
467 467
 }
468
+
469
+func TestCustomIfnameIsPreservedOnLiveRestore(t *testing.T) {
470
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "custom interface name is only supported by Linux netdrivers")
471
+	skip.If(t, testEnv.IsRootless, "rootless mode doesn't support live-restore")
472
+
473
+	ctx := setupTest(t)
474
+
475
+	d := daemon.New(t)
476
+	defer d.Stop(t)
477
+	d.StartWithBusybox(ctx, t, "--live-restore=true")
478
+
479
+	apiClient := d.NewClientT(t)
480
+	defer apiClient.Close()
481
+
482
+	ctrId := container.Run(ctx, t, apiClient,
483
+		container.WithCmd("top"),
484
+		container.WithEndpointSettings("bridge", &networktypes.EndpointSettings{
485
+			DriverOpts: map[string]string{
486
+				netlabel.Ifname: "foobar",
487
+			},
488
+		}))
489
+	defer container.Remove(ctx, t, apiClient, ctrId, containertypes.RemoveOptions{Force: true})
490
+
491
+	d.Restart(t, "--live-restore=true")
492
+
493
+	res, err := container.Exec(ctx, apiClient, ctrId, []string{"ip", "-o", "link", "show", "foobar"})
494
+	assert.NilError(t, err)
495
+	assert.Check(t, is.Equal(res.ExitCode, 0))
496
+	assert.Check(t, strings.Contains(res.Stdout(), ": foobar@if"), "expected ': foobar@if' in 'ip link show':\n%s", res.Stdout())
497
+
498
+	// On live-restore, the daemon rebuilds the list of interfaces for all
499
+	// containers. Call NetworkDisconnect here to make sure that the right
500
+	// dstName is used internally.
501
+	err = apiClient.NetworkDisconnect(ctx, "bridge", ctrId, true)
502
+	assert.NilError(t, err)
503
+}
504
+
505
+func TestCustomIfnameCollidesWithExistingIface(t *testing.T) {
506
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "custom interface name is only supported by Linux netdrivers")
507
+
508
+	ctx := setupTest(t)
509
+
510
+	d := daemon.New(t)
511
+	defer d.Stop(t)
512
+	d.StartWithBusybox(ctx, t, "--live-restore=true")
513
+
514
+	apiClient := d.NewClientT(t)
515
+	defer apiClient.Close()
516
+
517
+	const testnet = "testnet"
518
+	network.CreateNoError(ctx, t, apiClient, testnet, network.WithDriver("bridge"))
519
+
520
+	ctrId := container.Run(ctx, t, apiClient,
521
+		container.WithCmd("top"),
522
+		container.WithEndpointSettings("bridge", &networktypes.EndpointSettings{}))
523
+	defer container.Remove(ctx, t, apiClient, ctrId, containertypes.RemoveOptions{Force: true})
524
+
525
+	err := apiClient.NetworkConnect(ctx, testnet, ctrId, &networktypes.EndpointSettings{DriverOpts: map[string]string{
526
+		netlabel.Ifname: "eth0",
527
+	}})
528
+	assert.ErrorContains(t, err, "error renaming interface")
529
+	assert.ErrorContains(t, err, "file exists")
530
+}
... ...
@@ -55,7 +55,7 @@ func (d *manager) EndpointOperInfo(nid, eid string) (map[string]interface{}, err
55 55
 	return nil, types.NotImplementedErrorf("not implemented")
56 56
 }
57 57
 
58
-func (d *manager) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
58
+func (d *manager) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
59 59
 	return types.NotImplementedErrorf("not implemented")
60 60
 }
61 61
 
... ...
@@ -51,7 +51,7 @@ type Driver interface {
51 51
 	EndpointOperInfo(nid, eid string) (map[string]interface{}, error)
52 52
 
53 53
 	// Join method is invoked when a Sandbox is attached to an endpoint.
54
-	Join(ctx context.Context, nid, eid string, sboxKey string, jinfo JoinInfo, options map[string]interface{}) error
54
+	Join(ctx context.Context, nid, eid string, sboxKey string, jinfo JoinInfo, epOpts, sbOpts map[string]interface{}) error
55 55
 
56 56
 	// Leave method is invoked when a Sandbox detaches from an endpoint.
57 57
 	Leave(nid, eid string) error
... ...
@@ -140,8 +140,10 @@ type InterfaceInfo interface {
140 140
 // InterfaceNameInfo provides a go interface for the drivers to assign names
141 141
 // to interfaces.
142 142
 type InterfaceNameInfo interface {
143
-	// SetNames method assigns the srcName and dstPrefix for the interface.
144
-	SetNames(srcName, dstPrefix string) error
143
+	// SetNames method assigns the srcName, dstPrefix, and dstName for the
144
+	// interface. If both dstName and dstPrefix are set, dstName takes
145
+	// precedence.
146
+	SetNames(srcName, dstPrefix, dstName string) error
145 147
 }
146 148
 
147 149
 // JoinInfo represents a set of resources that the driver has the ability to provide during
... ...
@@ -1444,7 +1444,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
1444 1444
 }
1445 1445
 
1446 1446
 // Join method is invoked when a Sandbox is attached to an endpoint.
1447
-func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
1447
+func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, epOpts, sbOpts map[string]interface{}) error {
1448 1448
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.drivers.bridge.Join", trace.WithAttributes(
1449 1449
 		attribute.String("nid", nid),
1450 1450
 		attribute.String("eid", eid),
... ...
@@ -1465,7 +1465,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
1465 1465
 		return endpointNotFoundError(eid)
1466 1466
 	}
1467 1467
 
1468
-	endpoint.containerConfig, err = parseContainerOptions(options)
1468
+	endpoint.containerConfig, err = parseContainerOptions(sbOpts)
1469 1469
 	if err != nil {
1470 1470
 		return err
1471 1471
 	}
... ...
@@ -1475,7 +1475,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
1475 1475
 	if network.config.ContainerIfacePrefix != "" {
1476 1476
 		containerVethPrefix = network.config.ContainerIfacePrefix
1477 1477
 	}
1478
-	if err := iNames.SetNames(endpoint.srcName, containerVethPrefix); err != nil {
1478
+	if err := iNames.SetNames(endpoint.srcName, containerVethPrefix, netlabel.GetIfname(epOpts)); err != nil {
1479 1479
 		return err
1480 1480
 	}
1481 1481
 
... ...
@@ -642,6 +642,7 @@ type testInterface struct {
642 642
 	addr               *net.IPNet
643 643
 	addrv6             *net.IPNet
644 644
 	srcName            string
645
+	dstPrefix          string
645 646
 	dstName            string
646 647
 	createdInContainer bool
647 648
 	netnsPath          string
... ...
@@ -727,8 +728,9 @@ func (i *testInterface) SetCreatedInContainer(cic bool) {
727 727
 	i.createdInContainer = cic
728 728
 }
729 729
 
730
-func (i *testInterface) SetNames(srcName string, dstName string) error {
730
+func (i *testInterface) SetNames(srcName, dstPrefix, dstName string) error {
731 731
 	i.srcName = srcName
732
+	i.dstPrefix = dstPrefix
732 733
 	i.dstName = dstName
733 734
 	return nil
734 735
 }
... ...
@@ -818,7 +820,7 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
818 818
 		t.Fatalf("Failed to create an endpoint : %s", err.Error())
819 819
 	}
820 820
 
821
-	err = d.Join(context.Background(), "net1", "ep1", "sbox", te, sbOptions)
821
+	err = d.Join(context.Background(), "net1", "ep1", "sbox", te, nil, sbOptions)
822 822
 	if err != nil {
823 823
 		t.Fatalf("Failed to join the endpoint: %v", err)
824 824
 	}
... ...
@@ -922,7 +924,7 @@ func TestLinkContainers(t *testing.T) {
922 922
 	sbOptions := make(map[string]interface{})
923 923
 	sbOptions[netlabel.ExposedPorts] = exposedPorts
924 924
 
925
-	err = d.Join(context.Background(), "net1", "ep1", "sbox", te1, sbOptions)
925
+	err = d.Join(context.Background(), "net1", "ep1", "sbox", te1, nil, sbOptions)
926 926
 	if err != nil {
927 927
 		t.Fatalf("Failed to join the endpoint: %v", err)
928 928
 	}
... ...
@@ -953,7 +955,7 @@ func TestLinkContainers(t *testing.T) {
953 953
 		"ChildEndpoints": []string{"ep1"},
954 954
 	}
955 955
 
956
-	err = d.Join(context.Background(), "net1", "ep2", "", te2, sbOptions)
956
+	err = d.Join(context.Background(), "net1", "ep2", "", te2, nil, sbOptions)
957 957
 	if err != nil {
958 958
 		t.Fatal("Failed to link ep1 and ep2")
959 959
 	}
... ...
@@ -1011,7 +1013,7 @@ func TestLinkContainers(t *testing.T) {
1011 1011
 		"ChildEndpoints": []string{"ep1", "ep4"},
1012 1012
 	}
1013 1013
 
1014
-	err = d.Join(context.Background(), "net1", "ep2", "", te2, sbOptions)
1014
+	err = d.Join(context.Background(), "net1", "ep2", "", te2, nil, sbOptions)
1015 1015
 	if err != nil {
1016 1016
 		t.Fatal(err)
1017 1017
 	}
... ...
@@ -1228,7 +1230,7 @@ func TestSetDefaultGw(t *testing.T) {
1228 1228
 		t.Fatalf("Failed to create endpoint: %v", err)
1229 1229
 	}
1230 1230
 
1231
-	err = d.Join(context.Background(), "dummy", "ep", "sbox", te, nil)
1231
+	err = d.Join(context.Background(), "dummy", "ep", "sbox", te, nil, nil)
1232 1232
 	if err != nil {
1233 1233
 		t.Fatalf("Failed to join endpoint: %v", err)
1234 1234
 	}
... ...
@@ -55,7 +55,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
55 55
 	return nil, types.NotImplementedErrorf("not implemented")
56 56
 }
57 57
 
58
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
58
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
59 59
 	return types.NotImplementedErrorf("not implemented")
60 60
 }
61 61
 
... ...
@@ -43,8 +43,9 @@ func TestLinkCreate(t *testing.T) {
43 43
 	err = d.CreateEndpoint(context.Background(), "dummy", "ep", te.Interface(), nil)
44 44
 	assert.NilError(t, err)
45 45
 
46
-	err = d.Join(context.Background(), "dummy", "ep", "sbox", te, nil)
46
+	err = d.Join(context.Background(), "dummy", "ep", "sbox", te, nil, nil)
47 47
 	assert.NilError(t, err)
48
+	assert.Assert(t, te.iface.dstPrefix != "", "got: %q, want: %q", te.iface.dstPrefix, "")
48 49
 
49 50
 	// Verify sbox endpoint interface inherited MTU value from bridge config
50 51
 	sboxLnk, err := nlwrap.LinkByName(te.iface.srcName)
... ...
@@ -59,8 +60,6 @@ func TestLinkCreate(t *testing.T) {
59 59
 	assert.Check(t, is.ErrorType(err, errdefs.IsForbidden))
60 60
 	assert.Assert(t, is.Error(err, "Endpoint (ep) already exists (Only one endpoint allowed)"), "Failed to detect duplicate endpoint id on same network")
61 61
 
62
-	assert.Check(t, te.iface.dstName != "", "Invalid Dstname returned")
63
-
64 62
 	_, err = nlwrap.LinkByName(te.iface.srcName)
65 63
 	assert.Check(t, err, "Could not find source link %s", te.iface.srcName)
66 64
 
... ...
@@ -67,7 +67,7 @@ func TestPortMappingConfig(t *testing.T) {
67 67
 		t.Fatalf("Failed to create the endpoint: %s", err.Error())
68 68
 	}
69 69
 
70
-	if err = d.Join(context.Background(), "dummy", "ep1", "sbox", te, sbOptions); err != nil {
70
+	if err = d.Join(context.Background(), "dummy", "ep1", "sbox", te, nil, sbOptions); err != nil {
71 71
 		t.Fatalf("Failed to join the endpoint: %v", err)
72 72
 	}
73 73
 
... ...
@@ -153,7 +153,7 @@ func TestPortMappingV6Config(t *testing.T) {
153 153
 		t.Fatalf("Failed to create the endpoint: %s", err.Error())
154 154
 	}
155 155
 
156
-	if err = d.Join(context.Background(), "dummy", "ep1", "sbox", te, sbOptions); err != nil {
156
+	if err = d.Join(context.Background(), "dummy", "ep1", "sbox", te, nil, sbOptions); err != nil {
157 157
 		t.Fatalf("Failed to join the endpoint: %v", err)
158 158
 	}
159 159
 
... ...
@@ -68,7 +68,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
68 68
 }
69 69
 
70 70
 // Join method is invoked when a Sandbox is attached to an endpoint.
71
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
71
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
72 72
 	return nil
73 73
 }
74 74
 
... ...
@@ -9,6 +9,7 @@ import (
9 9
 
10 10
 	"github.com/containerd/log"
11 11
 	"github.com/docker/docker/libnetwork/driverapi"
12
+	"github.com/docker/docker/libnetwork/netlabel"
12 13
 	"github.com/docker/docker/libnetwork/netutils"
13 14
 	"github.com/docker/docker/libnetwork/ns"
14 15
 	"github.com/docker/docker/libnetwork/types"
... ...
@@ -29,7 +30,7 @@ const (
29 29
 )
30 30
 
31 31
 // Join method is invoked when a Sandbox is attached to an endpoint.
32
-func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
32
+func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, epOpts, _ map[string]interface{}) error {
33 33
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.drivers.ipvlan.Join", trace.WithAttributes(
34 34
 		attribute.String("nid", nid),
35 35
 		attribute.String("eid", eid),
... ...
@@ -143,7 +144,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
143 143
 		jinfo.DisableGatewayService()
144 144
 	}
145 145
 	iNames := jinfo.InterfaceName()
146
-	err = iNames.SetNames(vethName, containerVethPrefix)
146
+	err = iNames.SetNames(vethName, containerVethPrefix, netlabel.GetIfname(epOpts))
147 147
 	if err != nil {
148 148
 		return err
149 149
 	}
... ...
@@ -55,7 +55,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
55 55
 	return nil, types.NotImplementedErrorf("not implemented")
56 56
 }
57 57
 
58
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
58
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
59 59
 	return types.NotImplementedErrorf("not implemented")
60 60
 }
61 61
 
... ...
@@ -9,6 +9,7 @@ import (
9 9
 
10 10
 	"github.com/containerd/log"
11 11
 	"github.com/docker/docker/libnetwork/driverapi"
12
+	"github.com/docker/docker/libnetwork/netlabel"
12 13
 	"github.com/docker/docker/libnetwork/netutils"
13 14
 	"github.com/docker/docker/libnetwork/ns"
14 15
 	"go.opentelemetry.io/otel"
... ...
@@ -17,7 +18,7 @@ import (
17 17
 )
18 18
 
19 19
 // Join method is invoked when a Sandbox is attached to an endpoint.
20
-func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
20
+func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, epOpts, _ map[string]interface{}) error {
21 21
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.drivers.macvlan.Join", trace.WithAttributes(
22 22
 		attribute.String("nid", nid),
23 23
 		attribute.String("eid", eid),
... ...
@@ -102,7 +103,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
102 102
 		jinfo.DisableGatewayService()
103 103
 	}
104 104
 	iNames := jinfo.InterfaceName()
105
-	err = iNames.SetNames(vethName, containerVethPrefix)
105
+	err = iNames.SetNames(vethName, containerVethPrefix, netlabel.GetIfname(epOpts))
106 106
 	if err != nil {
107 107
 		return err
108 108
 	}
... ...
@@ -55,7 +55,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
55 55
 	return nil, types.NotImplementedErrorf("not implemented")
56 56
 }
57 57
 
58
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
58
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
59 59
 	return types.NotImplementedErrorf("not implemented")
60 60
 }
61 61
 
... ...
@@ -68,7 +68,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
68 68
 }
69 69
 
70 70
 // Join method is invoked when a Sandbox is attached to an endpoint.
71
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
71
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
72 72
 	return nil
73 73
 }
74 74
 
... ...
@@ -10,6 +10,7 @@ import (
10 10
 
11 11
 	"github.com/containerd/log"
12 12
 	"github.com/docker/docker/libnetwork/driverapi"
13
+	"github.com/docker/docker/libnetwork/netlabel"
13 14
 	"github.com/docker/docker/libnetwork/ns"
14 15
 	"github.com/docker/docker/libnetwork/osl"
15 16
 	"github.com/docker/docker/libnetwork/types"
... ...
@@ -20,7 +21,7 @@ import (
20 20
 )
21 21
 
22 22
 // Join method is invoked when a Sandbox is attached to an endpoint.
23
-func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
23
+func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, epOpts, _ map[string]interface{}) error {
24 24
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.drivers.overlay.Join", trace.WithAttributes(
25 25
 		attribute.String("nid", nid),
26 26
 		attribute.String("eid", eid),
... ...
@@ -83,7 +84,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
83 83
 		return err
84 84
 	}
85 85
 
86
-	if err = sbox.AddInterface(ctx, overlayIfName, "veth", osl.WithMaster(s.brName)); err != nil {
86
+	if err = sbox.AddInterface(ctx, overlayIfName, "veth", "", osl.WithMaster(s.brName)); err != nil {
87 87
 		return fmt.Errorf("could not add veth pair inside the network sandbox: %v", err)
88 88
 	}
89 89
 
... ...
@@ -110,7 +111,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
110 110
 	}
111 111
 
112 112
 	if iNames := jinfo.InterfaceName(); iNames != nil {
113
-		err = iNames.SetNames(containerIfName, "eth")
113
+		err = iNames.SetNames(containerIfName, "eth", netlabel.GetIfname(epOpts))
114 114
 		if err != nil {
115 115
 			return err
116 116
 		}
... ...
@@ -427,7 +427,7 @@ func (n *network) setupSubnetSandbox(s *subnet, brName, vxlanName string) error
427 427
 	// create a bridge and vxlan device for this subnet and move it to the sandbox
428 428
 	sbox := n.sbox
429 429
 
430
-	if err := sbox.AddInterface(context.TODO(), brName, "br", osl.WithIPv4Address(s.gwIP), osl.WithIsBridge(true)); err != nil {
430
+	if err := sbox.AddInterface(context.TODO(), brName, "br", "", osl.WithIPv4Address(s.gwIP), osl.WithIsBridge(true)); err != nil {
431 431
 		return fmt.Errorf("bridge creation in sandbox failed for subnet %q: %v", s.subnetIP.String(), err)
432 432
 	}
433 433
 
... ...
@@ -439,7 +439,7 @@ func (n *network) setupSubnetSandbox(s *subnet, brName, vxlanName string) error
439 439
 		return err
440 440
 	}
441 441
 
442
-	if err := sbox.AddInterface(context.TODO(), vxlanName, "vxlan", osl.WithMaster(brName)); err != nil {
442
+	if err := sbox.AddInterface(context.TODO(), vxlanName, "vxlan", "", osl.WithMaster(brName)); err != nil {
443 443
 		// If adding vxlan device to the overlay namespace fails, remove the bridge interface we
444 444
 		// already added to the namespace. This allows the caller to try the setup again.
445 445
 		for _, iface := range sbox.Interfaces() {
... ...
@@ -190,7 +190,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
190 190
 }
191 191
 
192 192
 // Join method is invoked when a Sandbox is attached to an endpoint.
193
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
193
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
194 194
 	return types.NotImplementedErrorf("not implemented")
195 195
 }
196 196
 
... ...
@@ -273,7 +273,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
273 273
 }
274 274
 
275 275
 // Join method is invoked when a Sandbox is attached to an endpoint.
276
-func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) (retErr error) {
276
+func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, options map[string]interface{}) (retErr error) {
277 277
 	join := &api.JoinRequest{
278 278
 		NetworkID:  nid,
279 279
 		EndpointID: eid,
... ...
@@ -300,7 +300,7 @@ func (d *driver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo
300 300
 
301 301
 	ifaceName := res.InterfaceName
302 302
 	if iface := jinfo.InterfaceName(); iface != nil && ifaceName != nil {
303
-		if err := iface.SetNames(ifaceName.SrcName, ifaceName.DstPrefix); err != nil {
303
+		if err := iface.SetNames(ifaceName.SrcName, ifaceName.DstPrefix, ""); err != nil {
304 304
 			return fmt.Errorf("failed to set interface name: %s", err)
305 305
 		}
306 306
 	}
... ...
@@ -21,6 +21,7 @@ import (
21 21
 	"github.com/docker/docker/libnetwork/scope"
22 22
 	"github.com/docker/docker/libnetwork/types"
23 23
 	"github.com/docker/docker/pkg/plugins"
24
+	"gotest.tools/v3/assert"
24 25
 )
25 26
 
26 27
 func decodeToMap(r *http.Request) (res map[string]interface{}, err error) {
... ...
@@ -82,8 +83,9 @@ func setupPlugin(t *testing.T, name string, mux *http.ServeMux) func() {
82 82
 
83 83
 type testEndpoint struct {
84 84
 	t                     *testing.T
85
-	src                   string
86
-	dst                   string
85
+	srcName               string
86
+	dstPrefix             string
87
+	dstName               string
87 88
 	address               string
88 89
 	addressIPv6           string
89 90
 	macAddress            string
... ...
@@ -188,13 +190,10 @@ func (test *testEndpoint) NetnsPath() string { return "" }
188 188
 
189 189
 func (test *testEndpoint) SetCreatedInContainer(bool) {}
190 190
 
191
-func (test *testEndpoint) SetNames(src string, dst string) error {
192
-	if test.src != src {
193
-		test.t.Fatalf(`Wrong SrcName; expected "%s", got "%s"`, test.src, src)
194
-	}
195
-	if test.dst != dst {
196
-		test.t.Fatalf(`Wrong DstPrefix; expected "%s", got "%s"`, test.dst, dst)
197
-	}
191
+func (test *testEndpoint) SetNames(srcName, dstPrefix, dstName string) error {
192
+	assert.Equal(test.t, test.srcName, srcName)
193
+	assert.Equal(test.t, test.dstPrefix, dstPrefix)
194
+	assert.Equal(test.t, test.dstName, dstName)
198 195
 	return nil
199 196
 }
200 197
 
... ...
@@ -322,8 +321,8 @@ func TestRemoteDriver(t *testing.T) {
322 322
 
323 323
 	ep := &testEndpoint{
324 324
 		t:              t,
325
-		src:            "vethsrc",
326
-		dst:            "vethdst",
325
+		srcName:        "vethsrc",
326
+		dstPrefix:      "vethdst",
327 327
 		address:        "192.168.5.7/16",
328 328
 		addressIPv6:    "2001:DB8::5:7/48",
329 329
 		macAddress:     "ab:cd:ef:ee:ee:ee",
... ...
@@ -390,8 +389,9 @@ func TestRemoteDriver(t *testing.T) {
390 390
 			"HostsPath":      ep.hostsPath,
391 391
 			"ResolvConfPath": ep.resolvConfPath,
392 392
 			"InterfaceName": map[string]interface{}{
393
-				"SrcName":   ep.src,
394
-				"DstPrefix": ep.dst,
393
+				"SrcName":   ep.srcName,
394
+				"DstPrefix": ep.dstPrefix,
395
+				"DstName":   ep.dstName,
395 396
 			},
396 397
 			"StaticRoutes": []map[string]interface{}{
397 398
 				{
... ...
@@ -478,7 +478,7 @@ func TestRemoteDriver(t *testing.T) {
478 478
 	}
479 479
 
480 480
 	joinOpts := map[string]interface{}{"foo": "fooValue"}
481
-	err = d.Join(context.Background(), netID, endID, "sandbox-key", ep, joinOpts)
481
+	err = d.Join(context.Background(), netID, endID, "sandbox-key", ep, nil, joinOpts)
482 482
 	if err != nil {
483 483
 		t.Fatal(err)
484 484
 	}
... ...
@@ -15,7 +15,7 @@ import (
15 15
 )
16 16
 
17 17
 // Join method is invoked when a Sandbox is attached to an endpoint.
18
-func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
18
+func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, options map[string]interface{}) error {
19 19
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.drivers.windows_overlay.Join", trace.WithAttributes(
20 20
 		attribute.String("nid", nid),
21 21
 		attribute.String("eid", eid),
... ...
@@ -846,7 +846,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
846 846
 }
847 847
 
848 848
 // Join method is invoked when a Sandbox is attached to an endpoint.
849
-func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
849
+func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, options map[string]interface{}) error {
850 850
 	ctx, span := otel.Tracer("").Start(ctx, fmt.Sprintf("libnetwork.drivers.windows_%s.Join", d.name), trace.WithAttributes(
851 851
 		attribute.String("nid", nid),
852 852
 		attribute.String("eid", eid),
... ...
@@ -134,7 +134,7 @@ func (test *testEndpoint) SetGatewayIPv6(ipv6 net.IP) error {
134 134
 	return nil
135 135
 }
136 136
 
137
-func (test *testEndpoint) SetNames(src string, dst string) error {
137
+func (test *testEndpoint) SetNames(_, _, _ string) error {
138 138
 	return nil
139 139
 }
140 140
 
... ...
@@ -536,7 +536,7 @@ func (ep *Endpoint) sbJoin(ctx context.Context, sb *Sandbox, options ...Endpoint
536 536
 		return fmt.Errorf("failed to get driver during join: %v", err)
537 537
 	}
538 538
 
539
-	if err := d.Join(ctx, nid, epid, sb.Key(), ep, sb.Labels()); err != nil {
539
+	if err := d.Join(ctx, nid, epid, sb.Key(), ep, ep.generic, sb.Labels()); err != nil {
540 540
 		return err
541 541
 	}
542 542
 	defer func() {
... ...
@@ -43,6 +43,7 @@ type EndpointInterface struct {
43 43
 	llAddrs            []*net.IPNet
44 44
 	srcName            string
45 45
 	dstPrefix          string
46
+	dstName            string // dstName is the name of the interface in the container namespace. It takes precedence over dstPrefix.
46 47
 	routes             []*net.IPNet
47 48
 	v4PoolID           string
48 49
 	v6PoolID           string
... ...
@@ -70,6 +71,7 @@ func (epi *EndpointInterface) MarshalJSON() ([]byte, error) {
70 70
 	}
71 71
 	epMap["srcName"] = epi.srcName
72 72
 	epMap["dstPrefix"] = epi.dstPrefix
73
+	epMap["dstName"] = epi.dstName
73 74
 	var routes []string
74 75
 	for _, route := range epi.routes {
75 76
 		routes = append(routes, route.String())
... ...
@@ -147,6 +149,7 @@ func (epi *EndpointInterface) CopyTo(dstEpi *EndpointInterface) error {
147 147
 	dstEpi.addrv6 = types.GetIPNetCopy(epi.addrv6)
148 148
 	dstEpi.srcName = epi.srcName
149 149
 	dstEpi.dstPrefix = epi.dstPrefix
150
+	dstEpi.dstName = epi.dstName
150 151
 	dstEpi.v4PoolID = epi.v4PoolID
151 152
 	dstEpi.v6PoolID = epi.v6PoolID
152 153
 	dstEpi.createdInContainer = epi.createdInContainer
... ...
@@ -269,10 +272,12 @@ func (epi *EndpointInterface) SrcName() string {
269 269
 	return epi.srcName
270 270
 }
271 271
 
272
-// SetNames method assigns the srcName and dstPrefix for the interface.
273
-func (epi *EndpointInterface) SetNames(srcName string, dstPrefix string) error {
272
+// SetNames method assigns the srcName, dstName, and dstPrefix for the
273
+// interface. If both dstName and dstPrefix are set, dstName takes precedence.
274
+func (epi *EndpointInterface) SetNames(srcName, dstPrefix, dstName string) error {
274 275
 	epi.srcName = srcName
275 276
 	epi.dstPrefix = dstPrefix
277
+	epi.dstName = dstName
276 278
 	return nil
277 279
 }
278 280
 
... ...
@@ -239,7 +239,7 @@ func compareEndpointInterface(a, b *EndpointInterface) bool {
239 239
 	if a == nil || b == nil {
240 240
 		return false
241 241
 	}
242
-	return a.srcName == b.srcName && a.dstPrefix == b.dstPrefix && a.v4PoolID == b.v4PoolID && a.v6PoolID == b.v6PoolID &&
242
+	return a.srcName == b.srcName && a.dstPrefix == b.dstPrefix && a.dstName == b.dstName && a.v4PoolID == b.v4PoolID && a.v6PoolID == b.v6PoolID &&
243 243
 		types.CompareIPNet(a.addr, b.addr) && types.CompareIPNet(a.addrv6, b.addrv6) && compareNwLists(a.llAddrs, b.llAddrs)
244 244
 }
245 245
 
... ...
@@ -807,7 +807,7 @@ func (b *badDriver) EndpointOperInfo(nid, eid string) (map[string]interface{}, e
807 807
 	return nil, nil
808 808
 }
809 809
 
810
-func (b *badDriver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
810
+func (b *badDriver) Join(_ context.Context, nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, _, _ map[string]interface{}) error {
811 811
 	return fmt.Errorf("I will not allow any join")
812 812
 }
813 813
 
... ...
@@ -30,6 +30,9 @@ const (
30 30
 	// where the interface name is represented by the string "IFNAME".
31 31
 	EndpointSysctls = Prefix + ".endpoint.sysctls"
32 32
 
33
+	// Ifname can be used to set the interface name used inside the container. It takes precedence over ContainerIfacePrefix.
34
+	Ifname = Prefix + ".endpoint.ifname"
35
+
33 36
 	// EnableIPv4 constant represents enabling IPV4 at network level
34 37
 	EnableIPv4 = Prefix + ".enable_ipv4"
35 38
 
... ...
@@ -70,3 +73,11 @@ const (
70 70
 	// is intended for internal use, it may be removed in a future release.
71 71
 	NoProxy6To4 = DriverPrivatePrefix + ".no_proxy_6to4"
72 72
 )
73
+
74
+// GetIfname returns the value associated to the Ifname netlabel from the
75
+// provided options. If there's no Ifname netlabel, or if the value isn't a
76
+// string, it returns an empty string.
77
+func GetIfname(opts map[string]interface{}) string {
78
+	ifname, _ := opts[Ifname].(string)
79
+	return ifname
80
+}
73 81
new file mode 100644
... ...
@@ -0,0 +1,60 @@
0
+package netlabel
1
+
2
+import (
3
+	"testing"
4
+
5
+	"gotest.tools/v3/assert"
6
+)
7
+
8
+func TestGetIfname(t *testing.T) {
9
+	testcases := []struct {
10
+		name      string
11
+		opts      map[string]interface{}
12
+		expIfname string
13
+	}{
14
+		{
15
+			name:      "nil opts",
16
+			opts:      nil,
17
+			expIfname: "",
18
+		},
19
+		{
20
+			name:      "no ifname",
21
+			opts:      map[string]interface{}{},
22
+			expIfname: "",
23
+		},
24
+		{
25
+			name: "ifname set",
26
+			opts: map[string]interface{}{
27
+				Ifname: "foobar",
28
+			},
29
+			expIfname: "foobar",
30
+		},
31
+		{
32
+			name: "ifname set to empty string",
33
+			opts: map[string]interface{}{
34
+				Ifname: "",
35
+			},
36
+			expIfname: "",
37
+		},
38
+		{
39
+			name: "ifname set to nil",
40
+			opts: map[string]interface{}{
41
+				Ifname: nil,
42
+			},
43
+			expIfname: "",
44
+		},
45
+		{
46
+			name: "ifname set to int",
47
+			opts: map[string]interface{}{
48
+				Ifname: 42,
49
+			},
50
+			expIfname: "",
51
+		},
52
+	}
53
+
54
+	for _, tc := range testcases {
55
+		t.Run(tc.name, func(t *testing.T) {
56
+			assert.Equal(t, tc.expIfname, GetIfname(tc.opts))
57
+		})
58
+	}
59
+}
... ...
@@ -56,11 +56,12 @@ const (
56 56
 
57 57
 // newInterface creates a new interface in the given namespace using the
58 58
 // provided options.
59
-func newInterface(ns *Namespace, srcName, dstPrefix string, options ...IfaceOption) (*Interface, error) {
59
+func newInterface(ns *Namespace, srcName, dstPrefix, dstName string, options ...IfaceOption) (*Interface, error) {
60 60
 	i := &Interface{
61 61
 		stopCh:                make(chan struct{}),
62 62
 		srcName:               srcName,
63 63
 		dstPrefix:             dstPrefix,
64
+		dstName:               dstName,
64 65
 		advertiseAddrNMsgs:    advertiseAddrNMsgsDefault,
65 66
 		advertiseAddrInterval: advertiseAddrIntervalDefault,
66 67
 		ns:                    ns,
... ...
@@ -116,10 +117,8 @@ func (i *Interface) SrcName() string {
116 116
 	return i.srcName
117 117
 }
118 118
 
119
-// DstName returns the name that will be assigned to the interface once
120
-// moved inside a network namespace. When the caller passes in a DstName,
121
-// it is only expected to pass a prefix. The name will be modified with an
122
-// auto-generated suffix.
119
+// DstName returns the final interface name in the target network namespace.
120
+// It's generated based on the prefix passed to [Namespace.AddInterface].
123 121
 func (i *Interface) DstName() string {
124 122
 	return i.dstName
125 123
 }
... ...
@@ -218,18 +217,19 @@ func moveLink(ctx context.Context, nlhHost nlwrap.Handle, iface netlink.Link, i
218 218
 	return nil
219 219
 }
220 220
 
221
-// AddInterface adds an existing Interface to the sandbox. The operation will rename
222
-// from the Interface SrcName to DstName as it moves, and reconfigure the
223
-// interface according to the specified settings. The caller is expected
224
-// to only provide a prefix for DstName. The AddInterface api will auto-generate
225
-// an appropriate suffix for the DstName to disambiguate.
226
-func (n *Namespace) AddInterface(ctx context.Context, srcName, dstPrefix string, options ...IfaceOption) error {
221
+// AddInterface creates an Interface that represents an existing network
222
+// interface (except for bridge interfaces, which are created here).
223
+//
224
+// The network interface will be reconfigured according the options passed, and
225
+// it'll be renamed from srcName into an auto-generated 'dest name' that
226
+// combines the provided dstPrefix and a numeric suffix.
227
+func (n *Namespace) AddInterface(ctx context.Context, srcName, dstPrefix, dstName string, options ...IfaceOption) error {
227 228
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.osl.AddInterface", trace.WithAttributes(
228 229
 		attribute.String("srcName", srcName),
229 230
 		attribute.String("dstPrefix", dstPrefix)))
230 231
 	defer span.End()
231 232
 
232
-	i, err := newInterface(n, srcName, dstPrefix, options...)
233
+	i, err := newInterface(n, srcName, dstPrefix, dstName, options...)
233 234
 	if err != nil {
234 235
 		return err
235 236
 	}
... ...
@@ -237,7 +237,7 @@ func (n *Namespace) AddInterface(ctx context.Context, srcName, dstPrefix string,
237 237
 	n.mu.Lock()
238 238
 	if n.isDefault {
239 239
 		i.dstName = i.srcName
240
-	} else {
240
+	} else if i.dstName == "" {
241 241
 		i.dstName = fmt.Sprintf("%s%d", dstPrefix, n.nextIfIndex[dstPrefix])
242 242
 		n.nextIfIndex[dstPrefix]++
243 243
 	}
... ...
@@ -402,7 +402,7 @@ func (n *Namespace) Destroy() error {
402 402
 func (n *Namespace) RestoreInterfaces(interfaces map[Iface][]IfaceOption) error {
403 403
 	// restore interfaces
404 404
 	for iface, opts := range interfaces {
405
-		i, err := newInterface(n, iface.SrcName, iface.DstPrefix, opts...)
405
+		i, err := newInterface(n, iface.SrcName, iface.DstPrefix, iface.DstName, opts...)
406 406
 		if err != nil {
407 407
 			return err
408 408
 		}
... ...
@@ -12,7 +12,7 @@ const (
12 12
 )
13 13
 
14 14
 type Iface struct {
15
-	SrcName, DstPrefix string
15
+	SrcName, DstPrefix, DstName string
16 16
 }
17 17
 
18 18
 // IfaceOption is a function option type to set interface options.
... ...
@@ -22,11 +22,11 @@ import (
22 22
 )
23 23
 
24 24
 const (
25
-	vethName1     = "wierdlongname1"
26
-	vethName2     = "wierdlongname2"
27
-	vethName3     = "wierdlongname3"
28
-	vethName4     = "wierdlongname4"
29
-	sboxIfaceName = "containername"
25
+	vethName1       = "wierdlongname1"
26
+	vethName2       = "wierdlongname2"
27
+	vethName3       = "wierdlongname3"
28
+	vethName4       = "wierdlongname4"
29
+	sboxIfacePrefix = "containername"
30 30
 )
31 31
 
32 32
 func generateRandomName(prefix string, size int) (string, error) {
... ...
@@ -85,16 +85,16 @@ func newInfo(t *testing.T, hnd nlwrap.Handle) (*Namespace, error) {
85 85
 	// This is needed for cleanup on DeleteEndpoint()
86 86
 	intf1 := &Interface{
87 87
 		srcName:     vethName2,
88
-		dstName:     sboxIfaceName,
88
+		dstPrefix:   sboxIfacePrefix,
89 89
 		address:     addr,
90 90
 		addressIPv6: addrv6,
91 91
 		routes:      []*net.IPNet{route},
92 92
 	}
93 93
 
94 94
 	intf2 := &Interface{
95
-		srcName: "testbridge",
96
-		dstName: sboxIfaceName,
97
-		bridge:  true,
95
+		srcName:   "testbridge",
96
+		dstPrefix: sboxIfacePrefix,
97
+		bridge:    true,
98 98
 	}
99 99
 
100 100
 	err = hnd.LinkAdd(&netlink.Veth{
... ...
@@ -106,9 +106,9 @@ func newInfo(t *testing.T, hnd nlwrap.Handle) (*Namespace, error) {
106 106
 	}
107 107
 
108 108
 	intf3 := &Interface{
109
-		srcName: vethName4,
110
-		dstName: sboxIfaceName,
111
-		master:  "testbridge",
109
+		srcName:   vethName4,
110
+		dstPrefix: sboxIfacePrefix,
111
+		master:    "testbridge",
112 112
 	}
113 113
 
114 114
 	return &Namespace{
... ...
@@ -132,10 +132,10 @@ func verifySandbox(t *testing.T, ns *Namespace, ifaceSuffixes []string) {
132 132
 	defer nh.Close()
133 133
 
134 134
 	for _, suffix := range ifaceSuffixes {
135
-		_, err = nh.LinkByName(sboxIfaceName + suffix)
135
+		_, err = nh.LinkByName(sboxIfacePrefix + suffix)
136 136
 		if err != nil {
137 137
 			t.Fatalf("Could not find the interface %s inside the sandbox: %v",
138
-				sboxIfaceName+suffix, err)
138
+				sboxIfacePrefix+suffix, err)
139 139
 		}
140 140
 	}
141 141
 }
... ...
@@ -381,7 +381,7 @@ func TestSandboxCreate(t *testing.T) {
381 381
 	}
382 382
 
383 383
 	for _, i := range tbox.Interfaces() {
384
-		err = s.AddInterface(context.Background(), i.SrcName(), i.DstName(),
384
+		err = s.AddInterface(context.Background(), i.SrcName(), i.dstPrefix, i.DstName(),
385 385
 			WithIsBridge(i.Bridge()),
386 386
 			WithIPv4Address(i.Address()),
387 387
 			WithIPv6Address(i.AddressIPv6()),
... ...
@@ -480,7 +480,7 @@ func TestAddRemoveInterface(t *testing.T) {
480 480
 	}
481 481
 
482 482
 	for _, i := range tbox.Interfaces() {
483
-		err = s.AddInterface(context.Background(), i.SrcName(), i.DstName(),
483
+		err = s.AddInterface(context.Background(), i.SrcName(), i.dstPrefix, i.DstName(),
484 484
 			WithIsBridge(i.Bridge()),
485 485
 			WithIPv4Address(i.Address()),
486 486
 			WithIPv6Address(i.AddressIPv6()),
... ...
@@ -501,7 +501,7 @@ func TestAddRemoveInterface(t *testing.T) {
501 501
 	verifySandbox(t, s, []string{"1", "2"})
502 502
 
503 503
 	i := tbox.Interfaces()[0]
504
-	err = s.AddInterface(context.Background(), i.SrcName(), i.DstName(),
504
+	err = s.AddInterface(context.Background(), i.SrcName(), i.dstPrefix, i.DstName(),
505 505
 		WithIsBridge(i.Bridge()),
506 506
 		WithIPv4Address(i.Address()),
507 507
 		WithIPv6Address(i.AddressIPv6()),
... ...
@@ -286,7 +286,8 @@ func (sb *Sandbox) restoreOslSandbox() error {
286 286
 		if len(i.llAddrs) != 0 {
287 287
 			ifaceOptions = append(ifaceOptions, osl.WithLinkLocalAddresses(i.llAddrs))
288 288
 		}
289
-		interfaces[osl.Iface{SrcName: i.srcName, DstPrefix: i.dstPrefix}] = ifaceOptions
289
+		iface := osl.Iface{SrcName: i.srcName, DstPrefix: i.dstPrefix, DstName: i.dstName}
290
+		interfaces[iface] = ifaceOptions
290 291
 		if joinInfo != nil {
291 292
 			routes = append(routes, joinInfo.StaticRoutes...)
292 293
 		}
... ...
@@ -362,7 +363,7 @@ func (sb *Sandbox) populateNetworkResources(ctx context.Context, ep *Endpoint) e
362 362
 		}
363 363
 		ifaceOptions = append(ifaceOptions, osl.WithCreatedInContainer(i.createdInContainer))
364 364
 
365
-		if err := sb.osSbox.AddInterface(ctx, i.srcName, i.dstPrefix, ifaceOptions...); err != nil {
365
+		if err := sb.osSbox.AddInterface(ctx, i.srcName, i.dstPrefix, i.dstName, ifaceOptions...); err != nil {
366 366
 			return fmt.Errorf("failed to add interface %s to sandbox: %v", i.srcName, err)
367 367
 		}
368 368