Skip to content

Commit 80f2b98

Browse files
committed
Improve performance on gcutil.ParseIPRange for IPv6
1 parent 9029163 commit 80f2b98

File tree

4 files changed

+232
-75
lines changed

4 files changed

+232
-75
lines changed

pkg/gcutil/iprange.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package gcutil
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
var (
12+
ErrInvalidIP = errors.New("invalid IP address")
13+
ErrInvalidSubnet = errors.New("invalid IP address or subnet mask")
14+
)
15+
16+
// ParseIPRange takes a single IP address or an IP range of the form "networkIP/netmaskbits" and
17+
// gives the starting IP and ending IP in the subnet
18+
//
19+
// More info: https://en.wikipedia.org/wiki/Subnet
20+
func ParseIPRange(ipOrCIDR string) (string, string, error) {
21+
parts := strings.Split(ipOrCIDR, "/")
22+
ip := net.ParseIP(parts[0])
23+
if ip == nil {
24+
return "", "", ErrInvalidIP
25+
}
26+
ipv4 := ip.To4()
27+
if ipv4 != nil {
28+
ip = ipv4
29+
}
30+
if len(parts) == 1 {
31+
// single IP
32+
return ipOrCIDR, ipOrCIDR, nil
33+
} else if len(parts) == 2 {
34+
// IP/mask
35+
netBits, err := strconv.Atoi(parts[1])
36+
if err != nil {
37+
return "", "", err
38+
}
39+
mask := net.CIDRMask(netBits, len(ip)*8)
40+
broadcast := net.IP(make([]byte, len(ip)))
41+
for i := range ip {
42+
broadcast[i] = ip[i] | ^mask[i]
43+
}
44+
return ip.Mask(mask).String(), broadcast.String(), nil
45+
}
46+
return "", "", ErrInvalidSubnet
47+
}
48+
49+
// GetIPRangeSubnet returns the smallest subnet that contains the start and end
50+
// IP addresses, and any errors that occured
51+
func GetIPRangeSubnet(start string, end string) (*net.IPNet, error) {
52+
startIP := net.ParseIP(start)
53+
endIP := net.ParseIP(end)
54+
if startIP == nil {
55+
return nil, fmt.Errorf("invalid IP address %s", start)
56+
}
57+
if endIP == nil {
58+
return nil, fmt.Errorf("invalid IP address %s", end)
59+
}
60+
if len(startIP) != len(endIP) {
61+
return nil, errors.New("ip addresses must both be IPv4 or IPv6")
62+
}
63+
64+
if startIP.To4() != nil {
65+
startIP = startIP.To4()
66+
endIP = endIP.To4()
67+
}
68+
69+
bits := 0
70+
var ipn *net.IPNet
71+
for b := range startIP {
72+
if startIP[b] == endIP[b] {
73+
bits += 8
74+
continue
75+
}
76+
for i := 7; i >= 0; i-- {
77+
if startIP[b]&(1<<i) == endIP[b]&(1<<i) {
78+
bits++
79+
continue
80+
}
81+
ipn = &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}
82+
return ipn, nil
83+
}
84+
}
85+
return &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}, nil
86+
}

pkg/gcutil/iprange_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package gcutil
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestIPRangeErrOnInvalidIP(t *testing.T) {
10+
_, _, err := ParseIPRange("not an IP")
11+
assert.Error(t, err)
12+
_, _, err = ParseIPRange("")
13+
assert.Error(t, err)
14+
_, _, err = ParseIPRange("192.168.56.0/")
15+
assert.Error(t, err)
16+
}
17+
18+
func TestIPRangeSingleIP(t *testing.T) {
19+
start, end, err := ParseIPRange("192.168.56.1")
20+
assert.NoError(t, err)
21+
assert.Equal(t, start, end)
22+
start, end, err = ParseIPRange("2801::")
23+
assert.NoError(t, err)
24+
assert.Equal(t, start, end)
25+
}
26+
27+
func TestIPRangeIPv4Range(t *testing.T) {
28+
ranges := []string{"192.168.56.0/24", "192.168.0.0/16", "192.0.0.0/8"}
29+
starts := []string{"192.168.56.0", "192.168.0.0", "192.0.0.0"}
30+
ends := []string{"192.168.56.255", "192.168.255.255", "192.255.255.255"}
31+
for i := range ranges {
32+
start, end, err := ParseIPRange(ranges[i])
33+
assert.NoError(t, err)
34+
assert.Equal(t, starts[i], start)
35+
assert.Equal(t, ends[i], end)
36+
}
37+
}
38+
39+
func TestIPRangeIPv6Range(t *testing.T) {
40+
ranges := []string{
41+
"2607:f8b0:400a:80a::2010/124",
42+
"2607:f8b0:400a:80a::2000/120",
43+
"2607:f8b0:400a:80a::2000/116",
44+
"2607:f8b0:400a:80a::/112",
45+
"2607:f8b0:400a:80a::/108",
46+
"2607:f8b0:400a:80a::/104",
47+
"2607:f8b0:400a:80a::/100",
48+
"2607:f8b0:400a:80a::/96",
49+
"2607:f8b0:400a:80a::/92",
50+
"2607:f8b0:400a:80a::/88",
51+
"2607:f8b0:400a:80a::/84",
52+
"2607:f8b0:400a:80a::/80",
53+
"2607:f8b0:400a:80a::/76",
54+
"2607:f8b0:400a:80a::/72",
55+
"2607:f8b0:400a:80a::/68",
56+
"2607:f8b0:400a:80a::/64",
57+
"2607:f8b0:400a:80a::/60",
58+
"2607:f8b0:400a:80a::/56",
59+
"2607:f8b0:400a:80a::/52",
60+
"2607:f8b0:400a:80a::/48",
61+
"2607:f8b0:400a:80a::/44",
62+
"2607:f8b0:400a:80a::/40",
63+
"2607:f8b0:400a:80a::/36",
64+
"2607:f8b0:400a:80a::/32",
65+
"2607:f8b0:400a:80a::/28",
66+
"2607:f8b0:400a:80a::/24",
67+
"2607:f8b0:400a:80a::/20",
68+
"2607:f8b0:400a:80a::/16",
69+
"2607:f8b0:400a:80a::/12",
70+
"2607:f8b0:400a:80a::/8",
71+
"2607:f8b0:400a:80a::/4",
72+
}
73+
starts := []string{
74+
"2607:f8b0:400a:80a::2010",
75+
"2607:f8b0:400a:80a::2000",
76+
"2607:f8b0:400a:80a::2000",
77+
"2607:f8b0:400a:80a::",
78+
"2607:f8b0:400a:80a::",
79+
"2607:f8b0:400a:80a::",
80+
"2607:f8b0:400a:80a::",
81+
"2607:f8b0:400a:80a::",
82+
"2607:f8b0:400a:80a::",
83+
"2607:f8b0:400a:80a::",
84+
"2607:f8b0:400a:80a::",
85+
"2607:f8b0:400a:80a::",
86+
"2607:f8b0:400a:80a::",
87+
"2607:f8b0:400a:80a::",
88+
"2607:f8b0:400a:80a::",
89+
"2607:f8b0:400a:80a::",
90+
"2607:f8b0:400a:800::",
91+
"2607:f8b0:400a:800::",
92+
"2607:f8b0:400a::",
93+
"2607:f8b0:400a::",
94+
"2607:f8b0:4000::",
95+
"2607:f8b0:4000::",
96+
"2607:f8b0:4000::",
97+
"2607:f8b0::",
98+
"2607:f8b0::",
99+
"2607:f800::",
100+
"2607:f000::",
101+
"2607::",
102+
"2600::",
103+
"2600::",
104+
"2000::",
105+
}
106+
ends := []string{
107+
"2607:f8b0:400a:80a::201f",
108+
"2607:f8b0:400a:80a::20ff",
109+
"2607:f8b0:400a:80a::2fff",
110+
"2607:f8b0:400a:80a::ffff",
111+
"2607:f8b0:400a:80a::f:ffff",
112+
"2607:f8b0:400a:80a::ff:ffff",
113+
"2607:f8b0:400a:80a::fff:ffff",
114+
"2607:f8b0:400a:80a::ffff:ffff",
115+
"2607:f8b0:400a:80a:0:f:ffff:ffff",
116+
"2607:f8b0:400a:80a:0:ff:ffff:ffff",
117+
"2607:f8b0:400a:80a:0:fff:ffff:ffff",
118+
"2607:f8b0:400a:80a:0:ffff:ffff:ffff",
119+
"2607:f8b0:400a:80a:f:ffff:ffff:ffff",
120+
"2607:f8b0:400a:80a:ff:ffff:ffff:ffff",
121+
"2607:f8b0:400a:80a:fff:ffff:ffff:ffff",
122+
"2607:f8b0:400a:80a:ffff:ffff:ffff:ffff",
123+
"2607:f8b0:400a:80f:ffff:ffff:ffff:ffff",
124+
"2607:f8b0:400a:8ff:ffff:ffff:ffff:ffff",
125+
"2607:f8b0:400a:fff:ffff:ffff:ffff:ffff",
126+
"2607:f8b0:400a:ffff:ffff:ffff:ffff:ffff",
127+
"2607:f8b0:400f:ffff:ffff:ffff:ffff:ffff",
128+
"2607:f8b0:40ff:ffff:ffff:ffff:ffff:ffff",
129+
"2607:f8b0:4fff:ffff:ffff:ffff:ffff:ffff",
130+
"2607:f8b0:ffff:ffff:ffff:ffff:ffff:ffff",
131+
"2607:f8bf:ffff:ffff:ffff:ffff:ffff:ffff",
132+
"2607:f8ff:ffff:ffff:ffff:ffff:ffff:ffff",
133+
"2607:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
134+
"2607:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
135+
"260f:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
136+
"26ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
137+
"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
138+
}
139+
for i := range ranges {
140+
start, end, err := ParseIPRange(ranges[i])
141+
assert.NoError(t, err)
142+
assert.Equal(t, starts[i], start, "unequal values at index %d", i)
143+
assert.Equal(t, ends[i], end, "unequal values at index %d", i)
144+
}
145+
}

pkg/gcutil/util.go

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"math/rand"
1111
"net"
1212
"net/http"
13-
"net/netip"
1413
"os"
1514
"path"
1615
"path/filepath"
@@ -161,79 +160,6 @@ func MarshalJSON(data interface{}, indent bool) (string, error) {
161160
return string(jsonBytes), err
162161
}
163162

164-
// ParseIPRange takes a single IP address or an IP range of the form "networkIP/netmaskbits" and
165-
// gives the starting IP and ending IP in the subnet
166-
//
167-
// More info: https://en.wikipedia.org/wiki/Subnet
168-
func ParseIPRange(ipOrCIDR string) (string, string, error) {
169-
var ipStart netip.Addr
170-
171-
if strings.ContainsRune(ipOrCIDR, '/') {
172-
var ipEnd netip.Addr
173-
// CIDR range
174-
prefix, err := netip.ParsePrefix(ipOrCIDR)
175-
if err != nil {
176-
return "", "", err
177-
}
178-
ipStart = prefix.Addr()
179-
ipEnd = prefix.Addr()
180-
var tmp netip.Addr
181-
for {
182-
tmp = ipEnd.Next()
183-
if !prefix.Contains(tmp) {
184-
break
185-
}
186-
ipEnd = tmp
187-
}
188-
return ipStart.String(), ipEnd.String(), nil
189-
}
190-
// single IP
191-
var err error
192-
if ipStart, err = netip.ParseAddr(ipOrCIDR); err != nil {
193-
return "", "", err
194-
}
195-
return ipStart.String(), ipStart.String(), nil
196-
}
197-
198-
// GetIPRangeSubnet returns the smallest subnet that contains the start and end
199-
// IP addresses, and any errors that occured
200-
func GetIPRangeSubnet(start string, end string) (*net.IPNet, error) {
201-
startIP := net.ParseIP(start)
202-
endIP := net.ParseIP(end)
203-
if startIP == nil {
204-
return nil, fmt.Errorf("invalid IP address %s", start)
205-
}
206-
if endIP == nil {
207-
return nil, fmt.Errorf("invalid IP address %s", end)
208-
}
209-
if len(startIP) != len(endIP) {
210-
return nil, errors.New("ip addresses must both be IPv4 or IPv6")
211-
}
212-
213-
if startIP.To4() != nil {
214-
startIP = startIP.To4()
215-
endIP = endIP.To4()
216-
}
217-
218-
bits := 0
219-
var ipn *net.IPNet
220-
for b := range startIP {
221-
if startIP[b] == endIP[b] {
222-
bits += 8
223-
continue
224-
}
225-
for i := 7; i >= 0; i-- {
226-
if startIP[b]&(1<<i) == endIP[b]&(1<<i) {
227-
bits++
228-
continue
229-
}
230-
ipn = &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}
231-
return ipn, nil
232-
}
233-
}
234-
return &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}, nil
235-
}
236-
237163
// ParseName takes a name string from a request object and returns the name and tripcode parts
238164
func ParseName(name string) (string, string) {
239165
var namePart string

pkg/posting/geoip/geoip.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var (
1313
geoipHandlers = make(map[string]GeoIPHandler)
1414
activeHandler GeoIPHandler
1515

16-
ErrInvalidIP = errors.New("invalid IP address")
16+
ErrInvalidIP = gcutil.ErrInvalidIP
1717
ErrNotConfigured = errors.New("geoip is not configured")
1818
ErrUnrecognized = errors.New("unrecognized GeoIP handler ID")
1919
)

0 commit comments

Comments
 (0)