Skip to content

Commit

Permalink
enable ipv6 network configuration options
Browse files Browse the repository at this point in the history
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
07e3f1b.

Signed-off-by: Antonio Ojea <[email protected]>
  • Loading branch information
Antonio Ojea committed Nov 10, 2020
1 parent a64c6dc commit e7a72d7
Show file tree
Hide file tree
Showing 9 changed files with 373 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 e7a72d7

Please sign in to comment.