Skip to content

Commit

Permalink
Merge pull request #8143 from aojea/dual
Browse files Browse the repository at this point in the history
    enable ipv6 networks
  • Loading branch information
openshift-merge-robot authored Nov 10, 2020
2 parents da95fb4 + aabf28a commit 20b26b5
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 57 deletions.
3 changes: 1 addition & 2 deletions cmd/podman/networks/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
11 changes: 11 additions & 0 deletions docs/source/markdown/podman-network-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
138 changes: 94 additions & 44 deletions libpod/network/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand Down
131 changes: 131 additions & 0 deletions libpod/network/create_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions libpod/network/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 5 additions & 9 deletions libpod/network/netconflist.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,27 @@ 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,
// Possible future support ? Leaving for clues
//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
Expand Down
Loading

0 comments on commit 20b26b5

Please sign in to comment.