1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-05 11:06:23 -07:00

Improve performance on gcutil.ParseIPRange for IPv6

This commit is contained in:
Eggbertx 2024-03-08 12:44:53 -08:00
parent 9029163c40
commit 80f2b98147
4 changed files with 232 additions and 75 deletions

86
pkg/gcutil/iprange.go Normal file
View file

@ -0,0 +1,86 @@
package gcutil
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
)
var (
ErrInvalidIP = errors.New("invalid IP address")
ErrInvalidSubnet = errors.New("invalid IP address or subnet mask")
)
// ParseIPRange takes a single IP address or an IP range of the form "networkIP/netmaskbits" and
// gives the starting IP and ending IP in the subnet
//
// More info: https://en.wikipedia.org/wiki/Subnet
func ParseIPRange(ipOrCIDR string) (string, string, error) {
parts := strings.Split(ipOrCIDR, "/")
ip := net.ParseIP(parts[0])
if ip == nil {
return "", "", ErrInvalidIP
}
ipv4 := ip.To4()
if ipv4 != nil {
ip = ipv4
}
if len(parts) == 1 {
// single IP
return ipOrCIDR, ipOrCIDR, nil
} else if len(parts) == 2 {
// IP/mask
netBits, err := strconv.Atoi(parts[1])
if err != nil {
return "", "", err
}
mask := net.CIDRMask(netBits, len(ip)*8)
broadcast := net.IP(make([]byte, len(ip)))
for i := range ip {
broadcast[i] = ip[i] | ^mask[i]
}
return ip.Mask(mask).String(), broadcast.String(), nil
}
return "", "", ErrInvalidSubnet
}
// GetIPRangeSubnet returns the smallest subnet that contains the start and end
// IP addresses, and any errors that occured
func GetIPRangeSubnet(start string, end string) (*net.IPNet, error) {
startIP := net.ParseIP(start)
endIP := net.ParseIP(end)
if startIP == nil {
return nil, fmt.Errorf("invalid IP address %s", start)
}
if endIP == nil {
return nil, fmt.Errorf("invalid IP address %s", end)
}
if len(startIP) != len(endIP) {
return nil, errors.New("ip addresses must both be IPv4 or IPv6")
}
if startIP.To4() != nil {
startIP = startIP.To4()
endIP = endIP.To4()
}
bits := 0
var ipn *net.IPNet
for b := range startIP {
if startIP[b] == endIP[b] {
bits += 8
continue
}
for i := 7; i >= 0; i-- {
if startIP[b]&(1<<i) == endIP[b]&(1<<i) {
bits++
continue
}
ipn = &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}
return ipn, nil
}
}
return &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}, nil
}

145
pkg/gcutil/iprange_test.go Normal file
View file

@ -0,0 +1,145 @@
package gcutil
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIPRangeErrOnInvalidIP(t *testing.T) {
_, _, err := ParseIPRange("not an IP")
assert.Error(t, err)
_, _, err = ParseIPRange("")
assert.Error(t, err)
_, _, err = ParseIPRange("192.168.56.0/")
assert.Error(t, err)
}
func TestIPRangeSingleIP(t *testing.T) {
start, end, err := ParseIPRange("192.168.56.1")
assert.NoError(t, err)
assert.Equal(t, start, end)
start, end, err = ParseIPRange("2801::")
assert.NoError(t, err)
assert.Equal(t, start, end)
}
func TestIPRangeIPv4Range(t *testing.T) {
ranges := []string{"192.168.56.0/24", "192.168.0.0/16", "192.0.0.0/8"}
starts := []string{"192.168.56.0", "192.168.0.0", "192.0.0.0"}
ends := []string{"192.168.56.255", "192.168.255.255", "192.255.255.255"}
for i := range ranges {
start, end, err := ParseIPRange(ranges[i])
assert.NoError(t, err)
assert.Equal(t, starts[i], start)
assert.Equal(t, ends[i], end)
}
}
func TestIPRangeIPv6Range(t *testing.T) {
ranges := []string{
"2607:f8b0:400a:80a::2010/124",
"2607:f8b0:400a:80a::2000/120",
"2607:f8b0:400a:80a::2000/116",
"2607:f8b0:400a:80a::/112",
"2607:f8b0:400a:80a::/108",
"2607:f8b0:400a:80a::/104",
"2607:f8b0:400a:80a::/100",
"2607:f8b0:400a:80a::/96",
"2607:f8b0:400a:80a::/92",
"2607:f8b0:400a:80a::/88",
"2607:f8b0:400a:80a::/84",
"2607:f8b0:400a:80a::/80",
"2607:f8b0:400a:80a::/76",
"2607:f8b0:400a:80a::/72",
"2607:f8b0:400a:80a::/68",
"2607:f8b0:400a:80a::/64",
"2607:f8b0:400a:80a::/60",
"2607:f8b0:400a:80a::/56",
"2607:f8b0:400a:80a::/52",
"2607:f8b0:400a:80a::/48",
"2607:f8b0:400a:80a::/44",
"2607:f8b0:400a:80a::/40",
"2607:f8b0:400a:80a::/36",
"2607:f8b0:400a:80a::/32",
"2607:f8b0:400a:80a::/28",
"2607:f8b0:400a:80a::/24",
"2607:f8b0:400a:80a::/20",
"2607:f8b0:400a:80a::/16",
"2607:f8b0:400a:80a::/12",
"2607:f8b0:400a:80a::/8",
"2607:f8b0:400a:80a::/4",
}
starts := []string{
"2607:f8b0:400a:80a::2010",
"2607:f8b0:400a:80a::2000",
"2607:f8b0:400a:80a::2000",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:80a::",
"2607:f8b0:400a:800::",
"2607:f8b0:400a:800::",
"2607:f8b0:400a::",
"2607:f8b0:400a::",
"2607:f8b0:4000::",
"2607:f8b0:4000::",
"2607:f8b0:4000::",
"2607:f8b0::",
"2607:f8b0::",
"2607:f800::",
"2607:f000::",
"2607::",
"2600::",
"2600::",
"2000::",
}
ends := []string{
"2607:f8b0:400a:80a::201f",
"2607:f8b0:400a:80a::20ff",
"2607:f8b0:400a:80a::2fff",
"2607:f8b0:400a:80a::ffff",
"2607:f8b0:400a:80a::f:ffff",
"2607:f8b0:400a:80a::ff:ffff",
"2607:f8b0:400a:80a::fff:ffff",
"2607:f8b0:400a:80a::ffff:ffff",
"2607:f8b0:400a:80a:0:f:ffff:ffff",
"2607:f8b0:400a:80a:0:ff:ffff:ffff",
"2607:f8b0:400a:80a:0:fff:ffff:ffff",
"2607:f8b0:400a:80a:0:ffff:ffff:ffff",
"2607:f8b0:400a:80a:f:ffff:ffff:ffff",
"2607:f8b0:400a:80a:ff:ffff:ffff:ffff",
"2607:f8b0:400a:80a:fff:ffff:ffff:ffff",
"2607:f8b0:400a:80a:ffff:ffff:ffff:ffff",
"2607:f8b0:400a:80f:ffff:ffff:ffff:ffff",
"2607:f8b0:400a:8ff:ffff:ffff:ffff:ffff",
"2607:f8b0:400a:fff:ffff:ffff:ffff:ffff",
"2607:f8b0:400a:ffff:ffff:ffff:ffff:ffff",
"2607:f8b0:400f:ffff:ffff:ffff:ffff:ffff",
"2607:f8b0:40ff:ffff:ffff:ffff:ffff:ffff",
"2607:f8b0:4fff:ffff:ffff:ffff:ffff:ffff",
"2607:f8b0:ffff:ffff:ffff:ffff:ffff:ffff",
"2607:f8bf:ffff:ffff:ffff:ffff:ffff:ffff",
"2607:f8ff:ffff:ffff:ffff:ffff:ffff:ffff",
"2607:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"2607:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"260f:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"26ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
}
for i := range ranges {
start, end, err := ParseIPRange(ranges[i])
assert.NoError(t, err)
assert.Equal(t, starts[i], start, "unequal values at index %d", i)
assert.Equal(t, ends[i], end, "unequal values at index %d", i)
}
}

View file

@ -10,7 +10,6 @@ import (
"math/rand"
"net"
"net/http"
"net/netip"
"os"
"path"
"path/filepath"
@ -161,79 +160,6 @@ func MarshalJSON(data interface{}, indent bool) (string, error) {
return string(jsonBytes), err
}
// ParseIPRange takes a single IP address or an IP range of the form "networkIP/netmaskbits" and
// gives the starting IP and ending IP in the subnet
//
// More info: https://en.wikipedia.org/wiki/Subnet
func ParseIPRange(ipOrCIDR string) (string, string, error) {
var ipStart netip.Addr
if strings.ContainsRune(ipOrCIDR, '/') {
var ipEnd netip.Addr
// CIDR range
prefix, err := netip.ParsePrefix(ipOrCIDR)
if err != nil {
return "", "", err
}
ipStart = prefix.Addr()
ipEnd = prefix.Addr()
var tmp netip.Addr
for {
tmp = ipEnd.Next()
if !prefix.Contains(tmp) {
break
}
ipEnd = tmp
}
return ipStart.String(), ipEnd.String(), nil
}
// single IP
var err error
if ipStart, err = netip.ParseAddr(ipOrCIDR); err != nil {
return "", "", err
}
return ipStart.String(), ipStart.String(), nil
}
// GetIPRangeSubnet returns the smallest subnet that contains the start and end
// IP addresses, and any errors that occured
func GetIPRangeSubnet(start string, end string) (*net.IPNet, error) {
startIP := net.ParseIP(start)
endIP := net.ParseIP(end)
if startIP == nil {
return nil, fmt.Errorf("invalid IP address %s", start)
}
if endIP == nil {
return nil, fmt.Errorf("invalid IP address %s", end)
}
if len(startIP) != len(endIP) {
return nil, errors.New("ip addresses must both be IPv4 or IPv6")
}
if startIP.To4() != nil {
startIP = startIP.To4()
endIP = endIP.To4()
}
bits := 0
var ipn *net.IPNet
for b := range startIP {
if startIP[b] == endIP[b] {
bits += 8
continue
}
for i := 7; i >= 0; i-- {
if startIP[b]&(1<<i) == endIP[b]&(1<<i) {
bits++
continue
}
ipn = &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}
return ipn, nil
}
}
return &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}, nil
}
// ParseName takes a name string from a request object and returns the name and tripcode parts
func ParseName(name string) (string, string) {
var namePart string

View file

@ -13,7 +13,7 @@ var (
geoipHandlers = make(map[string]GeoIPHandler)
activeHandler GeoIPHandler
ErrInvalidIP = errors.New("invalid IP address")
ErrInvalidIP = gcutil.ErrInvalidIP
ErrNotConfigured = errors.New("geoip is not configured")
ErrUnrecognized = errors.New("unrecognized GeoIP handler ID")
)