diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2023-02-04 16:32:49 -0500 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2023-02-04 21:01:14 -0500 |
| commit | edf224ccf7c66498b2d9743fcfb29bf4c0960931 (patch) | |
| tree | 33461cb75f2d63e91be1d8a7ec5a9e72ce962f12 /internal | |
| parent | af3e81ac52f93f07aea644b6397dbca97e5d30aa (diff) | |
Deprecate netaddr-based funcs
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cidr/cidr.go | 138 | ||||
| -rw-r--r-- | internal/cidr/cidr_test.go | 203 | ||||
| -rw-r--r-- | internal/tests/integration/net_test.go | 2 |
3 files changed, 342 insertions, 1 deletions
diff --git a/internal/cidr/cidr.go b/internal/cidr/cidr.go new file mode 100644 index 00000000..7ac4b053 --- /dev/null +++ b/internal/cidr/cidr.go @@ -0,0 +1,138 @@ +package cidr + +import ( + "fmt" + "math/big" + "net/netip" + + "go4.org/netipx" +) + +// taken from github.com/apparentelymart/go-cidr/ and modified to use the net/netip +// package instead of the stdlib net package - this will hopefully be merged back +// upstream at some point + +// SubnetBig takes a parent CIDR range and creates a subnet within it with the +// given number of additional prefix bits and the given network number. It +// differs from Subnet in that it takes a *big.Int for the num, instead of an int. +// +// For example, 10.3.0.0/16, extended by 8 bits, with a network number of 5, +// becomes 10.3.5.0/24 . +func SubnetBig(base netip.Prefix, newBits int, num *big.Int) (netip.Prefix, error) { + parentLen := base.Bits() + addrLen := base.Addr().BitLen() + + newPrefixLen := parentLen + newBits + + if newPrefixLen > addrLen { + return netip.Prefix{}, fmt.Errorf("insufficient address space to extend prefix of %d by %d", parentLen, newBits) + } + + maxNetNum := uint64(1<<uint64(newBits)) - 1 + if num.Uint64() > maxNetNum { + return netip.Prefix{}, fmt.Errorf("prefix extension of %d does not accommodate a subnet numbered %d", newBits, num) + } + + prefix := netip.PrefixFrom(insertNumIntoIP(base.Masked().Addr(), num, newPrefixLen), newPrefixLen) + + return prefix, nil +} + +// HostBig takes a parent CIDR range and turns it into a host IP address with +// the given host number. It differs from Host in that it takes a *big.Int for +// the num, instead of an int. +// +// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2. +func HostBig(base netip.Prefix, num *big.Int) (netip.Addr, error) { + parentLen := base.Bits() + addrLen := base.Addr().BitLen() + + hostLen := addrLen - parentLen + + maxHostNum := big.NewInt(int64(1)) + maxHostNum.Lsh(maxHostNum, uint(hostLen)) + maxHostNum.Sub(maxHostNum, big.NewInt(1)) + + numUint64 := big.NewInt(int64(num.Uint64())) + if num.Cmp(big.NewInt(0)) == -1 { + numUint64.Neg(num) + numUint64.Sub(numUint64, big.NewInt(int64(1))) + num.Sub(maxHostNum, numUint64) + } + + if numUint64.Cmp(maxHostNum) == 1 { + return netip.Addr{}, fmt.Errorf("prefix of %d does not accommodate a host numbered %d", parentLen, num) + } + + return insertNumIntoIP(base.Masked().Addr(), num, addrLen), nil +} + +func ipToInt(ip netip.Addr) (*big.Int, int) { + val := &big.Int{} + val.SetBytes(ip.AsSlice()) + + return val, ip.BitLen() +} + +func intToIP(ipInt *big.Int, bits int) netip.Addr { + ipBytes := ipInt.Bytes() + ret := make([]byte, bits/8) + // Pack our IP bytes into the end of the return array, + // since big.Int.Bytes() removes front zero padding. + for i := 1; i <= len(ipBytes); i++ { + ret[len(ret)-i] = ipBytes[len(ipBytes)-i] + } + + addr, ok := netip.AddrFromSlice(ret) + if !ok { + panic("invalid IP address") + } + + return addr +} + +func insertNumIntoIP(ip netip.Addr, bigNum *big.Int, prefixLen int) netip.Addr { + ipInt, totalBits := ipToInt(ip) + bigNum.Lsh(bigNum, uint(totalBits-prefixLen)) + ipInt.Or(ipInt, bigNum) + return intToIP(ipInt, totalBits) +} + +// PreviousSubnet returns the subnet of the desired mask in the IP space +// just lower than the start of Prefix provided. If the IP space rolls over +// then the second return value is true +func PreviousSubnet(network netip.Prefix, prefixLen int) (netip.Prefix, bool) { + previousIP := network.Masked().Addr().Prev() + + previous, err := previousIP.Prefix(prefixLen) + if err != nil { + return netip.Prefix{}, false + } + if !previous.IsValid() { + return previous, true + } + + return previous.Masked(), false +} + +// NextSubnet returns the next available subnet of the desired mask size +// starting for the maximum IP of the offset subnet +// If the IP exceeds the maxium IP then the second return value is true +func NextSubnet(network netip.Prefix, prefixLen int) (netip.Prefix, bool) { + currentLast := netipx.PrefixLastIP(network) + + currentSubnet, err := currentLast.Prefix(prefixLen) + if err != nil { + return netip.Prefix{}, false + } + + last := netipx.PrefixLastIP(currentSubnet).Next() + next, err := last.Prefix(prefixLen) + if err != nil { + return netip.Prefix{}, false + } + if !last.IsValid() { + return next, true + } + return next, false +} diff --git a/internal/cidr/cidr_test.go b/internal/cidr/cidr_test.go new file mode 100644 index 00000000..1b8cc90a --- /dev/null +++ b/internal/cidr/cidr_test.go @@ -0,0 +1,203 @@ +package cidr + +import ( + "fmt" + "math/big" + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubnetBig(t *testing.T) { + cases := []struct { + base string + num *big.Int + out string + bits int + err bool + }{ + { + base: "192.168.2.0/20", + bits: 4, + num: big.NewInt(int64(6)), + out: "192.168.6.0/24", + }, + { + base: "192.168.2.0/20", + bits: 4, + num: big.NewInt(int64(0)), + out: "192.168.0.0/24", + }, + { + base: "192.168.0.0/31", + bits: 1, + num: big.NewInt(int64(1)), + out: "192.168.0.1/32", + }, + { + base: "192.168.0.0/21", + bits: 4, + num: big.NewInt(int64(7)), + out: "192.168.3.128/25", + }, + { + base: "fe80::/48", + bits: 16, + num: big.NewInt(int64(6)), + out: "fe80:0:0:6::/64", + }, + { + base: "fe80::/48", + bits: 33, + num: big.NewInt(int64(6)), + out: "fe80::3:0:0:0/81", + }, + { + base: "fe80::/49", + bits: 16, + num: big.NewInt(int64(7)), + out: "fe80:0:0:3:8000::/65", + }, + { + base: "192.168.2.0/31", + bits: 2, + num: big.NewInt(int64(0)), + err: true, // not enough bits to expand into + }, + { + base: "fe80::/126", + bits: 4, + num: big.NewInt(int64(0)), + err: true, // not enough bits to expand into + }, + { + base: "192.168.2.0/24", + bits: 4, + num: big.NewInt(int64(16)), + err: true, // can't fit 16 into 4 bits + }, + } + + for _, testCase := range cases { + t.Run(fmt.Sprintf("SubnetBig(%#v,%#v,%#v)", testCase.base, testCase.bits, testCase.num), func(t *testing.T) { + base := netip.MustParsePrefix(testCase.base) + + subnet, err := SubnetBig(base, testCase.bits, testCase.num) + if testCase.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.out, subnet.String()) + } + }) + } +} + +func TestHostBig(t *testing.T) { + cases := []struct { + prefix string + num *big.Int + out string + err bool + }{ + { + prefix: "192.168.2.0/20", + num: big.NewInt(int64(6)), + out: "192.168.0.6", + }, + { + prefix: "192.168.0.0/20", + num: big.NewInt(int64(257)), + out: "192.168.1.1", + }, + { + prefix: "2001:db8::/32", + num: big.NewInt(int64(1)), + out: "2001:db8::1", + }, + { + prefix: "192.168.1.0/24", + num: big.NewInt(int64(256)), + err: true, // only 0-255 will fit in 8 bits + }, + { + prefix: "192.168.0.0/30", + num: big.NewInt(int64(-3)), + out: "192.168.0.1", // 4 address (0-3) in 2 bits; 3rd from end = 1 + }, + { + prefix: "192.168.0.0/30", + num: big.NewInt(int64(-4)), + out: "192.168.0.0", // 4 address (0-3) in 2 bits; 4th from end = 0 + }, + { + prefix: "192.168.0.0/30", + num: big.NewInt(int64(-5)), + err: true, // 4 address (0-3) in 2 bits; cannot accommodate 5 + }, + { + prefix: "fd9d:bc11:4020::/64", + num: big.NewInt(int64(2)), + out: "fd9d:bc11:4020::2", + }, + { + prefix: "fd9d:bc11:4020::/64", + num: big.NewInt(int64(-2)), + out: "fd9d:bc11:4020:0:ffff:ffff:ffff:fffe", + }, + } + + for _, testCase := range cases { + t.Run(fmt.Sprintf("HostBig(%v,%v)", testCase.prefix, testCase.num), func(t *testing.T) { + network := netip.MustParsePrefix(testCase.prefix) + + gotIP, err := HostBig(network, testCase.num) + if testCase.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.out, gotIP.String()) + } + }) + } +} + +func TestPreviousNextSubnet(t *testing.T) { + testCases := []struct { + next, prev string + overflow bool + }{ + {"10.0.0.0/24", "9.255.255.0/24", false}, + {"100.0.0.0/26", "99.255.255.192/26", false}, + {"0.0.0.0/26", "255.255.255.192/26", true}, + {"2001:db8:e000::/36", "2001:db8:d000::/36", false}, + {"::/64", "ffff:ffff:ffff:ffff::/64", true}, + } + for _, tc := range testCases { + c1 := netip.MustParsePrefix(tc.next) + c2 := netip.MustParsePrefix(tc.prev) + mask := c1.Bits() + + p1, rollback := PreviousSubnet(c1, mask) + if tc.overflow { + assert.True(t, rollback) + continue + } + + assert.Equal(t, c2.String(), p1.String()) + } + + for _, tc := range testCases { + c1 := netip.MustParsePrefix(tc.next) + c2 := netip.MustParsePrefix(tc.prev) + mask := c1.Bits() + + n1, rollover := NextSubnet(c2, mask) + if tc.overflow { + assert.True(t, rollover) + continue + } + assert.Equal(t, c1, n1) + } +} diff --git a/internal/tests/integration/net_test.go b/internal/tests/integration/net_test.go index 35218eaf..a0ff4e6f 100644 --- a/internal/tests/integration/net_test.go +++ b/internal/tests/integration/net_test.go @@ -9,7 +9,7 @@ func TestNet_LookupIP(t *testing.T) { } func TestNet_CIDRHost(t *testing.T) { - inOutTestExperimental(t, `{{ net.ParseIPPrefix "10.12.127.0/20" | net.CIDRHost 16 }}`, "10.12.112.16") + inOutTestExperimental(t, `{{ net.ParsePrefix "10.12.127.0/20" | net.CIDRHost 16 }}`, "10.12.112.16") inOutTestExperimental(t, `{{ "10.12.127.0/20" | net.CIDRHost 16 }}`, "10.12.112.16") inOutTestExperimental(t, `{{ net.CIDRHost 268 "10.12.127.0/20" }}`, "10.12.113.12") inOutTestExperimental(t, `{{ net.CIDRHost 34 "fd00:fd12:3456:7890:00a2::/72" }}`, "fd00:fd12:3456:7890::22") |
