all: sync with master; upd chlog
This commit is contained in:
132
internal/rdns/rdns.go
Normal file
132
internal/rdns/rdns.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Package rdns processes reverse DNS lookup queries.
|
||||
package rdns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/bluele/gcache"
|
||||
)
|
||||
|
||||
// Interface processes rDNS queries.
|
||||
type Interface interface {
|
||||
// Process makes rDNS request and returns domain name. changed indicates
|
||||
// that domain name was updated since last request.
|
||||
Process(ip netip.Addr) (host string, changed bool)
|
||||
}
|
||||
|
||||
// Empty is an empty [Inteface] implementation which does nothing.
|
||||
type Empty struct{}
|
||||
|
||||
// type check
|
||||
var _ Interface = (*Empty)(nil)
|
||||
|
||||
// Process implements the [Interface] interface for Empty.
|
||||
func (Empty) Process(_ netip.Addr) (host string, changed bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Exchanger is a resolver for clients' addresses.
|
||||
type Exchanger interface {
|
||||
// Exchange tries to resolve the ip in a suitable way, i.e. either as local
|
||||
// or as external.
|
||||
Exchange(ip netip.Addr) (host string, err error)
|
||||
}
|
||||
|
||||
// Config is the configuration structure for Default.
|
||||
type Config struct {
|
||||
// Exchanger resolves IP addresses to domain names.
|
||||
Exchanger Exchanger
|
||||
|
||||
// CacheSize is the maximum size of the cache. It must be greater than
|
||||
// zero.
|
||||
CacheSize int
|
||||
|
||||
// CacheTTL is the Time to Live duration for cached IP addresses.
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// Default is the default rDNS query processor.
|
||||
type Default struct {
|
||||
// cache is the cache containing IP addresses of clients. An active IP
|
||||
// address is resolved once again after it expires. If IP address couldn't
|
||||
// be resolved, it stays here for some time to prevent further attempts to
|
||||
// resolve the same IP.
|
||||
cache gcache.Cache
|
||||
|
||||
// exchanger resolves IP addresses to domain names.
|
||||
exchanger Exchanger
|
||||
|
||||
// cacheTTL is the Time to Live duration for cached IP addresses.
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// New returns a new default rDNS query processor. conf must not be nil.
|
||||
func New(conf *Config) (r *Default) {
|
||||
return &Default{
|
||||
cache: gcache.New(conf.CacheSize).LRU().Build(),
|
||||
exchanger: conf.Exchanger,
|
||||
cacheTTL: conf.CacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Interface = (*Default)(nil)
|
||||
|
||||
// Process implements the [Interface] interface for Default.
|
||||
func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
|
||||
fromCache, expired := r.findInCache(ip)
|
||||
if !expired {
|
||||
return fromCache, false
|
||||
}
|
||||
|
||||
host, err := r.exchanger.Exchange(ip)
|
||||
if err != nil {
|
||||
log.Debug("rdns: resolving %q: %s", ip, err)
|
||||
}
|
||||
|
||||
item := &cacheItem{
|
||||
expiry: time.Now().Add(r.cacheTTL),
|
||||
host: host,
|
||||
}
|
||||
|
||||
err = r.cache.Set(ip, item)
|
||||
if err != nil {
|
||||
log.Debug("rdns: cache: adding item %q: %s", ip, err)
|
||||
}
|
||||
|
||||
return host, fromCache == "" || host != fromCache
|
||||
}
|
||||
|
||||
// findInCache finds domain name in the cache. expired is true if host is not
|
||||
// valid anymore.
|
||||
func (r *Default) findInCache(ip netip.Addr) (host string, expired bool) {
|
||||
val, err := r.cache.Get(ip)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gcache.KeyNotFoundError) {
|
||||
log.Debug("rdns: cache: retrieving %q: %s", ip, err)
|
||||
}
|
||||
|
||||
return "", true
|
||||
}
|
||||
|
||||
item, ok := val.(*cacheItem)
|
||||
if !ok {
|
||||
log.Debug("rdns: cache: %q bad type %T", ip, val)
|
||||
|
||||
return "", true
|
||||
}
|
||||
|
||||
return item.host, time.Now().After(item.expiry)
|
||||
}
|
||||
|
||||
// cacheItem represents an item that we will store in the cache.
|
||||
type cacheItem struct {
|
||||
// expiry is the time when cacheItem will expire.
|
||||
expiry time.Time
|
||||
|
||||
// host is the domain name of a runtime client.
|
||||
host string
|
||||
}
|
||||
105
internal/rdns/rdns_test.go
Normal file
105
internal/rdns/rdns_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package rdns_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fakeRDNSExchanger is a mock [rdns.Exchanger] implementation for tests.
|
||||
type fakeRDNSExchanger struct {
|
||||
OnExchange func(ip netip.Addr) (host string, err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ rdns.Exchanger = (*fakeRDNSExchanger)(nil)
|
||||
|
||||
// Exchange implements [rdns.Exchanger] interface for *fakeRDNSExchanger.
|
||||
func (e *fakeRDNSExchanger) Exchange(ip netip.Addr) (host string, err error) {
|
||||
return e.OnExchange(ip)
|
||||
}
|
||||
|
||||
func TestDefault_Process(t *testing.T) {
|
||||
ip1 := netip.MustParseAddr("1.2.3.4")
|
||||
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
ip2 := netip.MustParseAddr("4.3.2.1")
|
||||
revAddr2, err := netutil.IPToReversedAddr(ip2.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
localIP := netip.MustParseAddr("192.168.0.1")
|
||||
localRevAddr1, err := netutil.IPToReversedAddr(localIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &rdns.Config{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Hour,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
addr netip.Addr
|
||||
want string
|
||||
}{{
|
||||
name: "first",
|
||||
addr: ip1,
|
||||
want: revAddr1,
|
||||
}, {
|
||||
name: "second",
|
||||
addr: ip2,
|
||||
want: revAddr2,
|
||||
}, {
|
||||
name: "empty",
|
||||
addr: netip.MustParseAddr("0.0.0.0"),
|
||||
want: "",
|
||||
}, {
|
||||
name: "private",
|
||||
addr: localIP,
|
||||
want: localRevAddr1,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hit := 0
|
||||
onExchange := func(ip netip.Addr) (host string, err error) {
|
||||
hit++
|
||||
|
||||
switch ip {
|
||||
case ip1:
|
||||
return revAddr1, nil
|
||||
case ip2:
|
||||
return revAddr2, nil
|
||||
case localIP:
|
||||
return localRevAddr1, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
exchanger := &fakeRDNSExchanger{
|
||||
OnExchange: onExchange,
|
||||
}
|
||||
|
||||
config.Exchanger = exchanger
|
||||
r := rdns.New(config)
|
||||
|
||||
got, changed := r.Process(tc.addr)
|
||||
require.True(t, changed)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, 1, hit)
|
||||
|
||||
// From cache.
|
||||
got, changed = r.Process(tc.addr)
|
||||
require.False(t, changed)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, 1, hit)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user