Skip to content

Commit d2f94f9

Browse files
authored
Merge pull request #193 from ipinfo/talhahameed/be-2715-handle-adjacent-prefixes-in-ipinfo-tool-aggregate
Handle adjacent prefixes in ipinfo tool aggregate
2 parents 32f1e7e + 2aedf33 commit d2f94f9

File tree

3 files changed

+148
-125
lines changed

3 files changed

+148
-125
lines changed

ipinfo/cmd_tool_aggregate.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ func printHelpToolAggregate() {
2323
`Usage: %s tool aggregate [<opts>] <cidr | ip | ip-range | filepath>
2424
2525
Description:
26-
Accepts IPs, IP ranges, and CIDRs, aggregating them efficiently.
27-
Input can be IPs, IP ranges, CIDRs, and/or filepath to a file
28-
containing any of these. Works for both IPv4 and IPv6.
26+
Accepts IPv4 IPs and CIDRs, aggregating them efficiently.
2927
3028
If input contains single IPs, it tries to merge them into the input CIDRs,
3129
otherwise they are printed to the output as they are.
@@ -37,9 +35,6 @@ Examples:
3735
# Aggregate two CIDRs.
3836
$ %[1]s tool aggregate 1.1.1.0/30 1.1.1.0/28
3937
40-
# Aggregate IP range and CIDR.
41-
$ %[1]s tool aggregate 1.1.1.0-1.1.1.244 1.1.1.0/28
42-
4338
# Aggregate enteries from 2 files.
4439
$ %[1]s tool aggregate /path/to/file1.txt /path/to/file2.txt
4540

lib/cidr.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package lib
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"math"
7+
"net"
8+
"sort"
9+
)
10+
11+
// CIDR represens a Classless Inter-Domain Routing structure.
12+
type CIDR struct {
13+
IP net.IP
14+
Network *net.IPNet
15+
}
16+
17+
// newCidr creates a newCidr CIDR structure.
18+
func newCidr(s string) *CIDR {
19+
ip, ipnet, err := net.ParseCIDR(s)
20+
if err != nil {
21+
panic(err)
22+
}
23+
return &CIDR{
24+
IP: ip,
25+
Network: ipnet,
26+
}
27+
}
28+
29+
func (c *CIDR) String() string {
30+
return c.Network.String()
31+
}
32+
33+
// MaskLen returns a network mask length.
34+
func (c *CIDR) MaskLen() uint32 {
35+
i, _ := c.Network.Mask.Size()
36+
return uint32(i)
37+
}
38+
39+
// PrefixUint32 returns a prefix.
40+
func (c *CIDR) PrefixUint32() uint32 {
41+
return binary.BigEndian.Uint32(c.IP.To4())
42+
}
43+
44+
// Size returns a size of a CIDR range.
45+
func (c *CIDR) Size() int {
46+
ones, bits := c.Network.Mask.Size()
47+
return int(math.Pow(2, float64(bits-ones)))
48+
}
49+
50+
// list returns a slice of sorted CIDR structures.
51+
func list(s []string) []*CIDR {
52+
out := make([]*CIDR, 0)
53+
for _, c := range s {
54+
out = append(out, newCidr(c))
55+
}
56+
sort.Sort(cidrSort(out))
57+
return out
58+
}
59+
60+
type cidrSort []*CIDR
61+
62+
func (s cidrSort) Len() int { return len(s) }
63+
func (s cidrSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
64+
65+
func (s cidrSort) Less(i, j int) bool {
66+
cmp := bytes.Compare(s[i].IP, s[j].IP)
67+
return cmp < 0 || (cmp == 0 && s[i].MaskLen() < s[j].MaskLen())
68+
}

lib/cmd_tool_aggregate.go

Lines changed: 79 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ package lib
22

33
import (
44
"bufio"
5-
"bytes"
65
"fmt"
76
"io"
87
"net"
98
"os"
10-
"sort"
119
"strings"
1210

1311
"github.com/spf13/pflag"
@@ -55,72 +53,21 @@ func CmdToolAggregate(
5553
return nil
5654
}
5755

58-
// Parses a list of CIDRs.
59-
parseCIDRs := func(cidrs []string) []net.IPNet {
60-
parsedCIDRs := make([]net.IPNet, 0)
61-
for _, cidrStr := range cidrs {
62-
_, ipNet, err := net.ParseCIDR(cidrStr)
63-
if err != nil {
64-
if !f.Quiet {
65-
fmt.Printf("Invalid CIDR: %s\n", cidrStr)
66-
}
67-
continue
68-
}
69-
parsedCIDRs = append(parsedCIDRs, *ipNet)
70-
}
71-
72-
return parsedCIDRs
73-
}
74-
7556
// Input parser.
76-
parseInput := func(rows []string) ([]net.IPNet, []net.IP) {
77-
parsedCIDRs := make([]net.IPNet, 0)
57+
parseInput := func(rows []string) ([]string, []net.IP) {
58+
parsedCIDRs := make([]string, 0)
7859
parsedIPs := make([]net.IP, 0)
79-
var separator string
8060
for _, rowStr := range rows {
8161
if strings.ContainsAny(rowStr, ",-") {
82-
if delim := strings.ContainsRune(rowStr, ','); delim {
83-
separator = ","
84-
} else {
85-
separator = "-"
86-
}
87-
88-
ipRange := strings.Split(rowStr, separator)
89-
if len(ipRange) != 2 {
90-
if !f.Quiet {
91-
fmt.Printf("Invalid IP range: %s\n", rowStr)
92-
}
93-
continue
94-
}
95-
96-
if strings.ContainsRune(rowStr, ':') {
97-
cidrs, err := CIDRsFromIP6RangeStrRaw(rowStr)
98-
if err == nil {
99-
parsedCIDRs = append(parsedCIDRs, parseCIDRs(cidrs)...)
100-
continue
101-
} else {
102-
if !f.Quiet {
103-
fmt.Printf("Invalid IP range %s. Err: %v\n", rowStr, err)
104-
}
105-
continue
106-
}
107-
} else {
108-
cidrs, err := CIDRsFromIPRangeStrRaw(rowStr)
109-
if err == nil {
110-
parsedCIDRs = append(parsedCIDRs, parseCIDRs(cidrs)...)
111-
continue
112-
} else {
113-
if !f.Quiet {
114-
fmt.Printf("Invalid IP range %s. Err: %v\n", rowStr, err)
115-
}
116-
continue
117-
}
118-
}
62+
continue
11963
} else if strings.ContainsRune(rowStr, '/') {
120-
parsedCIDRs = append(parsedCIDRs, parseCIDRs([]string{rowStr})...)
64+
_, ipnet, err := net.ParseCIDR(rowStr)
65+
if err == nil && IsCIDRIPv4(ipnet) {
66+
parsedCIDRs = append(parsedCIDRs, []string{rowStr}...)
67+
}
12168
continue
12269
} else {
123-
if ip := net.ParseIP(rowStr); ip != nil {
70+
if ip := net.ParseIP(rowStr); IsIPv4(ip) {
12471
parsedIPs = append(parsedIPs, ip)
12572
} else {
12673
if !f.Quiet {
@@ -165,7 +112,7 @@ func CmdToolAggregate(
165112
}
166113

167114
// Vars to contain CIDRs/IPs from all input sources.
168-
parsedCIDRs := make([]net.IPNet, 0)
115+
parsedCIDRs := make([]string, 0)
169116
parsedIPs := make([]net.IP, 0)
170117

171118
// Collect CIDRs/IPs from stdin.
@@ -187,93 +134,106 @@ func CmdToolAggregate(
187134
rows := scanrdr(file)
188135
file.Close()
189136
cidrs, ips := parseInput(rows)
137+
190138
parsedCIDRs = append(parsedCIDRs, cidrs...)
191139
parsedIPs = append(parsedIPs, ips...)
192140
}
193141

194-
// Sort and merge collected CIDRs and IPs.
195-
aggregatedCIDRs := aggregateCIDRs(parsedCIDRs)
142+
adjacentCombined := combineAdjacent(stripOverlapping(list(parsedCIDRs)))
143+
196144
outlierIPs := make([]net.IP, 0)
197-
length := len(aggregatedCIDRs)
198-
for _, ip := range parsedIPs {
199-
for i, cidr := range aggregatedCIDRs {
200-
if cidr.Contains(ip) {
201-
break
202-
} else if i == length-1 {
203-
outlierIPs = append(outlierIPs, ip)
145+
length := len(adjacentCombined)
146+
if length != 0 {
147+
for _, ip := range parsedIPs {
148+
for i, cidr := range adjacentCombined {
149+
if cidr.Network.Contains(ip) {
150+
break
151+
} else if i == length-1 {
152+
outlierIPs = append(outlierIPs, ip)
153+
}
204154
}
205155
}
156+
} else {
157+
outlierIPs = append(outlierIPs, parsedIPs...)
206158
}
207159

208160
// Print the aggregated CIDRs.
209-
for _, r := range aggregatedCIDRs {
161+
for _, r := range adjacentCombined {
210162
fmt.Println(r.String())
211163
}
212164

213-
// Print outliers.
165+
// Print the outlierIPs.
214166
for _, r := range outlierIPs {
215167
fmt.Println(r.String())
216168
}
217169

218170
return nil
219171
}
220172

221-
// Helper function to aggregate IP ranges.
222-
func aggregateCIDRs(cidrs []net.IPNet) []net.IPNet {
223-
aggregatedCIDRs := make([]net.IPNet, 0)
224-
225-
// Sort CIDRs by starting IP.
226-
sortCIDRs(cidrs)
227-
228-
for _, r := range cidrs {
229-
if len(aggregatedCIDRs) == 0 {
230-
aggregatedCIDRs = append(aggregatedCIDRs, r)
173+
// stripOverlapping returns a slice of CIDR structures with overlapping ranges
174+
// stripped.
175+
func stripOverlapping(s []*CIDR) []*CIDR {
176+
l := len(s)
177+
for i := 0; i < l-1; i++ {
178+
if s[i] == nil {
231179
continue
232180
}
233-
234-
last := len(aggregatedCIDRs) - 1
235-
prev := aggregatedCIDRs[last]
236-
237-
if canAggregate(prev, r) {
238-
// Merge overlapping CIDRs.
239-
aggregatedCIDRs[last] = aggregateCIDR(prev, r)
240-
} else {
241-
aggregatedCIDRs = append(aggregatedCIDRs, r)
181+
for j := i + 1; j < l; j++ {
182+
if overlaps(s[j], s[i]) {
183+
s[j] = nil
184+
}
242185
}
243186
}
244-
245-
return aggregatedCIDRs
246-
}
247-
248-
// Helper function to sort IP ranges by starting IP.
249-
func sortCIDRs(ipRanges []net.IPNet) {
250-
sort.SliceStable(ipRanges, func(i, j int) bool {
251-
return bytes.Compare(ipRanges[i].IP, ipRanges[j].IP) < 0
252-
})
187+
return filter(s)
253188
}
254189

255-
// Helper function to check if two CIDRs can be aggregated.
256-
func canAggregate(r1, r2 net.IPNet) bool {
257-
return r1.Contains(r2.IP) || r2.Contains(r1.IP)
190+
func overlaps(a, b *CIDR) bool {
191+
return (a.PrefixUint32() / (1 << (32 - b.MaskLen()))) ==
192+
(b.PrefixUint32() / (1 << (32 - b.MaskLen())))
258193
}
259194

260-
// Helper function to aggregate two CIDRs.
261-
func aggregateCIDR(r1, r2 net.IPNet) net.IPNet {
262-
mask1, _ := r1.Mask.Size()
263-
mask2, _ := r2.Mask.Size()
264-
265-
ipLen := net.IPv6len * 8
266-
if r1.IP.To4() != nil {
267-
ipLen = net.IPv4len * 8
268-
}
195+
// combineAdjacent returns a slice of CIDR structures with adjacent ranges
196+
// combined.
197+
func combineAdjacent(s []*CIDR) []*CIDR {
198+
for {
199+
found := false
200+
l := len(s)
201+
for i := 0; i < l-1; i++ {
202+
if s[i] == nil {
203+
continue
204+
}
205+
for j := i + 1; j < l; j++ {
206+
if s[j] == nil {
207+
continue
208+
}
209+
if adjacent(s[i], s[j]) {
210+
c := fmt.Sprintf("%s/%d", s[i].IP.String(), s[i].MaskLen()-1)
211+
s[i] = newCidr(c)
212+
s[j] = nil
213+
found = true
214+
}
215+
}
216+
}
269217

270-
// Find the common prefix length
271-
commonPrefixLen := mask1
272-
if mask2 < commonPrefixLen {
273-
commonPrefixLen = mask2
218+
if !found {
219+
break
220+
}
274221
}
222+
return filter(s)
223+
}
275224

276-
commonPrefix := r1.IP.Mask(net.CIDRMask(commonPrefixLen, ipLen))
225+
func adjacent(a, b *CIDR) bool {
226+
return (a.MaskLen() == b.MaskLen()) &&
227+
(a.PrefixUint32()%(2<<(32-b.MaskLen())) == 0) &&
228+
(b.PrefixUint32()-a.PrefixUint32() == (1 << (32 - a.MaskLen())))
229+
}
277230

278-
return net.IPNet{IP: commonPrefix, Mask: net.CIDRMask(commonPrefixLen, ipLen)}
231+
func filter(s []*CIDR) []*CIDR {
232+
out := s[:0]
233+
for _, x := range s {
234+
if x != nil {
235+
out = append(out, x)
236+
}
237+
}
238+
return out
279239
}

0 commit comments

Comments
 (0)