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
46 changes: 35 additions & 11 deletions pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
"github.com/containerd/nerdctl/v2/pkg/ipcutil"
"github.com/containerd/nerdctl/v2/pkg/labels"
subnetutil "github.com/containerd/nerdctl/v2/pkg/netutil/subnet"
"github.com/containerd/nerdctl/v2/pkg/ocihook/state"
)

Expand Down Expand Up @@ -1086,11 +1087,20 @@ type structuredCNI struct {
Name string `json:"name"`
Plugins []struct {
Ipam struct {
Ranges [][]IPAMConfig `json:"ranges"`
Ranges [][]cniIPAMRange `json:"ranges"`
} `json:"ipam"`
} `json:"plugins"`
}

// cniIPAMRange is the on-disk host-local range. Its bounds let inspect recompute
// the ip-range CIDR, which host-local has no field for.
type cniIPAMRange struct {
Subnet string `json:"subnet"`
Gateway string `json:"gateway"`
RangeStart string `json:"rangeStart"`
RangeEnd string `json:"rangeEnd"`
}

type MemorySetting struct {
Limit int64 `json:"limit"`
Swap int64 `json:"swap"`
Expand Down Expand Up @@ -1195,21 +1205,35 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
}

res.Name = sCNI.Name
// An aux-address reservation splits one subnet into several sub-ranges that
// share the subnet and gateway. Collapse each distinct subnet into a single
// IPAM.Config like Docker, keeping the first entry's gateway and its lowest
// start. host-local returns a split subnet's sub-ranges sorted, so widening the
// end as later ones arrive rebuilds the original allocation window.
idxBySubnet := make(map[string]int)
startBySubnet := make(map[string]string)
for _, plugin := range sCNI.Plugins {
for _, ranges := range plugin.Ipam.Ranges {
// A range-set normally describes one subnet; an aux-address
// reservation splits it into several sub-ranges that all share the
// subnet and gateway. Report the first entry per distinct subnet so a
// split subnet collapses to one IPAM.Config like Docker, without
// dropping entries for different subnets in the same set. The
// aux-addresses themselves are attached later from a nerdctl label.
seen := make(map[string]struct{}, len(ranges))
for _, r := range ranges {
if _, ok := seen[r.Subnet]; ok {
idx, ok := idxBySubnet[r.Subnet]
if !ok {
idx = len(res.IPAM.Config)
idxBySubnet[r.Subnet] = idx
startBySubnet[r.Subnet] = r.RangeStart
res.IPAM.Config = append(res.IPAM.Config, IPAMConfig{Subnet: r.Subnet, Gateway: r.Gateway})
}
// host-local has no ipRange field, so recompute it from the outermost
// bounds the way Docker reports it. A window that spans the whole
// subnet means no --ip-range was set, so report none. The
// aux-addresses themselves are attached later from a nerdctl label.
if r.RangeEnd == "" {
continue
}
seen[r.Subnet] = struct{}{}
res.IPAM.Config = append(res.IPAM.Config, r)
ipRange := subnetutil.CIDRFromRange(startBySubnet[r.Subnet], r.RangeEnd)
if ipRange == r.Subnet {
ipRange = ""
}
res.IPAM.Config[idx].IPRange = ipRange
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions pkg/inspecttypes/dockercompat/dockercompat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,3 +949,39 @@ func TestImageFromNative(t *testing.T) {
}
})
}

func TestNetworkFromNativeIPRange(t *testing.T) {
// host-local stores only rangeStart/rangeEnd; inspect must recompute the
// --ip-range CIDR from them and report it under IPRange like Docker, while a
// subnet without an ip-range reports no IPRange.
cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` +
`[{"subnet":"172.28.0.0/16","gateway":"172.28.5.254","rangeStart":"172.28.5.1","rangeEnd":"172.28.5.255"}],` +
`[{"subnet":"10.9.0.0/24","gateway":"10.9.0.1"}]` +
`]}}]}`
got, err := NetworkFromNative(&native.Network{CNI: []byte(cni)})
assert.NilError(t, err)
assert.DeepEqual(t, []IPAMConfig{
{Subnet: "172.28.0.0/16", Gateway: "172.28.5.254", IPRange: "172.28.5.0/24"},
{Subnet: "10.9.0.0/24", Gateway: "10.9.0.1"},
}, got.IPAM.Config)
}

func TestNetworkFromNativeIPRangeSplit(t *testing.T) {
// An aux-address reservation splits a subnet into sorted sub-ranges on disk.
// inspect must collapse them to one IPAM.Config and rebuild the ip-range from
// the outermost bounds: the first subnet reconstructs its original --ip-range,
// while the second spans its whole subnet (aux-address only, no --ip-range) and
// so reports no IPRange.
cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` +
`[{"subnet":"172.28.0.0/16","gateway":"172.28.5.254","rangeStart":"172.28.5.1","rangeEnd":"172.28.5.9"},` +
`{"subnet":"172.28.0.0/16","rangeStart":"172.28.5.11","rangeEnd":"172.28.5.255"}],` +
`[{"subnet":"10.9.0.0/24","gateway":"10.9.0.1","rangeStart":"10.9.0.1","rangeEnd":"10.9.0.4"},` +
`{"subnet":"10.9.0.0/24","rangeStart":"10.9.0.6","rangeEnd":"10.9.0.254"}]` +
`]}}]}`
got, err := NetworkFromNative(&native.Network{CNI: []byte(cni)})
assert.NilError(t, err)
assert.DeepEqual(t, []IPAMConfig{
{Subnet: "172.28.0.0/16", Gateway: "172.28.5.254", IPRange: "172.28.5.0/24"},
{Subnet: "10.9.0.0/24", Gateway: "10.9.0.1"},
}, got.IPAM.Config)
}
1 change: 0 additions & 1 deletion pkg/netutil/cni_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ type IPAMRange struct {
RangeStart string `json:"rangeStart,omitempty"`
RangeEnd string `json:"rangeEnd,omitempty"`
Gateway string `json:"gateway,omitempty"`
IPRange string `json:"ipRange,omitempty"`
}

type IPAMRoute struct {
Expand Down
12 changes: 5 additions & 7 deletions pkg/netutil/netutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,9 +627,9 @@ func parseIPAMRange(subnet *net.IPNet, gatewayStr, ipRangeStr string) (*IPAMRang
if !subnet.Contains(rangeStart) || !subnet.Contains(rangeEnd) {
return nil, fmt.Errorf("no matching subnet %q for ip-range %q", subnet, ipRangeStr)
}
// host-local has no ipRange field; store the bounds and recompute on inspect.
res.RangeStart = rangeStart.String()
res.RangeEnd = rangeEnd.String()
res.IPRange = ipRangeStr
}

return res, nil
Expand Down Expand Up @@ -664,9 +664,9 @@ func ParseAuxAddresses(raw []string) (map[string]string, error) {
// carving them out. host-local has no exclude list, but it does allocate across
// every range in a set, so the reserved IPs become gaps between sub-ranges and
// are never handed out. Reserved IPs outside the allocation window need no split
// (host-local cannot reach them anyway). The base range's gateway and ip-range
// are kept on the first sub-range so the rest of the pipeline and `network
// inspect` behave exactly as the un-split case.
// (host-local cannot reach them anyway). The base range's gateway is kept on
// every sub-range; inspect rebuilds the original ip-range from the outermost
// sub-range bounds, so nothing else has to be carried across the split.
func splitIPAMRange(subnet *net.IPNet, base *IPAMRange, reserved []net.IP) ([]IPAMRange, error) {
if len(reserved) == 0 {
return []IPAMRange{*base}, nil
Expand Down Expand Up @@ -736,12 +736,10 @@ func splitIPAMRange(subnet *net.IPNet, base *IPAMRange, reserved []net.IP) ([]IP

// host-local reserves the gateway only when it is set on the range it lands
// in, and after splitting the gateway can be in any sub-range, so set it on
// all of them. The original ip-range is nerdctl-only bookkeeping for inspect,
// so keep it on the first sub-range alone.
// all of them.
for i := range out {
out[i].Gateway = base.Gateway
}
out[0].IPRange = base.IPRange
return out, nil
}

Expand Down
10 changes: 4 additions & 6 deletions pkg/netutil/netutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ func TestParseIPAMRange(t *testing.T) {
expected: &IPAMRange{
Subnet: "10.1.0.0/16",
Gateway: "10.1.0.1",
IPRange: "10.1.100.0/24",
RangeStart: "10.1.100.1",
RangeEnd: "10.1.100.255",
},
Expand All @@ -115,7 +114,6 @@ func TestParseIPAMRange(t *testing.T) {
expected: &IPAMRange{
Subnet: "10.1.100.0/23",
Gateway: "10.1.100.1",
IPRange: "10.1.100.0/25",
RangeStart: "10.1.100.1",
RangeEnd: "10.1.100.127",
},
Expand Down Expand Up @@ -245,20 +243,20 @@ func TestSplitIPAMRange(t *testing.T) {
{
name: "a reservation inside an ip-range splits within its bounds",
subnet: "10.1.100.0/24",
base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"},
base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"},
reserved: ips("10.1.100.5"),
expected: []IPAMRange{
{Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28"},
{Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1"},
{Subnet: "10.1.100.0/24", RangeStart: "10.1.100.6", RangeEnd: "10.1.100.15", Gateway: "10.1.100.1"},
},
},
{
name: "a reservation outside the ip-range needs no split",
subnet: "10.1.100.0/24",
base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"},
base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"},
reserved: ips("10.1.100.200"),
expected: []IPAMRange{
{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"},
{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"},
},
},
{
Expand Down
8 changes: 5 additions & 3 deletions pkg/netutil/netutil_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ func TestPairIPAMRangesIPRange(t *testing.T) {
ranges, findIPv4, _, err := pairIPAMRanges(subnets, nil, ipRanges, nil, true)
assert.NilError(t, err)
assert.Equal(t, true, findIPv4)
// The ip-range is no longer stored verbatim; its effect shows up as the
// rangeStart/rangeEnd bounds host-local actually uses.
got := map[string]string{}
for _, r := range ranges {
got[r[0].Subnet] = r[0].IPRange
got[r[0].Subnet] = r[0].RangeStart
}
assert.Equal(t, "10.6.1.0/24", got["10.6.0.0/16"])
assert.Equal(t, "2001:db8:6::/80", got["2001:db8:6::/64"])
assert.Equal(t, "10.6.1.1", got["10.6.0.0/16"])
assert.Equal(t, "2001:db8:6::1", got["2001:db8:6::/64"])
})

t.Run("an ip-range matching no subnet errors", func(t *testing.T) {
Expand Down
40 changes: 40 additions & 0 deletions pkg/netutil/subnet/subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,43 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) {
cidr.IP[len(cidr.IP)-1]++
return cidr.IP, nil
}

// CIDRFromRange inverts FirstIPInSubnet/LastIPInSubnet: it rebuilds the CIDR from
// the start and end they produced. Returns "" for empty or unparsable bounds.
// A /31 or /127 has no distinct network and broadcast, so it recomputes as /32 or /128.
func CIDRFromRange(startStr, endStr string) string {
if startStr == "" || endStr == "" {
return ""
}
start, end := net.ParseIP(startStr), net.ParseIP(endStr)
if start == nil || end == nil {
return ""
}
// A single-address range is a /32 or /128.
if start.Equal(end) {
if start.To4() != nil {
return start.String() + "/32"
}
return start.String() + "/128"
}
// Canonical byte form: 4 for v4, 16 for v6.
s, e, bits := start.To4(), end.To4(), 32
if s == nil {
s, e, bits = start.To16(), end.To16(), 128
}
if e == nil || len(s) != len(e) {
return ""
}
// Undo FirstIPInSubnet's last-byte bump to get the network.
network := make(net.IP, len(s))
copy(network, s)
network[len(network)-1]--
// end is the broadcast, so network^end is the host mask; its width is the host bits.
hostBits := 0
for i := range network {
for b := network[i] ^ e[i]; b != 0; b >>= 1 {
hostBits++
}
}
return fmt.Sprintf("%s/%d", network.String(), bits-hostBits)
}
28 changes: 28 additions & 0 deletions pkg/netutil/subnet/subnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,31 @@ func TestNextSubnet(t *testing.T) {
assert.Equal(t, nextSubnet.String(), tc.expect)
}
}

func TestCIDRFromRange(t *testing.T) {
testCases := []struct {
name string
start, end string
expect string
}{
{"no range", "", "", ""},
{"v4 /24", "10.1.100.1", "10.1.100.255", "10.1.100.0/24"},
{"v4 /25", "10.24.24.1", "10.24.24.127", "10.24.24.0/25"},
{"v4 /16", "172.28.0.1", "172.28.255.255", "172.28.0.0/16"},
{"v4 offset /25", "10.1.100.129", "10.1.100.255", "10.1.100.128/25"},
{"v4 /32", "10.0.0.5", "10.0.0.5", "10.0.0.5/32"},
{"v4 /31 collapses to /32", "10.0.0.1", "10.0.0.1", "10.0.0.1/32"},
{"v6 /64", "fd00:55::1", "fd00:55::ffff:ffff:ffff:ffff", "fd00:55::/64"},
{"v6 /120", "fd00:7::1", "fd00:7::ff", "fd00:7::/120"},
{"v6 /128", "fd00::5", "fd00::5", "fd00::5/128"},
{"v6 /127 collapses to /128", "fd00::1", "fd00::1", "fd00::1/128"},
{"start unparsable", "bogus", "10.0.0.255", ""},
{"end unparsable", "10.0.0.1", "bogus", ""},
{"mismatched families", "10.0.0.1", "fd00::ff", ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expect, CIDRFromRange(tc.start, tc.end))
})
}
}
Loading