diff --git a/brute.go b/brute.go index 37a68be..2b060b9 100644 --- a/brute.go +++ b/brute.go @@ -108,6 +108,24 @@ func (b *bruteRanger) CoveredNetworks(network net.IPNet) ([]RangerEntry, error) return results, nil } +// Covering returns the list of RangerEntry(s) the given ipnet +// is covered. It's like ContainingNetworks() for ipnet. +func (b *bruteRanger) CoveringNetworks(network net.IPNet) ([]RangerEntry, error) { + entries, err := b.getEntriesByVersion(network.IP) + if err != nil { + return nil, err + } + var results []RangerEntry + testNetwork := rnet.NewNetwork(network) + for _, entry := range entries { + entryNetwork := rnet.NewNetwork(entry.Network()) + if entryNetwork.Covers(testNetwork) { + results = append(results, entry) + } + } + return results, nil +} + // Len returns number of networks in ranger. func (b *bruteRanger) Len() int { return len(b.ipV4Entries) + len(b.ipV6Entries) @@ -122,3 +140,8 @@ func (b *bruteRanger) getEntriesByVersion(ip net.IP) (map[string]RangerEntry, er } return nil, ErrInvalidNetworkInput } + +// Just to complete interface +func (p *bruteRanger) Adjacent(network net.IPNet) (RangerEntry, error) { + return nil, nil +} diff --git a/brute_test.go b/brute_test.go index 71ee637..846ac3d 100644 --- a/brute_test.go +++ b/brute_test.go @@ -175,3 +175,33 @@ func TestCoveredNetworks(t *testing.T) { }) } } + +func TestCoveringNetworks(t *testing.T) { + for _, tc := range coveringNetworkTests { + t.Run(tc.name, func(t *testing.T) { + ranger := newBruteRanger() + for _, insert := range tc.inserts { + _, network, _ := net.ParseCIDR(insert) + err := ranger.Insert(NewBasicRangerEntry(*network)) + assert.NoError(t, err) + } + var expectedEntries []string + for _, network := range tc.networks { + expectedEntries = append(expectedEntries, network) + } + sort.Strings(expectedEntries) + _, snet, _ := net.ParseCIDR(tc.search) + networks, err := ranger.CoveringNetworks(*snet) + assert.NoError(t, err) + + var results []string + for _, result := range networks { + net := result.Network() + results = append(results, net.String()) + } + sort.Strings(results) + + assert.Equal(t, expectedEntries, results) + }) + } +} diff --git a/cidranger.go b/cidranger.go index 2e8f118..72bd4bc 100644 --- a/cidranger.go +++ b/cidranger.go @@ -89,6 +89,8 @@ type Ranger interface { Contains(ip net.IP) (bool, error) ContainingNetworks(ip net.IP) ([]RangerEntry, error) CoveredNetworks(network net.IPNet) ([]RangerEntry, error) + CoveringNetworks(network net.IPNet) ([]RangerEntry, error) + Adjacent(network net.IPNet) (RangerEntry, error) Len() int } diff --git a/cidranger_test.go b/cidranger_test.go index c1c741e..9523e3a 100644 --- a/cidranger_test.go +++ b/cidranger_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + rnet "github.com/Ramzeth/cidranger/net" "github.com/stretchr/testify/assert" - rnet "github.com/yl2chen/cidranger/net" ) /* @@ -30,6 +30,10 @@ func TestCoveredNetworksAgainstBaseIPv4(t *testing.T) { testCoversNetworksAgainstBase(t, 100000, randomIPNetGenFactory(ipV4AWSRangesIPNets)) } +func TestCoveringNetworksAgainstBaseIPv4(t *testing.T) { + testCoveringNetworksAgainstBase(t, 100000, randomIPNetGenFactory(ipV4AWSRangesIPNets)) +} + // IPv6 spans an extremely large address space (2^128), randomly generated IPs // will often fall outside of the test ranges (AWS public CIDR blocks), so it // it more meaningful for testing to run from a curated list of IPv6 IPs. @@ -45,6 +49,10 @@ func TestCoveredNetworksAgainstBaseIPv6(t *testing.T) { testCoversNetworksAgainstBase(t, 100000, randomIPNetGenFactory(ipV6AWSRangesIPNets)) } +func TestCoveringNetworksAgainstBaseIPv6(t *testing.T) { + testCoveringNetworksAgainstBase(t, 100000, randomIPNetGenFactory(ipV6AWSRangesIPNets)) +} + func testContainsAgainstBase(t *testing.T, iterations int, ipGen ipGenerator) { if testing.Short() { t.Skip("Skipping memory test in `-short` mode") @@ -120,6 +128,32 @@ func testCoversNetworksAgainstBase(t *testing.T, iterations int, netGen networkG } } +func testCoveringNetworksAgainstBase(t *testing.T, iterations int, netGen networkGenerator) { + if testing.Short() { + t.Skip("Skipping memory test in `-short` mode") + } + rangers := []Ranger{NewPCTrieRanger()} + baseRanger := newBruteRanger() + for _, ranger := range rangers { + configureRangerWithAWSRanges(t, ranger) + } + configureRangerWithAWSRanges(t, baseRanger) + + for i := 0; i < iterations; i++ { + network := netGen() + expected, err := baseRanger.CoveringNetworks(network.IPNet) + assert.NoError(t, err) + for _, ranger := range rangers { + actual, err := ranger.CoveringNetworks(network.IPNet) + assert.NoError(t, err) + assert.Equal(t, len(expected), len(actual)) + for _, network := range actual { + assert.Contains(t, expected, network) + } + } + } +} + /* ****************************************************************** Benchmarks. diff --git a/go.mod b/go.mod index a35ea91..ed1524f 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,16 @@ -module github.com/yl2chen/cidranger +module github.com/Ramzeth/cidranger -go 1.13 +go 1.17 require ( - github.com/stretchr/testify v1.6.1 + github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/testify v1.4.0 + github.com/yl2chen/cidranger v1.0.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index d063842..552e5fe 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,19 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/net/ip.go b/net/ip.go index 75cb356..fad9b28 100644 --- a/net/ip.go +++ b/net/ip.go @@ -88,6 +88,8 @@ func (n NetworkNumber) ToIP() net.IP { } if len(ip) == net.IPv4len { ip = net.IPv4(ip[0], ip[1], ip[2], ip[3]) + // Convert to 4-byte value, because parseCIDR returns 4-byte IPv4 address - https://github.com/golang/go/issues/41214 + ip = ip.To4() } return ip } @@ -144,6 +146,20 @@ func (n NetworkNumber) Bit(position uint) (uint32, error) { return (n[idx] >> rShift) & 1, nil } +// FlipNthBit reverses the bit value at position. Position numbering is LSB 0. +func (n *NetworkNumber) FlipNthBit(position uint) error { + if int(position) > len(*n)*BitsPerUint32-1 { + return ErrInvalidBitPosition + } + idx := len(*n) - 1 - int(position/BitsPerUint32) + bitUintPosition := position % 32 + XORMask := 1 << bitUintPosition + //byteNum := net.IPv6len - (position / 8) - 1 + // getByteIndexOfBit(bitNum) + (*n)[idx] ^= uint32(XORMask) + return nil +} + // LeastCommonBitPosition returns the smallest position of the preceding common // bits of the 2 network numbers, and returns an error ErrNoGreatestCommonBit // if the two network number diverges from the first bit. @@ -209,7 +225,7 @@ func (n Network) Contains(nn NetworkNumber) bool { return true } -// Contains returns true if Network covers o, false otherwise +// Covers returns true if Network covers o, false otherwise func (n Network) Covers(o Network) bool { if len(n.Number) != len(o.Number) { return false diff --git a/net/ip_test.go b/net/ip_test.go index 1e915df..dd6cf9c 100644 --- a/net/ip_test.go +++ b/net/ip_test.go @@ -1,6 +1,7 @@ package net import ( + "errors" "math" "net" "testing" @@ -75,6 +76,52 @@ func TestNetworkNumberBit(t *testing.T) { } } +func TestNetworkNumber_FlipNthBit(t *testing.T) { + cases := []struct { + initial NetworkNumber + position uint + expected NetworkNumber + name string + expectedErr error + }{ + { + NewNetworkNumber(net.ParseIP("192.168.0.0")), + 8, + // 192.168.1.0 + NetworkNumber{0b11000000101010000000000100000000}, + "Flip bit 8", + nil, + }, + { + NewNetworkNumber(net.ParseIP("128.0.0.1")), + 31, + // 0.0.0.1 + NetworkNumber{0b00000000000000000000000000000001}, + "Flip bit 31", + nil, + }, + { + NewNetworkNumber(net.ParseIP("128.0.0.1")), + 32, + // 0.0.0.1 + NetworkNumber{0b00000000000000000000000000000001}, + "error in position", + errors.New("bit position not valid"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual := tc.initial + err := actual.FlipNthBit(tc.position) + assert.Equal(t, tc.expectedErr, err) + if err == nil { + assert.Equal(t, tc.expected, actual) + + } + }) + } +} + func TestNetworkNumberBitError(t *testing.T) { cases := []struct { ip NetworkNumber diff --git a/trie.go b/trie.go index 31976bd..6e9bffd 100644 --- a/trie.go +++ b/trie.go @@ -5,7 +5,7 @@ import ( "net" "strings" - rnet "github.com/yl2chen/cidranger/net" + rnet "github.com/Ramzeth/cidranger/net" ) // prefixTrie is a path-compressed (PC) trie implementation of the @@ -124,6 +124,13 @@ func (p *prefixTrie) CoveredNetworks(network net.IPNet) ([]RangerEntry, error) { return p.coveredNetworks(net) } +// Covering returns the list of RangerEntry(s) the given ipnet +// is covered by. It's like ContainingNetworks() for ipnet. +func (p *prefixTrie) CoveringNetworks(network net.IPNet) ([]RangerEntry, error) { + net := rnet.NewNetwork(network) + return p.coveringNetworks(net) +} + // Len returns number of networks in ranger. func (p *prefixTrie) Len() int { return p.size @@ -145,6 +152,43 @@ func (p *prefixTrie) String() string { p.targetBitPosition(), p.hasEntry(), strings.Join(children, "")) } +// Returns adjacent entries to givent entry, identified by given network. Returns nil if adjacent entry not exists. +// Adjacent networks are networks with only different lower bit in network address, e.g. 192.168.0.0/24 and 192.168.1.0/24 +// That networks can be mergeg, e.g 192.168.0.0/24 + 192.168.1.0/24 = 192.168.0.0/23 +func (p *prefixTrie) Adjacent(network net.IPNet) (RangerEntry, error) { + adjacentNumber := rnet.NewNetworkNumber(network.IP) + ones, size := network.Mask.Size() + if ones == 0 { + // It's a full network, e.g. 0.0.0.0/0, there is no adjacents + return nil, nil + } + position := size - ones + err := adjacentNumber.FlipNthBit(uint(position)) + if err != nil { + return nil, err + } + adjacentNet := rnet.NewNetwork(net.IPNet{adjacentNumber.ToIP(), network.Mask}) + return p.adjacent(adjacentNet) +} + +func (p *prefixTrie) adjacent(network rnet.Network) (RangerEntry, error) { + if p.hasEntry() && p.network.Equal(network) { + return p.entry, nil + } + if p.targetBitPosition() < 0 { + return nil, nil + } + bit, err := p.targetBitFromIP(network.Number) + if err != nil { + return nil, err + } + child := p.children[bit] + if child != nil { + return child.adjacent(network) + } + return nil, nil +} + func (p *prefixTrie) contains(number rnet.NetworkNumber) (bool, error) { if !p.network.Contains(number) { return false, nil @@ -217,6 +261,38 @@ func (p *prefixTrie) coveredNetworks(network rnet.Network) ([]RangerEntry, error return results, nil } +func (p *prefixTrie) coveringNetworks(network rnet.Network) ([]RangerEntry, error) { + var results []RangerEntry + if !p.network.Covers(network) { + return results, nil + } + if p.hasEntry() { + results = []RangerEntry{p.entry} + } + if p.targetBitPosition() < 0 { + return results, nil + } + bit, err := p.targetBitFromIP(network.Number) + if err != nil { + return nil, err + } + child := p.children[bit] + if child != nil { + ranges, err := child.coveringNetworks(network) + if err != nil { + return nil, err + } + if len(ranges) > 0 { + if len(results) > 0 { + results = append(results, ranges...) + } else { + results = ranges + } + } + } + return results, nil +} + func (p *prefixTrie) insert(network rnet.Network, entry RangerEntry) (bool, error) { if p.network.Equal(network) { sizeIncreased := p.entry == nil diff --git a/trie_test.go b/trie_test.go index 04f2900..b2d36fb 100644 --- a/trie_test.go +++ b/trie_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" + rnet "github.com/Ramzeth/cidranger/net" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - rnet "github.com/yl2chen/cidranger/net" ) func getAllByVersion(version rnet.IPVersion) *net.IPNet { @@ -397,7 +398,59 @@ func TestPrefixTrieContainingNetworks(t *testing.T) { } } -type coveredNetworkTest struct { +func TestPrefixTrie_Adjacent(t *testing.T) { + cases := []struct { + version rnet.IPVersion + inserts []string + network string + expected string + name string + }{ + { + rnet.IPv4, + []string{"192.168.0.0/24"}, + "192.168.1.0/24", + "192.168.0.0/24", + "check /24 adjacent", + }, + { + rnet.IPv4, + []string{"128.0.0.5/1"}, + "0.0.5.0/1", + "128.0.0.0/1", + "check /1 adjacent, with some bits in ip address range", + }, + { + rnet.IPv4, + []string{"0.0.0.0/0"}, + "0.0.0.0/0", + "", + "0.0.0.0/0 can't have adjacent", + }, + } + log.SetLevel(log.TraceLevel) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + trie := newPrefixTree(tc.version) + for _, insert := range tc.inserts { + _, network, _ := net.ParseCIDR(insert) + err := trie.Insert(NewBasicRangerEntry(*network)) + assert.NoError(t, err) + } + _, testNet, _ := net.ParseCIDR(tc.network) + entry, err := trie.Adjacent(*testNet) + assert.NoError(t, err) + netString := "" + if entry != nil { + entryNet := entry.Network() + netString = entryNet.String() + } + assert.Equal(t, tc.expected, netString) + }) + } +} + +type networkTest struct { version rnet.IPVersion inserts []string search string @@ -405,7 +458,7 @@ type coveredNetworkTest struct { name string } -var coveredNetworkTests = []coveredNetworkTest{ +var coveredNetworkTests = []networkTest{ { rnet.IPv4, []string{"192.168.0.0/24"}, @@ -413,6 +466,13 @@ var coveredNetworkTests = []coveredNetworkTest{ []string{"192.168.0.0/24"}, "basic covered networks", }, + { + rnet.IPv4, + []string{"192.168.0.0/24"}, + "192.168.0.0/24", + []string{"192.168.0.0/24"}, + "covered of equal networks", + }, { rnet.IPv4, []string{"192.168.0.0/24"}, @@ -469,6 +529,77 @@ var coveredNetworkTests = []coveredNetworkTest{ }, } +var coveringNetworkTests = []networkTest{ + { + rnet.IPv4, + []string{"192.168.0.0/16"}, + "192.168.0.0/24", + []string{"192.168.0.0/16"}, + "basic covering networks", + }, + { + rnet.IPv4, + []string{"192.168.0.0/24"}, + "192.168.0.0/24", + []string{"192.168.0.0/24"}, + "covering of equal networks", + }, + { + rnet.IPv4, + []string{"10.1.0.0/16"}, + "192.168.0.0/24", + nil, + "nothing", + }, + { + rnet.IPv4, + []string{"192.168.0.0/16", "192.168.0.0/24"}, + "192.168.0.0/25", + []string{"192.168.0.0/16", "192.168.0.0/24"}, + "multiple networks", + }, + { + rnet.IPv4, + []string{"192.168.0.0/16", "192.168.0.0/24", "192.168.0.0/25"}, + "192.168.0.1/32", + []string{"192.168.0.0/16", "192.168.0.0/24", "192.168.0.0/25"}, + "multiple networks 2", + }, + { + rnet.IPv4, + []string{"192.168.0.0/16"}, + "192.168.1.1/32", + []string{"192.168.0.0/16"}, + "leaf", + }, + { + rnet.IPv4, + []string{"0.0.0.0/0", "192.168.1.1/32"}, + "192.168.0.0/16", + []string{"0.0.0.0/0"}, + "leaf with root", + }, + { + rnet.IPv4, + []string{ + "0.0.0.0/0", "192.168.0.0/24", "192.168.1.1/32", + "10.1.0.0/16", "10.1.1.0/24", + }, + "192.168.0.0/16", + []string{"0.0.0.0/0"}, + "path not taken", + }, + { + rnet.IPv4, + []string{ + "192.168.0.0/16", + }, + "192.168.0.0/15", + nil, + "only masks different", + }, +} + func TestPrefixTrieCoveredNetworks(t *testing.T) { for _, tc := range coveredNetworkTests { t.Run(tc.name, func(t *testing.T) { @@ -492,6 +623,29 @@ func TestPrefixTrieCoveredNetworks(t *testing.T) { } } +func TestPrefixTrieCoveringNetworks(t *testing.T) { + for _, tc := range coveringNetworkTests { + t.Run(tc.name, func(t *testing.T) { + trie := newPrefixTree(tc.version) + for _, insert := range tc.inserts { + _, network, _ := net.ParseCIDR(insert) + err := trie.Insert(NewBasicRangerEntry(*network)) + assert.NoError(t, err) + } + var expectedEntries []RangerEntry + for _, network := range tc.networks { + _, net, _ := net.ParseCIDR(network) + expectedEntries = append(expectedEntries, + NewBasicRangerEntry(*net)) + } + _, snet, _ := net.ParseCIDR(tc.search) + networks, err := trie.CoveringNetworks(*snet) + assert.NoError(t, err) + assert.Equal(t, expectedEntries, networks) + }) + } +} + func TestTrieMemUsage(t *testing.T) { if testing.Short() { t.Skip("Skipping memory test in `-short` mode") diff --git a/version.go b/version.go index 2c3fe2b..0a828c6 100644 --- a/version.go +++ b/version.go @@ -3,7 +3,7 @@ package cidranger import ( "net" - rnet "github.com/yl2chen/cidranger/net" + rnet "github.com/Ramzeth/cidranger/net" ) type rangerFactory func(rnet.IPVersion) Ranger @@ -61,11 +61,28 @@ func (v *versionedRanger) CoveredNetworks(network net.IPNet) ([]RangerEntry, err return ranger.CoveredNetworks(network) } +func (v *versionedRanger) CoveringNetworks(network net.IPNet) ([]RangerEntry, error) { + ranger, err := v.getRangerForIP(network.IP) + if err != nil { + return nil, err + } + return ranger.CoveringNetworks(network) +} + // Len returns number of networks in ranger. func (v *versionedRanger) Len() int { return v.ipV4Ranger.Len() + v.ipV6Ranger.Len() } +// Len returns number of networks in ranger. +func (v *versionedRanger) Adjacent(network net.IPNet) (RangerEntry, error) { + ranger, err := v.getRangerForIP(network.IP) + if err != nil { + return nil, err + } + return ranger.Adjacent(network) +} + func (v *versionedRanger) getRangerForIP(ip net.IP) (Ranger, error) { if ip.To4() != nil { return v.ipV4Ranger, nil