diff --git a/common/libnetwork/internal/util/bridge.go b/common/libnetwork/internal/util/bridge.go index f245fb016c..1e485c518b 100644 --- a/common/libnetwork/internal/util/bridge.go +++ b/common/libnetwork/internal/util/bridge.go @@ -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) @@ -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 } diff --git a/common/libnetwork/internal/util/ip_test.go b/common/libnetwork/internal/util/ip_test.go index 527463f76c..6d046997db 100644 --- a/common/libnetwork/internal/util/ip_test.go +++ b/common/libnetwork/internal/util/ip_test.go @@ -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 { @@ -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) { diff --git a/common/libnetwork/internal/util/util.go b/common/libnetwork/internal/util/util.go index 8c64f1863a..ec459e77bc 100644 --- a/common/libnetwork/internal/util/util.go +++ b/common/libnetwork/internal/util/util.go @@ -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. @@ -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 diff --git a/common/libnetwork/netavark/config.go b/common/libnetwork/netavark/config.go index 3b78544352..6321356865 100644 --- a/common/libnetwork/netavark/config.go +++ b/common/libnetwork/netavark/config.go @@ -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"` } @@ -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, }, diff --git a/common/libnetwork/netavark/network.go b/common/libnetwork/netavark/network.go index c06d0486f2..cf765f3e83 100644 --- a/common/libnetwork/netavark/network.go +++ b/common/libnetwork/netavark/network.go @@ -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 @@ -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, @@ -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, @@ -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, }, diff --git a/common/pkg/config/config.go b/common/pkg/config/config.go index 22d161e27f..2038f7618f 100644 --- a/common/pkg/config/config.go +++ b/common/pkg/config/config.go @@ -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"` @@ -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 } diff --git a/common/pkg/config/config_local_test.go b/common/pkg/config/config_local_test.go index 833a1b60de..1b70cb6470 100644 --- a/common/pkg/config/config_local_test.go +++ b/common/pkg/config/config_local_test.go @@ -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("")) diff --git a/common/pkg/config/containers.conf b/common/pkg/config/containers.conf index 7f52238178..189d95b498 100644 --- a/common/pkg/config/containers.conf +++ b/common/pkg/config/containers.conf @@ -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}, @@ -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.