Skip to content

Commit 664f3fe

Browse files
authored
Merge pull request #65 from oalders/greg/networks-within-poc
Add NetworksWithin
2 parents 6a033e6 + 5800f39 commit 664f3fe

File tree

4 files changed

+283
-11
lines changed

4 files changed

+283
-11
lines changed

example_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,46 @@ func ExampleReader_Networks() {
155155
// 2003::/24: Cable/DSL
156156

157157
}
158+
159+
// This example demonstrates how to iterate over all networks in the
160+
// database which are contained within an arbitrary network.
161+
func ExampleReader_NetworksWithin() {
162+
db, err := maxminddb.Open("test-data/test-data/GeoIP2-Connection-Type-Test.mmdb")
163+
if err != nil {
164+
log.Fatal(err)
165+
}
166+
defer db.Close()
167+
168+
record := struct {
169+
Domain string `maxminddb:"connection_type"`
170+
}{}
171+
172+
_, network, err := net.ParseCIDR("1.0.0.0/8")
173+
if err != nil {
174+
log.Fatal(err)
175+
}
176+
177+
networks := db.NetworksWithin(network)
178+
for networks.Next() {
179+
subnet, err := networks.Network(&record)
180+
if err != nil {
181+
log.Fatal(err)
182+
}
183+
fmt.Printf("%s: %s\n", subnet.String(), record.Domain)
184+
}
185+
if networks.Err() != nil {
186+
log.Fatal(networks.Err())
187+
}
188+
189+
// Output:
190+
//1.0.0.0/24: Dialup
191+
//1.0.1.0/24: Cable/DSL
192+
//1.0.2.0/23: Dialup
193+
//1.0.4.0/22: Dialup
194+
//1.0.8.0/21: Dialup
195+
//1.0.16.0/20: Dialup
196+
//1.0.32.0/19: Dialup
197+
//1.0.64.0/18: Dialup
198+
//1.0.128.0/17: Dialup
199+
200+
}

reader.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,20 @@ func (r *Reader) lookupPointer(ip net.IP) (uint, int, net.IP, error) {
249249
if bitCount == 32 {
250250
node = r.ipv4Start
251251
}
252+
node, prefixLength := r.traverseTree(ip, node, bitCount)
252253

254+
nodeCount := r.Metadata.NodeCount
255+
if node == nodeCount {
256+
// Record is empty
257+
return 0, prefixLength, ip, nil
258+
} else if node > nodeCount {
259+
return node, prefixLength, ip, nil
260+
}
261+
262+
return 0, prefixLength, ip, newInvalidDatabaseError("invalid node in search tree")
263+
}
264+
265+
func (r *Reader) traverseTree(ip net.IP, node uint, bitCount uint) (uint, int) {
253266
nodeCount := r.Metadata.NodeCount
254267

255268
i := uint(0)
@@ -263,14 +276,8 @@ func (r *Reader) lookupPointer(ip net.IP) (uint, int, net.IP, error) {
263276
node = r.nodeReader.readRight(offset)
264277
}
265278
}
266-
if node == nodeCount {
267-
// Record is empty
268-
return 0, int(i), ip, nil
269-
} else if node > nodeCount {
270-
return node, int(i), ip, nil
271-
}
272279

273-
return 0, int(i), ip, newInvalidDatabaseError("invalid node in search tree")
280+
return node, int(i)
274281
}
275282

276283
func (r *Reader) retrieveData(pointer uint, result interface{}) error {

traverse.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package maxminddb
22

3-
import "net"
3+
import (
4+
"net"
5+
)
46

57
// Internal structure used to keep track of nodes we still need to visit.
68
type netNode struct {
@@ -17,22 +19,52 @@ type Networks struct {
1719
err error
1820
}
1921

22+
var allIPv4 = &net.IPNet{IP: make(net.IP, 4), Mask: net.CIDRMask(0, 32)}
23+
var allIPv6 = &net.IPNet{IP: make(net.IP, 16), Mask: net.CIDRMask(0, 128)}
24+
2025
// Networks returns an iterator that can be used to traverse all networks in
2126
// the database.
2227
//
2328
// Please note that a MaxMind DB may map IPv4 networks into several locations
2429
// in an IPv6 database. This iterator will iterate over all of these
2530
// locations separately.
2631
func (r *Reader) Networks() *Networks {
27-
s := 4
32+
var networks *Networks
2833
if r.Metadata.IPVersion == 6 {
29-
s = 16
34+
networks = r.NetworksWithin(allIPv6)
35+
} else {
36+
networks = r.NetworksWithin(allIPv4)
37+
}
38+
39+
return networks
40+
}
41+
42+
// NetworksWithin returns an iterator that can be used to traverse all networks
43+
// in the database which are contained in a given network.
44+
//
45+
// Please note that a MaxMind DB may map IPv4 networks into several locations
46+
// in an IPv6 database. This iterator will iterate over all of these locations
47+
// separately.
48+
//
49+
// If the provided network is contained within a network in the database, the
50+
// iterator will iterate over exactly one network, the containing network.
51+
func (r *Reader) NetworksWithin(network *net.IPNet) *Networks {
52+
ip := network.IP
53+
prefixLength, _ := network.Mask.Size()
54+
55+
if r.Metadata.IPVersion == 6 && len(ip) == net.IPv4len {
56+
ip = net.IP.To16(ip)
57+
prefixLength += 96
3058
}
59+
60+
pointer, bit := r.traverseTree(ip, 0, uint(prefixLength))
3161
return &Networks{
3262
reader: r,
3363
nodes: []netNode{
3464
{
35-
ip: make(net.IP, s),
65+
ip: ip,
66+
bit: uint(bit),
67+
pointer: pointer,
3668
},
3769
},
3870
}

traverse_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package maxminddb
22

33
import (
44
"fmt"
5+
"net"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -46,3 +47,192 @@ func TestNetworksWithInvalidSearchTree(t *testing.T) {
4647
assert.NotNil(t, n.Err(), "no error received when traversing an broken search tree")
4748
assert.Equal(t, n.Err().Error(), "invalid search tree at 128.128.128.128/32")
4849
}
50+
51+
type networkTest struct {
52+
Network string
53+
Database string
54+
Expected []string
55+
}
56+
57+
var tests = []networkTest{
58+
networkTest{
59+
Network: "0.0.0.0/0",
60+
Database: "ipv4",
61+
Expected: []string{
62+
"1.1.1.1/32",
63+
"1.1.1.2/31",
64+
"1.1.1.4/30",
65+
"1.1.1.8/29",
66+
"1.1.1.16/28",
67+
"1.1.1.32/32",
68+
},
69+
},
70+
networkTest{
71+
Network: "1.1.1.1/30",
72+
Database: "ipv4",
73+
Expected: []string{
74+
"1.1.1.1/32",
75+
"1.1.1.2/31",
76+
},
77+
},
78+
networkTest{
79+
Network: "1.1.1.1/32",
80+
Database: "ipv4",
81+
Expected: []string{
82+
"1.1.1.1/32",
83+
},
84+
},
85+
networkTest{
86+
Network: "255.255.255.0/24",
87+
Database: "ipv4",
88+
Expected: []string(nil),
89+
},
90+
networkTest{
91+
Network: "1.1.1.1/32",
92+
Database: "mixed",
93+
Expected: []string{
94+
"1.1.1.1/32",
95+
},
96+
},
97+
networkTest{
98+
Network: "255.255.255.0/24",
99+
Database: "mixed",
100+
Expected: []string(nil),
101+
},
102+
networkTest{
103+
Network: "::1:ffff:ffff/128",
104+
Database: "ipv6",
105+
Expected: []string{
106+
"::1:ffff:ffff/128",
107+
},
108+
},
109+
networkTest{
110+
Network: "::/0",
111+
Database: "ipv6",
112+
Expected: []string{
113+
"::1:ffff:ffff/128",
114+
"::2:0:0/122",
115+
"::2:0:40/124",
116+
"::2:0:50/125",
117+
"::2:0:58/127",
118+
},
119+
},
120+
networkTest{
121+
Network: "::2:0:40/123",
122+
Database: "ipv6",
123+
Expected: []string{
124+
"::2:0:40/124",
125+
"::2:0:50/125",
126+
"::2:0:58/127",
127+
},
128+
},
129+
networkTest{
130+
Network: "0:0:0:0:0:ffff:ffff:ff00/120",
131+
Database: "ipv6",
132+
Expected: []string(nil),
133+
},
134+
networkTest{
135+
Network: "0.0.0.0/0",
136+
Database: "mixed",
137+
Expected: []string{
138+
"1.1.1.1/32",
139+
"1.1.1.2/31",
140+
"1.1.1.4/30",
141+
"1.1.1.8/29",
142+
"1.1.1.16/28",
143+
"1.1.1.32/32",
144+
},
145+
},
146+
networkTest{
147+
Network: "1.1.1.16/28",
148+
Database: "mixed",
149+
Expected: []string{
150+
"1.1.1.16/28",
151+
},
152+
},
153+
networkTest{
154+
Network: "::/0",
155+
Database: "ipv4",
156+
Expected: []string{
157+
"101:101::/32",
158+
"101:102::/31",
159+
"101:104::/30",
160+
"101:108::/29",
161+
"101:110::/28",
162+
"101:120::/32",
163+
},
164+
},
165+
networkTest{
166+
Network: "101:104::/30",
167+
Database: "ipv4",
168+
Expected: []string{
169+
"101:104::/30",
170+
},
171+
},
172+
}
173+
174+
func TestNetworksWithin(t *testing.T) {
175+
for _, v := range tests {
176+
for _, recordSize := range []uint{24, 28, 32} {
177+
fileName := testFile(fmt.Sprintf("MaxMind-DB-test-%s-%d.mmdb", v.Database, recordSize))
178+
reader, err := Open(fileName)
179+
require.Nil(t, err, "unexpected error while opening database: %v", err)
180+
defer reader.Close()
181+
182+
_, network, err := net.ParseCIDR(v.Network)
183+
assert.Nil(t, err)
184+
n := reader.NetworksWithin(network)
185+
var innerIPs []string
186+
187+
for n.Next() {
188+
record := struct {
189+
IP string `maxminddb:"ip"`
190+
}{}
191+
network, err := n.Network(&record)
192+
assert.Nil(t, err)
193+
innerIPs = append(innerIPs, network.String())
194+
}
195+
196+
assert.Equal(t, v.Expected, innerIPs)
197+
assert.Nil(t, n.Err())
198+
}
199+
}
200+
}
201+
202+
var geoIPTests = []networkTest{
203+
networkTest{
204+
Network: "81.2.69.128/26",
205+
Database: "GeoIP2-Country-Test.mmdb",
206+
Expected: []string{
207+
"81.2.69.142/31",
208+
"81.2.69.144/28",
209+
"81.2.69.160/27",
210+
},
211+
},
212+
}
213+
214+
func TestGeoIPNetworksWithin(t *testing.T) {
215+
for _, v := range geoIPTests {
216+
fileName := testFile(v.Database)
217+
reader, err := Open(fileName)
218+
require.Nil(t, err, "unexpected error while opening database: %v", err)
219+
defer reader.Close()
220+
221+
_, network, err := net.ParseCIDR(v.Network)
222+
assert.Nil(t, err)
223+
n := reader.NetworksWithin(network)
224+
var innerIPs []string
225+
226+
for n.Next() {
227+
record := struct {
228+
IP string `maxminddb:"ip"`
229+
}{}
230+
network, err := n.Network(&record)
231+
assert.Nil(t, err)
232+
innerIPs = append(innerIPs, network.String())
233+
}
234+
235+
assert.Equal(t, v.Expected, innerIPs)
236+
assert.Nil(t, n.Err())
237+
}
238+
}

0 commit comments

Comments
 (0)