Skip to content

Commit

Permalink
Merge pull request moby#48299 from robmry/v6only/macvlan_ipvlan
Browse files Browse the repository at this point in the history
IPv6 only: macvlan and ipvlan drivers
  • Loading branch information
robmry authored Sep 13, 2024
2 parents e33f485 + 771377f commit 7156bfa
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 58 deletions.
122 changes: 106 additions & 16 deletions integration/network/ipvlan/ipvlan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func TestDockerNetworkIpvlan(t *testing.T) {
name: "L3Addressing",
test: testIpvlanL3Addressing,
}, {
name: "NoIPv6",
test: testIpvlanNoIPv6,
name: "IpvlanExperimentalV4Only",
test: testIpvlanExperimentalV4Only,
},
} {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -443,26 +443,116 @@ func testIpvlanL3Addressing(t *testing.T, ctx context.Context, client dclient.AP
assert.Check(t, is.Contains(result.Combined(), "default dev eth0"))
}

// Check that '--ipv4=false' is only allowed with '--experimental'.
// (Remember to remove `--experimental' from TestMacvlanIPAM when it's
// no longer needed, and maybe use a single daemon for all of its tests.)
func testIpvlanExperimentalV4Only(t *testing.T, ctx context.Context, client dclient.APIClient) {
_, err := net.Create(ctx, client, "testnet",
net.WithIPvlan("", "l3"),
net.WithIPv4(false),
)
defer client.NetworkRemove(ctx, "testnet")
assert.Assert(t, is.ErrorContains(err, "IPv4 can only be disabled if experimental features are enabled"))
}

// Check that an ipvlan interface with '--ipv6=false' doesn't get kernel-assigned
// IPv6 addresses, but the loopback interface does still have an IPv6 address ('::1').
func testIpvlanNoIPv6(t *testing.T, ctx context.Context, client dclient.APIClient) {
const netName = "ipvlannet"
net.CreateNoError(ctx, t, client, netName, net.WithIPvlan("", "l3"))
assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
// Also check that with '--ipv4=false', there's no IPAM-assigned IPv4 address.
func TestIpvlanIPAM(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")

id := container.Run(ctx, t, client, container.WithNetworkMode(netName))
ctx := testutil.StartSpan(baseContext, t)

loRes := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "lo"})
assert.Check(t, is.Contains(loRes.Combined(), " inet "))
assert.Check(t, is.Contains(loRes.Combined(), " inet6 "))
testcases := []struct {
name string
apiVersion string
enableIPv4 bool
enableIPv6 bool
expIPv4 bool
}{
{
name: "dual stack",
enableIPv4: true,
enableIPv6: true,
},
{
name: "v4 only",
enableIPv4: true,
},
{
name: "v6 only",
enableIPv6: true,
},
{
name: "no ipam",
},
{
name: "enableIPv4 ignored",
apiVersion: "1.46",
enableIPv4: false,
expIPv4: true,
},
}

eth0Res := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "eth0"})
assert.Check(t, is.Contains(eth0Res.Combined(), " inet "))
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "),
"result.Combined(): %s", eth0Res.Combined())
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)

sysctlRes := container.ExecT(ctx, t, client, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"})
assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1"))
var daemonOpts []daemon.Option
if !tc.enableIPv4 {
daemonOpts = append(daemonOpts, daemon.WithExperimental())
}
d := daemon.New(t, daemonOpts...)
d.StartWithBusybox(ctx, t)
t.Cleanup(func() { d.Stop(t) })
c := d.NewClientT(t, dclient.WithVersion(tc.apiVersion))

netOpts := []func(*network.CreateOptions){
net.WithIPvlan("", "l3"),
net.WithIPv4(tc.enableIPv4),
}
if tc.enableIPv6 {
netOpts = append(netOpts, net.WithIPv6())
}

const netName = "ipvlannet"
net.CreateNoError(ctx, t, c, netName, netOpts...)
defer c.NetworkRemove(ctx, netName)
assert.Check(t, n.IsNetworkAvailable(ctx, c, netName))

id := container.Run(ctx, t, c, container.WithNetworkMode(netName))
defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})

loRes := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "lo"})
assert.Check(t, is.Contains(loRes.Combined(), " inet "))
assert.Check(t, is.Contains(loRes.Combined(), " inet6 "))

eth0Res := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "eth0"})
if tc.enableIPv4 || tc.expIPv4 {
assert.Check(t, is.Contains(eth0Res.Combined(), " inet "),
"Expected IPv4 in: %s", eth0Res.Combined())
} else {
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet "),
"Expected no IPv4 in: %s", eth0Res.Combined())
}
if tc.enableIPv6 {
assert.Check(t, is.Contains(eth0Res.Combined(), " inet6 "),
"Expected IPv6 in: %s", eth0Res.Combined())
} else {
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "),
"Expected no IPv6 in: %s", eth0Res.Combined())
}

sysctlRes := container.ExecT(ctx, t, c, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"})
expDisableIPv6 := "1"
if tc.enableIPv6 {
expDisableIPv6 = "0"
}
assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), expDisableIPv6))
})
}
}

// TestIPVlanDNS checks whether DNS is forwarded, for combinations of l2/l3 mode,
Expand Down
125 changes: 106 additions & 19 deletions integration/network/macvlan/macvlan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ func TestDockerNetworkMacvlan(t *testing.T) {
name: "Addressing",
test: testMacvlanAddressing,
}, {
name: "NoIPv6",
test: testMacvlanNoIPv6,
name: "MacvlanExperimentalV4Only",
test: testMacvlanExperimentalV4Only,
},
} {
tc := tc
Expand Down Expand Up @@ -440,30 +440,117 @@ func testMacvlanAddressing(t *testing.T, ctx context.Context, client client.APIC
assert.Check(t, strings.Contains(result.Combined(), "default via 2001:db8:abca::254 dev eth0"))
}

// Check that '--ipv4=false' is only allowed with '--experimental'.
// (Remember to remove `--experimental' from TestMacvlanIPAM when it's
// no longer needed, and maybe use a single daemon for all of its tests.)
func testMacvlanExperimentalV4Only(t *testing.T, ctx context.Context, client client.APIClient) {
_, err := net.Create(ctx, client, "testnet",
net.WithMacvlan(""),
net.WithIPv4(false),
)
defer client.NetworkRemove(ctx, "testnet")
assert.Assert(t, is.ErrorContains(err, "IPv4 can only be disabled if experimental features are enabled"))
}

// Check that a macvlan interface with '--ipv6=false' doesn't get kernel-assigned
// IPv6 addresses, but the loopback interface does still have an IPv6 address ('::1').
func testMacvlanNoIPv6(t *testing.T, ctx context.Context, client client.APIClient) {
const netName = "macvlannet"
// Also check that with '--ipv4=false', there's no IPAM-assigned IPv4 address.
func TestMacvlanIPAM(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")

net.CreateNoError(ctx, t, client, netName,
net.WithMacvlan(""),
net.WithOption("macvlan_mode", "bridge"),
)
assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
ctx := testutil.StartSpan(baseContext, t)

id := container.Run(ctx, t, client, container.WithNetworkMode(netName))
testcases := []struct {
name string
apiVersion string
enableIPv4 bool
enableIPv6 bool
expIPv4 bool
}{
{
name: "dual stack",
enableIPv4: true,
enableIPv6: true,
},
{
name: "v4 only",
enableIPv4: true,
},
{
name: "v6 only",
enableIPv6: true,
},
{
name: "no ipam",
},
{
name: "enableIPv4 ignored",
apiVersion: "1.46",
enableIPv4: false,
expIPv4: true,
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)

loRes := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "lo"})
assert.Check(t, is.Contains(loRes.Combined(), " inet "))
assert.Check(t, is.Contains(loRes.Combined(), " inet6 "))
var daemonOpts []daemon.Option
if !tc.enableIPv4 {
daemonOpts = append(daemonOpts, daemon.WithExperimental())
}
d := daemon.New(t, daemonOpts...)
d.StartWithBusybox(ctx, t)
t.Cleanup(func() { d.Stop(t) })
c := d.NewClientT(t, client.WithVersion(tc.apiVersion))

eth0Res := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "eth0"})
assert.Check(t, is.Contains(eth0Res.Combined(), " inet "))
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "),
"result.Combined(): %s", eth0Res.Combined())
netOpts := []func(*network.CreateOptions){
net.WithMacvlan(""),
net.WithOption("macvlan_mode", "bridge"),
net.WithIPv4(tc.enableIPv4),
}
if tc.enableIPv6 {
netOpts = append(netOpts, net.WithIPv6())
}

sysctlRes := container.ExecT(ctx, t, client, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"})
assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1"))
const netName = "macvlannet"
net.CreateNoError(ctx, t, c, netName, netOpts...)
defer c.NetworkRemove(ctx, netName)
assert.Check(t, n.IsNetworkAvailable(ctx, c, netName))

id := container.Run(ctx, t, c, container.WithNetworkMode(netName))
defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})

loRes := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "lo"})
assert.Check(t, is.Contains(loRes.Combined(), " inet "))
assert.Check(t, is.Contains(loRes.Combined(), " inet6 "))

eth0Res := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "eth0"})
if tc.enableIPv4 || tc.expIPv4 {
assert.Check(t, is.Contains(eth0Res.Combined(), " inet "),
"Expected IPv4 in: %s", eth0Res.Combined())
} else {
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet "),
"Expected no IPv4 in: %s", eth0Res.Combined())
}
if tc.enableIPv6 {
assert.Check(t, is.Contains(eth0Res.Combined(), " inet6 "),
"Expected IPv6 in: %s", eth0Res.Combined())
} else {
assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "),
"Expected no IPv6 in: %s", eth0Res.Combined())
}

sysctlRes := container.ExecT(ctx, t, c, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"})
expDisableIPv6 := "1"
if tc.enableIPv6 {
expDisableIPv6 = "0"
}
assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), expDisableIPv6))
})
}
}

// TestMACVlanDNS checks whether DNS is forwarded, with/without a parent
Expand Down
6 changes: 2 additions & 4 deletions libnetwork/drivers/ipvlan/ipvlan_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"

"github.com/containerd/log"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/libnetwork/driverapi"
"github.com/docker/docker/libnetwork/netlabel"
"github.com/docker/docker/libnetwork/ns"
Expand All @@ -20,7 +21,7 @@ func (d *driver) CreateEndpoint(ctx context.Context, nid, eid string, ifInfo dri
}
n, err := d.getNetwork(nid)
if err != nil {
return fmt.Errorf("network id %q not found", nid)
return errdefs.System(fmt.Errorf("network id %q not found", nid))
}
if ifInfo.MacAddress() != nil {
return fmt.Errorf("ipvlan interfaces do not support custom mac address assignment")
Expand All @@ -31,9 +32,6 @@ func (d *driver) CreateEndpoint(ctx context.Context, nid, eid string, ifInfo dri
addr: ifInfo.Address(),
addrv6: ifInfo.AddressIPv6(),
}
if ep.addr == nil {
return fmt.Errorf("create endpoint was not passed an IP address")
}
// disallow port mapping -p
if opt, ok := epOptions[netlabel.PortMap]; ok {
if _, ok := opt.([]types.PortBinding); ok {
Expand Down
22 changes: 14 additions & 8 deletions libnetwork/drivers/ipvlan/ipvlan_joinleave.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,17 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
case modeL3, modeL3S:
// disable gateway services to add a default gw using dev eth0 only
jinfo.DisableGatewayService()
defaultRoute, err := ifaceGateway(defaultV4RouteCidr)
if err != nil {
return err
}
if err := jinfo.AddStaticRoute(defaultRoute.Destination, defaultRoute.RouteType, defaultRoute.NextHop); err != nil {
return fmt.Errorf("failed to set an ipvlan l3/l3s mode ipv4 default gateway: %v", err)
if ep.addr != nil {
defaultRoute, err := ifaceGateway(defaultV4RouteCidr)
if err != nil {
return err
}
if err := jinfo.AddStaticRoute(defaultRoute.Destination, defaultRoute.RouteType, defaultRoute.NextHop); err != nil {
return fmt.Errorf("failed to set an ipvlan l3/l3s mode ipv4 default gateway: %v", err)
}
log.G(ctx).Debugf("Ipvlan Endpoint Joined with IPv4_Addr: %s, Ipvlan_Mode: %s, Parent: %s",
ep.addr.IP.String(), n.config.IpvlanMode, n.config.Parent)
}
log.G(ctx).Debugf("Ipvlan Endpoint Joined with IPv4_Addr: %s, Ipvlan_Mode: %s, Parent: %s",
ep.addr.IP.String(), n.config.IpvlanMode, n.config.Parent)
// If the endpoint has a v6 address, set a v6 default route
if ep.addrv6 != nil {
default6Route, err := ifaceGateway(defaultV6RouteCidr)
Expand Down Expand Up @@ -121,6 +123,10 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
log.G(ctx).Debugf("Ipvlan Endpoint Joined with IPv6_Addr: %s, Gateway: %s, Ipvlan_Mode: %s, Parent: %s",
ep.addrv6.IP.String(), v6gw.String(), n.config.IpvlanMode, n.config.Parent)
}
if len(n.config.Ipv4Subnets) == 0 && len(n.config.Ipv6Subnets) == 0 {
// With no addresses, don't need a gateway.
jinfo.DisableGatewayService()
}
}
} else {
if len(n.config.Ipv4Subnets) > 0 {
Expand Down
15 changes: 12 additions & 3 deletions libnetwork/drivers/ipvlan/ipvlan_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"

"github.com/containerd/log"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/libnetwork/driverapi"
"github.com/docker/docker/libnetwork/netlabel"
"github.com/docker/docker/libnetwork/ns"
Expand All @@ -26,9 +27,17 @@ func (d *driver) CreateNetwork(nid string, option map[string]interface{}, nInfo
return fmt.Errorf("kernel version failed to meet the minimum ipvlan kernel requirement of %d.%d, found %d.%d.%d",
ipvlanKernelVer, ipvlanMajorVer, kv.Kernel, kv.Major, kv.Minor)
}
// reject a null v4 network
if len(ipV4Data) == 0 || ipV4Data[0].Pool.String() == "0.0.0.0/0" {
return fmt.Errorf("ipv4 pool is empty")
// reject a null v4 network if ipv4 is required
if v, ok := option[netlabel.EnableIPv4]; ok && v.(bool) {
if len(ipV4Data) == 0 || ipV4Data[0].Pool.String() == "0.0.0.0/0" {
return errdefs.InvalidParameter(fmt.Errorf("ipv4 pool is empty"))
}
}
// reject a null v6 network if ipv6 is required
if v, ok := option[netlabel.EnableIPv6]; ok && v.(bool) {
if len(ipV6Data) == 0 || ipV6Data[0].Pool.String() == "::/0" {
return errdefs.InvalidParameter(fmt.Errorf("ipv6 pool is empty"))
}
}
// parse and validate the config and bind to networkConfiguration
config, err := parseNetworkOptions(nid, option)
Expand Down
Loading

0 comments on commit 7156bfa

Please sign in to comment.