Pull request 1927: 6006-use-address-processor

Updates #6006.

Squashed commit of the following:

commit ac27db95c12858b6ef182a0bd4acebab67a23993
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jul 18 15:47:17 2023 +0300

    all: imp code

commit 3936288512bfc2d44902ead6ab1bb5711f92b73c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jul 17 19:23:46 2023 +0300

    all: imp client resolving
This commit is contained in:
Ainar Garipov
2023-07-18 17:02:07 +03:00
parent dead10e033
commit 7bfad08dde
16 changed files with 443 additions and 397 deletions

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
@@ -141,7 +142,7 @@ func (clients *clientsContainer) handleHostsUpdates() {
}
}
// webHandlersRegistered prevents a [clientsContainer] from regisering its web
// webHandlersRegistered prevents a [clientsContainer] from registering its web
// handlers more than once.
//
// TODO(a.garipov): Refactor HTTP handler registration logic.
@@ -743,11 +744,9 @@ func (clients *clientsContainer) Update(prev, c *Client) (err error) {
return nil
}
// setWHOISInfo sets the WHOIS information for a client.
// setWHOISInfo sets the WHOIS information for a client. clients.lock is
// expected to be locked.
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok := clients.findLocked(ip.String())
if ok {
log.Debug("clients: client for %s is already created, ignore whois info", ip)
@@ -774,9 +773,11 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
rc.WHOIS = wi
}
// AddHost adds a new IP-hostname pairing. The priorities of the sources are
// addHost adds a new IP-hostname pairing. The priorities of the sources are
// taken into account. ok is true if the pairing was added.
func (clients *clientsContainer) AddHost(
//
// TODO(a.garipov): Only used in internal tests. Consider removing.
func (clients *clientsContainer) addHost(
ip netip.Addr,
host string,
src clientSource,
@@ -787,6 +788,32 @@ func (clients *clientsContainer) AddHost(
return clients.addHostLocked(ip, host, src)
}
// type check
var _ client.AddressUpdater = (*clientsContainer)(nil)
// UpdateAddress implements the [client.AddressUpdater] interface for
// *clientsContainer
func (clients *clientsContainer) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
// Common fast path optimization.
if host == "" && info == nil {
return
}
clients.lock.Lock()
defer clients.lock.Unlock()
if host != "" {
ok := clients.addHostLocked(ip, host, ClientSourceRDNS)
if !ok {
log.Debug("clients: host for client %q already set with higher priority source", ip)
}
}
if info != nil {
clients.setWHOISInfo(ip, info)
}
}
// addHostLocked adds a new IP-hostname pairing. clients.lock is expected to be
// locked.
func (clients *clientsContainer) addHostLocked(

View File

@@ -168,13 +168,13 @@ func TestClients(t *testing.T) {
t.Run("addhost_success", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
ok := clients.AddHost(ip, "host", ClientSourceARP)
ok := clients.addHost(ip, "host", ClientSourceARP)
assert.True(t, ok)
ok = clients.AddHost(ip, "host2", ClientSourceARP)
ok = clients.addHost(ip, "host2", ClientSourceARP)
assert.True(t, ok)
ok = clients.AddHost(ip, "host3", ClientSourceHostsFile)
ok = clients.addHost(ip, "host3", ClientSourceHostsFile)
assert.True(t, ok)
assert.Equal(t, clients.clientSource(ip), ClientSourceHostsFile)
@@ -182,18 +182,18 @@ func TestClients(t *testing.T) {
t.Run("dhcp_replaces_arp", func(t *testing.T) {
ip := netip.MustParseAddr("1.2.3.4")
ok := clients.AddHost(ip, "from_arp", ClientSourceARP)
ok := clients.addHost(ip, "from_arp", ClientSourceARP)
assert.True(t, ok)
assert.Equal(t, clients.clientSource(ip), ClientSourceARP)
ok = clients.AddHost(ip, "from_dhcp", ClientSourceDHCP)
ok = clients.addHost(ip, "from_dhcp", ClientSourceDHCP)
assert.True(t, ok)
assert.Equal(t, clients.clientSource(ip), ClientSourceDHCP)
})
t.Run("addhost_fail", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
ok := clients.AddHost(ip, "host1", ClientSourceRDNS)
ok := clients.addHost(ip, "host1", ClientSourceRDNS)
assert.False(t, ok)
})
}
@@ -216,7 +216,7 @@ func TestClientsWHOIS(t *testing.T) {
t.Run("existing_auto-client", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
ok := clients.AddHost(ip, "host", ClientSourceRDNS)
ok := clients.addHost(ip, "host", ClientSourceRDNS)
assert.True(t, ok)
clients.setWHOISInfo(ip, whois)
@@ -259,7 +259,7 @@ func TestClientsAddExisting(t *testing.T) {
assert.True(t, ok)
// Now add an auto-client with the same IP.
ok = clients.AddHost(ip, "test", ClientSourceRDNS)
ok = clients.addHost(ip, "test", ClientSourceRDNS)
assert.True(t, ok)
})

View File

@@ -590,7 +590,13 @@ func (c *configuration) write() (err error) {
s.WriteDiskConfig(&c)
dns := &config.DNS
dns.FilteringConfig = c
dns.LocalPTRResolvers, config.Clients.Sources.RDNS, dns.UsePrivateRDNS = s.RDNSSettings()
dns.LocalPTRResolvers = s.LocalPTRResolvers()
addrProcConf := s.AddrProcConfig()
config.Clients.Sources.RDNS = addrProcConf.UseRDNS
config.Clients.Sources.WHOIS = addrProcConf.UseWHOIS
dns.UsePrivateRDNS = addrProcConf.UsePrivateRDNS
}
if Context.dhcpServer != nil {

View File

@@ -13,14 +13,12 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
@@ -135,7 +133,7 @@ func initDNSServer(
return fmt.Errorf("preparing set of private subnets: %w", err)
}
p := dnsforward.DNSCreateParams{
Context.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
DNSFilter: filters,
Stats: sts,
QueryLog: qlog,
@@ -143,9 +141,7 @@ func initDNSServer(
Anonymizer: anonymizer,
LocalDomain: config.DHCP.LocalDomainName,
DHCPServer: dhcpSrv,
}
Context.dnsServer, err = dnsforward.NewServer(p)
})
if err != nil {
closeDNSServer()
@@ -154,134 +150,23 @@ func initDNSServer(
Context.clients.dnsServer = Context.dnsServer
dnsConf, err := generateServerConfig(tlsConf, httpReg)
dnsConf, err := newServerConfig(tlsConf, httpReg)
if err != nil {
closeDNSServer()
return fmt.Errorf("generateServerConfig: %w", err)
return fmt.Errorf("newServerConfig: %w", err)
}
err = Context.dnsServer.Prepare(&dnsConf)
err = Context.dnsServer.Prepare(dnsConf)
if err != nil {
closeDNSServer()
return fmt.Errorf("dnsServer.Prepare: %w", err)
}
initRDNS()
initWHOIS()
return nil
}
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
)
// initRDNS initializes the rDNS.
func initRDNS() {
Context.rdnsCh = make(chan netip.Addr, defaultQueueSize)
// TODO(s.chzhen): Add ability to disable it on dns server configuration
// update in [dnsforward] package.
r := rdns.New(&rdns.Config{
Exchanger: Context.dnsServer,
CacheSize: defaultCacheSize,
CacheTTL: defaultIPTTL,
})
go processRDNS(r)
}
// processRDNS processes reverse DNS lookup queries. It is intended to be used
// as a goroutine.
func processRDNS(r rdns.Interface) {
defer log.OnPanic("rdns")
for ip := range Context.rdnsCh {
ok := Context.dnsServer.ShouldResolveClient(ip)
if !ok {
continue
}
host, changed := r.Process(ip)
if host == "" || !changed {
continue
}
ok = Context.clients.AddHost(ip, host, ClientSourceRDNS)
if ok {
continue
}
log.Debug(
"dns: can't set rdns info for client %q: already set with higher priority source",
ip,
)
}
}
// initWHOIS initializes the WHOIS.
//
// TODO(s.chzhen): Consider making configurable.
func initWHOIS() {
const (
// defaultTimeout is the timeout for WHOIS requests.
defaultTimeout = 5 * time.Second
// defaultMaxConnReadSize is an upper limit in bytes for reading from
// net.Conn.
defaultMaxConnReadSize = 64 * 1024
// defaultMaxRedirects is the maximum redirects count.
defaultMaxRedirects = 5
// defaultMaxInfoLen is the maximum length of whois.Info fields.
defaultMaxInfoLen = 250
)
Context.whoisCh = make(chan netip.Addr, defaultQueueSize)
var w whois.Interface
if config.Clients.Sources.WHOIS {
w = whois.New(&whois.Config{
DialContext: customDialContext,
ServerAddr: whois.DefaultServer,
Port: whois.DefaultPort,
Timeout: defaultTimeout,
CacheSize: defaultCacheSize,
MaxConnReadSize: defaultMaxConnReadSize,
MaxRedirects: defaultMaxRedirects,
MaxInfoLen: defaultMaxInfoLen,
CacheTTL: defaultIPTTL,
})
} else {
w = whois.Empty{}
}
go func() {
defer log.OnPanic("whois")
for ip := range Context.whoisCh {
info, changed := w.Process(context.Background(), ip)
if info != nil && changed {
Context.clients.setWHOISInfo(ip, info)
}
}
}()
}
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
// a subnet set that matches all locally served networks, see
// [netutil.IsLocallyServed].
@@ -312,17 +197,6 @@ func isRunning() bool {
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
}
func onDNSRequest(pctx *proxy.DNSContext) {
ip := netutil.NetAddrToAddrPort(pctx.Addr).Addr()
if ip == (netip.Addr{}) {
// This would be quite weird if we get here.
return
}
Context.rdnsCh <- ip
Context.whoisCh <- ip
}
func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
if ips == nil {
return nil
@@ -349,23 +223,35 @@ func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
return udpAddrs
}
func generateServerConfig(
func newServerConfig(
tlsConf *tlsConfigSettings,
httpReg aghhttp.RegisterFunc,
) (newConf dnsforward.ServerConfig, err error) {
) (newConf *dnsforward.ServerConfig, err error) {
dnsConf := config.DNS
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
newConf = dnsforward.ServerConfig{
newConf = &dnsforward.ServerConfig{
UDPListenAddrs: ipsToUDPAddrs(hosts, dnsConf.Port),
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
FilteringConfig: dnsConf.FilteringConfig,
ConfigModified: onConfigModified,
HTTPRegister: httpReg,
OnDNSRequest: onDNSRequest,
UseDNS64: config.DNS.UseDNS64,
DNS64Prefixes: config.DNS.DNS64Prefixes,
}
const initialClientsNum = 100
// Do not set DialContext, PrivateSubnets, and UsePrivateRDNS, because they
// are set by [dnsforward.Server.Prepare].
newConf.AddrProcConf = &client.DefaultAddrProcConfig{
Exchanger: Context.dnsServer,
AddressUpdater: &Context.clients,
InitialAddresses: Context.stats.TopClientsIP(initialClientsNum),
UseRDNS: config.Clients.Sources.RDNS,
UseWHOIS: config.Clients.Sources.WHOIS,
}
if tlsConf.Enabled {
newConf.TLSConfig = tlsConf.TLSConfig
newConf.TLSConfig.ServerName = tlsConf.ServerName
@@ -385,9 +271,9 @@ func generateServerConfig(
if tlsConf.PortDNSCrypt != 0 {
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf)
if err != nil {
// Don't wrap the error, because it's already
// wrapped by newDNSCrypt.
return dnsforward.ServerConfig{}, err
// Don't wrap the error, because it's already wrapped by
// newDNSCrypt.
return nil, err
}
}
}
@@ -401,7 +287,6 @@ func generateServerConfig(
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
newConf.ResolveClients = config.Clients.Sources.RDNS
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
newConf.ServeHTTP3 = dnsConf.ServeHTTP3
newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams
@@ -556,27 +441,19 @@ func startDNSServer() error {
Context.stats.Start()
Context.queryLog.Start()
const topClientsNumber = 100 // the number of clients to get
for _, ip := range Context.stats.TopClientsIP(topClientsNumber) {
Context.rdnsCh <- ip
Context.whoisCh <- ip
}
return nil
}
func reconfigureDNSServer() (err error) {
var newConf dnsforward.ServerConfig
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
newConf, err = generateServerConfig(tlsConf, httpRegister)
newConf, err := newServerConfig(tlsConf, httpRegister)
if err != nil {
return fmt.Errorf("generating forwarding dns server config: %w", err)
}
err = Context.dnsServer.Reconfigure(&newConf)
err = Context.dnsServer.Reconfigure(newConf)
if err != nil {
return fmt.Errorf("starting forwarding dns server: %w", err)
}

View File

@@ -3,14 +3,12 @@ package home
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/fs"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"os/signal"
"path/filepath"
@@ -79,15 +77,8 @@ type homeContext struct {
pidFileName string // PID file name. Empty if no PID file was created.
controlLock sync.Mutex
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
client *http.Client
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
// rdnsCh is the channel for receiving IPs for rDNS processing.
rdnsCh chan netip.Addr
// whoisCh is the channel for receiving IPs for WHOIS processing.
whoisCh chan netip.Addr
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
tlsCipherIDs []uint16
@@ -156,19 +147,6 @@ func setupContext(opts options) (err error) {
setupContextFlags(opts)
Context.tlsRoots = aghtls.SystemRootCAs()
Context.client = &http.Client{
Timeout: time.Minute * 5,
Transport: &http.Transport{
DialContext: customDialContext,
Proxy: getHTTPProxy,
TLSClientConfig: &tls.Config{
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
},
}
Context.mux = http.NewServeMux()
if !Context.firstRun {
@@ -341,7 +319,7 @@ func initContextClients() (err error) {
}
Context.updater = updater.NewUpdater(&updater.Config{
Client: Context.client,
Client: config.DNS.DnsfilterConf.HTTPClient,
Version: version.Version(),
Channel: version.Channel(),
GOARCH: runtime.GOARCH,
@@ -433,7 +411,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
conf.Filters = slices.Clone(config.Filters)
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
conf.UserRules = slices.Clone(config.UserRules)
conf.HTTPClient = Context.client
conf.HTTPClient = httpClient()
cacheTime := time.Duration(conf.CacheTime) * time.Minute
@@ -634,10 +612,10 @@ func run(opts options, clientBuildFS fs.FS) {
Context.tls.start()
go func() {
sErr := startDNSServer()
if sErr != nil {
startErr := startDNSServer()
if startErr != nil {
closeDNSServer()
fatalOnError(sErr)
fatalOnError(startErr)
}
}()
@@ -996,62 +974,6 @@ func detectFirstRun() bool {
return errors.Is(err, os.ErrNotExist)
}
// Connect to a remote server resolving hostname using our own DNS server.
//
// TODO(e.burkov): This messy logic should be decomposed and clarified.
//
// TODO(a.garipov): Support network.
func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
log.Debug("home: customdial: dialing addr %q for network %s", addr, network)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
return dialer.DialContext(ctx, network, addr)
}
addrs, err := Context.dnsServer.Resolve(host)
if err != nil {
return nil, fmt.Errorf("resolving %q: %w", host, err)
}
log.Debug("dnsServer.Resolve: %q: %v", host, addrs)
if len(addrs) == 0 {
return nil, fmt.Errorf("couldn't lookup host: %q", host)
}
var dialErrs []error
for _, a := range addrs {
addr = net.JoinHostPort(a.String(), port)
conn, err = dialer.DialContext(ctx, network, addr)
if err != nil {
dialErrs = append(dialErrs, err)
continue
}
return conn, err
}
return nil, errors.List(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
}
func getHTTPProxy(_ *http.Request) (*url.URL, error) {
if config.ProxyURL == "" {
return nil, nil
}
return url.Parse(config.ProxyURL)
}
// jsonError is a generic JSON error response.
//
// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other

View File

@@ -0,0 +1,47 @@
package home
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"time"
)
// httpClient returns a new HTTP client that uses the AdGuard Home's own DNS
// server for resolving hostnames. The resulting client should not be used
// until [Context.dnsServer] is initialized.
//
// TODO(a.garipov, e.burkov): This is rather messy. Refactor.
func httpClient() (c *http.Client) {
// Do not use Context.dnsServer.DialContext directly in the struct literal
// below, since Context.dnsServer may be nil when this function is called.
dialContext := func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
return Context.dnsServer.DialContext(ctx, network, addr)
}
return &http.Client{
// TODO(a.garipov): Make configurable.
Timeout: time.Minute * 5,
Transport: &http.Transport{
DialContext: dialContext,
Proxy: httpProxy,
TLSClientConfig: &tls.Config{
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
},
}
}
// httpProxy returns parses and returns an HTTP proxy URL from the config, if
// any.
func httpProxy(_ *http.Request) (u *url.URL, err error) {
if config.ProxyURL == "" {
return nil, nil
}
return url.Parse(config.ProxyURL)
}