Skip to content

Commit

Permalink
netutil: add arpa subnet parser
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneOne1 committed Feb 11, 2022
1 parent 8e3d818 commit 420e442
Show file tree
Hide file tree
Showing 5 changed files with 500 additions and 30 deletions.
4 changes: 4 additions & 0 deletions netutil/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const (
// ErrNotAReversedIP is the underlying error returned from validation
// functions when a domain name is not a full reversed IP address.
ErrNotAReversedIP errors.Error = "not a full reversed ip address"

// ErrNotAReversedSubnet is the underlying error returned from validation
// functions when a domain name is not a valid reversed IP network.
ErrNotAReversedSubnet errors.Error = "not a reversed ip network"
)

// AddrKind is the kind of address or address part used for error reporting.
Expand Down
31 changes: 21 additions & 10 deletions netutil/ip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,48 +244,59 @@ var (
ipNetSink *net.IPNet
)

// flushSinks should be used in Cleanup functions to get rid of values of global
// variables.
func flushSinks() {
errSink = nil
ipNetSink = nil
}

func BenchmarkParseSubnet(b *testing.B) {
b.Run("good_cidr", func(b *testing.B) {
var err error
b.Cleanup(flushSinks)

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ipNetSink, err = netutil.ParseSubnet("1.2.3.4/16")
ipNetSink, errSink = netutil.ParseSubnet("1.2.3.4/16")
}

assert.NotNil(b, ipNetSink)
assert.NoError(b, err)
require.NotNil(b, ipNetSink)
require.NoError(b, errSink)
})

b.Run("good_ip", func(b *testing.B) {
var err error
b.Cleanup(flushSinks)

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ipNetSink, err = netutil.ParseSubnet("1.2.3.4")
ipNetSink, errSink = netutil.ParseSubnet("1.2.3.4")
}

assert.NotNil(b, ipNetSink)
assert.NoError(b, err)
require.NotNil(b, ipNetSink)
require.NoError(b, errSink)
})

b.Run("bad_cidr", func(b *testing.B) {
b.Cleanup(flushSinks)

b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, errSink = netutil.ParseSubnet("1.2.3.4//567")
}

assert.Error(b, errSink)
require.Error(b, errSink)
})

b.Run("bad_ip", func(b *testing.B) {
b.Cleanup(flushSinks)

b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, errSink = netutil.ParseSubnet("1.2.3.4.5")
}

assert.Error(b, errSink)
require.Error(b, errSink)
})
}
10 changes: 9 additions & 1 deletion netutil/netutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import "net"

// Common test IPs. Do not mutate.
var (
testIPv4 = net.IP{1, 2, 3, 4}
testIPv4 = net.IP{1, 2, 3, 4}
testIPv4ZeroTail = net.IP{10, 0, 0, 0}

testIPv6 = net.IP{
0x12, 0x34, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xcd, 0xef,
}
testIPv6ZeroTail = net.IP{
0x12, 0x34, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
}
)
188 changes: 176 additions & 12 deletions netutil/reversed.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strconv"
"strings"

"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/stringutil"
)

Expand Down Expand Up @@ -57,25 +58,23 @@ func reverseIP(ip net.IP) {
}
}

// ipv6FromReversedAddr parses an IPv6 reverse address. It assumes that arpa is
// a valid domain name.
func ipv6FromReversedAddr(arpa string) (ip net.IP, err error) {
const kind = "arpa domain name"
// ipv6FromReversed parses an IPv6 reverse address. It assumes that arpa is a
// valid domain name.
func ipv6FromReversed(arpa string) (ip net.IP, err error) {
const addrStep = len("0.0.")

ip = make(net.IP, net.IPv6len)

const addrStep = len("0.0.")
for i := range ip {
// Get the two half-byte and merge them together. Validate the
// dots between them since while arpa is assumed to be a valid
// domain name, those labels can still be invalid on their own.
// Get the two half-byte and merge them together. Validate the dots
// between them since while arpa is assumed to be a valid domain name,
// those labels can still be invalid on their own.
sIdx := i * addrStep

c := arpa[sIdx]
lo := fromHexByte(c)
if lo == 0xff {
return nil, &RuneError{
Kind: kind,
Kind: AddrKindARPA,
Rune: rune(c),
}
}
Expand All @@ -84,7 +83,7 @@ func ipv6FromReversedAddr(arpa string) (ip net.IP, err error) {
hi := fromHexByte(c)
if hi == 0xff {
return nil, &RuneError{
Kind: kind,
Kind: AddrKindARPA,
Rune: rune(c),
}
}
Expand Down Expand Up @@ -139,7 +138,7 @@ func IPFromReversedAddr(arpa string) (ip net.IP, err error) {
}
}

ip, err = ipv6FromReversedAddr(arpa)
ip, err = ipv6FromReversed(arpa)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -196,3 +195,168 @@ func IPToReversedAddr(ip net.IP) (arpa string, err error) {

return b.String(), nil
}

// ipv4NetFromReversed parses an IPv4 reverse network. It assumes that arpa is
// a valid domain name and not a single address IPv4 network.
func ipv4NetFromReversed(arpa string) (subnet *net.IPNet, err error) {
var b uint64
ip := make(net.IP, net.IPv4len)
l := 0
for arpa != "" {
dotIdx := strings.LastIndexByte(arpa, '.')

// Don't check for out of range since the domain is validated to have no
// empty labels.
b, err = strconv.ParseUint(arpa[dotIdx+1:], 10, 8)
if err != nil {
return nil, err
} else if b != 0 && arpa[dotIdx+1] == '0' {
// Octets of an ARPA domain name shouldn't contain leading zero
// except an octet itself equals zero.
//
// See RFC 1035 Section 3.5.
return nil, &AddrError{
Err: errors.Error("leading zero is forbidden at this position"),
Kind: AddrKindLabel,
Addr: arpa[dotIdx+1:],
}
}

ip[l] = byte(b)
l++

if dotIdx == -1 {
break
}

arpa = arpa[:dotIdx]
}

if b == 0 {
return nil, ErrNotAReversedSubnet
}

return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(l*8, IPv4BitLen),
}, nil
}

// ipv6NetFromReversed parses an IPv6 reverse network. It assumes that arpa is
// a valid domain name and not a single address IPv6 network.
func ipv6NetFromReversed(arpa string) (subnet *net.IPNet, err error) {
nibIdx := len(arpa) - len(arpaV6Suffix) + len(".") - len("0.")
if nibIdx%2 != 0 {
return nil, ErrNotAReversedSubnet
}

ip := make(net.IP, net.IPv6len)
l := 0
var b byte
for ; nibIdx >= 0; nibIdx -= len("0.") {
if arpa[nibIdx+1] != '.' {
return nil, ErrNotAReversedSubnet
}

c := arpa[nibIdx]
b = fromHexByte(c)
if b == 0xff {
return nil, &RuneError{
Kind: AddrKindARPA,
Rune: rune(c),
}
}

if l%2 == 0 {
// An even digit stands for higher nibble of a byte.
ip[l/2] |= b << 4
} else {
// An odd digit stands for lower nibble of a byte.
ip[l/2] |= b
}
l++
}

if ip[l/2] == 0 {
return nil, ErrNotAReversedSubnet
}

return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(l*4, IPv6BitLen),
}, nil
}

// subnetFromReversedV4 tries to convert arpa into IPv4 network. It expects
// arpa being a valid domain name in a lower case.
func subnetFromReversedV4(arpa string) (subnet *net.IPNet, err error) {
arpa = arpa[:len(arpa)-len(arpaV4Suffix)]

if dots := strings.Count(arpa, "."); dots == 3 {
var ip net.IP
ip, err = ParseIPv4(arpa)
if err != nil {
return nil, err
}

reverseIP(ip)

return SingleIPSubnet(ip), nil
} else if dots > 3 {
return nil, ErrNotAReversedSubnet
}

return ipv4NetFromReversed(arpa)
}

// subnetFromReversedV6 tries to convert arpa into IPv6 network. It expects
// arpa being a valid domain name in a lower case.
func subnetFromReversedV6(arpa string) (subnet *net.IPNet, err error) {
if l := len(arpa); l == arpaV6MaxLen {
var ip net.IP
ip, err = ipv6FromReversed(arpa)
if err != nil {
return nil, err
}

return SingleIPSubnet(ip), nil
} else if l > arpaV6MaxLen {
return nil, &LengthError{
Kind: AddrKindARPA,
Max: arpaV6MaxLen,
Length: l,
}
}

return ipv6NetFromReversed(arpa)
}

// SubnetFromReversedAddr tries to convert a reversed ARPA address to an IP
// network. arpa can be domain name or an FQDN.
//
// Any error returned will have the underlying type of *AddrError.
func SubnetFromReversedAddr(arpa string) (subnet *net.IPNet, err error) {
arpa = strings.TrimSuffix(arpa, ".")
err = ValidateDomainName(arpa)
if err != nil {
bdErr := err.(*AddrError)
bdErr.Kind = AddrKindARPA

return nil, bdErr
}

defer makeAddrError(&err, arpa, AddrKindARPA)

// TODO(a.garipov): Add stringutil.HasSuffixFold and remove this.
arpa = strings.ToLower(arpa)

if strings.HasSuffix(arpa, arpaV4Suffix) {
return subnetFromReversedV4(arpa)
}

if strings.HasSuffix(arpa, arpaV6Suffix) {
return subnetFromReversedV6(arpa)
}

return nil, ErrNotAReversedSubnet
}
Loading

0 comments on commit 420e442

Please sign in to comment.