Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common/libnetwork/internal/util/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"go.podman.io/common/pkg/config"
)

func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet, subnetPools []config.SubnetPool, checkBridgeConflict bool) error {
func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet, subnetPools []config.SubnetPool, subnetPoolsV6 []config.SubnetPool, checkBridgeConflict bool) error {
if network.NetworkInterface != "" {
if checkBridgeConflict {
bridges := GetBridgeInterfaceNames(n)
Expand Down Expand Up @@ -60,7 +60,7 @@ func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet,
network.Subnets = append(network.Subnets, *freeSubnet)
}
if !ipv6 {
freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks)
freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks, subnetPoolsV6)
if err != nil {
return err
}
Expand Down
70 changes: 70 additions & 0 deletions common/libnetwork/internal/util/ip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"net"
"reflect"
"testing"

"go.podman.io/common/libnetwork/types"
"go.podman.io/common/pkg/config"
)

func parseCIDR(n string) *net.IPNet {
Expand Down Expand Up @@ -52,6 +55,73 @@ func TestNextSubnet(t *testing.T) {
}
}

func parseV6Pool(base string, size int) config.SubnetPool {
_, parsedNet, _ := net.ParseCIDR(base)
return config.SubnetPool{
Base: &types.IPNet{IPNet: *parsedNet},
Size: size,
}
}

func TestGetFreeIPv6SubnetFromPools(t *testing.T) {
t.Run("allocate from single pool", func(t *testing.T) {
pools := []config.SubnetPool{parseV6Pool("fd00::/48", 64)}
subnet, err := GetFreeIPv6NetworkSubnet(nil, pools)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ones, bits := subnet.Subnet.Mask.Size()
if ones != 64 || bits != 128 {
t.Errorf("expected /64, got /%d (bits=%d)", ones, bits)
}
expected := parseCIDR("fd00::/64")
if subnet.Subnet.String() != expected.String() {
t.Errorf("expected %s, got %s", expected, subnet.Subnet)
}
})

t.Run("skip used subnets", func(t *testing.T) {
pools := []config.SubnetPool{parseV6Pool("fd00::/48", 64)}
used := []*net.IPNet{parseCIDR("fd00::/64")}
subnet, err := GetFreeIPv6NetworkSubnet(used, pools)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := parseCIDR("fd00:0:0:1::/64")
if subnet.Subnet.String() != expected.String() {
t.Errorf("expected %s, got %s", expected, subnet.Subnet)
}
})

t.Run("fall through to second pool", func(t *testing.T) {
pool1 := parseV6Pool("fd00:1::/112", 112)
pool2 := parseV6Pool("fd00:2::/48", 64)
used := []*net.IPNet{parseCIDR("fd00:1::/112")}
subnet, err := GetFreeIPv6NetworkSubnet(used, []config.SubnetPool{pool1, pool2})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := parseCIDR("fd00:2::/64")
if subnet.Subnet.String() != expected.String() {
t.Errorf("expected %s, got %s", expected, subnet.Subnet)
}
})

t.Run("fallback to random when no pools", func(t *testing.T) {
subnet, err := GetFreeIPv6NetworkSubnet(nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if subnet.Subnet.IP.To4() != nil {
t.Errorf("expected ipv6 address, got %s", subnet.Subnet.IP)
}
ones, _ := subnet.Subnet.Mask.Size()
if ones != 64 {
t.Errorf("expected /64, got /%d", ones)
}
})
}

func TestGetRandomIPv6Subnet(t *testing.T) {
for i := range 1000 {
t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) {
Expand Down
35 changes: 34 additions & 1 deletion common/libnetwork/internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ func GetFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet, subnetPools []config.Su
}

// GetFreeIPv6NetworkSubnet returns a unused ipv6 subnet.
func GetFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) {
// When subnetPools is non-empty, subnets are allocated sequentially from the pools.
// Otherwise, subnets are generated randomly per RFC 4193.
func GetFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet, subnetPools []config.SubnetPool) (*types.Subnet, error) {
if len(subnetPools) > 0 {
return getFreeIPv6SubnetFromPools(usedNetworks, subnetPools)
}
// FIXME: Is 10000 fine as limit? We should prevent an endless loop.
for range 10000 {
// RFC4193: Choose the ipv6 subnet random and NOT sequentially.
Expand All @@ -128,6 +133,34 @@ func GetFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error)
return nil, errors.New("failed to get random ipv6 subnet")
}

func getFreeIPv6SubnetFromPools(usedNetworks []*net.IPNet, subnetPools []config.SubnetPool) (*types.Subnet, error) {
var err error
for _, pool := range subnetPools {
netIP := make(net.IP, net.IPv6len)
copy(netIP, pool.Base.IP)
network := &net.IPNet{
IP: netIP,
Mask: net.CIDRMask(pool.Size, 128),
}
for pool.Base.Contains(network.IP) {
if !NetworkIntersectsWithNetworks(network, usedNetworks) {
logrus.Debugf("found free ipv6 network subnet %s", network.String())
return &types.Subnet{
Subnet: types.IPNet{IPNet: *network},
}, nil
}
network, err = NextSubnet(network)
if err != nil {
break
}
}
}
if err != nil {
return nil, err
}
return nil, errors.New("could not find free ipv6 subnet from subnet pools")
}

// MapDockerBridgeDriverOptions docker driver network options to podman network options.
func MapDockerBridgeDriverOptions(n *types.Network) {
// validate the given options
Expand Down
2 changes: 2 additions & 0 deletions common/libnetwork/netavark/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func (n *netavarkNetwork) networkCreate(newNetwork *types.Network, defaultNet bo

type ConfigOpts struct {
SubnetPools []config.SubnetPool `json:"subnet_pools"`
SubnetPoolsV6 []config.SubnetPool `json:"subnet_pools_v6,omitempty"`
DefaultInterfaceName string `json:"default_interface_name"`
CheckUsedSubnets bool `json:"check_used_subnets"`
}
Expand All @@ -190,6 +191,7 @@ func (n *netavarkNetwork) networkCreate(newNetwork *types.Network, defaultNet bo
},
Options: ConfigOpts{
SubnetPools: n.defaultsubnetPools,
SubnetPoolsV6: n.defaultSubnetPoolsV6,
DefaultInterfaceName: n.DefaultInterfaceName(),
CheckUsedSubnets: !defaultNet,
},
Expand Down
39 changes: 33 additions & 6 deletions common/libnetwork/netavark/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ type netavarkNetwork struct {
// defaultsubnetPools contains the subnets which must be used to allocate a free subnet by network create
defaultsubnetPools []config.SubnetPool

// defaultSubnetV6 is the default ipv6 subnet for the default network (may be zero value).
defaultSubnetV6 types.IPNet

// defaultSubnetPoolsV6 contains ipv6 subnets for allocating free subnets by network create
defaultSubnetPoolsV6 []config.SubnetPool

// dnsBindPort is set the port to pass to netavark for aardvark
dnsBindPort uint16

Expand Down Expand Up @@ -150,6 +156,16 @@ func NewNetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
defaultSubnetPools = config.DefaultSubnetPools
}

var defaultSubnetV6 types.IPNet
if v6 := conf.Config.Network.DefaultSubnetV6; v6 != "" {
defaultSubnetV6, err = types.ParseCIDR(v6)
if err != nil {
return nil, fmt.Errorf("failed to parse default ipv6 subnet: %w", err)
}
}

defaultSubnetPoolsV6 := conf.Config.Network.DefaultSubnetPoolsV6

n := &netavarkNetwork{
networkConfigDir: conf.NetworkConfigDir,
networkRunDir: conf.NetworkRunDir,
Expand All @@ -161,6 +177,8 @@ func NewNetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
defaultNetwork: defaultNetworkName,
defaultSubnet: defaultNet,
defaultsubnetPools: defaultSubnetPools,
defaultSubnetV6: defaultSubnetV6,
defaultSubnetPoolsV6: defaultSubnetPoolsV6,
dnsBindPort: conf.Config.Network.DNSBindPort,
pluginDirs: conf.Config.Network.NetavarkPluginDirs.Get(),
lock: lock,
Expand Down Expand Up @@ -303,16 +321,25 @@ func parseNetwork(network *types.Network) error {
}

func (n *netavarkNetwork) createDefaultNetwork() (*types.Network, error) {
subnets := []types.Subnet{
{Subnet: n.defaultSubnet},
}

ipv6Enabled := false
if n.defaultSubnetV6.IP != nil {
subnets = append(subnets, types.Subnet{Subnet: n.defaultSubnetV6})
ipv6Enabled = true
}

network := &types.Network{
Name: n.defaultNetwork,
NetworkInterface: defaultBridgeName + "0",
// Important do not change this ID
ID: "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
Driver: types.BridgeNetworkDriver,
Created: time.Now(),
Subnets: []types.Subnet{
{Subnet: n.defaultSubnet},
},
ID: "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
Driver: types.BridgeNetworkDriver,
Created: time.Now(),
Subnets: subnets,
IPv6Enabled: ipv6Enabled,
IPAMOptions: map[string]string{
"driver": types.HostLocalIPAMDriver,
},
Expand Down
26 changes: 24 additions & 2 deletions common/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,10 +604,19 @@ type NetworkConfig struct {
// DefaultSubnetPools is a list of subnets and size which are used to
// allocate subnets automatically for podman network create.
// It will iterate through the list and will pick the first free subnet
// with the given size. This is only used for ipv4 subnets, ipv6 subnets
// are always assigned randomly.
// with the given size. This is only used for ipv4 subnets.
DefaultSubnetPools []SubnetPool `toml:"default_subnet_pools,omitempty"`

// DefaultSubnetV6 is the IPv6 subnet for the default network.
// If empty, no IPv6 subnet is added to the default network.
// Must be a valid IPv6 CIDR block.
DefaultSubnetV6 string `toml:"default_subnet_v6,omitempty"`

// DefaultSubnetPoolsV6 is a list of IPv6 subnets and prefix lengths used to
// allocate subnets automatically for podman network create with --ipv6.
// If empty, IPv6 subnets are assigned randomly from fd00::/8 (RFC 4193).
DefaultSubnetPoolsV6 []SubnetPool `toml:"default_subnet_pools_v6,omitempty"`

// DefaultRootlessNetworkCmd is used to set the default rootless network
// program, either "slirp4nents" (default) or "pasta".
DefaultRootlessNetworkCmd string `toml:"default_rootless_network_cmd,omitempty"`
Expand Down Expand Up @@ -948,6 +957,19 @@ func (c *NetworkConfig) Validate() error {
}
}

for _, pool := range c.DefaultSubnetPoolsV6 {
if pool.Base.IP.To4() != nil {
return fmt.Errorf("invalid ipv6 subnet pool ip %q, must be ipv6", pool.Base.IP)
}
ones, _ := pool.Base.IPNet.Mask.Size()
if ones > pool.Size {
return fmt.Errorf("invalid ipv6 subnet pool, size is bigger than subnet %q", &pool.Base.IPNet)
}
if pool.Size > 128 {
return errors.New("invalid ipv6 subnet pool size, must be between 0-128")
}
}

return nil
}

Expand Down
55 changes: 55 additions & 0 deletions common/pkg/config/config_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,61 @@ var _ = Describe("Config Local", func() {
))
})

It("should fail on invalid ipv6 subnet pool with ipv4 address", func() {
defConf, err := defaultConfig()
gomega.Expect(err).ToNot(gomega.HaveOccurred())

net, _ := types.ParseCIDR("10.0.0.0/8")
defConf.Network.DefaultSubnetPoolsV6 = []SubnetPool{
{Base: &net, Size: 24},
}

err = defConf.Network.Validate()
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).To(gomega.ContainSubstring("must be ipv6"))
})

It("should fail on invalid ipv6 subnet pool size bigger than subnet", func() {
defConf, err := defaultConfig()
gomega.Expect(err).ToNot(gomega.HaveOccurred())

net, _ := types.ParseCIDR("fd00::/48")
defConf.Network.DefaultSubnetPoolsV6 = []SubnetPool{
{Base: &net, Size: 32},
}

err = defConf.Network.Validate()
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).To(gomega.ContainSubstring("size is bigger than subnet"))
})

It("should fail on invalid ipv6 subnet pool size exceeding 128", func() {
defConf, err := defaultConfig()
gomega.Expect(err).ToNot(gomega.HaveOccurred())

net, _ := types.ParseCIDR("fd00::/8")
defConf.Network.DefaultSubnetPoolsV6 = []SubnetPool{
{Base: &net, Size: 129},
}

err = defConf.Network.Validate()
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).To(gomega.ContainSubstring("must be between 0-128"))
})

It("should pass on valid ipv6 subnet pool", func() {
defConf, err := defaultConfig()
gomega.Expect(err).ToNot(gomega.HaveOccurred())

net, _ := types.ParseCIDR("fd00::/8")
defConf.Network.DefaultSubnetPoolsV6 = []SubnetPool{
{Base: &net, Size: 64},
}

err = defConf.Network.Validate()
gomega.Expect(err).ToNot(gomega.HaveOccurred())
})

It("parse dns port", func() {
// Given
config, err := newLocked(&Options{}, testConfigPath(""))
Expand Down
19 changes: 17 additions & 2 deletions common/pkg/config/containers.conf
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,7 @@ default_sysctls = [
# DefaultSubnetPools is a list of subnets and size which are used to
# allocate subnets automatically for podman network create.
# It will iterate through the list and will pick the first free subnet
# with the given size. This is only used for ipv4 subnets, ipv6 subnets
# are always assigned randomly.
# with the given size. This is only used for ipv4 subnets.
#
#default_subnet_pools = [
# {"base" = "10.89.0.0/16", "size" = 24},
Expand All @@ -403,6 +402,22 @@ default_sysctls = [
# {"base" = "10.128.0.0/9", "size" = 24},
#]

# The IPv6 subnet for the default network.
# If empty, no IPv6 subnet is added to the default network.
# Must be a valid IPv6 CIDR block.
#
#default_subnet_v6 = ""

# DefaultSubnetPoolsV6 is a list of IPv6 subnets and prefix lengths used to
# allocate subnets automatically for podman network create with --ipv6.
# It will iterate through the list and will pick the first free subnet
# with the given size. If empty, IPv6 subnets are assigned randomly
# from fd00::/8 (RFC 4193).
#
#default_subnet_pools_v6 = [
# {"base" = "fd00::/48", "size" = 64},
#]



# Configure which rootless network program to use by default.
Expand Down