mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-24 22:15:45 +03:00
Pull request 1925: 6006-client-processor
Updates #6006.
Squashed commit of the following:
commit c72d6375e9c472c73b0bb9d025a8e197f404ba38
Merge: 02d64b10e 0cd441f04
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Tue Jul 18 13:56:26 2023 +0300
Merge branch 'master' into 6006-client-processor
commit 02d64b10e19b2e937e45cab58d2310231a19bfbc
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Mon Jul 17 19:42:07 2023 +0300
client: imp code, tests
commit b1613463089b4dde97484ff6a44b05888f0c2276
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Mon Jul 17 18:42:19 2023 +0300
client: imp code, docs, tests
commit f71a17983b70d79839cf35dbe3279f0fdcac2ed7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Fri Jul 14 21:53:47 2023 +0300
all: add new client processor; imp code
This commit is contained in:
parent
0cd441f04f
commit
dead10e033
10 changed files with 664 additions and 33 deletions
|
@ -4,9 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
@ -135,6 +139,59 @@ func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||||
return s.OnConfig()
|
return s.OnConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package client
|
||||||
|
|
||||||
|
// AddressProcessor is a fake [client.AddressProcessor] implementation for
|
||||||
|
// tests.
|
||||||
|
type AddressProcessor struct {
|
||||||
|
OnProcess func(ip netip.Addr)
|
||||||
|
OnClose func() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ client.AddressProcessor = (*AddressProcessor)(nil)
|
||||||
|
|
||||||
|
// Process implements the [client.AddressProcessor] interface for
|
||||||
|
// *AddressProcessor.
|
||||||
|
func (p *AddressProcessor) Process(ip netip.Addr) {
|
||||||
|
p.OnProcess(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the [client.AddressProcessor] interface for
|
||||||
|
// *AddressProcessor.
|
||||||
|
func (p *AddressProcessor) Close() (err error) {
|
||||||
|
return p.OnClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddressUpdater is a fake [client.AddressUpdater] implementation for tests.
|
||||||
|
type AddressUpdater struct {
|
||||||
|
OnUpdateAddress func(ip netip.Addr, host string, info *whois.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ client.AddressUpdater = (*AddressUpdater)(nil)
|
||||||
|
|
||||||
|
// UpdateAddress implements the [client.AddressUpdater] interface for
|
||||||
|
// *AddressUpdater.
|
||||||
|
func (p *AddressUpdater) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
|
||||||
|
p.OnUpdateAddress(ip, host, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package rdns
|
||||||
|
|
||||||
|
// Exchanger is a fake [rdns.Exchanger] implementation for tests.
|
||||||
|
type Exchanger struct {
|
||||||
|
OnExchange func(ip netip.Addr) (host string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ rdns.Exchanger = (*Exchanger)(nil)
|
||||||
|
|
||||||
|
// Exchange implements [rdns.Exchanger] interface for *Exchanger.
|
||||||
|
func (e *Exchanger) Exchange(ip netip.Addr) (host string, err error) {
|
||||||
|
return e.OnExchange(ip)
|
||||||
|
}
|
||||||
|
|
||||||
// Module dnsproxy
|
// Module dnsproxy
|
||||||
|
|
||||||
// Package upstream
|
// Package upstream
|
||||||
|
|
293
internal/client/addrproc.go
Normal file
293
internal/client/addrproc.go
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrClosed is returned from [AddressProcessor.Close] if it's closed more than
|
||||||
|
// once.
|
||||||
|
const ErrClosed errors.Error = "use of closed address processor"
|
||||||
|
|
||||||
|
// AddressProcessor is the interface for types that can process clients.
|
||||||
|
type AddressProcessor interface {
|
||||||
|
Process(ip netip.Addr)
|
||||||
|
Close() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyAddrProc is an [AddressProcessor] that does nothing.
|
||||||
|
type EmptyAddrProc struct{}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ AddressProcessor = EmptyAddrProc{}
|
||||||
|
|
||||||
|
// Process implements the [AddressProcessor] interface for EmptyAddrProc.
|
||||||
|
func (EmptyAddrProc) Process(_ netip.Addr) {}
|
||||||
|
|
||||||
|
// Close implements the [AddressProcessor] interface for EmptyAddrProc.
|
||||||
|
func (EmptyAddrProc) Close() (_ error) { return nil }
|
||||||
|
|
||||||
|
// DefaultAddrProcConfig is the configuration structure for address processors.
|
||||||
|
type DefaultAddrProcConfig struct {
|
||||||
|
// DialContext is used to create TCP connections to WHOIS servers.
|
||||||
|
// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
|
||||||
|
DialContext whois.DialContextFunc
|
||||||
|
|
||||||
|
// Exchanger is used to perform rDNS queries. Exchanger must not be nil if
|
||||||
|
// [DefaultAddrProcConfig.UseRDNS] is true.
|
||||||
|
Exchanger rdns.Exchanger
|
||||||
|
|
||||||
|
// PrivateSubnets are used to determine if an incoming IP address is
|
||||||
|
// private. It must not be nil.
|
||||||
|
PrivateSubnets netutil.SubnetSet
|
||||||
|
|
||||||
|
// AddressUpdater is used to update the information about a client's IP
|
||||||
|
// address. It must not be nil.
|
||||||
|
AddressUpdater AddressUpdater
|
||||||
|
|
||||||
|
// InitialAddresses are the addresses that are queued for processing
|
||||||
|
// immediately by [NewDefaultAddrProc].
|
||||||
|
InitialAddresses []netip.Addr
|
||||||
|
|
||||||
|
// UseRDNS, if true, enables resolving of client IP addresses using reverse
|
||||||
|
// DNS.
|
||||||
|
UseRDNS bool
|
||||||
|
|
||||||
|
// UsePrivateRDNS, if true, enables resolving of private client IP addresses
|
||||||
|
// using reverse DNS. See [DefaultAddrProcConfig.PrivateSubnets].
|
||||||
|
UsePrivateRDNS bool
|
||||||
|
|
||||||
|
// UseWHOIS, if true, enables resolving of client IP addresses using WHOIS.
|
||||||
|
UseWHOIS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddressUpdater is the interface for storages of DNS clients that can update
|
||||||
|
// information about them.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider using the actual client storage once it is moved
|
||||||
|
// into this package.
|
||||||
|
type AddressUpdater interface {
|
||||||
|
// UpdateAddress updates information about an IP address, setting host (if
|
||||||
|
// not empty) and WHOIS information (if not nil).
|
||||||
|
UpdateAddress(ip netip.Addr, host string, info *whois.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAddrProc processes incoming client addresses with rDNS and WHOIS, if
|
||||||
|
// configured, and updates that information in a client storage.
|
||||||
|
type DefaultAddrProc struct {
|
||||||
|
// clientIPsMu serializes closure of clientIPs and access to isClosed.
|
||||||
|
clientIPsMu *sync.Mutex
|
||||||
|
|
||||||
|
// clientIPs is the channel queueing client processing tasks.
|
||||||
|
clientIPs chan netip.Addr
|
||||||
|
|
||||||
|
// rdns is used to perform rDNS lookups of clients' IP addresses.
|
||||||
|
rdns rdns.Interface
|
||||||
|
|
||||||
|
// whois is used to perform WHOIS lookups of clients' IP addresses.
|
||||||
|
whois whois.Interface
|
||||||
|
|
||||||
|
// addrUpdater is used to update the information about a client's IP
|
||||||
|
// address.
|
||||||
|
addrUpdater AddressUpdater
|
||||||
|
|
||||||
|
// privateSubnets are used to determine if an incoming IP address is
|
||||||
|
// private.
|
||||||
|
privateSubnets netutil.SubnetSet
|
||||||
|
|
||||||
|
// isClosed is set to true once the address processor is closed.
|
||||||
|
isClosed bool
|
||||||
|
|
||||||
|
// usePrivateRDNS, if true, enables resolving of private client IP addresses
|
||||||
|
// using reverse DNS.
|
||||||
|
usePrivateRDNS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultQueueSize is the size of queue of IPs for rDNS and WHOIS
|
||||||
|
// processing.
|
||||||
|
defaultQueueSize = 255
|
||||||
|
|
||||||
|
// defaultCacheSize is the maximum size of the cache for rDNS and WHOIS
|
||||||
|
// processing. It must be greater than zero.
|
||||||
|
defaultCacheSize = 10_000
|
||||||
|
|
||||||
|
// defaultIPTTL is the Time to Live duration for IP addresses cached by
|
||||||
|
// rDNS and WHOIS.
|
||||||
|
defaultIPTTL = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDefaultAddrProc returns a new running client address processor. c must
|
||||||
|
// not be nil.
|
||||||
|
func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
||||||
|
p = &DefaultAddrProc{
|
||||||
|
clientIPsMu: &sync.Mutex{},
|
||||||
|
clientIPs: make(chan netip.Addr, defaultQueueSize),
|
||||||
|
rdns: &rdns.Empty{},
|
||||||
|
addrUpdater: c.AddressUpdater,
|
||||||
|
whois: &whois.Empty{},
|
||||||
|
privateSubnets: c.PrivateSubnets,
|
||||||
|
usePrivateRDNS: c.UsePrivateRDNS,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseRDNS {
|
||||||
|
p.rdns = rdns.New(&rdns.Config{
|
||||||
|
Exchanger: c.Exchanger,
|
||||||
|
CacheSize: defaultCacheSize,
|
||||||
|
CacheTTL: defaultIPTTL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseWHOIS {
|
||||||
|
p.whois = newWHOIS(c.DialContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
go p.process()
|
||||||
|
|
||||||
|
for _, ip := range c.InitialAddresses {
|
||||||
|
p.Process(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWHOIS returns a whois.Interface instance using the given function for
|
||||||
|
// dialing.
|
||||||
|
func newWHOIS(dialFunc whois.DialContextFunc) (w whois.Interface) {
|
||||||
|
// TODO(s.chzhen): Consider making configurable.
|
||||||
|
const (
|
||||||
|
// defaultTimeout is the timeout for WHOIS requests.
|
||||||
|
defaultTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// defaultMaxConnReadSize is an upper limit in bytes for reading from a
|
||||||
|
// net.Conn.
|
||||||
|
defaultMaxConnReadSize = 64 * 1024
|
||||||
|
|
||||||
|
// defaultMaxRedirects is the maximum redirects count.
|
||||||
|
defaultMaxRedirects = 5
|
||||||
|
|
||||||
|
// defaultMaxInfoLen is the maximum length of whois.Info fields.
|
||||||
|
defaultMaxInfoLen = 250
|
||||||
|
)
|
||||||
|
|
||||||
|
return whois.New(&whois.Config{
|
||||||
|
DialContext: dialFunc,
|
||||||
|
ServerAddr: whois.DefaultServer,
|
||||||
|
Port: whois.DefaultPort,
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
CacheSize: defaultCacheSize,
|
||||||
|
MaxConnReadSize: defaultMaxConnReadSize,
|
||||||
|
MaxRedirects: defaultMaxRedirects,
|
||||||
|
MaxInfoLen: defaultMaxInfoLen,
|
||||||
|
CacheTTL: defaultIPTTL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ AddressProcessor = (*DefaultAddrProc)(nil)
|
||||||
|
|
||||||
|
// Process implements the [AddressProcessor] interface for *DefaultAddrProc.
|
||||||
|
func (p *DefaultAddrProc) Process(ip netip.Addr) {
|
||||||
|
p.clientIPsMu.Lock()
|
||||||
|
defer p.clientIPsMu.Unlock()
|
||||||
|
|
||||||
|
if p.isClosed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case p.clientIPs <- ip:
|
||||||
|
// Go on.
|
||||||
|
default:
|
||||||
|
log.Debug("clients: ip channel is full; len: %d", len(p.clientIPs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process processes the incoming client IP-address information. It is intended
|
||||||
|
// to be used as a goroutine. Once clientIPs is closed, process exits.
|
||||||
|
func (p *DefaultAddrProc) process() {
|
||||||
|
defer log.OnPanic("addrProcessor.process")
|
||||||
|
|
||||||
|
log.Info("clients: processing addresses")
|
||||||
|
|
||||||
|
for ip := range p.clientIPs {
|
||||||
|
host := p.processRDNS(ip)
|
||||||
|
info := p.processWHOIS(ip)
|
||||||
|
|
||||||
|
p.addrUpdater.UpdateAddress(ip, host, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("clients: finished processing addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
// processRDNS resolves the clients' IP addresses using reverse DNS. host is
|
||||||
|
// empty if there were errors or if the information hasn't changed.
|
||||||
|
func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
|
||||||
|
start := time.Now()
|
||||||
|
log.Debug("clients: processing %s with rdns", ip)
|
||||||
|
defer func() {
|
||||||
|
log.Debug("clients: finished processing %s with rdns in %s", ip, time.Since(start))
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok := p.shouldResolve(ip)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host, changed := p.rdns.Process(ip)
|
||||||
|
if !changed {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldResolve returns false if ip is a loopback address, or ip is private and
|
||||||
|
// resolving of private addresses is disabled.
|
||||||
|
func (p *DefaultAddrProc) shouldResolve(ip netip.Addr) (ok bool) {
|
||||||
|
return !ip.IsLoopback() &&
|
||||||
|
(p.usePrivateRDNS || !p.privateSubnets.Contains(ip.AsSlice()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// processWHOIS looks up the information about clients' IP addresses in the
|
||||||
|
// WHOIS databases. info is nil if there were errors or if the information
|
||||||
|
// hasn't changed.
|
||||||
|
func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
|
||||||
|
start := time.Now()
|
||||||
|
log.Debug("clients: processing %s with whois", ip)
|
||||||
|
defer func() {
|
||||||
|
log.Debug("clients: finished processing %s with whois in %s", ip, time.Since(start))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the
|
||||||
|
// context.
|
||||||
|
info, changed := p.whois.Process(context.Background(), ip)
|
||||||
|
if !changed {
|
||||||
|
info = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the [AddressProcessor] interface for *DefaultAddrProc.
|
||||||
|
func (p *DefaultAddrProc) Close() (err error) {
|
||||||
|
p.clientIPsMu.Lock()
|
||||||
|
defer p.clientIPsMu.Unlock()
|
||||||
|
|
||||||
|
if p.isClosed {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
close(p.clientIPs)
|
||||||
|
p.isClosed = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
259
internal/client/addrproc_test.go
Normal file
259
internal/client/addrproc_test.go
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
package client_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil/fakenet"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmptyAddrProc(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p := client.EmptyAddrProc{}
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
p.Process(testIP)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
err := p.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
privateIP := netip.MustParseAddr("192.168.0.1")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rdnsErr error
|
||||||
|
ip netip.Addr
|
||||||
|
name string
|
||||||
|
host string
|
||||||
|
usePrivate bool
|
||||||
|
wantUpd bool
|
||||||
|
}{{
|
||||||
|
rdnsErr: nil,
|
||||||
|
ip: testIP,
|
||||||
|
name: "success",
|
||||||
|
host: testHost,
|
||||||
|
usePrivate: false,
|
||||||
|
wantUpd: true,
|
||||||
|
}, {
|
||||||
|
rdnsErr: nil,
|
||||||
|
ip: testIP,
|
||||||
|
name: "no_host",
|
||||||
|
host: "",
|
||||||
|
usePrivate: false,
|
||||||
|
wantUpd: false,
|
||||||
|
}, {
|
||||||
|
rdnsErr: nil,
|
||||||
|
ip: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
name: "localhost",
|
||||||
|
host: "",
|
||||||
|
usePrivate: false,
|
||||||
|
wantUpd: false,
|
||||||
|
}, {
|
||||||
|
rdnsErr: nil,
|
||||||
|
ip: privateIP,
|
||||||
|
name: "private_ignored",
|
||||||
|
host: "",
|
||||||
|
usePrivate: false,
|
||||||
|
wantUpd: false,
|
||||||
|
}, {
|
||||||
|
rdnsErr: nil,
|
||||||
|
ip: privateIP,
|
||||||
|
name: "private_processed",
|
||||||
|
host: "private.example",
|
||||||
|
usePrivate: true,
|
||||||
|
wantUpd: true,
|
||||||
|
}, {
|
||||||
|
rdnsErr: errors.Error("rdns error"),
|
||||||
|
ip: testIP,
|
||||||
|
name: "rdns_error",
|
||||||
|
host: "",
|
||||||
|
usePrivate: false,
|
||||||
|
wantUpd: false,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
updIPCh := make(chan netip.Addr, 1)
|
||||||
|
updHostCh := make(chan string, 1)
|
||||||
|
updInfoCh := make(chan *whois.Info, 1)
|
||||||
|
|
||||||
|
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||||
|
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
Exchanger: &aghtest.Exchanger{
|
||||||
|
OnExchange: func(ip netip.Addr) (host string, err error) {
|
||||||
|
return tc.host, tc.rdnsErr
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
AddressUpdater: &aghtest.AddressUpdater{
|
||||||
|
OnUpdateAddress: newOnUpdateAddress(tc.wantUpd, updIPCh, updHostCh, updInfoCh),
|
||||||
|
},
|
||||||
|
UseRDNS: true,
|
||||||
|
UsePrivateRDNS: tc.usePrivate,
|
||||||
|
UseWHOIS: false,
|
||||||
|
})
|
||||||
|
testutil.CleanupAndRequireSuccess(t, p.Close)
|
||||||
|
|
||||||
|
p.Process(tc.ip)
|
||||||
|
|
||||||
|
if !tc.wantUpd {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gotIP, _ := testutil.RequireReceive(t, updIPCh, testTimeout)
|
||||||
|
assert.Equal(t, tc.ip, gotIP)
|
||||||
|
|
||||||
|
gotHost, _ := testutil.RequireReceive(t, updHostCh, testTimeout)
|
||||||
|
assert.Equal(t, tc.host, gotHost)
|
||||||
|
|
||||||
|
gotInfo, _ := testutil.RequireReceive(t, updInfoCh, testTimeout)
|
||||||
|
assert.Nil(t, gotInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOnUpdateAddress is a test helper that returns a new OnUpdateAddress
|
||||||
|
// callback using the provided channels if an update is expected and panicking
|
||||||
|
// otherwise.
|
||||||
|
func newOnUpdateAddress(
|
||||||
|
want bool,
|
||||||
|
ips chan<- netip.Addr,
|
||||||
|
hosts chan<- string,
|
||||||
|
infos chan<- *whois.Info,
|
||||||
|
) (f func(ip netip.Addr, host string, info *whois.Info)) {
|
||||||
|
return func(ip netip.Addr, host string, info *whois.Info) {
|
||||||
|
if !want {
|
||||||
|
panic("got unexpected update")
|
||||||
|
}
|
||||||
|
|
||||||
|
ips <- ip
|
||||||
|
hosts <- host
|
||||||
|
infos <- info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
wantInfo *whois.Info
|
||||||
|
exchErr error
|
||||||
|
name string
|
||||||
|
wantUpd bool
|
||||||
|
}{{
|
||||||
|
wantInfo: &whois.Info{
|
||||||
|
City: testWHOISCity,
|
||||||
|
},
|
||||||
|
exchErr: nil,
|
||||||
|
name: "success",
|
||||||
|
wantUpd: true,
|
||||||
|
}, {
|
||||||
|
wantInfo: nil,
|
||||||
|
exchErr: nil,
|
||||||
|
name: "no_info",
|
||||||
|
wantUpd: false,
|
||||||
|
}, {
|
||||||
|
wantInfo: nil,
|
||||||
|
exchErr: errors.Error("whois error"),
|
||||||
|
name: "whois_error",
|
||||||
|
wantUpd: false,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
whoisConn := &fakenet.Conn{
|
||||||
|
OnClose: func() (err error) { return nil },
|
||||||
|
OnRead: func(b []byte) (n int, err error) {
|
||||||
|
if tc.wantInfo == nil {
|
||||||
|
return 0, tc.exchErr
|
||||||
|
}
|
||||||
|
|
||||||
|
data := "city: " + tc.wantInfo.City + "\n"
|
||||||
|
copy(b, data)
|
||||||
|
|
||||||
|
return len(data), io.EOF
|
||||||
|
},
|
||||||
|
OnSetDeadline: func(_ time.Time) (err error) { return nil },
|
||||||
|
OnWrite: func(b []byte) (n int, err error) { return len(b), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
updIPCh := make(chan netip.Addr, 1)
|
||||||
|
updHostCh := make(chan string, 1)
|
||||||
|
updInfoCh := make(chan *whois.Info, 1)
|
||||||
|
|
||||||
|
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||||
|
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||||
|
return whoisConn, nil
|
||||||
|
},
|
||||||
|
Exchanger: &aghtest.Exchanger{
|
||||||
|
OnExchange: func(_ netip.Addr) (host string, err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
AddressUpdater: &aghtest.AddressUpdater{
|
||||||
|
OnUpdateAddress: newOnUpdateAddress(tc.wantUpd, updIPCh, updHostCh, updInfoCh),
|
||||||
|
},
|
||||||
|
UseRDNS: false,
|
||||||
|
UsePrivateRDNS: false,
|
||||||
|
UseWHOIS: true,
|
||||||
|
})
|
||||||
|
testutil.CleanupAndRequireSuccess(t, p.Close)
|
||||||
|
|
||||||
|
p.Process(testIP)
|
||||||
|
|
||||||
|
if !tc.wantUpd {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gotIP, _ := testutil.RequireReceive(t, updIPCh, testTimeout)
|
||||||
|
assert.Equal(t, testIP, gotIP)
|
||||||
|
|
||||||
|
gotHost, _ := testutil.RequireReceive(t, updHostCh, testTimeout)
|
||||||
|
assert.Empty(t, gotHost)
|
||||||
|
|
||||||
|
gotInfo, _ := testutil.RequireReceive(t, updInfoCh, testTimeout)
|
||||||
|
assert.Equal(t, tc.wantInfo, gotInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultAddrProc_Close(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{})
|
||||||
|
|
||||||
|
err := p.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = p.Close()
|
||||||
|
assert.ErrorIs(t, err, client.ErrClosed)
|
||||||
|
}
|
5
internal/client/client.go
Normal file
5
internal/client/client.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Package client contains types and logic dealing with AdGuard Home's DNS
|
||||||
|
// clients.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Expand.
|
||||||
|
package client
|
25
internal/client/client_test.go
Normal file
25
internal/client/client_test.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package client_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testutil.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testHost is the common hostname for tests.
|
||||||
|
const testHost = "client.example"
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
// testWHOISCity is the common city for tests.
|
||||||
|
const testWHOISCity = "Brussels"
|
||||||
|
|
||||||
|
// testIP is the common IP address for tests.
|
||||||
|
var testIP = netip.MustParseAddr("1.2.3.4")
|
|
@ -17,7 +17,7 @@ type Interface interface {
|
||||||
Process(ip netip.Addr) (host string, changed bool)
|
Process(ip netip.Addr) (host string, changed bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty is an empty [Inteface] implementation which does nothing.
|
// Empty is an empty [Interface] implementation which does nothing.
|
||||||
type Empty struct{}
|
type Empty struct{}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
|
|
|
@ -5,25 +5,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestDefault_Process(t *testing.T) {
|
||||||
ip1 := netip.MustParseAddr("1.2.3.4")
|
ip1 := netip.MustParseAddr("1.2.3.4")
|
||||||
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
|
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
|
||||||
|
@ -81,7 +69,7 @@ func TestDefault_Process(t *testing.T) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exchanger := &fakeRDNSExchanger{
|
exchanger := &aghtest.Exchanger{
|
||||||
OnExchange: onExchange,
|
OnExchange: onExchange,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,8 @@ func (Empty) Process(_ context.Context, _ netip.Addr) (info *Info, changed bool)
|
||||||
|
|
||||||
// Config is the configuration structure for Default.
|
// Config is the configuration structure for Default.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// DialContext specifies the dial function for creating unencrypted TCP
|
// DialContext is used to create TCP connections to WHOIS servers.
|
||||||
// connections.
|
DialContext DialContextFunc
|
||||||
DialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
|
|
||||||
|
|
||||||
// ServerAddr is the address of the WHOIS server.
|
// ServerAddr is the address of the WHOIS server.
|
||||||
ServerAddr string
|
ServerAddr string
|
||||||
|
@ -78,6 +77,13 @@ type Config struct {
|
||||||
Port uint16
|
Port uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContextFunc is the semantic alias for dialing functions, such as
|
||||||
|
// [http.Transport.DialContext].
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Move to aghnet once it stops importing aghtest, because
|
||||||
|
// otherwise there is an import cycle.
|
||||||
|
type DialContextFunc = func(ctx context.Context, network, addr string) (conn net.Conn, err error)
|
||||||
|
|
||||||
// Default is the default WHOIS information processor.
|
// Default is the default WHOIS information processor.
|
||||||
type Default struct {
|
type Default struct {
|
||||||
// cache is the cache containing IP addresses of clients. An active IP
|
// cache is the cache containing IP addresses of clients. An active IP
|
||||||
|
@ -86,9 +92,8 @@ type Default struct {
|
||||||
// resolve the same IP.
|
// resolve the same IP.
|
||||||
cache gcache.Cache
|
cache gcache.Cache
|
||||||
|
|
||||||
// dialContext connects to a remote server resolving hostname using our own
|
// dialContext is used to create TCP connections to WHOIS servers.
|
||||||
// DNS server and unecrypted TCP connection.
|
dialContext DialContextFunc
|
||||||
dialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
|
|
||||||
|
|
||||||
// serverAddr is the address of the WHOIS server.
|
// serverAddr is the address of the WHOIS server.
|
||||||
serverAddr string
|
serverAddr string
|
||||||
|
@ -215,7 +220,7 @@ func (w *Default) query(ctx context.Context, target, serverAddr string) (data []
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(w.timeout))
|
_ = conn.SetDeadline(time.Now().Add(w.timeout))
|
||||||
_, err = io.WriteString(conn, target+"\r\n")
|
_, err = io.WriteString(conn, target+"\r\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
|
@ -310,7 +315,7 @@ func (w *Default) requestInfo(
|
||||||
|
|
||||||
kv, err := w.queryAll(ctx, ip.String())
|
kv, err := w.queryAll(ctx, ip.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("whois: quering about %q: %s", ip, err)
|
log.Debug("whois: querying %q: %s", ip, err)
|
||||||
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,20 +113,14 @@ func TestDefault_Process(t *testing.T) {
|
||||||
|
|
||||||
return copy(b, tc.data), io.EOF
|
return copy(b, tc.data), io.EOF
|
||||||
},
|
},
|
||||||
OnWrite: func(b []byte) (n int, err error) {
|
OnWrite: func(b []byte) (n int, err error) { return len(b), nil },
|
||||||
return len(b), nil
|
OnClose: func() (err error) { return nil },
|
||||||
},
|
OnSetDeadline: func(t time.Time) (err error) { return nil },
|
||||||
OnClose: func() (err error) {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
OnSetReadDeadline: func(t time.Time) (err error) {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w := whois.New(&whois.Config{
|
w := whois.New(&whois.Config{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
DialContext: func(_ context.Context, _, addr string) (_ net.Conn, _ error) {
|
DialContext: func(_ context.Context, _, _ string) (_ net.Conn, _ error) {
|
||||||
hit = 0
|
hit = 0
|
||||||
|
|
||||||
return fakeConn, nil
|
return fakeConn, nil
|
||||||
|
|
|
@ -176,10 +176,13 @@ run_linter gocognit --over 10\
|
||||||
./internal/aghchan/\
|
./internal/aghchan/\
|
||||||
./internal/aghhttp/\
|
./internal/aghhttp/\
|
||||||
./internal/aghio/\
|
./internal/aghio/\
|
||||||
|
./internal/client/\
|
||||||
|
./internal/dhcpsvc\
|
||||||
./internal/filtering/hashprefix/\
|
./internal/filtering/hashprefix/\
|
||||||
./internal/filtering/rulelist/\
|
./internal/filtering/rulelist/\
|
||||||
./internal/next/\
|
./internal/next/\
|
||||||
./internal/rdns/\
|
./internal/rdns/\
|
||||||
|
./internal/schedule/\
|
||||||
./internal/tools/\
|
./internal/tools/\
|
||||||
./internal/version/\
|
./internal/version/\
|
||||||
./internal/whois/\
|
./internal/whois/\
|
||||||
|
@ -211,12 +214,14 @@ run_linter gosec --quiet\
|
||||||
./internal/aghnet\
|
./internal/aghnet\
|
||||||
./internal/aghos\
|
./internal/aghos\
|
||||||
./internal/aghtest\
|
./internal/aghtest\
|
||||||
|
./internal/client\
|
||||||
./internal/dhcpd\
|
./internal/dhcpd\
|
||||||
./internal/dhcpsvc\
|
./internal/dhcpsvc\
|
||||||
./internal/dnsforward\
|
./internal/dnsforward\
|
||||||
./internal/filtering/hashprefix/\
|
./internal/filtering/hashprefix/\
|
||||||
./internal/filtering/rulelist/\
|
./internal/filtering/rulelist/\
|
||||||
./internal/next\
|
./internal/next\
|
||||||
|
./internal/rdns\
|
||||||
./internal/schedule\
|
./internal/schedule\
|
||||||
./internal/stats\
|
./internal/stats\
|
||||||
./internal/tools\
|
./internal/tools\
|
||||||
|
|
Loading…
Reference in a new issue