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
7 changes: 6 additions & 1 deletion cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,12 @@ func IPToInteger(ip net.IP) (*big.Int, int, error) {
func IntegerToIP(ipInt *big.Int, bits int) net.IP {
ipBytes := ipInt.Bytes()
ret := make([]byte, bits/8) //nolint
for i := 1; i <= len(ipBytes); i++ {
// copy the low-order bytes; ipInt >= 2^bits wraps (mod 2^bits) instead of panicking
n := len(ipBytes)
if n > len(ret) {
n = len(ret)
}
for i := 1; i <= n; i++ {
ret[len(ret)-i] = ipBytes[len(ipBytes)-i]
}
return net.IP(ret)
Expand Down
24 changes: 24 additions & 0 deletions cidr_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
package mapcidr

import (
"math/big"
"net"
"reflect"
"testing"
)

// IntegerToIP round-trips IPToInteger and, for ipInt >= 2^bits, wraps (mod 2^bits)
// instead of panicking with a negative index.
func TestIntegerToIPOverflow(t *testing.T) {
for _, s := range []string{"0.0.0.0", "10.0.0.1", "255.255.255.255", "192.168.1.1"} {
i, bits, err := IPToInteger(net.ParseIP(s))
if err != nil {
t.Fatalf("IPToInteger(%s): %v", s, err)
}
if got := IntegerToIP(i, bits).String(); got != s {
t.Errorf("round-trip %s => %s", s, got)
}
}
// 2^32 wraps to 0.0.0.0; 2^32 + 0x0a000001 wraps to 10.0.0.1.
over := new(big.Int).Lsh(big.NewInt(1), 32)
if got := IntegerToIP(over, 32).String(); got != "0.0.0.0" {
t.Errorf("overflow 2^32 => %s, want 0.0.0.0", got)
}
over.Add(over, big.NewInt(0x0a000001))
if got := IntegerToIP(over, 32).String(); got != "10.0.0.1" {
t.Errorf("overflow 2^32+10.0.0.1 => %s, want 10.0.0.1", got)
}
}

func TestSplitIPNetIntoN(t *testing.T) {
tests := []struct {
name string
Expand Down
7 changes: 7 additions & 0 deletions ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,13 @@ The intent here is to get the CIDR range from the IP range.
This function will return the sorted list of CIDR ranges.
*/
func GetCIDRFromIPRange(firstIP, lastIP net.IP) ([]*net.IPNet, error) {
// net.IP is 4- or 16-byte; normalise both to 16-byte so the range math below
// never depends on the input representation.
first16, last16 := firstIP.To16(), lastIP.To16()
if first16 == nil || last16 == nil {
return nil, fmt.Errorf("invalid IP address in range %s-%s", firstIP, lastIP)
}
firstIP, lastIP = first16, last16
// check if range is valid or not
if bytes.Compare(firstIP, lastIP) > 0 {
return nil, fmt.Errorf("start IP:%s must be less than End IP:%s", firstIP, lastIP)
Expand Down
76 changes: 76 additions & 0 deletions ip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,82 @@ func TestRangeToCIDRs(t *testing.T) {
}
}

// cidrStrings runs GetCIDRFromIPRange and returns the CIDR list as strings.
func cidrStrings(t *testing.T, first, last net.IP) []string {
t.Helper()
got, err := GetCIDRFromIPRange(first, last)
require.NoError(t, err)
var out []string
for _, n := range got {
out = append(out, n.String())
}
return out
}

// GetCIDRFromIPRange must accept any legitimate net.IP byte-length. Previously a
// 4-byte (To4) or mixed-length pair panicked, and a mixed-length valid range was
// wrongly rejected by the initial start>end check.
func TestGetCIDRFromIPRangeByteLength(t *testing.T) {
first16 := net.ParseIP("10.0.0.1")
last16 := net.ParseIP("10.0.0.9")
want := cidrStrings(t, first16, last16)
require.Equal(t, []string{"10.0.0.1/32", "10.0.0.2/31", "10.0.0.4/30", "10.0.0.8/31"}, want)

cases := []struct {
name string
first, last net.IP
}{
{"both4byte", first16.To4(), last16.To4()},
{"first16last4", first16, last16.To4()},
{"first4last16", first16.To4(), last16},
{"integerToIPOutput", intToIPHelper(t, "10.0.0.1"), intToIPHelper(t, "10.0.0.9")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, want, cidrStrings(t, tc.first, tc.last))
})
}
}

func intToIPHelper(t *testing.T, s string) net.IP {
t.Helper()
i, bits, err := IPToInteger(net.ParseIP(s))
require.NoError(t, err)
return IntegerToIP(i, bits) // 4-byte form
}

// tiling parity: 4-byte, 16-byte and mixed inputs must yield identical, correct
// CIDR sets (aligned, contiguous, non-overlapping, exact cover of [start,end]).
func TestGetCIDRFromIPRangeTilingParity(t *testing.T) {
base := new(big.Int).SetBytes(net.ParseIP("172.16.0.0").To4())
for k := 0; k < 4000; k++ {
a := int64((k * 2654435761) % (1 << 20))
b := a + int64((k*40503)%(1<<12))
s4 := IntegerToIP(new(big.Int).Add(base, big.NewInt(a)), 32)
e4 := IntegerToIP(new(big.Int).Add(base, big.NewInt(b)), 32)
got16 := cidrStrings(t, s4.To16(), e4.To16())
require.Equal(t, got16, cidrStrings(t, s4, e4), "4-byte parity")
require.Equal(t, got16, cidrStrings(t, s4.To16(), e4), "mixed parity")
assertTiling(t, s4, e4, got16)
}
}

func assertTiling(t *testing.T, start, end net.IP, cidrs []string) {
t.Helper()
lo := new(big.Int).SetBytes(start.To4())
end4 := new(big.Int).SetBytes(end.To4())
for _, c := range cidrs {
_, n, err := net.ParseCIDR(c)
require.NoError(t, err)
ones, size := n.Mask.Size()
netLo := new(big.Int).SetBytes(n.IP.To4())
require.Equal(t, 0, lo.Cmp(netLo), "gap/overlap or misalignment at %s", c)
blk := new(big.Int).Lsh(big.NewInt(1), uint(size-ones))
lo = new(big.Int).Add(netLo, blk)
}
require.Equal(t, 0, lo.Cmp(new(big.Int).Add(end4, big.NewInt(1))), "range not covered exactly")
}

func TestFindSmallestIPRange(t *testing.T) {
// Input IP addresses
ips := []string{
Expand Down