diff --git a/go.mod b/go.mod index 348d64b63..baf775535 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome go 1.25.7 require ( - github.com/AdguardTeam/dnsproxy v0.79.0 + github.com/AdguardTeam/dnsproxy v0.80.0 github.com/AdguardTeam/golibs v0.35.8 github.com/AdguardTeam/urlfilter v0.23.2 github.com/NYTimes/gziphandler v1.1.1 diff --git a/go.sum b/go.sum index 7a622ee47..fa65d09e2 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,10 @@ cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/AdguardTeam/dnsproxy v0.79.0 h1:wvNTny4u6x95bWGRyyqr1PVkHbYyAhPsv4EvnqVlmf4= -github.com/AdguardTeam/dnsproxy v0.79.0/go.mod h1:gwr+7Dc0e7QddQLC9JLGjL5NSKcqw0ESsNMRI5Q67Ps= +github.com/AdguardTeam/dnsproxy v0.80.0 h1:PttgkZfnAe9itH8vGVhpOTS9FLGZq8A49Qa7l4+/11Q= +github.com/AdguardTeam/dnsproxy v0.80.0/go.mod h1:gwr+7Dc0e7QddQLC9JLGjL5NSKcqw0ESsNMRI5Q67Ps= github.com/AdguardTeam/golibs v0.35.8 h1:KsyF3SWwj05Ey4GiAWU6FGD9oJTDNMp1ixVdS+Nw50M= github.com/AdguardTeam/golibs v0.35.8/go.mod h1:kuLQ0yNRTl0Em2FmmXtSri7ZdVT7p62oojyc51RvP38= -github.com/AdguardTeam/urlfilter v0.23.2-0.20260226112426-a60617ec1594 h1:VoT4UKSgRcbIpsCGLuVenMWFFdi1834SUkc9+hp5Ivs= -github.com/AdguardTeam/urlfilter v0.23.2-0.20260226112426-a60617ec1594/go.mod h1:JteAKoeka1Yr2oZ3P94dqYBfPOHWyFaOcu3uZa9Yl+I= github.com/AdguardTeam/urlfilter v0.23.2 h1:EiS/PQZO/X2S6cduFW6BBoRLyjd6SqZj1ZiFbU1KaFE= github.com/AdguardTeam/urlfilter v0.23.2/go.mod h1:JteAKoeka1Yr2oZ3P94dqYBfPOHWyFaOcu3uZa9Yl+I= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= diff --git a/internal/dnsforward/beforerequest.go b/internal/dnsforward/beforerequest.go deleted file mode 100644 index 339775796..000000000 --- a/internal/dnsforward/beforerequest.go +++ /dev/null @@ -1,127 +0,0 @@ -package dnsforward - -import ( - "context" - "encoding/binary" - "fmt" - - "github.com/AdguardTeam/AdGuardHome/internal/aghnet" - "github.com/AdguardTeam/dnsproxy/proxy" - "github.com/AdguardTeam/golibs/errors" - "github.com/miekg/dns" -) - -// type check -var _ proxy.BeforeRequestHandler = (*Server)(nil) - -// HandleBefore is the handler that is called before any other processing, -// including logs. It performs access checks and puts the ClientID, if there -// is one, into the server's cache. -// -// TODO(d.kolyshev): Extract to separate package. -func (s *Server) HandleBefore( - _ *proxy.Proxy, - pctx *proxy.DNSContext, -) (err error) { - // TODO(f.setrakov): Obtain context from arguments. - ctx := context.TODO() - clientID, err := s.clientIDFromDNSContext(ctx, pctx) - if err != nil { - return &proxy.BeforeRequestError{ - Err: fmt.Errorf("getting clientid: %w", err), - Response: s.NewMsgSERVFAIL(pctx.Req), - } - } - - blocked, _ := s.IsBlockedClient(pctx.Addr.Addr(), clientID) - if blocked { - return s.preBlockedResponse(pctx) - } - - if len(pctx.Req.Question) == 1 { - q := pctx.Req.Question[0] - qt := q.Qtype - host := aghnet.NormalizeDomain(q.Name) - if s.access.isBlockedHost(host, qt) { - // TODO(s.chzhen): Pass context. - s.logger.DebugContext( - context.TODO(), - "request is in access blocklist", - "dns_type", dns.Type(qt), - "host", host, - ) - - return s.preBlockedResponse(pctx) - } - } - - if clientID != "" { - key := [8]byte{} - binary.BigEndian.PutUint64(key[:], pctx.RequestID) - s.clientIDCache.Set(key[:], []byte(clientID)) - } - - return nil -} - -// clientIDFromDNSContext extracts the client's ID from the server name of the -// client's DoT or DoQ request or the path of the client's DoH. If the protocol -// is not one of these, clientID is an empty string and err is nil. -func (s *Server) clientIDFromDNSContext( - ctx context.Context, - pctx *proxy.DNSContext, -) (clientID string, err error) { - proto := pctx.Proto - if proto == proxy.ProtoHTTPS { - clientID, err = clientIDFromDNSContextHTTPS(pctx) - if err != nil { - return "", fmt.Errorf("checking url: %w", err) - } else if clientID != "" { - return clientID, nil - } - - // Go on and check the domain name as well. - } else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC { - return "", nil - } - - hostSrvName := s.conf.TLSConf.ServerName - if hostSrvName == "" { - return "", nil - } - - cliSrvName, err := clientServerName(ctx, s.logger, pctx, proto) - if err != nil { - return "", fmt.Errorf("getting client server-name: %w", err) - } - - clientID, err = clientIDFromClientServerName( - hostSrvName, - cliSrvName, - s.conf.TLSConf.StrictSNICheck, - ) - if err != nil { - return "", fmt.Errorf("clientid check: %w", err) - } - - return clientID, nil -} - -// errAccessBlocked is a sentinel error returned when a request is blocked by -// access settings. -var errAccessBlocked errors.Error = "blocked by access settings" - -// preBlockedResponse returns a protocol-appropriate response for a request that -// was blocked by access settings. -func (s *Server) preBlockedResponse(pctx *proxy.DNSContext) (err error) { - if pctx.Proto == proxy.ProtoUDP || pctx.Proto == proxy.ProtoDNSCrypt { - // Return nil so that dnsproxy drops the connection and thus - // prevent DNS amplification attacks. - return errAccessBlocked - } - - return &proxy.BeforeRequestError{ - Err: errAccessBlocked, - Response: s.makeResponseREFUSED(pctx.Req), - } -} diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 489dbeeee..48e3d56a4 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -205,7 +205,7 @@ type TLSConfig struct { // HTTPSListenAddrs should be the addresses AdGuard Home is listening on for // DoH connections. These addresses are announced with DDR. Each item in // the list must be non-nil. - HTTPSListenAddrs []*net.TCPAddr + HTTPSListenAddrs []netip.AddrPort // ServerName is the hostname of the server. Currently, it is only being // used for ClientID checking and Discovery of Designated Resolvers (DDR). @@ -322,6 +322,8 @@ const ( ) // newProxyConfig creates and validates configuration for the main proxy. +// +// TODO(d.kolyshev): Improve maintainability. func (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err error) { srvConf := s.conf trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies) @@ -331,9 +333,13 @@ func (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err er return nil, fmt.Errorf("ratelimit middleware: %w", err) } + httpConf := &proxy.HTTPConfig{ + ServerHeader: aghhttp.UserAgent(), + InsecureEnabled: s.conf.TLSAllowUnencryptedDoH, + } + conf = &proxy.Config{ Logger: s.baseLogger.With(slogutil.KeyPrefix, aghslog.PrefixDNSProxy), - HTTP3: srvConf.ServeHTTP3, RefuseAny: srvConf.RefuseAny, TrustedProxies: netutil.SliceSubnetSet(trustedPrefixes), CacheMinTTL: srvConf.CacheMinTTL, @@ -343,9 +349,7 @@ func (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err er CacheOptimisticMaxAge: time.Duration(srvConf.CacheOptimisticMaxAge), UpstreamConfig: srvConf.UpstreamConfig, PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig, - BeforeRequestHandler: s, - RequestHandler: ratelimitMw.Wrap(s), - HTTPSServerName: aghhttp.UserAgent(), + RequestHandler: ratelimitMw.Wrap(s.Wrap(s)), EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled, MaxGoroutines: srvConf.MaxGoroutines, UseDNS64: srvConf.UseDNS64, @@ -356,6 +360,7 @@ func (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err er PendingRequests: &proxy.PendingRequestsConfig{ Enabled: srvConf.PendingRequestsEnabled, }, + HTTPConfig: httpConf, } if srvConf.EDNSClientSubnet.UseCustom { @@ -798,7 +803,7 @@ func (s *Server) preparePlain(ctx context.Context, proxyConf *proxy.Config) (err lenEncrypted := len(proxyConf.DNSCryptTCPListenAddr) + len(proxyConf.DNSCryptUDPListenAddr) + - len(proxyConf.HTTPSListenAddr) + + len(proxyConf.HTTPConfig.ListenAddresses) + len(proxyConf.QUICListenAddr) + len(proxyConf.TLSListenAddr) if lenEncrypted == 0 { diff --git a/internal/dnsforward/middleware.go b/internal/dnsforward/middleware.go new file mode 100644 index 000000000..d4663c2eb --- /dev/null +++ b/internal/dnsforward/middleware.go @@ -0,0 +1,137 @@ +package dnsforward + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/miekg/dns" +) + +// type check +var _ proxy.Middleware = (*Server)(nil) + +// Wrap implements the [proxy.Middleware] interface for *Server. +// +// TODO(d.kolyshev): Move to a dedicated package. +func (s *Server) Wrap(h proxy.Handler) (wrapped proxy.Handler) { + f := func(p *proxy.Proxy, pctx *proxy.DNSContext) (err error) { + // TODO(f.setrakov): Obtain context from arguments. + ctx := context.TODO() + + clientID, err := s.clientIDFromDNSContext(ctx, pctx) + if err != nil { + s.logger.WarnContext(ctx, "resolving client id", slogutil.KeyError, err) + + pctx.Res = s.NewMsgSERVFAIL(pctx.Req) + pctx.Res.Compress = true + + return nil + } + + blocked, _ := s.IsBlockedClient(pctx.Addr.Addr(), clientID) + if blocked { + return s.serveBlockedResponse(pctx) + } + + blocked = s.isBlockedHost(ctx, pctx.Req.Question) + if blocked { + return s.serveBlockedResponse(pctx) + } + + if clientID != "" { + key := [8]byte{} + binary.BigEndian.PutUint64(key[:], pctx.RequestID) + s.clientIDCache.Set(key[:], []byte(clientID)) + } + + return h.ServeDNS(p, pctx) + } + + return proxy.HandlerFunc(f) +} + +// serveBlockedResponse sets a protocol-appropriate response for a request that +// was blocked by access settings. +func (s *Server) serveBlockedResponse(pctx *proxy.DNSContext) (err error) { + if pctx.Proto == proxy.ProtoUDP || pctx.Proto == proxy.ProtoDNSCrypt { + // Return nil so that dnsproxy drops the connection and thus prevent DNS + // amplification attacks. + return proxy.ErrDrop + } + + pctx.Res = s.makeResponseREFUSED(pctx.Req) + pctx.Res.Compress = true + + return nil +} + +// isBlockedHost checks if the request is in the access blocklist. +func (s *Server) isBlockedHost(ctx context.Context, question []dns.Question) (blocked bool) { + if len(question) != 1 { + return false + } + + q := question[0] + qt := q.Qtype + host := aghnet.NormalizeDomain(q.Name) + + if s.access.isBlockedHost(host, qt) { + s.logger.DebugContext( + ctx, + "request is in access blocklist", + "dns_type", dns.Type(qt), + "host", host, + ) + + return true + } + + return false +} + +// clientIDFromDNSContext extracts the client's ID from the server name of the +// client's DoT or DoQ request or the path of the client's DoH. If the protocol +// is not one of these, clientID is an empty string and err is nil. +func (s *Server) clientIDFromDNSContext( + ctx context.Context, + pctx *proxy.DNSContext, +) (clientID string, err error) { + proto := pctx.Proto + if proto == proxy.ProtoHTTPS { + clientID, err = clientIDFromDNSContextHTTPS(pctx) + if err != nil { + return "", fmt.Errorf("checking url: %w", err) + } else if clientID != "" { + return clientID, nil + } + + // Go on and check the domain name as well. + } else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC { + return "", nil + } + + hostSrvName := s.conf.TLSConf.ServerName + if hostSrvName == "" { + return "", nil + } + + cliSrvName, err := clientServerName(ctx, s.logger, pctx, proto) + if err != nil { + return "", fmt.Errorf("getting client server-name: %w", err) + } + + clientID, err = clientIDFromClientServerName( + hostSrvName, + cliSrvName, + s.conf.TLSConf.StrictSNICheck, + ) + if err != nil { + return "", fmt.Errorf("clientid check: %w", err) + } + + return clientID, nil +} diff --git a/internal/dnsforward/beforerequest_internal_test.go b/internal/dnsforward/middleware_internal_test.go similarity index 97% rename from internal/dnsforward/beforerequest_internal_test.go rename to internal/dnsforward/middleware_internal_test.go index d9b872b81..71c09a7be 100644 --- a/internal/dnsforward/beforerequest_internal_test.go +++ b/internal/dnsforward/middleware_internal_test.go @@ -15,13 +15,15 @@ import ( "github.com/stretchr/testify/require" ) +// Common constants for tests. const ( - blockedHost = "blockedhost.org" - testFQDN = "example.org." + blockedHost = "blockedhost.org" + testFQDN = "example.org." + dnsClientTimeout = 200 * time.Millisecond ) -func TestServer_HandleBefore_tls(t *testing.T) { +func TestServer_middlewareTLS(t *testing.T) { t.Parallel() const clientID = "client-1" @@ -165,7 +167,7 @@ func TestServer_HandleBefore_tls(t *testing.T) { } } -func TestServer_HandleBefore_udp(t *testing.T) { +func TestServer_middlewareUDP(t *testing.T) { t.Parallel() const ( diff --git a/internal/dnsforward/process.go b/internal/dnsforward/process.go index 0e320da22..8b4ca7114 100644 --- a/internal/dnsforward/process.go +++ b/internal/dnsforward/process.go @@ -201,7 +201,7 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) { for _, addr := range s.conf.TLSConf.HTTPSListenAddrs { values := []dns.SVCBKeyValue{ &dns.SVCBAlpn{Alpn: []string{"h2"}}, - &dns.SVCBPort{Port: uint16(addr.Port)}, + &dns.SVCBPort{Port: addr.Port()}, &dns.SVCBDoHPath{Template: "/dns-query{?dns}"}, } diff --git a/internal/dnsforward/process_internal_test.go b/internal/dnsforward/process_internal_test.go index 39a86121d..525486d9d 100644 --- a/internal/dnsforward/process_internal_test.go +++ b/internal/dnsforward/process_internal_test.go @@ -244,12 +244,16 @@ func TestServer_ProcessDDRQuery(t *testing.T) { }, } + addrsDoH := []netip.AddrPort{netip.AddrPortFrom(netutil.IPv4Localhost(), 8044)} + addrsDoT := []*net.TCPAddr{{Port: 8043}} + addrsDoQ := []*net.UDPAddr{{Port: 8042}} + testCases := []struct { name string host string want []*dns.SVCB wantRes resultCode - addrsDoH []*net.TCPAddr + addrsDoH []netip.AddrPort addrsDoT []*net.TCPAddr addrsDoQ []*net.UDPAddr qtype uint16 @@ -260,14 +264,14 @@ func TestServer_ProcessDDRQuery(t *testing.T) { host: testQuestionTarget, qtype: dns.TypeSVCB, ddrEnabled: true, - addrsDoH: []*net.TCPAddr{{Port: 8043}}, + addrsDoH: addrsDoH, }, { name: "pass_qtype", wantRes: resultCodeFinish, host: ddrHostFQDN, qtype: dns.TypeA, ddrEnabled: true, - addrsDoH: []*net.TCPAddr{{Port: 8043}}, + addrsDoH: addrsDoH, }, { name: "pass_disabled_tls", wantRes: resultCodeFinish, @@ -280,7 +284,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) { host: ddrHostFQDN, qtype: dns.TypeSVCB, ddrEnabled: false, - addrsDoH: []*net.TCPAddr{{Port: 8043}}, + addrsDoH: addrsDoH, }, { name: "dot", wantRes: resultCodeFinish, @@ -288,7 +292,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) { host: ddrHostFQDN, qtype: dns.TypeSVCB, ddrEnabled: true, - addrsDoT: []*net.TCPAddr{{Port: 8043}}, + addrsDoT: addrsDoT, }, { name: "doh", wantRes: resultCodeFinish, @@ -296,7 +300,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) { host: ddrHostFQDN, qtype: dns.TypeSVCB, ddrEnabled: true, - addrsDoH: []*net.TCPAddr{{Port: 8044}}, + addrsDoH: addrsDoH, }, { name: "doq", wantRes: resultCodeFinish, @@ -304,7 +308,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) { host: ddrHostFQDN, qtype: dns.TypeSVCB, ddrEnabled: true, - addrsDoQ: []*net.UDPAddr{{Port: 8042}}, + addrsDoQ: addrsDoQ, }, { name: "dot_doh", wantRes: resultCodeFinish, @@ -312,8 +316,8 @@ func TestServer_ProcessDDRQuery(t *testing.T) { host: ddrHostFQDN, qtype: dns.TypeSVCB, ddrEnabled: true, - addrsDoT: []*net.TCPAddr{{Port: 8043}}, - addrsDoH: []*net.TCPAddr{{Port: 8044}}, + addrsDoT: addrsDoT, + addrsDoH: addrsDoH, }} _, certPem, keyPem := createServerTLSConfig(t) diff --git a/internal/home/dns.go b/internal/home/dns.go index 1b8789bcb..a8d068400 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -222,6 +222,21 @@ func ipsToTCPAddrs(ips []netip.Addr, port uint16) (tcpAddrs []*net.TCPAddr) { return tcpAddrs } +// ipsToAddrPorts converts a slice of [netip.Addr] into a slice of +// [netip.AddrPort] with the given port. +func ipsToAddrPorts(ips []netip.Addr, port uint16) (addrs []netip.AddrPort) { + if ips == nil { + return nil + } + + addrs = make([]netip.AddrPort, 0, len(ips)) + for _, ip := range ips { + addrs = append(addrs, netip.AddrPortFrom(ip, port)) + } + + return addrs +} + func ipsToUDPAddrs(ips []netip.Addr, port uint16) (udpAddrs []*net.UDPAddr) { if ips == nil { return nil @@ -323,7 +338,7 @@ func newDNSTLSConfig( } if conf.PortHTTPS != 0 { - dnsConf.HTTPSListenAddrs = ipsToTCPAddrs(addrs, conf.PortHTTPS) + dnsConf.HTTPSListenAddrs = ipsToAddrPorts(addrs, conf.PortHTTPS) } if conf.PortDNSOverTLS != 0 { diff --git a/internal/home/tls_internal_test.go b/internal/home/tls_internal_test.go index 2d27a445f..80e6172b1 100644 --- a/internal/home/tls_internal_test.go +++ b/internal/home/tls_internal_test.go @@ -635,6 +635,7 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) { res := &tlsConfig{ tlsConfigStatus: &tlsConfigStatus{}, } + err = json.NewDecoder(w.Body).Decode(res) require.NoError(t, err)