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()