diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ce673f173..9ef09b506 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,7 +1,7 @@ name: Build 'env': - 'GO_VERSION': '1.21.7' + 'GO_VERSION': '1.21.8' 'on': 'push': diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 24aed319e..e76556e1f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,7 +1,7 @@ 'name': Docker 'env': - 'GO_VERSION': '1.21.7' + 'GO_VERSION': '1.21.8' 'on': 'push': diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ae523b2f9..169732730 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,7 +1,7 @@ 'name': 'lint' 'env': - 'GO_VERSION': '1.21.7' + 'GO_VERSION': '1.21.8' 'on': 'push': diff --git a/bamboo-specs/bamboo.yaml b/bamboo-specs/bamboo.yaml index c8c76b520..2a615b2ad 100644 --- a/bamboo-specs/bamboo.yaml +++ b/bamboo-specs/bamboo.yaml @@ -46,7 +46,7 @@ - | set -e -f -u -x - make VERBOSE=1 go-tools go-lint + make VERBOSE=1 GOMAXPROCS=1 go-tools go-lint 'Test': 'docker': diff --git a/go.mod b/go.mod index ff726ae2a..2b6f88434 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AdguardTeam/dnsproxy go 1.21.8 require ( - github.com/AdguardTeam/golibs v0.20.1 + github.com/AdguardTeam/golibs v0.20.4 github.com/ameshkov/dnscrypt/v2 v2.2.7 github.com/ameshkov/dnsstamps v1.0.3 github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 diff --git a/go.sum b/go.sum index c3f3607b7..764ce2d66 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/AdguardTeam/golibs v0.20.1 h1:ol8qLjWGZhU9paMMwN+OLWVTUigGsXa29iVTyd62VKY= -github.com/AdguardTeam/golibs v0.20.1/go.mod h1:bgcMgRviCKyU6mkrX+RtT/OsKPFzyppelfRsksMG3KU= +github.com/AdguardTeam/golibs v0.20.4 h1:grcXP/Fm1zSeAfQVehZ/l8IinSTQt3UPbKKOGpUq7p0= +github.com/AdguardTeam/golibs v0.20.4/go.mod h1:/votX6WK1PdcZ3T2kBOPjPCGmfhlKixhI6ljYrFRPvI= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 5231a0b98..0ec963426 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -11,7 +11,6 @@ import ( "slices" "time" - proxynetutil "github.com/AdguardTeam/dnsproxy/internal/netutil" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" @@ -78,9 +77,9 @@ func ResolveDialContext( } if preferV6 { - slices.SortStableFunc(ips, proxynetutil.PreferIPv6) + slices.SortStableFunc(ips, netutil.PreferIPv6) } else { - slices.SortStableFunc(ips, proxynetutil.PreferIPv4) + slices.SortStableFunc(ips, netutil.PreferIPv4) } addrs := make([]string, 0, len(ips)) diff --git a/internal/netutil/netutil.go b/internal/netutil/netutil.go index dd9bf8866..b9591f084 100644 --- a/internal/netutil/netutil.go +++ b/internal/netutil/netutil.go @@ -7,86 +7,13 @@ package netutil import ( "net/netip" - "slices" "strings" ) -// PreferIPv4 compares two addresses, preferring IPv4 addresses over IPv6 ones. -// Invalid addresses are sorted near the end. -func PreferIPv4(a, b netip.Addr) (res int) { - if !a.IsValid() { - return 1 - } else if !b.IsValid() { - return -1 - } - - if aIs4 := a.Is4(); aIs4 == b.Is4() { - return a.Compare(b) - } else if aIs4 { - return -1 - } - - return 1 -} - -// PreferIPv6 compares two addresses, preferring IPv6 addresses over IPv4 ones. -// Invalid addresses are sorted near the end. -func PreferIPv6(a, b netip.Addr) (res int) { - if !a.IsValid() { - return 1 - } else if !b.IsValid() { - return -1 - } - - if aIs6 := a.Is6(); aIs6 == b.Is6() { - return a.Compare(b) - } else if aIs6 { - return -1 - } - - return 1 -} - -// SortNetIPAddrs sorts addrs in accordance with the protocol preferences. -// Invalid addresses are sorted near the end. Zones are ignored. -func SortNetIPAddrs(addrs []netip.Addr, preferIPv6 bool) { - l := len(addrs) - if l <= 1 { - return - } - - slices.SortStableFunc(addrs, func(addrA, addrB netip.Addr) (res int) { - if !addrA.IsValid() { - return 1 - } else if !addrB.IsValid() { - return -1 - } - - aIs4, bIs4 := addrA.Is4(), addrB.Is4() - if aIs4 == bIs4 { - return addrA.Compare(addrB) - } - - if aIs4 { - if preferIPv6 { - return 1 - } - - return -1 - } - - if preferIPv6 { - return -1 - } - - return 1 - }) -} - // ParseSubnet parses s either as a CIDR prefix itself, or as an IP address, // returning the corresponding single-IP CIDR prefix. // -// TODO(e.burkov): Move to golibs. +// TODO(e.burkov): Replace usages with [netutil.Prefix]. func ParseSubnet(s string) (p netip.Prefix, err error) { if strings.Contains(s, "/") { p, err = netip.ParsePrefix(s) diff --git a/internal/netutil/netutil_example_test.go b/internal/netutil/netutil_example_test.go deleted file mode 100644 index f40c4ad23..000000000 --- a/internal/netutil/netutil_example_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package netutil_test - -import ( - "fmt" - "net/netip" - - "github.com/AdguardTeam/dnsproxy/internal/netutil" -) - -func ExampleSortNetIPAddrs() { - printAddrs := func(header string, addrs []netip.Addr) { - fmt.Printf("%s:\n", header) - for i, a := range addrs { - fmt.Printf("%d: %s\n", i+1, a) - } - - fmt.Println() - } - - addrs := []netip.Addr{ - netip.MustParseAddr("1.2.3.4"), - netip.MustParseAddr("1.2.3.5"), - netip.MustParseAddr("2a00::1234"), - netip.MustParseAddr("2a00::1235"), - {}, - } - netutil.SortNetIPAddrs(addrs, false) - printAddrs("IPv4 preferred", addrs) - - netutil.SortNetIPAddrs(addrs, true) - printAddrs("IPv6 preferred", addrs) - - // Output: - // - // IPv4 preferred: - // 1: 1.2.3.4 - // 2: 1.2.3.5 - // 3: 2a00::1234 - // 4: 2a00::1235 - // 5: invalid IP - // - // IPv6 preferred: - // 1: 2a00::1234 - // 2: 2a00::1235 - // 3: 1.2.3.4 - // 4: 1.2.3.5 - // 5: invalid IP -} diff --git a/internal/netutil/netutil_test.go b/internal/netutil/netutil_test.go deleted file mode 100644 index 7bbf3f84a..000000000 --- a/internal/netutil/netutil_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package netutil_test - -import ( - "net/netip" - "slices" - "testing" - - "github.com/AdguardTeam/dnsproxy/internal/netutil" - "github.com/stretchr/testify/assert" -) - -func TestSortNetIPAddrs(t *testing.T) { - var ( - aIPv4 = netip.MustParseAddr("1.2.3.4") - bIPv4 = netip.MustParseAddr("4.3.2.1") - aIPv6 = netip.MustParseAddr("2a00::1234") - bIPv6 = netip.MustParseAddr("2a00::4321") - badIP, _ = netip.ParseAddr("bad") - ) - - testCases := []struct { - name string - addrs []netip.Addr - want []netip.Addr - preferIPv6 bool - }{{ - name: "v4_preferred", - addrs: []netip.Addr{aIPv6, bIPv6, badIP, aIPv4, bIPv4}, - want: []netip.Addr{aIPv4, bIPv4, aIPv6, bIPv6, badIP}, - preferIPv6: false, - }, { - name: "v6_preferred", - addrs: []netip.Addr{aIPv4, bIPv4, badIP, aIPv6, bIPv6}, - want: []netip.Addr{aIPv6, bIPv6, aIPv4, bIPv4, badIP}, - preferIPv6: true, - }, { - name: "shuffled_v4_preferred", - addrs: []netip.Addr{badIP, aIPv4, bIPv6, aIPv6, bIPv4}, - want: []netip.Addr{aIPv4, bIPv4, aIPv6, bIPv6, badIP}, - preferIPv6: false, - }, { - name: "shuffled_v6_preferred", - addrs: []netip.Addr{badIP, aIPv4, bIPv6, aIPv6, bIPv4}, - want: []netip.Addr{aIPv6, bIPv6, aIPv4, bIPv4, badIP}, - preferIPv6: true, - }, { - name: "empty", - addrs: []netip.Addr{}, - want: []netip.Addr{}, - preferIPv6: false, - }, { - name: "single", - addrs: []netip.Addr{aIPv4}, - want: []netip.Addr{aIPv4}, - preferIPv6: false, - }, { - name: "start_with_ipv4", - addrs: []netip.Addr{aIPv4, aIPv6, bIPv4, bIPv6}, - want: []netip.Addr{aIPv6, bIPv6, aIPv4, bIPv4}, - preferIPv6: true, - }, { - name: "start_with_ipv6", - addrs: []netip.Addr{aIPv6, aIPv4, bIPv6, bIPv4}, - want: []netip.Addr{aIPv6, bIPv6, aIPv4, bIPv4}, - preferIPv6: true, - }} - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ips := slices.Clone(tc.addrs) - netutil.SortNetIPAddrs(ips, tc.preferIPv6) - assert.Equal(t, tc.want, ips) - }) - } -} diff --git a/main.go b/main.go index 89be75fb2..da0d56fcc 100644 --- a/main.go +++ b/main.go @@ -40,176 +40,157 @@ type Options struct { // options. ConfigPath string `long:"config-path" description:"yaml configuration file. Minimal working configuration in config.yaml.dist. Options passed through command line will override the ones from this file." default:""` - // Log settings - // -- + // LogOutput is the path to the log file. + LogOutput string `yaml:"output" short:"o" long:"output" description:"Path to the log file. If not set, write to stdout."` - // Should we write - Verbose bool `yaml:"verbose" short:"v" long:"verbose" description:"Verbose output (optional)" optional:"yes" optional-value:"true"` + // TLSCertPath is the path to the .crt with the certificate chain. + TLSCertPath string `yaml:"tls-crt" short:"c" long:"tls-crt" description:"Path to a file with the certificate chain"` - // Path to a log file - LogOutput string `yaml:"output" short:"o" long:"output" description:"Path to the log file. If not set, write to stdout."` + // TLSKeyPath is the path to the file with the private key. + TLSKeyPath string `yaml:"tls-key" short:"k" long:"tls-key" description:"Path to a file with the private key"` + + // HTTPSServerName sets Server header for the HTTPS server. + HTTPSServerName string `yaml:"https-server-name" long:"https-server-name" description:"Set the Server header for the responses from the HTTPS server." default:"dnsproxy"` - // Listen addrs - // -- + // HTTPSUserinfo is the sole permitted userinfo for the DoH basic + // authentication. If it is set, all DoH queries are required to have this + // basic authentication information. + HTTPSUserinfo string `yaml:"https-userinfo" long:"https-userinfo" description:"If set, all DoH queries are required to have this basic authentication information."` + + // DNSCryptConfigPath is the path to the DNSCrypt configuration file. + DNSCryptConfigPath string `yaml:"dnscrypt-config" short:"g" long:"dnscrypt-config" description:"Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt"` - // Server listen address + // EDNSAddr is the custom EDNS Client Address to send. + EDNSAddr string `yaml:"edns-addr" long:"edns-addr" description:"Send EDNS Client Address"` + + // ListenAddrs is the list of server's listen addresses. ListenAddrs []string `yaml:"listen-addrs" short:"l" long:"listen" description:"Listening addresses"` - // Server listen ports + // ListenPorts are the ports server listens on. ListenPorts []int `yaml:"listen-ports" short:"p" long:"port" description:"Listening ports. Zero value disables TCP and UDP listeners"` - // HTTPS listen ports + // HTTPSListenPorts are the ports server listens on for DNS-over-HTTPS. HTTPSListenPorts []int `yaml:"https-port" short:"s" long:"https-port" description:"Listening ports for DNS-over-HTTPS"` - // TLS listen ports + // TLSListenPorts are the ports server listens on for DNS-over-TLS. TLSListenPorts []int `yaml:"tls-port" short:"t" long:"tls-port" description:"Listening ports for DNS-over-TLS"` - // QUIC listen ports + // QUICListenPorts are the ports server listens on for DNS-over-QUIC. QUICListenPorts []int `yaml:"quic-port" short:"q" long:"quic-port" description:"Listening ports for DNS-over-QUIC"` - // DNSCrypt listen ports + // DNSCryptListenPorts are the ports server listens on for DNSCrypt. DNSCryptListenPorts []int `yaml:"dnscrypt-port" short:"y" long:"dnscrypt-port" description:"Listening ports for DNSCrypt"` - // Encryption config - // -- - - // Path to the .crt with the certificate chain - TLSCertPath string `yaml:"tls-crt" short:"c" long:"tls-crt" description:"Path to a file with the certificate chain"` - - // Path to the file with the private key - TLSKeyPath string `yaml:"tls-key" short:"k" long:"tls-key" description:"Path to a file with the private key"` - - // Minimum TLS version - TLSMinVersion float32 `yaml:"tls-min-version" long:"tls-min-version" description:"Minimum TLS version, for example 1.0" optional:"yes"` - - // Maximum TLS version - TLSMaxVersion float32 `yaml:"tls-max-version" long:"tls-max-version" description:"Maximum TLS version, for example 1.3" optional:"yes"` - - // Disable TLS certificate verification - Insecure bool `yaml:"insecure" long:"insecure" description:"Disable secure TLS certificate validation" optional:"yes" optional-value:"false"` - - // Path to the DNSCrypt configuration file - DNSCryptConfigPath string `yaml:"dnscrypt-config" short:"g" long:"dnscrypt-config" description:"Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt"` - - // HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy. - // It enables HTTP/3 support for both the DoH upstreams and the DoH server. - HTTP3 bool `yaml:"http3" long:"http3" description:"Enable HTTP/3 support" optional:"yes" optional-value:"false"` - - // Upstream DNS servers settings - // -- - - // DNS upstreams + // Upstreams is the list of DNS upstream servers. Upstreams []string `yaml:"upstream" short:"u" long:"upstream" description:"An upstream to be used (can be specified multiple times). You can also specify path to a file with the list of servers" optional:"false"` - // Bootstrap DNS + // BootstrapDNS is the list of bootstrap DNS upstream servers. BootstrapDNS []string `yaml:"bootstrap" short:"b" long:"bootstrap" description:"Bootstrap DNS for DoH and DoT, can be specified multiple times (default: use system-provided)"` - // Fallback DNS resolver + // Fallbacks is the list of fallback DNS upstream servers. Fallbacks []string `yaml:"fallback" short:"f" long:"fallback" description:"Fallback resolvers to use when regular ones are unavailable, can be specified multiple times. You can also specify path to a file with the list of servers"` // PrivateRDNSUpstreams are upstreams to use for reverse DNS lookups of // private addresses. PrivateRDNSUpstreams []string `yaml:"private-rdns-upstream" long:"private-rdns-upstream" description:"Private DNS upstreams to use for reverse DNS lookups of private addresses, can be specified multiple times"` - // If true, parallel queries to all configured upstream servers - AllServers bool `yaml:"all-servers" long:"all-servers" description:"If specified, parallel queries to all configured upstream servers are enabled" optional:"yes" optional-value:"true"` + // DNS64Prefix defines the DNS64 prefixes that dnsproxy should use when it + // acts as a DNS64 server. If not specified, dnsproxy uses the default + // Well-Known Prefix. This option can be specified multiple times. + DNS64Prefix []string `yaml:"dns64-prefix" long:"dns64-prefix" description:"Prefix used to handle DNS64. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::. Can be specified multiple times" required:"false"` - // Respond to A or AAAA requests only with the fastest IP address - // detected by ICMP response time or TCP connection time - FastestAddress bool `yaml:"fastest-addr" long:"fastest-addr" description:"Respond to A or AAAA requests only with the fastest IP address" optional:"yes" optional-value:"true"` + // BogusNXDomain transforms responses that contain at least one of the given + // IP addresses into NXDOMAIN. + // + // TODO(a.garipov): Find a way to use [netutil.Prefix]. Currently, package + // go-flags doesn't support text unmarshalers. + BogusNXDomain []string `yaml:"bogus-nxdomain" long:"bogus-nxdomain" description:"Transform the responses containing at least a single IP that matches specified addresses and CIDRs into NXDOMAIN. Can be specified multiple times."` // Timeout for outbound DNS queries to remote upstream servers in a // human-readable form. Default is 10s. Timeout timeutil.Duration `yaml:"timeout" long:"timeout" description:"Timeout for outbound DNS queries to remote upstream servers in a human-readable form" default:"10s"` - // Cache settings - // -- - - // If true, DNS cache is enabled - Cache bool `yaml:"cache" long:"cache" description:"If specified, DNS cache is enabled" optional:"yes" optional-value:"true"` - - // Cache size value - CacheSizeBytes int `yaml:"cache-size" long:"cache-size" description:"Cache size (in bytes). Default: 64k"` - - // DNS cache minimum TTL value - overrides record value + // CacheMinTTL is the minimum TTL value for caching DNS entries, in seconds. + // It overrides the TTL value from the upstream server, if the one is less. CacheMinTTL uint32 `yaml:"cache-min-ttl" long:"cache-min-ttl" description:"Minimum TTL value for DNS entries, in seconds. Capped at 3600. Artificially extending TTLs should only be done with careful consideration."` - // DNS cache maximum TTL value - overrides record value + // CacheMaxTTL is the maximum TTL value for caching DNS entries, in seconds. + // It overrides the TTL value from the upstream server, if the one is + // greater. CacheMaxTTL uint32 `yaml:"cache-max-ttl" long:"cache-max-ttl" description:"Maximum TTL value for DNS entries, in seconds."` - // CacheOptimistic, if set to true, enables the optimistic DNS cache. That means that cached results will be served even if their cache TTL has already expired. - CacheOptimistic bool `yaml:"cache-optimistic" long:"cache-optimistic" description:"If specified, optimistic DNS cache is enabled" optional:"yes" optional-value:"true"` - - // Anti-DNS amplification measures - // -- + // CacheSizeBytes is the cache size in bytes. Default is 64k. + CacheSizeBytes int `yaml:"cache-size" long:"cache-size" description:"Cache size (in bytes). Default: 64k"` - // Ratelimit value + // Ratelimit is the maximum number of requests per second. Ratelimit int `yaml:"ratelimit" short:"r" long:"ratelimit" description:"Ratelimit (requests per second)"` // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for - // rate limiting requests + // rate limiting requests. RatelimitSubnetLenIPv4 int `yaml:"ratelimit-subnet-len-ipv4" long:"ratelimit-subnet-len-ipv4" description:"Ratelimit subnet length for IPv4." default:"24"` // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for - // rate limiting requests + // rate limiting requests. RatelimitSubnetLenIPv6 int `yaml:"ratelimit-subnet-len-ipv6" long:"ratelimit-subnet-len-ipv6" description:"Ratelimit subnet length for IPv6." default:"56"` - // If true, refuse ANY requests - RefuseAny bool `yaml:"refuse-any" long:"refuse-any" description:"If specified, refuse ANY requests" optional:"yes" optional-value:"true"` + // UDPBufferSize is the size of the UDP buffer in bytes. A value <= 0 will + // use the system default. + UDPBufferSize int `yaml:"udp-buf-size" long:"udp-buf-size" description:"Set the size of the UDP buffer in bytes. A value <= 0 will use the system default."` - // ECS settings - // -- + // MaxGoRoutines is the maximum number of goroutines. + MaxGoRoutines uint `yaml:"max-go-routines" long:"max-go-routines" description:"Set the maximum number of go routines. A zero value will not not set a maximum."` - // Use EDNS Client Subnet extension - EnableEDNSSubnet bool `yaml:"edns" long:"edns" description:"Use EDNS Client Subnet extension" optional:"yes" optional-value:"true"` + // TLSMinVersion is the minimum allowed version of TLS. + TLSMinVersion float32 `yaml:"tls-min-version" long:"tls-min-version" description:"Minimum TLS version, for example 1.0" optional:"yes"` - // Use Custom EDNS Client Address - EDNSAddr string `yaml:"edns-addr" long:"edns-addr" description:"Send EDNS Client Address"` + // TLSMaxVersion is the maximum allowed version of TLS. + TLSMaxVersion float32 `yaml:"tls-max-version" long:"tls-max-version" description:"Maximum TLS version, for example 1.3" optional:"yes"` - // DNS64 settings - // -- + // Pprof defines whether the pprof information needs to be exposed via + // localhost:6060 or not. + Pprof bool `yaml:"pprof" long:"pprof" description:"If present, exposes pprof information on localhost:6060." optional:"yes" optional-value:"true"` - // Defines whether DNS64 functionality is enabled or not - DNS64 bool `yaml:"dns64" long:"dns64" description:"If specified, dnsproxy will act as a DNS64 server" optional:"yes" optional-value:"true"` + // Version, if true, prints the program version, and exits. + Version bool `yaml:"version" long:"version" description:"Prints the program version"` - // DNS64Prefix defines the DNS64 prefixes that dnsproxy should use when it - // acts as a DNS64 server. If not specified, dnsproxy uses the default - // Well-Known Prefix. This option can be specified multiple times. - DNS64Prefix []string `yaml:"dns64-prefix" long:"dns64-prefix" description:"Prefix used to handle DNS64. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::. Can be specified multiple times" required:"false"` + // Verbose controls the verbosity of the output. + Verbose bool `yaml:"verbose" short:"v" long:"verbose" description:"Verbose output (optional)" optional:"yes" optional-value:"true"` - // Other settings and options - // -- + // Insecure disables upstream servers TLS certificate verification. + Insecure bool `yaml:"insecure" long:"insecure" description:"Disable secure TLS certificate validation" optional:"yes" optional-value:"false"` - // Set Server header for the HTTPS server - HTTPSServerName string `yaml:"https-server-name" long:"https-server-name" description:"Set the Server header for the responses from the HTTPS server." default:"dnsproxy"` + // IPv6Disabled makes the server to respond with NODATA to all AAAA queries. + IPv6Disabled bool `yaml:"ipv6-disabled" long:"ipv6-disabled" description:"If specified, all AAAA requests will be replied with NoError RCode and empty answer" optional:"yes" optional-value:"true"` - // HTTPSUserinfo is the sole permitted userinfo for the DoH basic - // authentication. If it is set, all DoH queries are required to have this - // basic authentication information. - HTTPSUserinfo string `yaml:"https-userinfo" long:"https-userinfo" description:"If set, all DoH queries are required to have this basic authentication information."` + // HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy. + // It enables HTTP/3 support for both the DoH upstreams and the DoH server. + HTTP3 bool `yaml:"http3" long:"http3" description:"Enable HTTP/3 support" optional:"yes" optional-value:"false"` - // If true, all AAAA requests will be replied with NoError RCode and empty answer - IPv6Disabled bool `yaml:"ipv6-disabled" long:"ipv6-disabled" description:"If specified, all AAAA requests will be replied with NoError RCode and empty answer" optional:"yes" optional-value:"true"` + // AllServers makes server to query all configured upstream servers in + // parallel. + AllServers bool `yaml:"all-servers" long:"all-servers" description:"If specified, parallel queries to all configured upstream servers are enabled" optional:"yes" optional-value:"true"` - // Transform responses that contain at least one of the given IP addresses - // into NXDOMAIN. - // - // TODO(a.garipov): Find a way to use [netutil.Prefix]. Currently, package - // go-flags doesn't support text unmarshalers. - BogusNXDomain []string `yaml:"bogus-nxdomain" long:"bogus-nxdomain" description:"Transform the responses containing at least a single IP that matches specified addresses and CIDRs into NXDOMAIN. Can be specified multiple times."` + // FastestAddress controls whether the server should respond to A or AAAA + // requests only with the fastest IP address detected by ICMP response time + // or TCP connection time. + FastestAddress bool `yaml:"fastest-addr" long:"fastest-addr" description:"Respond to A or AAAA requests only with the fastest IP address" optional:"yes" optional-value:"true"` - // UDP buffer size value - UDPBufferSize int `yaml:"udp-buf-size" long:"udp-buf-size" description:"Set the size of the UDP buffer in bytes. A value <= 0 will use the system default."` + // CacheOptimistic, if set to true, enables the optimistic DNS cache. That + // means that cached results will be served even if their cache TTL has + // already expired. + CacheOptimistic bool `yaml:"cache-optimistic" long:"cache-optimistic" description:"If specified, optimistic DNS cache is enabled" optional:"yes" optional-value:"true"` - // MaxGoRoutines is the maximum number of goroutines. - MaxGoRoutines uint `yaml:"max-go-routines" long:"max-go-routines" description:"Set the maximum number of go routines. A zero value will not not set a maximum."` + // Cache controls whether DNS responses are cached or not. + Cache bool `yaml:"cache" long:"cache" description:"If specified, DNS cache is enabled" optional:"yes" optional-value:"true"` - // Pprof defines whether the pprof information needs to be exposed via - // localhost:6060 or not. - Pprof bool `yaml:"pprof" long:"pprof" description:"If present, exposes pprof information on localhost:6060." optional:"yes" optional-value:"true"` + // RefuseAny makes the server to refuse requests of type ANY. + RefuseAny bool `yaml:"refuse-any" long:"refuse-any" description:"If specified, refuse ANY requests" optional:"yes" optional-value:"true"` - // Print DNSProxy version (just for the help) - Version bool `yaml:"version" long:"version" description:"Prints the program version"` + // EnableEDNSSubnet uses EDNS Client Subnet extension. + EnableEDNSSubnet bool `yaml:"edns" long:"edns" description:"Use EDNS Client Subnet extension" optional:"yes" optional-value:"true"` + + // DNS64 defines whether DNS64 functionality is enabled or not. + DNS64 bool `yaml:"dns64" long:"dns64" description:"If specified, dnsproxy will act as a DNS64 server" optional:"yes" optional-value:"true"` } const ( diff --git a/proxy/config.go b/proxy/config.go index ecde750ea..9a5b69d44 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -28,71 +28,38 @@ const ( // BeforeRequestHandler is an optional custom handler called before DNS requests // If it returns false, the request won't be processed at all -type BeforeRequestHandler func(p *Proxy, d *DNSContext) (bool, error) +type BeforeRequestHandler func(p *Proxy, dctx *DNSContext) (bool, error) // RequestHandler is an optional custom handler for DNS requests // It is called instead of the default method (Proxy.Resolve()) // See handler_test.go for examples -type RequestHandler func(p *Proxy, d *DNSContext) error +type RequestHandler func(p *Proxy, dctx *DNSContext) error // ResponseHandler is a callback method that is called when DNS query has been processed // d -- current DNS query context (contains response if it was successful) // err -- error (if any) -type ResponseHandler func(d *DNSContext, err error) +type ResponseHandler func(dctx *DNSContext, err error) // Config contains all the fields necessary for proxy configuration // // TODO(a.garipov): Consider extracting conf blocks for better fieldalignment. type Config struct { - // Listeners - // -- - - UDPListenAddr []*net.UDPAddr // if nil, then it does not listen for UDP - TCPListenAddr []*net.TCPAddr // if nil, then it does not listen for TCP - HTTPSListenAddr []*net.TCPAddr // if nil, then it does not listen for HTTPS (DoH) - TLSListenAddr []*net.TCPAddr // if nil, then it does not listen for TLS (DoT) - QUICListenAddr []*net.UDPAddr // if nil, then it does not listen for QUIC (DoQ) - DNSCryptUDPListenAddr []*net.UDPAddr // if nil, then it does not listen for DNSCrypt - DNSCryptTCPListenAddr []*net.TCPAddr // if nil, then it does not listen for DNSCrypt - - // Encryption configuration - // -- - - TLSConfig *tls.Config // necessary for TLS, HTTPS, QUIC - HTTP3 bool // if true, HTTPS server will also support HTTP/3 - DNSCryptProviderName string // DNSCrypt provider name - DNSCryptResolverCert *dnscrypt.Cert // DNSCrypt resolver certificate - - // Rate-limiting and anti-DNS amplification measures - // -- - // - // TODO(s.chzhen): Extract ratelimit settings to a separate structure. - - // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for - // rate limiting requests. - RatelimitSubnetLenIPv4 int - - // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for - // rate limiting requests. - RatelimitSubnetLenIPv6 int - - // Ratelimit is a maximum number of requests per second from a given IP (0 - // to disable). - Ratelimit int - - // RatelimitWhitelist is a list of IP addresses excluded from rate limiting. - RatelimitWhitelist []netip.Addr - - // RefuseAny makes proxy refuse the requests of type ANY. - RefuseAny bool - // TrustedProxies is the trusted list of CIDR networks to detect proxy // servers addresses from where the DoH requests should be handled. The // value of nil makes Proxy not trust any address. TrustedProxies netutil.SubnetSet - // Upstream DNS servers and their settings - // -- + // BeforeRequestHandler is an optional custom handler called before each DNS + // request is started processing. See [BeforeRequestHandler]. + BeforeRequestHandler BeforeRequestHandler + + // RequestHandler is an optional custom handler for DNS requests. It's used + // instead of [Proxy.Resolve] if set. See [RequestHandler]. + RequestHandler RequestHandler + + // ResponseHandler is an optional custom handler called when DNS query has + // been processed. See [ResponseHandler]. + ResponseHandler ResponseHandler // UpstreamConfig is a general set of DNS servers to forward requests to. UpstreamConfig *UpstreamConfig @@ -107,6 +74,105 @@ type Config struct { // general set fails responding. Fallbacks *UpstreamConfig + // Userinfo is the sole permitted userinfo for the DoH basic authentication. + // If Userinfo is set, all DoH queries are required to have this basic + // authentication information. + Userinfo *url.Userinfo + + // TLSConfig is the TLS configuration. Required for DNS-over-TLS, + // DNS-over-HTTP, and DNS-over-QUIC servers. + TLSConfig *tls.Config + + // DNSCryptResolverCert is the DNSCrypt resolver certificate. Required for + // DNSCrypt server. + DNSCryptResolverCert *dnscrypt.Cert + + // DNSCryptProviderName is the DNSCrypt provider name. Required for + // DNSCrypt server. + DNSCryptProviderName string + + // HTTPSServerName sets the Server header of the HTTPS server responses, if + // not empty. + HTTPSServerName string + + // UDPListenAddr is the set of UDP addresses to listen for plain + // DNS-over-UDP requests. + UDPListenAddr []*net.UDPAddr + + // TCPListenAddr is the set of TCP addresses to listen for plain + // DNS-over-TCP requests. + TCPListenAddr []*net.TCPAddr + + // HTTPSListenAddr is the set of TCP addresses to listen for DNS-over-HTTPS + // requests. + HTTPSListenAddr []*net.TCPAddr + + // TLSListenAddr is the set of TCP addresses to listen for DNS-over-TLS + // requests. + TLSListenAddr []*net.TCPAddr + + // QUICListenAddr is the set of UDP addresses to listen for DNS-over-QUIC + // requests. + QUICListenAddr []*net.UDPAddr + + // DNSCryptUDPListenAddr is the set of UDP addresses to listen for DNSCrypt + // requests. + DNSCryptUDPListenAddr []*net.UDPAddr + + // DNSCryptTCPListenAddr is the set of TCP addresses to listen for DNSCrypt + // requests. + DNSCryptTCPListenAddr []*net.TCPAddr + + // BogusNXDomain is the set of networks used to transform responses into + // NXDOMAIN ones if they contain at least a single IP address within these + // networks. It's similar to dnsmasq's "bogus-nxdomain". + BogusNXDomain []netip.Prefix + + // DNS64Prefs is the set of NAT64 prefixes used for DNS64 handling. nil + // value disables the feature. An empty value will be interpreted as the + // default Well-Known Prefix. + DNS64Prefs []netip.Prefix + + // RatelimitWhitelist is a list of IP addresses excluded from rate limiting. + RatelimitWhitelist []netip.Addr + + // EDNSAddr is the ECS IP used in request. + EDNSAddr net.IP + + // TODO(s.chzhen): Extract ratelimit settings to a separate structure. + + // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for + // rate limiting requests. + RatelimitSubnetLenIPv4 int + + // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for + // rate limiting requests. + RatelimitSubnetLenIPv6 int + + // Ratelimit is a maximum number of requests per second from a given IP (0 + // to disable). + Ratelimit int + + // CacheSizeBytes is the maximum cache size in bytes. + CacheSizeBytes int + + // CacheMinTTL is the minimum TTL for cached DNS responses in seconds. + CacheMinTTL uint32 + + // CacheMaxTTL is the maximum TTL for cached DNS responses in seconds. + CacheMaxTTL uint32 + + // MaxGoroutines is the maximum number of goroutines processing DNS + // requests. Important for mobile users. + // + // TODO(a.garipov): Rename this to something like “MaxDNSRequestGoroutines” + // in a later major version, as it doesn't actually limit all goroutines. + MaxGoroutines uint + + // The size of the read buffer on the underlying socket. Larger read + // buffers can handle larger bursts of requests before packets get dropped. + UDPBufferSize int + // UpstreamMode determines the logic through which upstreams will be used. UpstreamMode UpstreamModeType @@ -115,10 +181,11 @@ type Config struct { // value will be replaced with the default one. FastestPingTimeout time.Duration - // BogusNXDomain is the set of networks used to transform responses into - // NXDOMAIN ones if they contain at least a single IP address within these - // networks. It's similar to dnsmasq's "bogus-nxdomain". - BogusNXDomain []netip.Prefix + // RefuseAny makes proxy refuse the requests of type ANY. + RefuseAny bool + + // HTTP3 enables HTTP/3 support for HTTPS server. + HTTP3 bool // Enable EDNS Client Subnet option DNS requests to the upstream server will // contain an OPT record with Client Subnet option. If the original request @@ -142,62 +209,18 @@ type Config struct { // never be used for clients with public IP addresses. EnableEDNSClientSubnet bool - // EDNSAddr is the ECS IP used in request. - EDNSAddr net.IP - - // Cache settings - // -- + // CacheEnabled defines if the response cache should be used. + CacheEnabled bool - CacheEnabled bool // cache status - CacheSizeBytes int // Cache size (in bytes). Default: 64k - CacheMinTTL uint32 // Minimum TTL for DNS entries (in seconds). - CacheMaxTTL uint32 // Maximum TTL for DNS entries (in seconds). - // CacheOptimistic defines if the optimistic cache mechanism should be - // used. + // CacheOptimistic defines if the optimistic cache mechanism should be used. CacheOptimistic bool - // Handlers (for the case when dnsproxy is used as a library) - // -- - - BeforeRequestHandler BeforeRequestHandler // callback that is called before each request - RequestHandler RequestHandler // callback that can handle incoming DNS requests - ResponseHandler ResponseHandler // response callback - - // Other settings - // -- - - // HTTPSServerName sets the Server header of the HTTPS server responses, if - // not empty. - HTTPSServerName string - - // Userinfo is the sole permitted userinfo for the DoH basic authentication. - // If Userinfo is set, all DoH queries are required to have this basic - // authentication information. - Userinfo *url.Userinfo - - // MaxGoroutines is the maximum number of goroutines processing DNS - // requests. Important for mobile users. - // - // TODO(a.garipov): Rename this to something like - // “MaxDNSRequestGoroutines” in a later major version, as it doesn't - // actually limit all goroutines. - MaxGoroutines uint - - // The size of the read buffer on the underlying socket. Larger read buffers can handle - // larger bursts of requests before packets get dropped. - UDPBufferSize int - // UseDNS64 enables DNS64 handling. If true, proxy will translate IPv4 // answers into IPv6 answers using first of DNS64Prefs. Note also that PTR // requests for addresses within the specified networks are considered // private and will be forwarded as PrivateRDNSUpstreamConfig specifies. UseDNS64 bool - // DNS64Prefs is the set of NAT64 prefixes used for DNS64 handling. nil - // value disables the feature. An empty value will be interpreted as the - // default Well-Known Prefix. - DNS64Prefs []netip.Prefix - // PreferIPv6 tells the proxy to prefer IPv6 addresses when bootstrapping // upstreams that use hostnames. PreferIPv6 bool diff --git a/proxy/dns64.go b/proxy/dns64.go index daf332ed5..e5615fe3f 100644 --- a/proxy/dns64.go +++ b/proxy/dns64.go @@ -120,7 +120,7 @@ func (p *Proxy) filterNAT64Answers(rrs []dns.RR) (filtered []dns.RR, hasAnswers case *dns.AAAA: addr, err := netutil.IPToAddrNoMapped(ans.AAAA) if err != nil { - log.Error("proxy: bad aaaa record: %s", err) + log.Error("dnsproxy: bad aaaa record: %s", err) } else if p.withinDNS64(addr) { // Filter the record. continue @@ -222,9 +222,9 @@ func (p *Proxy) shouldStripDNS64(addr netip.Addr) (ok bool) { switch { case p.withinDNS64(addr): - log.Debug("proxy: %s is within DNS64 custom prefix set", addr) + log.Debug("dnsproxy: %s is within DNS64 custom prefix set", addr) case dns64WellKnownPref.Contains(addr): - log.Debug("proxy: %s is within DNS64 well-known prefix", addr) + log.Debug("dnsproxy: %s is within DNS64 well-known prefix", addr) default: return false } @@ -262,7 +262,7 @@ func (p *Proxy) synthRR(rr dns.RR, soaTTL uint32) (result dns.RR) { addr, err := netutil.IPToAddr(aResp.A, netutil.AddrFamilyIPv4) if err != nil { - log.Error("proxy: bad a record: %s", err) + log.Error("dnsproxy: bad a record: %s", err) return nil } @@ -297,11 +297,11 @@ func (p *Proxy) performDNS64( } host := origReq.Question[0].Name - log.Debug("proxy: received an empty aaaa response for %q, checking dns64", host) + log.Debug("dnsproxy: received an empty aaaa response for %q, checking dns64", host) dns64Resp, u, err := p.exchangeUpstreams(dns64Req, upstreams) if err != nil { - log.Error("proxy: dns64 request failed: %s", err) + log.Error("dnsproxy: dns64 request failed: %s", err) return nil } diff --git a/proxy/dnscontext.go b/proxy/dnscontext.go index 545e5244b..c90b580de 100644 --- a/proxy/dnscontext.go +++ b/proxy/dnscontext.go @@ -23,12 +23,9 @@ type DNSContext struct { QUICConnection quic.Connection // QUICStream is the QUIC stream from which we got the query. For - // ProtoQUIC only. + // [ProtoQUIC] only. QUICStream quic.Stream - // Addr is the address of the client. - Addr netip.AddrPort - // Upstream is the upstream that resolved the request. In case of cached // response it's nil. Upstream upstream.Upstream @@ -64,6 +61,9 @@ type DNSContext struct { // localIP - local IP address (for UDP socket to call udpMakeOOBWithSrc) localIP netip.Addr + // Addr is the address of the client. + Addr netip.AddrPort + // QueryDuration is the duration of a successful query to an upstream // server or, if the upstream server is unavailable, to a fallback server. QueryDuration time.Duration @@ -90,6 +90,19 @@ type DNSContext struct { doBit bool } +// newDNSContext returns a new properly initialized *DNSContext. +// +// TODO(e.burkov): Consider creating DNSContext with this everywhere, to +// actually respect the contract of DNSContext.RequestID field. +func (p *Proxy) newDNSContext(proto Proto, req *dns.Msg) (d *DNSContext) { + return &DNSContext{ + Proto: proto, + Req: req, + + RequestID: p.counter.Add(1), + } +} + // calcFlagsAndSize lazily calculates some values required for Resolve method. func (dctx *DNSContext) calcFlagsAndSize() { if dctx.udpSize != 0 || dctx.Req == nil { diff --git a/proxy/lookup.go b/proxy/lookup.go index bdd2dc345..2746e1a28 100644 --- a/proxy/lookup.go +++ b/proxy/lookup.go @@ -3,11 +3,12 @@ package proxy import ( "context" "net/netip" + "slices" - proxynetutil "github.com/AdguardTeam/dnsproxy/internal/netutil" "github.com/AdguardTeam/dnsproxy/proxyutil" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/netutil" "github.com/miekg/dns" ) @@ -19,16 +20,7 @@ type lookupResult struct { // lookupIPAddr resolves the specified host IP addresses. func (p *Proxy) lookupIPAddr(host string, qtype uint16, ch chan *lookupResult) { - req := &dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - { - Name: host, - Qtype: qtype, - Qclass: dns.ClassINET, - }, - } + req := (&dns.Msg{}).SetQuestion(host, qtype) d := p.newDNSContext(ProtoUDP, req) err := p.Resolve(d) @@ -81,7 +73,11 @@ func (p *Proxy) LookupNetIP( return addrs, errors.Join(errs...) } - proxynetutil.SortNetIPAddrs(addrs, p.Config.PreferIPv6) + if p.Config.PreferIPv6 { + slices.SortStableFunc(addrs, netutil.PreferIPv6) + } else { + slices.SortStableFunc(addrs, netutil.PreferIPv4) + } return addrs, nil } diff --git a/proxy/proxy.go b/proxy/proxy.go index 767ebb793..c561af147 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -64,18 +64,47 @@ const ( // // TODO(a.garipov): Consider extracting conf blocks for better fieldalignment. type Proxy struct { - // counter is the counter of messages. It must only be incremented - // atomically, so it must be the first member of the struct to make sure - // that it has a 64-bit alignment. + // requestsSema limits the number of simultaneous requests. + // + // TODO(a.garipov): Currently we have to pass this exact semaphore to the + // workers, to prevent races on restart. In the future we will need a + // better restarting mechanism that completely prevents such invalid states. + // + // See also: https://github.com/AdguardTeam/AdGuardHome/issues/2242. + requestsSema syncutil.Semaphore + + // time provides the current time. // - // See https://golang.org/pkg/sync/atomic/#pkg-note-BUG. - counter uint64 + // TODO(e.burkov): Consider configuring it. + time clock - // started indicates if the proxy has been started. - started bool + // randSrc provides the source of randomness. + // + // TODO(e.burkov): Consider configuring it. + randSrc rand.Source + + // dnsCryptServer serves DNSCrypt queries. + dnsCryptServer *dnscrypt.Server - // Listeners - // -- + // ratelimitBuckets is a storage for ratelimiters for individual IPs. + ratelimitBuckets *gocache.Cache + + // fastestAddr finds the fastest IP address for the resolved domain. + fastestAddr *fastip.FastestAddr + + // cache is used to cache requests. It is disabled if nil. + // + // TODO(d.kolyshev): Move this cache to [Proxy.UpstreamConfig] field. + cache *cache + + // shortFlighter is used to resolve the expired cached requests without + // repetitions. + shortFlighter *optimisticResolver + + // bytesPool is a pool of byte slices used to read DNS packets. + // + // TODO(e.burkov): Use [syncutil.Pool]. + bytesPool *sync.Pool // udpListen are the listened UDP connections. udpListen []*net.UDPConn @@ -107,101 +136,48 @@ type Proxy struct { // dnsCryptTCPListen are the listened TCP connections for DNSCrypt. dnsCryptTCPListen []net.Listener - // dnsCryptServer serves DNSCrypt queries. - dnsCryptServer *dnscrypt.Server - - // Upstream - // -- - // upstreamRTTStats maps the upstream address to its round-trip time // statistics. It's holds the statistics for all upstreams to perform a // weighted random selection when using the load balancing mode. upstreamRTTStats map[string]upstreamRTTStats - // rttLock protects upstreamRTTStats. - // - // TODO(e.burkov): Make it a pointer. - rttLock sync.Mutex - - // DNS64 (in case dnsproxy works in a NAT64/DNS64 network) - // -- - // dns64Prefs is a set of NAT64 prefixes that are used to detect and // construct DNS64 responses. The DNS64 function is disabled if it is // empty. dns64Prefs []netip.Prefix - // Ratelimit - // -- - - // ratelimitBuckets is a storage for ratelimiters for individual IPs. - ratelimitBuckets *gocache.Cache - - // ratelimitLock protects ratelimitBuckets. - ratelimitLock sync.Mutex - - // DNS cache - // -- - - // cache is used to cache requests. It is disabled if nil. - // - // TODO(d.kolyshev): Move this cache to [Proxy.UpstreamConfig] field. - cache *cache - - // shortFlighter is used to resolve the expired cached requests without - // repetitions. - shortFlighter *optimisticResolver - - // FastestAddr module - // -- - - // fastestAddr finds the fastest IP address for the resolved domain. - fastestAddr *fastip.FastestAddr - - // Other - // -- - - // bytesPool is a pool of byte slices used to read DNS packets. + // Config is the proxy configuration. // - // TODO(e.burkov): Use [syncutil.Pool]. - bytesPool *sync.Pool + // TODO(a.garipov): Remove this embed and create a proper initializer. + Config // udpOOBSize is the size of the out-of-band data for UDP connections. udpOOBSize int + // counter is the counter of messages. It must only be incremented + // atomically, so it must be the first member of the struct to make sure + // that it has a 64-bit alignment. + counter atomic.Uint64 + // RWMutex protects the whole proxy. // // TODO(e.burkov): Find out what exactly it protects and name it properly. // Also make it a pointer. sync.RWMutex - // requestsSema limits the number of simultaneous requests. - // - // TODO(a.garipov): Currently we have to pass this exact semaphore to - // the workers, to prevent races on restart. In the future we will need - // a better restarting mechanism that completely prevents such invalid - // states. - // - // See also: https://github.com/AdguardTeam/AdGuardHome/issues/2242. - requestsSema syncutil.Semaphore - - // time provides the current time. - // - // TODO(e.burkov): Consider configuring it. - time clock + // ratelimitLock protects ratelimitBuckets. + ratelimitLock sync.Mutex - // randSrc provides the source of randomness. + // rttLock protects upstreamRTTStats. // - // TODO(e.burkov): Consider configuring it. - randSrc rand.Source + // TODO(e.burkov): Make it a pointer. + rttLock sync.Mutex - // Config is the proxy configuration. - // - // TODO(a.garipov): Remove this embed and create a proper initializer. - Config + // started indicates if the proxy has been started. + started bool } -// New creates a new Proxy with the specified configuration. +// New creates a new Proxy with the specified configuration. c must not be nil. func New(c *Config) (p *Proxy, err error) { p = &Proxy{ Config: *c, @@ -617,12 +593,12 @@ func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) { if dns64Ups := p.performDNS64(req, resp, upstreams); dns64Ups != nil { u = dns64Ups } else if p.isBogusNXDomain(resp) { - log.Debug("proxy: replying from upstream: response contains bogus-nxdomain ip") + log.Debug("dnsproxy: replying from upstream: response contains bogus-nxdomain ip") resp = p.genWithRCode(req, dns.RcodeNameError) } if err != nil && p.Fallbacks != nil { - log.Debug("proxy: replying from upstream: using fallback due to %s", err) + log.Debug("dnsproxy: replying from upstream: using fallback due to %s", err) // Reset the timer. start = time.Now() @@ -637,12 +613,12 @@ func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) { } if err != nil { - log.Debug("proxy: replying from %s: %s", src, err) + log.Debug("dnsproxy: replying from %s: %s", src, err) } if resp != nil { rtt := time.Since(start) - log.Debug("proxy: replying from %s: rtt is %s", src, rtt) + log.Debug("dnsproxy: replying from %s: rtt is %s", src, rtt) d.QueryDuration = rtt } @@ -800,13 +776,3 @@ func (dctx *DNSContext) processECS(cliIP net.IP) { log.Debug("dnsproxy: setting ecs: %s", dctx.ReqECS) } } - -// newDNSContext returns a new properly initialized *DNSContext. -func (p *Proxy) newDNSContext(proto Proto, req *dns.Msg) (d *DNSContext) { - return &DNSContext{ - Proto: proto, - Req: req, - - RequestID: atomic.AddUint64(&p.counter, 1), - } -} diff --git a/proxy/server.go b/proxy/server.go index a01b2bc78..fefcd76f9 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -6,6 +6,7 @@ import ( "net" "time" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/miekg/dns" "github.com/quic-go/quic-go" @@ -78,44 +79,51 @@ func (p *Proxy) startListeners(ctx context.Context) error { return nil } +// handleBefore calls the [BeforeRequestHandler] if it's set and returns true if +// the request should be processed further. +func (p *Proxy) handleBefore(d *DNSContext) (cont bool) { + if p.BeforeRequestHandler == nil { + return true + } + + ok, err := p.BeforeRequestHandler(p, d) + if err != nil { + log.Error("dnsproxy: handling before request: %s", err) + + d.Res = p.genServerFailure(d.Req) + p.respond(d) + + return false + } + + return ok +} + // handleDNSRequest processes the incoming packet bytes and returns with an optional response packet. func (p *Proxy) handleDNSRequest(d *DNSContext) error { p.logDNSMessage(d.Req) if d.Req.Response { - log.Debug("Dropping incoming Reply packet from %s", d.Addr.String()) + log.Debug("dnsproxy: dropping incoming response packet from %s", d.Addr) + return nil } - if p.BeforeRequestHandler != nil { - ok, err := p.BeforeRequestHandler(p, d) - if err != nil { - log.Error("Error in the BeforeRequestHandler: %s", err) - d.Res = p.genServerFailure(d.Req) - p.respond(d) - return nil - } - if !ok { - return nil // do nothing, don't reply - } + if !p.handleBefore(d) { + return nil } // ratelimit based on IP only, protects CPU cycles and outbound connections + // + // TODO(e.burkov): Investigate if written above true and move to UDP server + // implementation? if d.Proto == ProtoUDP && p.isRatelimited(d.Addr.Addr()) { - log.Tracef("Ratelimiting %v based on IP only", d.Addr) - return nil // do nothing, don't reply, we got ratelimited - } + log.Debug("dnsproxy: ratelimiting %s based on IP only", d.Addr) - if len(d.Req.Question) != 1 { - log.Debug("got invalid number of questions: %v", len(d.Req.Question)) - d.Res = p.genServerFailure(d.Req) + return nil // do nothing, don't reply, we got ratelimited } - // refuse ANY requests (anti-DDOS measure) - if p.RefuseAny && len(d.Req.Question) > 0 && d.Req.Question[0].Qtype == dns.TypeANY { - log.Tracef("Refusing type=ANY request") - d.Res = p.genNotImpl(d.Req) - } + d.Res = p.validateRequest(d) var err error @@ -124,6 +132,8 @@ func (p *Proxy) handleDNSRequest(d *DNSContext) error { panic("SHOULD NOT HAPPEN: no default upstreams specified") } + defer func() { err = errors.Annotate(err, "handling request: %w") }() + // execute the DNS request // if there is a custom middleware configured, use it if p.RequestHandler != nil { @@ -131,10 +141,6 @@ func (p *Proxy) handleDNSRequest(d *DNSContext) error { } else { err = p.Resolve(d) } - - if err != nil { - err = fmt.Errorf("talking to dns upstream: %w", err) - } } p.logDNSMessage(d.Res) @@ -143,6 +149,24 @@ func (p *Proxy) handleDNSRequest(d *DNSContext) error { return err } +// validateRequest returns a response for invalid request or nil if the request +// is ok. +func (p *Proxy) validateRequest(d *DNSContext) (resp *dns.Msg) { + switch { + case len(d.Req.Question) != 1: + log.Debug("dnsproxy: got invalid number of questions: %d", len(d.Req.Question)) + + return p.genServerFailure(d.Req) + case p.RefuseAny && d.Req.Question[0].Qtype == dns.TypeANY: + // Refuse requests of type ANY (anti-DDOS measure). + log.Debug("dnsproxy: refusing type=ANY request") + + return p.genNotImpl(d.Req) + default: + return nil + } +} + // respond writes the specified response to the client (or does nothing if d.Res is empty) func (p *Proxy) respond(d *DNSContext) { // d.Conn can be nil in the case of a DoH request. diff --git a/proxy/server_https.go b/proxy/server_https.go index f842eeeb8..022cd2194 100644 --- a/proxy/server_https.go +++ b/proxy/server_https.go @@ -195,8 +195,7 @@ func (p *Proxy) checkBasicAuth( log.Error("dnsproxy: basic auth failed for user %q from raddr %s", user, raddr) h := w.Header() - // TODO(a.garipov): Add to httphdr. - h.Set("Www-Authenticate", `Basic realm="DNS", charset="UTF-8"`) + h.Set(httphdr.WWWAuthenticate, `Basic realm="DNS", charset="UTF-8"`) http.Error(w, "Authorization required", http.StatusUnauthorized) return false diff --git a/scripts/make/go-lint.sh b/scripts/make/go-lint.sh index ca3c73335..1d3af71f1 100644 --- a/scripts/make/go-lint.sh +++ b/scripts/make/go-lint.sh @@ -183,11 +183,7 @@ run_linter looppointer ./... run_linter nilness ./... -# TODO(a.garipov): Enable for all. -run_linter fieldalignment ./fastip/... -run_linter fieldalignment ./internal/... -run_linter fieldalignment ./proxyutil/... -run_linter fieldalignment ./upstream/... +run_linter fieldalignment ./... run_linter -e shadow --strict ./...