Pull request: all: mv some utilities to netutil

Merge in DNS/adguard-home from mv-netutil to master

Squashed commit of the following:

commit 5698fceed656dca7f8644e7dbd7e1a7fc57a68ce
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 9 15:44:17 2021 +0300

    dnsforward: add todos

commit 122fb6e3de658b296931e0f608cf24ef85547666
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 9 14:27:46 2021 +0300

    all: mv some utilities to netutil
This commit is contained in:
Ainar Garipov
2021-08-09 16:03:37 +03:00
parent 133a1c57b1
commit 9bd7294fad
37 changed files with 242 additions and 1033 deletions

View File

@@ -1,206 +0,0 @@
package aghnet
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/AdguardTeam/golibs/errors"
"golang.org/x/net/idna"
)
// CloneIP returns a clone of an IP address.
func CloneIP(ip net.IP) (clone net.IP) {
if ip != nil && len(ip) == 0 {
return net.IP{}
}
return append(clone, ip...)
}
// CloneMAC returns a clone of a MAC address.
func CloneMAC(mac net.HardwareAddr) (clone net.HardwareAddr) {
if mac != nil && len(mac) == 0 {
return net.HardwareAddr{}
}
return append(clone, mac...)
}
// IPFromAddr returns an IP address from addr. If addr is neither
// a *net.TCPAddr nor a *net.UDPAddr, it returns nil.
func IPFromAddr(addr net.Addr) (ip net.IP) {
switch addr := addr.(type) {
case *net.TCPAddr:
return addr.IP
case *net.UDPAddr:
return addr.IP
}
return nil
}
// IsValidHostOuterRune returns true if r is a valid initial or final rune for
// a hostname label.
func IsValidHostOuterRune(r rune) (ok bool) {
return (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9')
}
// JoinHostPort is a convinient wrapper for net.JoinHostPort with port of type
// int.
func JoinHostPort(host string, port int) (hostport string) {
return net.JoinHostPort(host, strconv.Itoa(port))
}
// isValidHostRune returns true if r is a valid rune for a hostname label.
func isValidHostRune(r rune) (ok bool) {
return r == '-' || IsValidHostOuterRune(r)
}
// ValidateHardwareAddress returns an error if hwa is not a valid EUI-48,
// EUI-64, or 20-octet InfiniBand link-layer address.
func ValidateHardwareAddress(hwa net.HardwareAddr) (err error) {
defer func() { err = errors.Annotate(err, "validating hardware address %q: %w", hwa) }()
switch l := len(hwa); l {
case 0:
return errors.Error("address is empty")
case 6, 8, 20:
return nil
default:
return fmt.Errorf("bad len: %d", l)
}
}
// maxDomainLabelLen is the maximum allowed length of a domain name label
// according to RFC 1035.
const maxDomainLabelLen = 63
// MaxDomainNameLen is the maximum allowed length of a full domain name
// according to RFC 1035.
//
// See https://stackoverflow.com/a/32294443/1892060.
const MaxDomainNameLen = 253
// ValidateDomainNameLabel returns an error if label is not a valid label of
// a domain name.
func ValidateDomainNameLabel(label string) (err error) {
defer func() { err = errors.Annotate(err, "validating label %q: %w", label) }()
l := len(label)
if l > maxDomainLabelLen {
return fmt.Errorf("label is too long, max: %d", maxDomainLabelLen)
} else if l == 0 {
return errors.Error("label is empty")
}
if r := label[0]; !IsValidHostOuterRune(rune(r)) {
return fmt.Errorf("invalid char %q at index %d", r, 0)
} else if l == 1 {
return nil
}
for i, r := range label[1 : l-1] {
if !isValidHostRune(r) {
return fmt.Errorf("invalid char %q at index %d", r, i+1)
}
}
if r := label[l-1]; !IsValidHostOuterRune(rune(r)) {
return fmt.Errorf("invalid char %q at index %d", r, l-1)
}
return nil
}
// ValidateDomainName validates the domain name in accordance to RFC 952, RFC
// 1035, and with RFC-1123's inclusion of digits at the start of the host. It
// doesn't validate against two or more hyphens to allow punycode and
// internationalized domains.
//
// TODO(a.garipov): After making sure that this works correctly, port this into
// module golibs.
func ValidateDomainName(name string) (err error) {
defer func() { err = errors.Annotate(err, "validating domain name %q: %w", name) }()
name, err = idna.ToASCII(name)
if err != nil {
return err
}
l := len(name)
if l == 0 {
return errors.Error("domain name is empty")
} else if l > MaxDomainNameLen {
return fmt.Errorf("too long, max: %d", MaxDomainNameLen)
}
labels := strings.Split(name, ".")
for i, l := range labels {
err = ValidateDomainNameLabel(l)
if err != nil {
return fmt.Errorf("invalid domain name label at index %d: %w", i, err)
}
}
return nil
}
// The maximum lengths of generated hostnames for different IP versions.
const (
ipv4HostnameMaxLen = len("192-168-100-100")
ipv6HostnameMaxLen = len("ff80-f076-0000-0000-0000-0000-0000-0010")
)
// generateIPv4Hostname generates the hostname for specific IP version.
func generateIPv4Hostname(ipv4 net.IP) (hostname string) {
hnData := make([]byte, 0, ipv4HostnameMaxLen)
for i, part := range ipv4 {
if i > 0 {
hnData = append(hnData, '-')
}
hnData = strconv.AppendUint(hnData, uint64(part), 10)
}
return string(hnData)
}
// generateIPv6Hostname generates the hostname for specific IP version.
func generateIPv6Hostname(ipv6 net.IP) (hostname string) {
hnData := make([]byte, 0, ipv6HostnameMaxLen)
for i, partsNum := 0, net.IPv6len/2; i < partsNum; i++ {
if i > 0 {
hnData = append(hnData, '-')
}
for _, val := range ipv6[i*2 : i*2+2] {
if val < 10 {
hnData = append(hnData, '0')
}
hnData = strconv.AppendUint(hnData, uint64(val), 16)
}
}
return string(hnData)
}
// GenerateHostname generates the hostname from ip. In case of using IPv4 the
// result should be like:
//
// 192-168-10-1
//
// In case of using IPv6, the result is like:
//
// ff80-f076-0000-0000-0000-0000-0000-0010
//
func GenerateHostname(ip net.IP) (hostname string) {
if ipv4 := ip.To4(); ipv4 != nil {
return generateIPv4Hostname(ipv4)
} else if ipv6 := ip.To16(); ipv6 != nil {
return generateIPv6Hostname(ipv6)
}
return ""
}

View File

@@ -1,228 +0,0 @@
package aghnet
import (
"net"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCloneIP(t *testing.T) {
assert.Equal(t, net.IP(nil), CloneIP(nil))
assert.Equal(t, net.IP{}, CloneIP(net.IP{}))
ip := net.IP{1, 2, 3, 4}
clone := CloneIP(ip)
assert.Equal(t, ip, clone)
assert.NotSame(t, &ip[0], &clone[0])
}
func TestCloneMAC(t *testing.T) {
assert.Equal(t, net.HardwareAddr(nil), CloneMAC(nil))
assert.Equal(t, net.HardwareAddr{}, CloneMAC(net.HardwareAddr{}))
mac := net.HardwareAddr{0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}
clone := CloneMAC(mac)
assert.Equal(t, mac, clone)
assert.NotSame(t, &mac[0], &clone[0])
}
func TestIPFromAddr(t *testing.T) {
ip := net.IP{1, 2, 3, 4}
assert.Equal(t, net.IP(nil), IPFromAddr(nil))
assert.Equal(t, net.IP(nil), IPFromAddr(struct{ net.Addr }{}))
assert.Equal(t, ip, IPFromAddr(&net.TCPAddr{IP: ip}))
assert.Equal(t, ip, IPFromAddr(&net.UDPAddr{IP: ip}))
}
func TestValidateHardwareAddress(t *testing.T) {
testCases := []struct {
name string
wantErrMsg string
in net.HardwareAddr
}{{
name: "success_eui_48",
wantErrMsg: "",
in: net.HardwareAddr{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},
}, {
name: "success_eui_64",
wantErrMsg: "",
in: net.HardwareAddr{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
}, {
name: "success_infiniband",
wantErrMsg: "",
in: net.HardwareAddr{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13,
},
}, {
name: "error_nil",
wantErrMsg: `validating hardware address "": address is empty`,
in: nil,
}, {
name: "error_empty",
wantErrMsg: `validating hardware address "": address is empty`,
in: net.HardwareAddr{},
}, {
name: "error_bad",
wantErrMsg: `validating hardware address "00:01:02:03": bad len: 4`,
in: net.HardwareAddr{0x00, 0x01, 0x02, 0x03},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateHardwareAddress(tc.in)
if tc.wantErrMsg == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Equal(t, tc.wantErrMsg, err.Error())
}
})
}
}
func TestJoinHostPort(t *testing.T) {
assert.Equal(t, ":0", JoinHostPort("", 0))
assert.Equal(t, "host:12345", JoinHostPort("host", 12345))
assert.Equal(t, "1.2.3.4:12345", JoinHostPort("1.2.3.4", 12345))
assert.Equal(t, "[1234::5678]:12345", JoinHostPort("1234::5678", 12345))
assert.Equal(t, "[1234::5678%lo]:12345", JoinHostPort("1234::5678%lo", 12345))
}
func repeatStr(b *strings.Builder, s string, n int) {
for i := 0; i < n; i++ {
_, _ = b.WriteString(s)
}
}
func TestValidateDomainName(t *testing.T) {
b := &strings.Builder{}
repeatStr(b, "a", 255)
longDomainName := b.String()
b.Reset()
repeatStr(b, "a", 64)
longLabel := b.String()
_, _ = b.WriteString(".com")
longLabelDomainName := b.String()
testCases := []struct {
name string
in string
wantErrMsg string
}{{
name: "success",
in: "example.com",
wantErrMsg: "",
}, {
name: "success_idna",
in: "пример.рф",
wantErrMsg: "",
}, {
name: "success_one",
in: "e",
wantErrMsg: "",
}, {
name: "empty",
in: "",
wantErrMsg: `validating domain name "": domain name is empty`,
}, {
name: "bad_symbol",
in: "!!!",
wantErrMsg: `validating domain name "!!!": invalid domain name label at index 0: ` +
`validating label "!!!": invalid char '!' at index 0`,
}, {
name: "bad_length",
in: longDomainName,
wantErrMsg: `validating domain name "` + longDomainName + `": too long, max: 253`,
}, {
name: "bad_label_length",
in: longLabelDomainName,
wantErrMsg: `validating domain name "` + longLabelDomainName + `": ` +
`invalid domain name label at index 0: validating label "` + longLabel +
`": label is too long, max: 63`,
}, {
name: "bad_label_empty",
in: "example..com",
wantErrMsg: `validating domain name "example..com": ` +
`invalid domain name label at index 1: ` +
`validating label "": label is empty`,
}, {
name: "bad_label_first_symbol",
in: "example.-aa.com",
wantErrMsg: `validating domain name "example.-aa.com": ` +
`invalid domain name label at index 1: ` +
`validating label "-aa": invalid char '-' at index 0`,
}, {
name: "bad_label_last_symbol",
in: "example-.aa.com",
wantErrMsg: `validating domain name "example-.aa.com": ` +
`invalid domain name label at index 0: ` +
`validating label "example-": invalid char '-' at index 7`,
}, {
name: "bad_label_symbol",
in: "example.a!!!.com",
wantErrMsg: `validating domain name "example.a!!!.com": ` +
`invalid domain name label at index 1: ` +
`validating label "a!!!": invalid char '!' at index 1`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateDomainName(tc.in)
if tc.wantErrMsg == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Equal(t, tc.wantErrMsg, err.Error())
}
})
}
}
func TestGenerateHostName(t *testing.T) {
testCases := []struct {
name string
want string
ip net.IP
}{{
name: "good_ipv4",
want: "127-0-0-1",
ip: net.IP{127, 0, 0, 1},
}, {
name: "bad_ipv4",
want: "",
ip: net.IP{127, 0, 0, 1, 0},
}, {
name: "good_ipv6",
want: "fe00-0000-0000-0000-0000-0000-0000-0001",
ip: net.ParseIP("fe00::1"),
}, {
name: "bad_ipv6",
want: "",
ip: net.IP{
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
},
}, {
name: "nil",
want: "",
ip: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hostname := GenerateHostname(tc.ip)
assert.Equal(t, tc.want, hostname)
})
}
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/fsnotify/fsnotify"
"github.com/miekg/dns"
)
@@ -29,7 +30,7 @@ type EtcHostsContainer struct {
table map[string][]net.IP
// tableReverse is the IP-to-hosts map. The type of the values in the
// map is []string.
tableReverse *IPMap
tableReverse *netutil.IPMap
hostsFn string // path to the main hosts-file
hostsDirs []string // paths to OS-specific directories with hosts-files
@@ -150,8 +151,10 @@ func (ehc *EtcHostsContainer) ProcessReverse(addr string, qtype uint16) (hosts [
return nil
}
ip := UnreverseAddr(addr)
if ip == nil {
ip, err := netutil.IPFromReversedAddr(addr)
if err != nil {
log.Error("etchosts: reversed addr: %s", err)
return nil
}
@@ -179,7 +182,7 @@ func (ehc *EtcHostsContainer) ProcessReverse(addr string, qtype uint16) (hosts [
// List returns an IP-to-hostnames table. The type of the values in the map is
// []string. It is safe for concurrent use.
func (ehc *EtcHostsContainer) List() (ipToHosts *IPMap) {
func (ehc *EtcHostsContainer) List() (ipToHosts *netutil.IPMap) {
ehc.lock.RLock()
defer ehc.lock.RUnlock()
@@ -211,7 +214,7 @@ func (ehc *EtcHostsContainer) updateTable(table map[string][]net.IP, host string
}
// updateTableRev updates the reverse address table.
func (ehc *EtcHostsContainer) updateTableRev(tableRev *IPMap, newHost string, ip net.IP) {
func (ehc *EtcHostsContainer) updateTableRev(tableRev *netutil.IPMap, newHost string, ip net.IP) {
v, ok := tableRev.Get(ip)
if !ok {
tableRev.Set(ip, []string{newHost})
@@ -258,7 +261,7 @@ func parseHostsLine(fields []string) (hosts []string) {
// line for one IP are supported.
func (ehc *EtcHostsContainer) load(
table map[string][]net.IP,
tableRev *IPMap,
tableRev *netutil.IPMap,
fn string,
) {
f, err := os.Open(fn)
@@ -353,7 +356,7 @@ func (ehc *EtcHostsContainer) watcherLoop() {
// updateHosts - loads system hosts
func (ehc *EtcHostsContainer) updateHosts() {
table := make(map[string][]net.IP)
tableRev := NewIPMap(0)
tableRev := netutil.NewIPMap(0)
ehc.load(table, tableRev, ehc.hostsFn)

View File

@@ -0,0 +1,62 @@
package aghnet
import (
"net"
"strconv"
)
// The maximum lengths of generated hostnames for different IP versions.
const (
ipv4HostnameMaxLen = len("192-168-100-100")
ipv6HostnameMaxLen = len("ff80-f076-0000-0000-0000-0000-0000-0010")
)
// generateIPv4Hostname generates the hostname for specific IP version.
func generateIPv4Hostname(ipv4 net.IP) (hostname string) {
hnData := make([]byte, 0, ipv4HostnameMaxLen)
for i, part := range ipv4 {
if i > 0 {
hnData = append(hnData, '-')
}
hnData = strconv.AppendUint(hnData, uint64(part), 10)
}
return string(hnData)
}
// generateIPv6Hostname generates the hostname for specific IP version.
func generateIPv6Hostname(ipv6 net.IP) (hostname string) {
hnData := make([]byte, 0, ipv6HostnameMaxLen)
for i, partsNum := 0, net.IPv6len/2; i < partsNum; i++ {
if i > 0 {
hnData = append(hnData, '-')
}
for _, val := range ipv6[i*2 : i*2+2] {
if val < 10 {
hnData = append(hnData, '0')
}
hnData = strconv.AppendUint(hnData, uint64(val), 16)
}
}
return string(hnData)
}
// GenerateHostname generates the hostname from ip. In case of using IPv4 the
// result should be like:
//
// 192-168-10-1
//
// In case of using IPv6, the result is like:
//
// ff80-f076-0000-0000-0000-0000-0000-0010
//
func GenerateHostname(ip net.IP) (hostname string) {
if ipv4 := ip.To4(); ipv4 != nil {
return generateIPv4Hostname(ipv4)
} else if ipv6 := ip.To16(); ipv6 != nil {
return generateIPv6Hostname(ipv6)
}
return ""
}

View File

@@ -0,0 +1,48 @@
package aghnet
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGenerateHostName(t *testing.T) {
testCases := []struct {
name string
want string
ip net.IP
}{{
name: "good_ipv4",
want: "127-0-0-1",
ip: net.IP{127, 0, 0, 1},
}, {
name: "bad_ipv4",
want: "",
ip: net.IP{127, 0, 0, 1, 0},
}, {
name: "good_ipv6",
want: "fe00-0000-0000-0000-0000-0000-0000-0001",
ip: net.ParseIP("fe00::1"),
}, {
name: "bad_ipv6",
want: "",
ip: net.IP{
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
},
}, {
name: "nil",
want: "",
ip: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hostname := GenerateHostname(tc.ip)
assert.Equal(t, tc.want, hostname)
})
}
}

View File

@@ -1,117 +0,0 @@
package aghnet
import (
"fmt"
"net"
)
// ipArr is a representation of an IP address as an array of bytes.
type ipArr [16]byte
// String implements the fmt.Stringer interface for ipArr.
func (a ipArr) String() (s string) {
return net.IP(a[:]).String()
}
// IPMap is a map of IP addresses.
type IPMap struct {
m map[ipArr]interface{}
}
// NewIPMap returns a new empty IP map using hint as a size hint for the
// underlying map.
func NewIPMap(hint int) (m *IPMap) {
return &IPMap{
m: make(map[ipArr]interface{}, hint),
}
}
// ipToArr converts a net.IP into an ipArr.
//
// TODO(a.garipov): Use the slice-to-array conversion in Go 1.17.
func ipToArr(ip net.IP) (a ipArr) {
copy(a[:], ip.To16())
return a
}
// Del deletes ip from the map. Calling Del on a nil *IPMap has no effect, just
// like delete on an empty map doesn't.
func (m *IPMap) Del(ip net.IP) {
if m != nil {
delete(m.m, ipToArr(ip))
}
}
// Get returns the value from the map. Calling Get on a nil *IPMap returns nil
// and false, just like indexing on an empty map does.
func (m *IPMap) Get(ip net.IP) (v interface{}, ok bool) {
if m != nil {
v, ok = m.m[ipToArr(ip)]
return v, ok
}
return nil, false
}
// Len returns the length of the map. A nil *IPMap has a length of zero, just
// like an empty map.
func (m *IPMap) Len() (n int) {
if m == nil {
return 0
}
return len(m.m)
}
// Range calls f for each key and value present in the map in an undefined
// order. If cont is false, range stops the iteration. Calling Range on a nil
// *IPMap has no effect, just like ranging over a nil map.
func (m *IPMap) Range(f func(ip net.IP, v interface{}) (cont bool)) {
if m == nil {
return
}
for k, v := range m.m {
// Array slicing produces a pointer, so copy the array here.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3346
// as well as https://github.com/kyoh86/looppointer/issues/9.
k := k
if !f(net.IP(k[:]), v) {
break
}
}
}
// Set sets the value. Set panics if the m is a nil *IPMap, just like a nil map
// does.
func (m *IPMap) Set(ip net.IP, v interface{}) {
m.m[ipToArr(ip)] = v
}
// ShallowClone returns a shallow clone of the map.
func (m *IPMap) ShallowClone() (sclone *IPMap) {
if m == nil {
return nil
}
sclone = NewIPMap(m.Len())
m.Range(func(ip net.IP, v interface{}) (cont bool) {
sclone.Set(ip, v)
return true
})
return sclone
}
// String implements the fmt.Stringer interface for *IPMap.
func (m *IPMap) String() (s string) {
if m == nil {
return "<nil>"
}
return fmt.Sprint(m.m)
}

View File

@@ -1,142 +0,0 @@
package aghnet
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIPMap_allocs(t *testing.T) {
ip4 := net.IP{1, 2, 3, 4}
m := NewIPMap(0)
m.Set(ip4, 42)
t.Run("get", func(t *testing.T) {
var v interface{}
var ok bool
allocs := testing.AllocsPerRun(100, func() {
v, ok = m.Get(ip4)
})
require.True(t, ok)
require.Equal(t, 42, v)
assert.Equal(t, float64(0), allocs)
})
t.Run("len", func(t *testing.T) {
var n int
allocs := testing.AllocsPerRun(100, func() {
n = m.Len()
})
require.Equal(t, 1, n)
assert.Equal(t, float64(0), allocs)
})
}
func TestIPMap(t *testing.T) {
ip4 := net.IP{1, 2, 3, 4}
ip6 := net.IP{
0x12, 0x34, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x56, 0x78,
}
val := 42
t.Run("nil", func(t *testing.T) {
var m *IPMap
assert.NotPanics(t, func() {
m.Del(ip4)
m.Del(ip6)
})
assert.NotPanics(t, func() {
v, ok := m.Get(ip4)
assert.Nil(t, v)
assert.False(t, ok)
v, ok = m.Get(ip6)
assert.Nil(t, v)
assert.False(t, ok)
})
assert.NotPanics(t, func() {
assert.Equal(t, 0, m.Len())
})
assert.NotPanics(t, func() {
n := 0
m.Range(func(_ net.IP, _ interface{}) (cont bool) {
n++
return true
})
assert.Equal(t, 0, n)
})
assert.Panics(t, func() {
m.Set(ip4, val)
})
assert.Panics(t, func() {
m.Set(ip6, val)
})
assert.NotPanics(t, func() {
sclone := m.ShallowClone()
assert.Nil(t, sclone)
})
})
testIPMap := func(t *testing.T, ip net.IP, s string) {
m := NewIPMap(0)
assert.Equal(t, 0, m.Len())
v, ok := m.Get(ip)
assert.Nil(t, v)
assert.False(t, ok)
m.Set(ip, val)
v, ok = m.Get(ip)
assert.Equal(t, val, v)
assert.True(t, ok)
n := 0
m.Range(func(ipKey net.IP, v interface{}) (cont bool) {
assert.Equal(t, ip.To16(), ipKey)
assert.Equal(t, val, v)
n++
return false
})
assert.Equal(t, 1, n)
sclone := m.ShallowClone()
assert.Equal(t, m, sclone)
assert.Equal(t, s, m.String())
m.Del(ip)
v, ok = m.Get(ip)
assert.Nil(t, v)
assert.False(t, ok)
assert.Equal(t, 0, m.Len())
}
t.Run("ipv4", func(t *testing.T) {
testIPMap(t, ip4, "map[1.2.3.4:42]")
})
t.Run("ipv6", func(t *testing.T) {
testIPMap(t, ip6, "map[1234::5678:42]")
})
}

View File

@@ -8,14 +8,13 @@ import (
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/netutil"
)
// ErrNoStaticIPInfo is returned by IfaceHasStaticIP when no information about
@@ -79,14 +78,14 @@ func CanBindPrivilegedPorts() (can bool, err error) {
// NetInterface represents an entry of network interfaces map.
type NetInterface struct {
MTU int `json:"mtu"`
// Addresses are the network interface addresses.
Addresses []net.IP `json:"ip_addresses,omitempty"`
// Subnets are the IP networks for this network interface.
Subnets []*net.IPNet `json:"-"`
Name string `json:"name"`
HardwareAddr net.HardwareAddr `json:"hardware_address"`
Flags net.Flags `json:"flags"`
// Array with the network interface addresses.
Addresses []net.IP `json:"ip_addresses,omitempty"`
// Array with IP networks for this network interface.
Subnets []*net.IPNet `json:"-"`
MTU int `json:"mtu"`
}
// MarshalJSON implements the json.Marshaler interface for NetInterface.
@@ -192,7 +191,7 @@ func GetSubnet(ifaceName string) *net.IPNet {
// CheckPortAvailable - check if TCP port is available
func CheckPortAvailable(host net.IP, port int) error {
ln, err := net.Listen("tcp", JoinHostPort(host.String(), port))
ln, err := net.Listen("tcp", netutil.JoinHostPort(host.String(), port))
if err != nil {
return err
}
@@ -206,7 +205,7 @@ func CheckPortAvailable(host net.IP, port int) error {
// CheckPacketPortAvailable - check if UDP port is available
func CheckPacketPortAvailable(host net.IP, port int) error {
ln, err := net.ListenPacket("udp", JoinHostPort(host.String(), port))
ln, err := net.ListenPacket("udp", netutil.JoinHostPort(host.String(), port))
if err != nil {
return err
}
@@ -265,141 +264,6 @@ func SplitHost(hostport string) (host string, err error) {
return host, nil
}
// TODO(e.burkov): Inspect the charToHex, ipParseARPA6, ipReverse and
// UnreverseAddr and maybe refactor it.
// charToHex converts character to a hexadecimal.
func charToHex(n byte) int8 {
if n >= '0' && n <= '9' {
return int8(n) - '0'
} else if (n|0x20) >= 'a' && (n|0x20) <= 'f' {
return (int8(n) | 0x20) - 'a' + 10
}
return -1
}
// ipParseARPA6 parse IPv6 reverse address
func ipParseARPA6(s string) (ip6 net.IP) {
if len(s) != 63 {
return nil
}
ip6 = make(net.IP, 16)
for i := 0; i != 64; i += 4 {
// parse "0.1."
n := charToHex(s[i])
n2 := charToHex(s[i+2])
if s[i+1] != '.' || (i != 60 && s[i+3] != '.') ||
n < 0 || n2 < 0 {
return nil
}
ip6[16-i/4-1] = byte(n2<<4) | byte(n&0x0f)
}
return ip6
}
// ipReverse inverts byte order of ip.
func ipReverse(ip net.IP) (rev net.IP) {
ipLen := len(ip)
rev = make(net.IP, ipLen)
for i, b := range ip {
rev[ipLen-i-1] = b
}
return rev
}
// ARPA addresses' suffixes.
const (
arpaV4Suffix = ".in-addr.arpa"
arpaV6Suffix = ".ip6.arpa"
)
// UnreverseAddr tries to convert reversed ARPA to a normal IP address.
func UnreverseAddr(arpa string) (unreversed net.IP) {
// Unify the input data.
arpa = strings.TrimSuffix(arpa, ".")
arpa = strings.ToLower(arpa)
if strings.HasSuffix(arpa, arpaV4Suffix) {
ip := strings.TrimSuffix(arpa, arpaV4Suffix)
ip4 := net.ParseIP(ip).To4()
if ip4 == nil {
return nil
}
return ipReverse(ip4)
} else if strings.HasSuffix(arpa, arpaV6Suffix) {
ip := strings.TrimSuffix(arpa, arpaV6Suffix)
return ipParseARPA6(ip)
}
// The suffix unrecognizable.
return nil
}
// The length of extreme cases of arpa formatted addresses.
//
// The example of IPv4 with maximum length:
//
// 49.91.20.104.in-addr.arpa
//
// The example of IPv6 with maximum length:
//
// 1.3.b.5.4.1.8.6.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.7.4.6.0.6.2.ip6.arpa
//
const (
arpaV4MaxLen = len("000.000.000.000") + len(arpaV4Suffix)
arpaV6MaxLen = len("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") +
len(arpaV6Suffix)
)
// ReverseAddr returns the ARPA hostname of the ip suitable for reverse DNS
// (PTR) record lookups. This is the modified version of ReverseAddr from
// github.com/miekg/dns package with no error among returned values.
func ReverseAddr(ip net.IP) (arpa string) {
const dot = "."
var strLen int
var suffix string
var writeByte func(val byte)
b := &strings.Builder{}
if ip4 := ip.To4(); ip4 != nil {
strLen, suffix = arpaV4MaxLen, arpaV4Suffix[1:]
ip = ip4
writeByte = func(val byte) {
stringutil.WriteToBuilder(b, strconv.Itoa(int(val)), dot)
}
} else if ip6 := ip.To16(); ip6 != nil {
strLen, suffix = arpaV6MaxLen, arpaV6Suffix[1:]
ip = ip6
writeByte = func(val byte) {
stringutil.WriteToBuilder(
b,
strconv.FormatUint(uint64(val&0xF), 16),
dot,
strconv.FormatUint(uint64(val>>4), 16),
dot,
)
}
} else {
return ""
}
b.Grow(strLen)
for i := len(ip) - 1; i >= 0; i-- {
writeByte(ip[i])
}
stringutil.WriteToBuilder(b, suffix)
return b.String()
}
// CollectAllIfacesAddrs returns the slice of all network interfaces IP
// addresses without port number.
func CollectAllIfacesAddrs() (addrs []string, err error) {

View File

@@ -1,10 +1,8 @@
package aghnet
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -16,92 +14,3 @@ func TestGetValidNetInterfacesForWeb(t *testing.T) {
require.NotEmptyf(t, iface.Addresses, "no addresses found for %s", iface.Name)
}
}
func TestUnreverseAddr(t *testing.T) {
testCases := []struct {
name string
have string
want net.IP
}{{
name: "good_ipv4",
have: "1.0.0.127.in-addr.arpa",
want: net.IP{127, 0, 0, 1},
}, {
name: "good_ipv6",
have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
want: net.ParseIP("::abcd:1234"),
}, {
name: "good_ipv6_case",
have: "4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.iP6.ArPa",
want: net.ParseIP("::abcd:1234"),
}, {
name: "good_ipv4_dot",
have: "1.0.0.127.in-addr.arpa.",
want: net.IP{127, 0, 0, 1},
}, {
name: "good_ipv4_case",
have: "1.0.0.127.In-Addr.Arpa",
want: net.IP{127, 0, 0, 1},
}, {
name: "wrong_ipv4",
have: ".0.0.127.in-addr.arpa",
want: nil,
}, {
name: "wrong_ipv6",
have: ".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
want: nil,
}, {
name: "bad_ipv6_dot",
have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa",
want: nil,
}, {
name: "bad_ipv6_space",
have: "4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ip := UnreverseAddr(tc.have)
assert.True(t, tc.want.Equal(ip))
})
}
}
func TestReverseAddr(t *testing.T) {
testCases := []struct {
name string
want string
ip net.IP
}{{
name: "valid_ipv4",
want: "4.3.2.1.in-addr.arpa",
ip: net.IP{1, 2, 3, 4},
}, {
name: "valid_ipv6",
want: "1.3.b.5.4.1.8.6.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.7.4.6.0.6.2.ip6.arpa",
ip: net.ParseIP("2606:4700:10::6814:5b31"),
}, {
name: "nil_ip",
want: "",
ip: nil,
}, {
name: "unspecified_ipv6",
want: "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
ip: net.IPv6unspecified,
}, {
name: "unspecified_ipv4",
want: "0.0.0.0.in-addr.arpa",
ip: net.IPv4zero,
}, {
name: "wrong_length_ip",
want: "",
ip: net.IP{1, 2, 3, 4, 5},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, ReverseAddr(tc.ip))
})
}
}

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
)
@@ -104,7 +105,7 @@ const dockerEmbeddedDNS = "127.0.0.11"
func (sr *systemResolvers) dialFunc(_ context.Context, _, address string) (_ net.Conn, err error) {
// Just validate the passed address is a valid IP.
var host string
host, err = SplitHost(address)
host, err = netutil.SplitHost(address)
if err != nil {
// TODO(e.burkov): Maybe use a structured errBadAddrPassed to
// allow unwrapping of the real error.