From e7a72d72fd598b0de3c1049c91cd788440c08f2d Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sun, 25 Oct 2020 18:28:44 +0100 Subject: [PATCH 1/3] enable ipv6 network configuration options enable the ipv6 flag in podman network to be able to create dual-stack networks for containers. This is required to be compatible with docker, where --ipv6 really means dual stack. podman, unlike docker, support IPv6 only containers since 07e3f1bba9674c0cb93a0fa260930bfebbf75728. Signed-off-by: Antonio Ojea --- cmd/podman/networks/create.go | 3 +- .../markdown/podman-network-create.1.md | 11 ++ libpod/network/create.go | 138 ++++++++++++------ libpod/network/create_test.go | 131 +++++++++++++++++ libpod/network/files.go | 1 + libpod/network/netconflist.go | 14 +- libpod/network/netconflist_test.go | 70 +++++++++ pkg/domain/entities/network.go | 1 + test/e2e/network_create_test.go | 61 +++++++- 9 files changed, 373 insertions(+), 57 deletions(-) create mode 100644 libpod/network/create_test.go diff --git a/cmd/podman/networks/create.go b/cmd/podman/networks/create.go index 74646090d0..c06011ce9a 100644 --- a/cmd/podman/networks/create.go +++ b/cmd/podman/networks/create.go @@ -35,8 +35,7 @@ func networkCreateFlags(flags *pflag.FlagSet) { flags.StringVar(&networkCreateOptions.MacVLAN, "macvlan", "", "create a Macvlan connection based on this device") // TODO not supported yet // flags.StringVar(&networkCreateOptions.IPamDriver, "ipam-driver", "", "IP Address Management Driver") - // TODO enable when IPv6 is working - // flags.BoolVar(&networkCreateOptions.IPV6, "IPv6", false, "enable IPv6 networking") + flags.BoolVar(&networkCreateOptions.IPv6, "ipv6", false, "enable IPv6 networking") flags.IPNetVar(&networkCreateOptions.Subnet, "subnet", net.IPNet{}, "subnet in CIDR format") flags.BoolVar(&networkCreateOptions.DisableDNS, "disable-dns", false, "disable dns plugin") } diff --git a/docs/source/markdown/podman-network-create.1.md b/docs/source/markdown/podman-network-create.1.md index 45d9d9b0be..0a7ea05867 100644 --- a/docs/source/markdown/podman-network-create.1.md +++ b/docs/source/markdown/podman-network-create.1.md @@ -49,6 +49,10 @@ Macvlan connection. The subnet in CIDR notation. +**--ipv6** + +Enable IPv6 (Dual Stack) networking. You must pass a IPv6 subnet. The *subnet* option must be used with the *ipv6* option. + ## EXAMPLE Create a network with no options @@ -63,6 +67,13 @@ Create a network named *newnet* that uses *192.5.0.0/16* for its subnet. /etc/cni/net.d/newnet.conflist ``` +Create an IPv6 network named *newnetv6*, you must specify the subnet for this network, otherwise the command will fail. +For this example, we use *2001:db8::/64* for its subnet. +``` +# podman network create --subnet 2001:db8::/64 --ipv6 newnetv6 +/etc/cni/net.d/newnetv6.conflist +``` + Create a network named *newnet* that uses *192.168.33.0/24* and defines a gateway as *192.168.133.3* ``` # podman network create --subnet 192.168.33.0/24 --gateway 192.168.33.3 newnet diff --git a/libpod/network/create.go b/libpod/network/create.go index bf11631bfc..c11904ecfe 100644 --- a/libpod/network/create.go +++ b/libpod/network/create.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" ) +// Create the CNI network func Create(name string, options entities.NetworkCreateOptions, r *libpod.Runtime) (*entities.NetworkCreateReport, error) { var fileName string if err := isSupportedDriver(options.Driver); err != nil { @@ -41,60 +42,120 @@ func Create(name string, options entities.NetworkCreateOptions, r *libpod.Runtim return &entities.NetworkCreateReport{Filename: fileName}, nil } +// validateBridgeOptions validate the bridge networking options +func validateBridgeOptions(options entities.NetworkCreateOptions) error { + subnet := &options.Subnet + ipRange := &options.Range + gateway := options.Gateway + // if IPv6 is set an IPv6 subnet MUST be specified + if options.IPv6 && ((subnet.IP == nil) || (subnet.IP != nil && !IsIPv6(subnet.IP))) { + return errors.Errorf("ipv6 option requires an IPv6 --subnet to be provided") + } + // range and gateway depend on subnet + if subnet.IP == nil && (ipRange.IP != nil || gateway != nil) { + return errors.Errorf("every ip-range or gateway must have a corresponding subnet") + } + + // if a range is given, we need to ensure it is "in" the network range. + if ipRange.IP != nil { + firstIP, err := FirstIPInSubnet(ipRange) + if err != nil { + return errors.Wrapf(err, "failed to get first IP address from ip-range") + } + lastIP, err := LastIPInSubnet(ipRange) + if err != nil { + return errors.Wrapf(err, "failed to get last IP address from ip-range") + } + if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) { + return errors.Errorf("the ip range %s does not fall within the subnet range %s", ipRange.String(), subnet.String()) + } + } + + // if network is provided and if gateway is provided, make sure it is "in" network + if gateway != nil && !subnet.Contains(gateway) { + return errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String()) + } + + return nil + +} + // createBridge creates a CNI network func createBridge(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) { isGateway := true ipMasq := true - subnet := &options.Subnet - ipRange := options.Range runtimeConfig, err := r.GetConfig() if err != nil { return "", err } - // if range is provided, make sure it is "in" network - if subnet.IP != nil { - // if network is provided, does it conflict with existing CNI or live networks - err = ValidateUserNetworkIsAvailable(runtimeConfig, subnet) - } else { - // if no network is provided, figure out network - subnet, err = GetFreeNetwork(runtimeConfig) - } + + // validate options + err = validateBridgeOptions(options) if err != nil { return "", err } + + // For compatibility with the docker implementation: + // if IPv6 is enabled (it really means dual-stack) then an IPv6 subnet has to be provided, and one free network is allocated for IPv4 + // if IPv6 is not specified the subnet may be specified and can be either IPv4 or IPv6 (podman, unlike docker, allows IPv6 only networks) + // If not subnet is specified an IPv4 subnet will be allocated + subnet := &options.Subnet + ipRange := &options.Range gateway := options.Gateway - if gateway == nil { - // if no gateway is provided, provide it as first ip of network - gateway = CalcGatewayIP(subnet) - } - // if network is provided and if gateway is provided, make sure it is "in" network - if options.Subnet.IP != nil && options.Gateway != nil { - if !subnet.Contains(gateway) { - return "", errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String()) + var ipamRanges [][]IPAMLocalHostRangeConf + var routes []IPAMRoute + if subnet.IP != nil { + // if network is provided, does it conflict with existing CNI or live networks + err = ValidateUserNetworkIsAvailable(runtimeConfig, subnet) + if err != nil { + return "", err } - } - if options.Internal { - isGateway = false - ipMasq = false - } - - // if a range is given, we need to ensure it is "in" the network range. - if options.Range.IP != nil { - if options.Subnet.IP == nil { - return "", errors.New("you must define a subnet range to define an ip-range") + // obtain CNI subnet default route + defaultRoute, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP)) + if err != nil { + return "", err } - firstIP, err := FirstIPInSubnet(&options.Range) + routes = append(routes, defaultRoute) + // obtain CNI range + ipamRange, err := NewIPAMLocalHostRange(subnet, ipRange, gateway) if err != nil { return "", err } - lastIP, err := LastIPInSubnet(&options.Range) + ipamRanges = append(ipamRanges, ipamRange) + } + // if no network is provided or IPv6 flag used, figure out the IPv4 network + if options.IPv6 || len(routes) == 0 { + subnetV4, err := GetFreeNetwork(runtimeConfig) if err != nil { return "", err } - if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) { - return "", errors.Errorf("the ip range %s does not fall within the subnet range %s", options.Range.String(), subnet.String()) + // obtain IPv4 default route + defaultRoute, err := NewIPAMDefaultRoute(false) + if err != nil { + return "", err } + routes = append(routes, defaultRoute) + // the CNI bridge plugin does not need to set + // the range or gateway options explicitly + ipamRange, err := NewIPAMLocalHostRange(subnetV4, nil, nil) + if err != nil { + return "", err + } + ipamRanges = append(ipamRanges, ipamRange) + } + + // create CNI config + ipamConfig, err := NewIPAMHostLocalConf(routes, ipamRanges) + if err != nil { + return "", err } + + if options.Internal { + isGateway = false + ipMasq = false + } + + // obtain host bridge name bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig) if err != nil { return "", err @@ -113,20 +174,9 @@ func createBridge(r *libpod.Runtime, name string, options entities.NetworkCreate name = bridgeDeviceName } + // create CNI plugin configuration ncList := NewNcList(name, version.Current()) var plugins []CNIPlugins - var routes []IPAMRoute - - defaultRoute, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP)) - if err != nil { - return "", err - } - routes = append(routes, defaultRoute) - ipamConfig, err := NewIPAMHostLocalConf(subnet, routes, ipRange, gateway) - if err != nil { - return "", err - } - // TODO need to iron out the role of isDefaultGW and IPMasq bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) plugins = append(plugins, bridge) diff --git a/libpod/network/create_test.go b/libpod/network/create_test.go new file mode 100644 index 0000000000..16188e4976 --- /dev/null +++ b/libpod/network/create_test.go @@ -0,0 +1,131 @@ +package network + +import ( + "net" + "testing" + + "github.com/containers/podman/v2/pkg/domain/entities" +) + +func Test_validateBridgeOptions(t *testing.T) { + + tests := []struct { + name string + subnet net.IPNet + ipRange net.IPNet + gateway net.IP + isIPv6 bool + wantErr bool + }{ + { + name: "IPv4 subnet only", + subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + }, + { + name: "IPv4 subnet and range", + subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, + }, + { + name: "IPv4 subnet and gateway", + subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + gateway: net.ParseIP("192.168.0.10"), + }, + { + name: "IPv4 subnet, range and gateway", + subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, + gateway: net.ParseIP("192.168.0.10"), + }, + { + name: "IPv6 subnet only", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + }, + { + name: "IPv6 subnet and range", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + ipRange: net.IPNet{IP: net.ParseIP("2001:DB8:0:0:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, + isIPv6: true, + }, + { + name: "IPv6 subnet and gateway", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + gateway: net.ParseIP("2001:DB8::2"), + isIPv6: true, + }, + { + name: "IPv6 subnet, range and gateway", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + ipRange: net.IPNet{IP: net.ParseIP("2001:DB8:0:0:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, + gateway: net.ParseIP("2001:DB8::2"), + isIPv6: true, + }, + { + name: "IPv6 subnet, range and gateway without IPv6 option (PODMAN SUPPORTS IT UNLIKE DOCKEr)", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + ipRange: net.IPNet{IP: net.ParseIP("2001:DB8:0:0:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, + gateway: net.ParseIP("2001:DB8::2"), + isIPv6: false, + }, + { + name: "range provided but not subnet", + ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, + wantErr: true, + }, + { + name: "gateway provided but not subnet", + gateway: net.ParseIP("192.168.0.10"), + wantErr: true, + }, + { + name: "IPv4 subnet but IPv6 required", + subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, + gateway: net.ParseIP("192.168.0.10"), + isIPv6: true, + wantErr: true, + }, + { + name: "IPv6 required but IPv4 options used", + subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, + gateway: net.ParseIP("192.168.0.10"), + isIPv6: true, + wantErr: true, + }, + { + name: "IPv6 required but not subnet provided", + isIPv6: true, + wantErr: true, + }, + { + name: "range out of the subnet", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + ipRange: net.IPNet{IP: net.ParseIP("2001:1:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, + gateway: net.ParseIP("2001:DB8::2"), + isIPv6: true, + wantErr: true, + }, + { + name: "gateway out of the subnet", + subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + gateway: net.ParseIP("2001::2"), + isIPv6: true, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + options := entities.NetworkCreateOptions{ + Subnet: tt.subnet, + Range: tt.ipRange, + Gateway: tt.gateway, + IPv6: tt.isIPv6, + } + if err := validateBridgeOptions(options); (err != nil) != tt.wantErr { + t.Errorf("validateBridgeOptions() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/libpod/network/files.go b/libpod/network/files.go index a2090491f8..846e5c62da 100644 --- a/libpod/network/files.go +++ b/libpod/network/files.go @@ -14,6 +14,7 @@ import ( "github.com/pkg/errors" ) +// GetCNIConfDir get CNI configuration directory func GetCNIConfDir(configArg *config.Config) string { if len(configArg.Network.NetworkConfigDir) < 1 { dc, err := config.DefaultConfig() diff --git a/libpod/network/netconflist.go b/libpod/network/netconflist.go index 8187fdb393..b95980529a 100644 --- a/libpod/network/netconflist.go +++ b/libpod/network/netconflist.go @@ -42,8 +42,7 @@ func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, ipamCo } // NewIPAMHostLocalConf creates a new IPAMHostLocal configfuration -func NewIPAMHostLocalConf(subnet *net.IPNet, routes []IPAMRoute, ipRange net.IPNet, gw net.IP) (IPAMHostLocalConf, error) { - var ipamRanges [][]IPAMLocalHostRangeConf +func NewIPAMHostLocalConf(routes []IPAMRoute, ipamRanges [][]IPAMLocalHostRangeConf) (IPAMHostLocalConf, error) { ipamConf := IPAMHostLocalConf{ PluginType: "host-local", Routes: routes, @@ -51,22 +50,19 @@ func NewIPAMHostLocalConf(subnet *net.IPNet, routes []IPAMRoute, ipRange net.IPN //ResolveConf: "", //DataDir: "" } - IPAMRange, err := newIPAMLocalHostRange(subnet, &ipRange, &gw) - if err != nil { - return ipamConf, err - } - ipamRanges = append(ipamRanges, IPAMRange) + ipamConf.Ranges = ipamRanges return ipamConf, nil } -func newIPAMLocalHostRange(subnet *net.IPNet, ipRange *net.IPNet, gw *net.IP) ([]IPAMLocalHostRangeConf, error) { //nolint:interfacer +// NewIPAMLocalHostRange create a new IPAM range +func NewIPAMLocalHostRange(subnet *net.IPNet, ipRange *net.IPNet, gw net.IP) ([]IPAMLocalHostRangeConf, error) { //nolint:interfacer var ranges []IPAMLocalHostRangeConf hostRange := IPAMLocalHostRangeConf{ Subnet: subnet.String(), } // an user provided a range, we add it here - if ipRange.IP != nil { + if ipRange != nil && ipRange.IP != nil { first, err := FirstIPInSubnet(ipRange) if err != nil { return nil, err diff --git a/libpod/network/netconflist_test.go b/libpod/network/netconflist_test.go index 5893bf985b..6bf1a97778 100644 --- a/libpod/network/netconflist_test.go +++ b/libpod/network/netconflist_test.go @@ -1,6 +1,7 @@ package network import ( + "net" "reflect" "testing" ) @@ -36,3 +37,72 @@ func TestNewIPAMDefaultRoute(t *testing.T) { }) } } + +func TestNewIPAMLocalHostRange(t *testing.T) { + tests := []struct { + name string + subnet *net.IPNet + ipRange *net.IPNet + gw net.IP + want []IPAMLocalHostRangeConf + }{ + { + name: "IPv4 subnet", + subnet: &net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + want: []IPAMLocalHostRangeConf{ + { + Subnet: "192.168.0.0/24", + }, + }, + }, + { + name: "IPv4 subnet, range and gateway", + subnet: &net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, + ipRange: &net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, + gw: net.ParseIP("192.168.0.10"), + want: []IPAMLocalHostRangeConf{ + { + Subnet: "192.168.0.0/24", + RangeStart: "192.168.0.129", + RangeEnd: "192.168.0.255", + Gateway: "192.168.0.10", + }, + }, + }, + { + name: "IPv6 subnet", + subnet: &net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + want: []IPAMLocalHostRangeConf{ + { + Subnet: "2001:db8::/48", + }, + }, + }, + { + name: "IPv6 subnet, range and gateway", + subnet: &net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, + ipRange: &net.IPNet{IP: net.ParseIP("2001:DB8:1:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, + gw: net.ParseIP("2001:DB8::2"), + want: []IPAMLocalHostRangeConf{ + { + Subnet: "2001:db8::/48", + RangeStart: "2001:db8:1:1::1", + RangeEnd: "2001:db8:1:1:ffff:ffff:ffff:ffff", + Gateway: "2001:db8::2", + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := NewIPAMLocalHostRange(tt.subnet, tt.ipRange, tt.gw) + if err != nil { + t.Errorf("no error expected: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewIPAMLocalHostRange() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go index 0bab672a77..3cc9705316 100644 --- a/pkg/domain/entities/network.go +++ b/pkg/domain/entities/network.go @@ -42,6 +42,7 @@ type NetworkCreateOptions struct { MacVLAN string Range net.IPNet Subnet net.IPNet + IPv6 bool } // NetworkCreateReport describes a created network for the cli diff --git a/test/e2e/network_create_test.go b/test/e2e/network_create_test.go index 21f03901b2..5391b29405 100644 --- a/test/e2e/network_create_test.go +++ b/test/e2e/network_create_test.go @@ -177,8 +177,6 @@ var _ = Describe("Podman network create", func() { }) It("podman network create with name and IPv6 subnet", func() { - SkipIfRootless("FIXME I believe this should work in rootlessmode") - var ( results []network.NcList ) @@ -218,12 +216,71 @@ var _ = Describe("Podman network create", func() { Expect(subnet.Contains(containerIP)).To(BeTrue()) }) + It("podman network create with name and IPv6 flag (dual-stack)", func() { + var ( + results []network.NcList + ) + nc := podmanTest.Podman([]string{"network", "create", "--subnet", "fd00:4:3:2:1::/64", "--ipv6", "newDualStacknetwork"}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + + defer podmanTest.removeCNINetwork("newDualStacknetwork") + + // Inspect the network configuration + inspect := podmanTest.Podman([]string{"network", "inspect", "newDualStacknetwork"}) + inspect.WaitWithDefaultTimeout() + + // JSON the network configuration into something usable + err := json.Unmarshal([]byte(inspect.OutputToString()), &results) + Expect(err).To(BeNil()) + result := results[0] + Expect(result["name"]).To(Equal("newDualStacknetwork")) + + // JSON the bridge info + bridgePlugin, err := genericPluginsToBridge(result["plugins"], "bridge") + Expect(err).To(BeNil()) + Expect(bridgePlugin.IPAM.Routes[0].Dest).To(Equal("::/0")) + Expect(bridgePlugin.IPAM.Routes[1].Dest).To(Equal("0.0.0.0/0")) + + // Once a container executes a new network, the nic will be created. We should clean those up + // best we can + defer removeNetworkDevice(bridgePlugin.BrName) + + try := podmanTest.Podman([]string{"run", "-it", "--rm", "--network", "newDualStacknetwork", ALPINE, "sh", "-c", "ip addr show eth0 | grep global | awk ' /inet6 / {print $2}'"}) + try.WaitWithDefaultTimeout() + + _, subnet, err := net.ParseCIDR("fd00:4:3:2:1::/64") + Expect(err).To(BeNil()) + containerIP, _, err := net.ParseCIDR(try.OutputToString()) + Expect(err).To(BeNil()) + // Ensure that the IP the container got is within the subnet the user asked for + Expect(subnet.Contains(containerIP)).To(BeTrue()) + // verify the container has an IPv4 address too (the IPv4 subnet is autogenerated) + try = podmanTest.Podman([]string{"run", "-it", "--rm", "--network", "newDualStacknetwork", ALPINE, "sh", "-c", "ip addr show eth0 | awk ' /inet / {print $2}'"}) + try.WaitWithDefaultTimeout() + containerIP, _, err = net.ParseCIDR(try.OutputToString()) + Expect(err).To(BeNil()) + Expect(containerIP.To4()).To(Not(BeNil())) + }) + It("podman network create with invalid subnet", func() { nc := podmanTest.Podman([]string{"network", "create", "--subnet", "10.11.12.0/17000", "fail"}) nc.WaitWithDefaultTimeout() Expect(nc).To(ExitWithError()) }) + It("podman network create with ipv4 subnet and ipv6 flag", func() { + nc := podmanTest.Podman([]string{"network", "create", "--subnet", "10.11.12.0/24", "--ipv6", "fail"}) + nc.WaitWithDefaultTimeout() + Expect(nc).To(ExitWithError()) + }) + + It("podman network create with empty subnet and ipv6 flag", func() { + nc := podmanTest.Podman([]string{"network", "create", "--ipv6", "fail"}) + nc.WaitWithDefaultTimeout() + Expect(nc).To(ExitWithError()) + }) + It("podman network create with invalid IP", func() { nc := podmanTest.Podman([]string{"network", "create", "--subnet", "10.11.0/17000", "fail"}) nc.WaitWithDefaultTimeout() From 98d770747e13f4286958a7d68f1c96223ca96c98 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Mon, 9 Nov 2020 23:35:04 +0100 Subject: [PATCH 2/3] add e2e test for network with same subnet add e2e test that checks that is not possible to create different networks with the same subnet, in IPv6 neither in IPv4 Signed-off-by: Antonio Ojea --- test/e2e/network_create_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/e2e/network_create_test.go b/test/e2e/network_create_test.go index 5391b29405..38e0c0bbfe 100644 --- a/test/e2e/network_create_test.go +++ b/test/e2e/network_create_test.go @@ -304,6 +304,29 @@ var _ = Describe("Podman network create", func() { Expect(ncFail).To(ExitWithError()) }) + It("podman network create two networks with same subnet should fail", func() { + nc := podmanTest.Podman([]string{"network", "create", "--subnet", "10.11.13.0/24", "subnet1"}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork("subnet1") + + ncFail := podmanTest.Podman([]string{"network", "create", "--subnet", "10.11.13.0/24", "subnet2"}) + ncFail.WaitWithDefaultTimeout() + Expect(ncFail).To(ExitWithError()) + }) + + It("podman network create two IPv6 networks with same subnet should fail", func() { + SkipIfRootless("FIXME It needs the ip6tables modules loaded") + nc := podmanTest.Podman([]string{"network", "create", "--subnet", "fd00:4:4:4:4::/64", "--ipv6", "subnet1v6"}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork("subnet1v6") + + ncFail := podmanTest.Podman([]string{"network", "create", "--subnet", "fd00:4:4:4:4::/64", "--ipv6", "subnet2v6"}) + ncFail.WaitWithDefaultTimeout() + Expect(ncFail).To(ExitWithError()) + }) + It("podman network create with invalid network name", func() { nc := podmanTest.Podman([]string{"network", "create", "foo "}) nc.WaitWithDefaultTimeout() From aabf28a16825aec643206d5e3084940ea48b9c99 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 10 Nov 2020 08:30:32 +0100 Subject: [PATCH 3/3] skip ipv6 e2e tests on rootless The IPv6 e2e tests on the CI for rootles mode fails because it needs the ip6tables modules loaded. Example error: stdout="", stderr="failed to list chains: running [/sbin/ip6tables -t nat -S --wait]: exit status 3: modprobe: can't change directory to '/lib/modules': No such file or directory\nip6tables v1.8.4 (legacy): can't initialize ip6tables table `nat': Table does not exist (do you need to insmod?)\nPerhaps ip6tables or your kernel needs to be upgraded.\n\n" Signed-off-by: Antonio Ojea --- test/e2e/network_create_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/network_create_test.go b/test/e2e/network_create_test.go index 38e0c0bbfe..cb997d10ae 100644 --- a/test/e2e/network_create_test.go +++ b/test/e2e/network_create_test.go @@ -177,6 +177,7 @@ var _ = Describe("Podman network create", func() { }) It("podman network create with name and IPv6 subnet", func() { + SkipIfRootless("FIXME It needs the ip6tables modules loaded") var ( results []network.NcList ) @@ -217,6 +218,7 @@ var _ = Describe("Podman network create", func() { }) It("podman network create with name and IPv6 flag (dual-stack)", func() { + SkipIfRootless("FIXME It needs the ip6tables modules loaded") var ( results []network.NcList )