diff --git a/cidr.go b/cidr.go new file mode 100644 index 0000000..3c3c045 --- /dev/null +++ b/cidr.go @@ -0,0 +1,79 @@ +// Package cidr is a collection of assorted utilities for computing +// network and host addresses within network ranges. +// +// It expects a CIDR-type address structure where addresses are divided into +// some number of prefix bits representing the network and then the remaining +// suffix bits represent the host. +// +// For example, it can help to calculate addresses for sub-networks of a +// parent network, or to calculate host addresses within a particular prefix. +// +// At present this package is prioritizing simplicity of implementation and +// de-prioritizing speed and memory usage. Thus caution is advised before +// using this package in performance-critical applications or hot codepaths. +// Patches to improve the speed and memory usage may be accepted as long as +// they do not result in a significant increase in code complexity. +package main + +import ( + "fmt" + "math/big" + "net" +) + +// AddressRange returns the first and last addresses in the given CIDR range. +func AddressRange(network *net.IPNet) (net.IP, net.IP) { + // the first IP is easy + firstIP := network.IP + + // the last IP is the network address OR NOT the mask address + prefixLen, bits := network.Mask.Size() + if prefixLen == bits { + // Easy! + // But make sure that our two slices are distinct, since they + // would be in all other cases. + lastIP := make([]byte, len(firstIP)) + copy(lastIP, firstIP) + return firstIP, lastIP + } + + firstIPInt, bits := ipToInt(firstIP) + hostLen := uint(bits) - uint(prefixLen) + lastIPInt := big.NewInt(1) + lastIPInt.Lsh(lastIPInt, hostLen) + lastIPInt.Sub(lastIPInt, big.NewInt(1)) + lastIPInt.Or(lastIPInt, firstIPInt) + + return firstIP, intToIP(lastIPInt, bits) +} + +// AddressCount returns the number of distinct host addresses within the given +// CIDR range. +// +// Since the result is a uint64, this function returns meaningful information +// only for IPv4 ranges and IPv6 ranges with a prefix size of at least 65. +func AddressCount(network *net.IPNet) uint64 { + prefixLen, bits := network.Mask.Size() + return 1 << (uint64(bits) - uint64(prefixLen)) +} + +//NoOverlap takes a list subnets and supernet (CIDRBlock) and verifies +//none of the subnets overlap and all subnets are in the supernet +//it returns an error if any of those conditions are not satisfied +func NoOverlap(subnets []*net.IPNet) error { + firstLastIP := make([][]net.IP, len(subnets)) + for i, s := range subnets { + first, last := AddressRange(s) + firstLastIP[i] = []net.IP{first, last} + } + for i, s := range subnets { + for j := i + 1; j < len(subnets); j++ { + first := firstLastIP[j][0] + last := firstLastIP[j][1] + if s.Contains(first) || s.Contains(last) { + return fmt.Errorf("%s overlaps with %s", subnets[j].String(), s.String()) + } + } + } + return nil +} diff --git a/cidr_test.go b/cidr_test.go new file mode 100644 index 0000000..e1d5fb9 --- /dev/null +++ b/cidr_test.go @@ -0,0 +1,186 @@ +package main + +import ( + "fmt" + "net" + "testing" +) + +func TestAddressRange(t *testing.T) { + type Case struct { + Range string + First string + Last string + } + + cases := []Case{ + Case{ + Range: "192.168.0.0/16", + First: "192.168.0.0", + Last: "192.168.255.255", + }, + Case{ + Range: "192.168.0.0/17", + First: "192.168.0.0", + Last: "192.168.127.255", + }, + Case{ + Range: "fe80::/64", + First: "fe80::", + Last: "fe80::ffff:ffff:ffff:ffff", + }, + } + + for _, testCase := range cases { + _, network, _ := net.ParseCIDR(testCase.Range) + firstIP, lastIP := AddressRange(network) + desc := fmt.Sprintf("AddressRange(%#v)", testCase.Range) + gotFirstIP := firstIP.String() + gotLastIP := lastIP.String() + if gotFirstIP != testCase.First { + t.Errorf("%s first is %s; want %s", desc, gotFirstIP, testCase.First) + } + if gotLastIP != testCase.Last { + t.Errorf("%s last is %s; want %s", desc, gotLastIP, testCase.Last) + } + } + +} + +func TestAddressCount(t *testing.T) { + type Case struct { + Range string + Count uint64 + } + + cases := []Case{ + Case{ + Range: "192.168.0.0/16", + Count: 65536, + }, + Case{ + Range: "192.168.0.0/17", + Count: 32768, + }, + Case{ + Range: "192.168.0.0/32", + Count: 1, + }, + Case{ + Range: "192.168.0.0/31", + Count: 2, + }, + Case{ + Range: "0.0.0.0/0", + Count: 4294967296, + }, + Case{ + Range: "0.0.0.0/1", + Count: 2147483648, + }, + Case{ + Range: "::/65", + Count: 9223372036854775808, + }, + Case{ + Range: "::/128", + Count: 1, + }, + Case{ + Range: "::/127", + Count: 2, + }, + } + + for _, testCase := range cases { + _, network, _ := net.ParseCIDR(testCase.Range) + gotCount := AddressCount(network) + desc := fmt.Sprintf("AddressCount(%#v)", testCase.Range) + if gotCount != testCase.Count { + t.Errorf("%s = %d; want %d", desc, gotCount, testCase.Count) + } + } + +} + +func TestVerifyNetowrk(t *testing.T) { + + type testVerifyNetwork struct { + CIDRBlock string + CIDRList []string + } + + testCases := []*testVerifyNetwork{ + &testVerifyNetwork{ + CIDRList: []string{ + "192.168.8.0/24", + "192.168.9.0/24", + "192.168.10.0/24", + "192.168.11.0/25", + "192.168.11.128/25", + "192.168.12.0/25", + "192.168.12.128/26", + "192.168.12.192/26", + "192.168.13.0/26", + "192.168.13.64/27", + "192.168.13.96/27", + "192.168.13.128/27", + }, + }, + } + failCases := []*testVerifyNetwork{ + &testVerifyNetwork{ + CIDRList: []string{ + "192.168.8.0/24", + "192.168.9.0/24", + "192.168.10.0/24", + "192.168.11.0/25", + "192.168.11.128/25", + "192.168.12.0/25", + "192.168.12.64/26", + "192.168.12.128/26", + }, + }, + &testVerifyNetwork{ + CIDRList: []string{ + "192.168.7.0/24", + "192.168.9.0/24", + "192.168.10.0/24", + "192.168.11.0/25", + "192.168.11.128/25", + "192.168.12.0/25", + "192.168.12.64/26", + "192.168.12.128/26", + }, + }, + } + + for _, tc := range testCases { + subnets := make([]*net.IPNet, len(tc.CIDRList)) + for i, s := range tc.CIDRList { + _, n, err := net.ParseCIDR(s) + if err != nil { + t.Errorf("Bad test data %s\n", s) + } + subnets[i] = n + } + test := NoOverlap(subnets) + if test != nil { + t.Errorf("Failed test with %v\n", test) + } + } + for _, tc := range failCases { + subnets := make([]*net.IPNet, len(tc.CIDRList)) + for i, s := range tc.CIDRList { + _, n, err := net.ParseCIDR(s) + if err != nil { + t.Errorf("Bad test data %s\n", s) + } + subnets[i] = n + } + test := NoOverlap(subnets) + if test == nil { + t.Errorf("Test should have failed with CIDR %s\n", tc.CIDRBlock) + } + } +} diff --git a/main.go b/main.go index fe19c37..cb615c7 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,6 @@ import ( "fmt" "net" "os" - - "github.com/apparentlymart/go-cidr/cidr" ) const usage = "sn " @@ -24,11 +22,11 @@ func main() { os.Exit(1) } nets = append(nets, net) - a, b := cidr.AddressRange(net) - l := cidr.AddressCount(net) + a, b := AddressRange(net) + l := AddressCount(net) fmt.Printf("%-16s %-16s %10d\n", a, b, l) } - if err := cidr.NoOverlap(nets); err != nil { + if err := NoOverlap(nets); err != nil { fmt.Fprintf(os.Stderr, "overlaping networks: %v\n", err) os.Exit(1) } diff --git a/wrangling.go b/wrangling.go new file mode 100644 index 0000000..9eb28c3 --- /dev/null +++ b/wrangling.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "math/big" + "net" +) + +func ipToInt(ip net.IP) (*big.Int, int) { + val := &big.Int{} + val.SetBytes([]byte(ip)) + if len(ip) == net.IPv4len { + return val, 32 + } else if len(ip) == net.IPv6len { + return val, 128 + } else { + panic(fmt.Errorf("Unsupported address length %d", len(ip))) + } +} + +func intToIP(ipInt *big.Int, bits int) net.IP { + 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] + } + return net.IP(ret) +}