Skip to content
Commits on Source (1)
  • Eugene Burkov's avatar
    Pull request: 5117 backport dns64 · 5d50fff0
    Eugene Burkov authored
    Merge in DNS/dnsproxy from 5117-backport-dns64 to master
    
    Updates AdguardTeam/AdGuardHome#5117.
    
    Squashed commit of the following:
    
    commit 7fcd7d84
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Fri Feb 3 15:03:17 2023 +0300
    
        proxy: fix test
    
    commit ec1b3d76
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Fri Feb 3 14:00:42 2023 +0300
    
        proxy: imp docs
    
    commit eb0b1e3d
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Feb 2 20:08:40 2023 +0300
    
        proxy: imp and test
    
    commit d4b9133c
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Feb 2 17:40:56 2023 +0300
    
        proxy: fix race in test
    
    commit ef90bd32
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Feb 2 15:33:04 2023 +0300
    
        proxy: fit gocyclo into 10, imp logs
    
    commit 0e25eb68
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Feb 2 14:01:12 2023 +0300
    
        all: imp code, tests
    
    commit a58f2f2a
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Feb 1 14:35:30 2023 +0300
    
        proxy: fix const name
    
    commit 266095a9
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Feb 1 14:31:11 2023 +0300
    
        proxy: rm unused, exp const
    
    commit ccafca50
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Feb 1 13:41:19 2023 +0300
    
        proxy: backport dns64, depr old api
    5d50fff0
......@@ -37,50 +37,52 @@ Usage:
dnsproxy [OPTIONS]
Application Options:
--config-path= yaml configuration file. Minimal working configuration in config.yaml.dist.
Options passed through command line will override the ones from this file.
-v, --verbose Verbose output (optional)
-o, --output= Path to the log file. If not set, write to stdout.
-l, --listen= Listening addresses
-p, --port= Listening ports. Zero value disables TCP and UDP listeners
-s, --https-port= Listening ports for DNS-over-HTTPS
-t, --tls-port= Listening ports for DNS-over-TLS
-q, --quic-port= Listening ports for DNS-over-QUIC
-y, --dnscrypt-port= Listening ports for DNSCrypt
-c, --tls-crt= Path to a file with the certificate chain
-k, --tls-key= Path to a file with the private key
--tls-min-version= Minimum TLS version, for example 1.0
--tls-max-version= Maximum TLS version, for example 1.3
--insecure Disable secure TLS certificate validation
-g, --dnscrypt-config= Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt
--http3 Enable HTTP/3 support
-u, --upstream= An upstream to be used (can be specified multiple times).
You can also specify path to a file with the list of servers
-b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53)
-f, --fallback= 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
--all-servers If specified, parallel queries to all configured upstream servers are enabled
--fastest-addr Respond to A or AAAA requests only with the fastest IP address
--cache If specified, DNS cache is enabled
--cache-size= Cache size (in bytes). Default: 64k
--cache-min-ttl= Minimum TTL value for DNS entries, in seconds. Capped at 3600.
Artificially extending TTLs should only be done with careful consideration.
--cache-max-ttl= Maximum TTL value for DNS entries, in seconds.
--cache-optimistic If specified, optimistic DNS cache is enabled
-r, --ratelimit= Ratelimit (requests per second)
--refuse-any If specified, refuse ANY requests
--edns Use EDNS Client Subnet extension
--edns-addr= Send EDNS Client Address
--dns64 If specified, dnsproxy will act as a DNS64 server
--dns64-prefix= If specified, this is the DNS64 prefix dnsproxy will be using when it works as a DNS64 server.
If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::
--ipv6-disabled If specified, all AAAA requests will be replied with NoError RCode and empty answer
--bogus-nxdomain= Transform the responses containing at least a single IP that matches specified addresses
and CIDRs into NXDOMAIN. Can be specified multiple times.
--udp-buf-size= Set the size of the UDP buffer in bytes. A value <= 0 will use the system default.
--max-go-routines= Set the maximum number of go routines. A value <= 0 will not not set a maximum.
--pprof If present, exposes pprof information on localhost:6060.
--version Prints the program version
--config-path= yaml configuration file. Minimal working configuration in config.yaml.dist.
Options passed through command line will override the ones from this file.
-v, --verbose Verbose output (optional)
-o, --output= Path to the log file. If not set, write to stdout.
-l, --listen= Listening addresses
-p, --port= Listening ports. Zero value disables TCP and UDP listeners
-s, --https-port= Listening ports for DNS-over-HTTPS
-t, --tls-port= Listening ports for DNS-over-TLS
-q, --quic-port= Listening ports for DNS-over-QUIC
-y, --dnscrypt-port= Listening ports for DNSCrypt
-c, --tls-crt= Path to a file with the certificate chain
-k, --tls-key= Path to a file with the private key
--tls-min-version= Minimum TLS version, for example 1.0
--tls-max-version= Maximum TLS version, for example 1.3
--insecure Disable secure TLS certificate validation
-g, --dnscrypt-config= Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt
--http3 Enable HTTP/3 support
-u, --upstream= An upstream to be used (can be specified multiple times).
You can also specify path to a file with the list of servers
-b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53)
-f, --fallback= 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
--private-rdns-upstream= Private DNS upstreams to use for reverse DNS lookups of private addresses, can
be specified multiple times
--all-servers If specified, parallel queries to all configured upstream servers are enabled
--fastest-addr Respond to A or AAAA requests only with the fastest IP address
--cache If specified, DNS cache is enabled
--cache-size= Cache size (in bytes). Default: 64k
--cache-min-ttl= Minimum TTL value for DNS entries, in seconds. Capped at 3600.
Artificially extending TTLs should only be done with careful consideration.
--cache-max-ttl= Maximum TTL value for DNS entries, in seconds.
--cache-optimistic If specified, optimistic DNS cache is enabled
-r, --ratelimit= Ratelimit (requests per second)
--refuse-any If specified, refuse ANY requests
--edns Use EDNS Client Subnet extension
--edns-addr= Send EDNS Client Address
--dns64 If specified, dnsproxy will act as a DNS64 server
--dns64-prefix= Prefix used to handle DNS64. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::.
Can be specified multiple times
--ipv6-disabled If specified, all AAAA requests will be replied with NoError RCode and empty answer
--bogus-nxdomain= Transform the responses containing at least a single IP that matches specified addresses
and CIDRs into NXDOMAIN. Can be specified multiple times.
--udp-buf-size= Set the size of the UDP buffer in bytes. A value <= 0 will use the system default.
--max-go-routines= Set the maximum number of go routines. A value <= 0 will not not set a maximum.
--pprof If present, exposes pprof information on localhost:6060.
--version Prints the program version
Help Options:
-h, --help Show this help message
......@@ -230,16 +232,27 @@ Loads upstreams list from a file.
> (with A but not AAAA records in the DNS). This lets IPv6-only clients use
> NAT64 gateways without any other configuration.
Enables DNS64 with the default "Well-Known Prefix" `64:ff9b::/96`:
See also [RFC 6147](https://datatracker.ietf.org/doc/html/rfc6147).
Enables DNS64 with the default [Well-Known Prefix][wkp]:
```shell
./dnsproxy -l 127.0.0.1 -p 5353 -u 8.8.8.8 --dns64
./dnsproxy -l 127.0.0.1 -p 5353 -u 8.8.8.8 --private-rdns-upstream=127.0.0.1 --dns64
```
You can also specify a custom DNS64 prefix:
You can also specify any number of custom DNS64 prefixes:
```shell
./dnsproxy -l 127.0.0.1 -p 5353 -u 8.8.8.8 --dns64 --dns64-prefix=64:ffff::
./dnsproxy -l 127.0.0.1 -p 5353 -u 8.8.8.8 --private-rdns-upstream=127.0.0.1 --dns64 --dns64-prefix=64:ffff:: --dns64-prefix=32:ffff::
```
Note that only the first specified prefix will be used for synthesis.
PTR queries for addresses within the specified ranges or the
[Well-Known one][wkp] could only be answered with locally appropriate data, so
dnsproxy will route those to the local upstream servers. Those should be
specified if DNS64 is enabled.
[wkp]: https://datatracker.ietf.org/doc/html/rfc6052#section-2.1
### Fastest addr + cache-min-ttl
This option would be useful to the users with problematic network connection.
......
......@@ -6,6 +6,7 @@ import (
"net"
"net/http"
"net/http/pprof"
"net/netip"
"os"
"os/signal"
"strings"
......@@ -25,7 +26,6 @@ import (
// use the default option since it will cause some problems when config files
// are used.
type Options struct {
// Configuration file path (yaml), the config path should be read without
// using goFlags in order not to have default values overriding yaml
// options.
......@@ -98,6 +98,10 @@ type Options struct {
// Fallback DNS resolver
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"`
......@@ -147,8 +151,10 @@ type Options struct {
// 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"`
// DNS64Prefix defines the DNS64 prefix that dnsproxy should use when it acts as a DNS64 server
DNS64Prefix string `yaml:"dns64-prefix" long:"dns64-prefix" description:"If specified, this is the DNS64 prefix dnsproxy will be using when it works as a DNS64 server. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::" required:"false"`
// 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"`
// Other settings and options
// --
......@@ -176,11 +182,10 @@ type Options struct {
// VersionString will be set through ldflags, contains current version
var VersionString = "dev" // nolint:gochecknoglobals
const defaultTimeout = 10 * time.Second
// defaultDNS64Prefix is a so-called "Well-Known Prefix" for DNS64.
// if dnsproxy operates as a DNS64 server, we'll be using it.
const defaultDNS64Prefix = "64:ff9b::/96"
const (
defaultTimeout = 10 * time.Second
defaultLocalTimeout = 1 * time.Second
)
func main() {
options := &Options{}
......@@ -243,9 +248,6 @@ func run(options *Options) {
config := createProxyConfig(options)
dnsProxy := &proxy.Proxy{Config: config}
// Init DNS64 if needed.
initDNS64(dnsProxy, options)
// Add extra handler if needed.
if options.IPv6Disabled {
ipv6Configuration := ipv6Configuration{ipv6Disabled: options.IPv6Disabled}
......@@ -321,12 +323,14 @@ func createProxyConfig(options *Options) proxy.Config {
MaxGoroutines: options.MaxGoRoutines,
}
// TODO(e.burkov): Make these methods of [Options].
initUpstreams(&config, options)
initEDNS(&config, options)
initBogusNXDomain(&config, options)
initTLSConfig(&config, options)
initDNSCryptConfig(&config, options)
initListenAddrs(&config, options)
initDNS64(&config, options)
return config
}
......@@ -334,7 +338,6 @@ func createProxyConfig(options *Options) proxy.Config {
// initUpstreams inits upstream-related config
func initUpstreams(config *proxy.Config, options *Options) {
// Init upstreams
upstreams := loadServersList(options.Upstreams)
httpVersions := upstream.DefaultHTTPVersions
if options.HTTP3 {
......@@ -345,24 +348,29 @@ func initUpstreams(config *proxy.Config, options *Options) {
}
}
var err error
upstreams := loadServersList(options.Upstreams)
upsOpts := &upstream.Options{
HTTPVersions: httpVersions,
InsecureSkipVerify: options.Insecure,
Bootstrap: options.BootstrapDNS,
Timeout: defaultTimeout,
}
upstreamConfig, err := proxy.ParseUpstreamsConfig(upstreams, upsOpts)
config.UpstreamConfig, err = proxy.ParseUpstreamsConfig(upstreams, upsOpts)
if err != nil {
log.Fatalf("error while parsing upstreams configuration: %s", err)
}
config.UpstreamConfig = upstreamConfig
if options.AllServers {
config.UpstreamMode = proxy.UModeParallel
} else if options.FastestAddress {
config.UpstreamMode = proxy.UModeFastestAddr
} else {
config.UpstreamMode = proxy.UModeLoadBalance
privUpstreams := loadServersList(options.PrivateRDNSUpstreams)
privUpsOpts := &upstream.Options{
HTTPVersions: httpVersions,
Bootstrap: options.BootstrapDNS,
Timeout: defaultLocalTimeout,
}
config.PrivateRDNSUpstreamConfig, err = proxy.ParseUpstreamsConfig(privUpstreams, privUpsOpts)
if err != nil {
log.Fatalf("error while parsing private rdns upstreams configuration: %s", err)
}
if options.Fallbacks != nil {
......@@ -373,15 +381,27 @@ func initUpstreams(config *proxy.Config, options *Options) {
// separately.
//
// See https://github.com/AdguardTeam/dnsproxy/issues/161.
fallback, err := upstream.AddressToUpstream(f, upsOpts)
var fallback upstream.Upstream
fallback, err = upstream.AddressToUpstream(f, upsOpts)
if err != nil {
log.Fatalf("cannot parse the fallback %s (%s): %s", f, options.BootstrapDNS, err)
}
log.Printf("Fallback %d is %s", i, fallback.Address())
log.Printf("fallback at index %d is %s", i, fallback.Address())
fallbacks = append(fallbacks, fallback)
}
config.Fallbacks = fallbacks
}
if options.AllServers {
config.UpstreamMode = proxy.UModeParallel
} else if options.FastestAddress {
config.UpstreamMode = proxy.UModeFastestAddr
} else {
config.UpstreamMode = proxy.UModeLoadBalance
}
}
// initEDNS inits EDNS-related config
......@@ -527,30 +547,27 @@ func initListenAddrs(config *proxy.Config, options *Options) {
}
}
// initDNS64 inits the DNS64 configuration for dnsproxy
func initDNS64(p *proxy.Proxy, options *Options) {
if !options.DNS64 {
// initDNS64 sets the DNS64 configuration into conf.
func initDNS64(conf *proxy.Config, options *Options) {
if conf.UseDNS64 = options.DNS64; !conf.UseDNS64 {
return
}
dns64Prefix := options.DNS64Prefix
if dns64Prefix == "" {
dns64Prefix = defaultDNS64Prefix
if len(conf.PrivateRDNSUpstreamConfig.Upstreams) == 0 {
log.Fatalf("at least one private upstream must be configured to use dns64")
}
// DNS64 prefix may be specified as a CIDR: "64:ff9b::/96"
ip, _, err := net.ParseCIDR(dns64Prefix)
if err != nil {
// Or it could be specified as an IP address: "64:ff9b::"
ip = net.ParseIP(dns64Prefix)
}
var prefs []netip.Prefix
for i, p := range options.DNS64Prefix {
pref, err := netip.ParsePrefix(p)
if err != nil {
log.Fatalf("parsing prefix at index %d: %v", i, err)
}
if ip == nil || len(ip) < net.IPv6len {
log.Fatalf("Invalid DNS64 prefix: %s", dns64Prefix)
return
prefs = append(prefs, pref)
}
p.SetNAT64Prefix(ip[:proxy.NAT64PrefixLength])
conf.DNS64Prefs = prefs
}
// IPv6 configuration
......
package proxy
import (
"fmt"
"net"
"strings"
"sync"
......@@ -20,16 +19,6 @@ import (
// testCacheSize is the maximum size of cache for tests.
const testCacheSize = 4096
func newRR(t *testing.T, rr string) (r dns.RR) {
t.Helper()
var err error
r, err = dns.NewRR(rr)
require.NoError(t, err)
return r
}
const testUpsAddr = "https://upstream.address"
var upstreamWithAddr = &funcUpstream{
......@@ -52,7 +41,7 @@ func TestServeCached(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
Answer: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
}).SetQuestion("google.com.", dns.TypeA)
reply.SetEdns0(defaultUDPBufSize, false)
......@@ -161,7 +150,7 @@ func TestCacheDO(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
Answer: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
}).SetQuestion("google.com.", dns.TypeA)
reply.SetEdns0(4096, true)
......@@ -204,7 +193,7 @@ func TestCacheCNAME(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, "google.com. 3600 IN CNAME test.google.com.")},
Answer: []dns.RR{newRR(t, "google.com.", dns.TypeCNAME, 3600, "test.google.com.")},
}).SetQuestion("google.com.", dns.TypeA)
testCache.set(reply, upstreamWithAddr)
......@@ -218,7 +207,7 @@ func TestCacheCNAME(t *testing.T) {
})
// Now fill the cache with a cacheable CNAME response.
reply.Answer = append(reply.Answer, newRR(t, "google.com. 3600 IN A 8.8.8.8"))
reply.Answer = append(reply.Answer, newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8}))
testCache.set(reply, upstreamWithAddr)
// We are testing that a proper CNAME response gets cached
......@@ -280,10 +269,10 @@ func TestCacheExpiration(t *testing.T) {
testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop)
// Create dns messages with TTL of 1 second.
rrs := []string{
"youtube.com 1 IN A 173.194.221.198",
"google.com. 1 IN A 8.8.8.8",
"yandex.com. 1 IN A 213.180.204.62",
rrs := []dns.RR{
newRR(t, "youtube.com.", dns.TypeA, 1, net.IP{173, 194, 221, 198}),
newRR(t, "google.com.", dns.TypeA, 1, net.IP{8, 8, 8, 8}),
newRR(t, "yandex.com.", dns.TypeA, 1, net.IP{213, 180, 204, 62}),
}
replies := make([]*dns.Msg, len(rrs))
for i, rr := range rrs {
......@@ -291,8 +280,8 @@ func TestCacheExpiration(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, rr)},
}).SetQuestion(dns.Fqdn(strings.Fields(rr)[0]), dns.TypeA)
Answer: []dns.RR{dns.Copy(rr)},
}).SetQuestion(rr.Header().Name, dns.TypeA)
dnsProxy.cache.set(rep, upstreamWithAddr)
replies[i] = rep
}
......@@ -405,13 +394,13 @@ func TestCache(t *testing.T) {
testCases{
cache: []testEntry{{
q: "google.com.",
a: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}},
cases: []testCase{{
ok: require.True,
q: "google.com.",
a: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}, {
ok: require.False,
......@@ -425,23 +414,23 @@ func TestCache(t *testing.T) {
testCases{
cache: []testEntry{{
q: "gOOgle.com.",
a: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}},
cases: []testCase{{
ok: require.True,
q: "gOOgle.com.",
a: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}, {
ok: require.True,
q: "google.com.",
a: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}, {
ok: require.True,
q: "GOOGLE.COM.",
a: []dns.RR{newRR(t, "google.com. 3600 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}, {
q: "gOOgle.com.",
......@@ -463,7 +452,7 @@ func TestCache(t *testing.T) {
testCases{
cache: []testEntry{{
q: "gOOgle.com.",
a: []dns.RR{newRR(t, "google.com. 0 IN A 8.8.8.8")},
a: []dns.RR{newRR(t, "google.com.", dns.TypeA, 0, net.IP{8, 8, 8, 8})},
t: dns.TypeA,
}},
cases: []testCase{{
......@@ -566,11 +555,13 @@ func requireEqualMsgs(t *testing.T, expected, actual *dns.Msg) {
func setAndGetCache(t *testing.T, c *cache, g *sync.WaitGroup, host, ip string) {
defer g.Done()
ipAddr := net.ParseIP(ip)
dnsMsg := (&dns.Msg{
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, fmt.Sprintf("%s 1 IN A %s", host, ip))},
Answer: []dns.RR{newRR(t, host, dns.TypeA, 1, ipAddr)},
}).SetQuestion(host, dns.TypeA)
c.set(dnsMsg, upstreamWithAddr)
......@@ -611,7 +602,7 @@ func TestSubnet(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, "example.com. 1 IN A 1.1.1.1")},
Answer: []dns.RR{newRR(t, "example.com.", dns.TypeA, 1, net.IP{1, 1, 1, 1})},
}).SetQuestion("example.com.", dns.TypeA)
c.setWithSubnet(
resp,
......@@ -635,7 +626,7 @@ func TestSubnet(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, "example.com. 1 IN A 2.2.2.2")},
Answer: []dns.RR{newRR(t, "example.com.", dns.TypeA, 1, net.IP{2, 2, 2, 2})},
}).SetQuestion("example.com.", dns.TypeA)
c.setWithSubnet(
resp,
......@@ -648,7 +639,7 @@ func TestSubnet(t *testing.T) {
MsgHdr: dns.MsgHdr{
Response: true,
},
Answer: []dns.RR{newRR(t, "example.com. 1 IN A 3.3.3.3")},
Answer: []dns.RR{newRR(t, "example.com.", dns.TypeA, 1, net.IP{3, 3, 3, 3})},
}).SetQuestion("example.com.", dns.TypeA)
c.setWithSubnet(
resp,
......
......@@ -3,6 +3,7 @@ package proxy
import (
"crypto/tls"
"net"
"net/netip"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
......@@ -74,16 +75,30 @@ type Config struct {
// Upstream DNS servers and their settings
// --
UpstreamConfig *UpstreamConfig // Upstream DNS servers configuration
Fallbacks []upstream.Upstream // list of fallback resolvers (which will be used if regular upstream failed to answer)
UpstreamMode UpstreamModeType // How to request the upstream servers
// UpstreamConfig is a general set of DNS servers to forward requests to.
UpstreamConfig *UpstreamConfig
// PrivateRDNSUpstreamConfig is the set of upstream DNS servers for
// resolving private IP addresses. All the requests considered private will
// be resolved via these upstream servers. Such queries will finish with
// [upstream.ErrNoUpstream] if it's empty.
PrivateRDNSUpstreamConfig *UpstreamConfig
// Fallbacks is a list of fallback resolvers. Those will be used if the
// general set fails responding.
Fallbacks []upstream.Upstream
// UpstreamMode determines the logic through which upstreams will be used.
UpstreamMode UpstreamModeType
// FastestPingTimeout is the timeout for waiting the first successful
// dialing when the UpstreamMode is set to UModeFastestAddr.
// Non-positive value will be replaced with the default one.
// dialing when the UpstreamMode is set to UModeFastestAddr. Non-positive
// value will be replaced with the default one.
FastestPingTimeout time.Duration
// BogusNXDomain - transforms responses that contain at least one of the given IP addresses into NXDOMAIN
// Similar to dnsmasq's "bogus-nxdomain"
// 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 []*net.IPNet
// Enable EDNS Client Subnet option DNS requests to the upstream server will
......@@ -107,7 +122,9 @@ type Config struct {
// store these responses in general cache (without subnet) so they will
// never be used for clients with public IP addresses.
EnableEDNSClientSubnet bool
EDNSAddr net.IP // ECS IP used in request
// EDNSAddr is the ECS IP used in request.
EDNSAddr net.IP
// Cache settings
// --
......@@ -141,6 +158,17 @@ type Config struct {
// 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
}
// validateConfig verifies that the supplied configuration is valid and returns an error if it's not
......
......@@ -3,134 +3,325 @@ package proxy
import (
"fmt"
"net"
"net/netip"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
// NAT64PrefixLength is the length of a NAT64 prefix
const NAT64PrefixLength = 12
const (
// maxNAT64PrefixBitLen is the maximum length of a NAT64 prefix in bits.
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.2.
maxNAT64PrefixBitLen = 96
// isEmptyAAAAResponse checks if there are no AAAA records in response
func (p *Proxy) isEmptyAAAAResponse(resp, req *dns.Msg) bool {
return (resp == nil || len(resp.Answer) == 0) &&
req.Question[0].Qtype == dns.TypeAAAA
}
// NAT64PrefixLength is the length of a NAT64 prefix in bytes.
NAT64PrefixLength = net.IPv6len - net.IPv4len
// isNAT64PrefixAvailable returns true if NAT64 prefix was calculated.
func (p *Proxy) isNAT64PrefixAvailable() bool {
p.nat64PrefixLock.Lock()
defer p.nat64PrefixLock.Unlock()
// maxDNS64SynTTL is the maximum TTL for synthesized DNS64 responses with no
// SOA records in seconds.
//
// If the SOA RR was not delivered with the negative response to the AAAA
// query, then the DNS64 SHOULD use the TTL of the original A RR or 600
// seconds, whichever is shorter.
//
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.1.7.
maxDNS64SynTTL uint32 = 600
)
return len(p.nat64Prefix) == NAT64PrefixLength
}
// setupDNS64 initializes DNS64 settings, the NAT64 prefixes in particular. If
// the DNS64 feature is enabled and no prefixes are configured, the default
// Well-Known Prefix is used, just like Section 5.2 of RFC 6147 prescribes. Any
// configured set of prefixes discards the default Well-Known prefix unless it
// is specified explicitly. Each prefix also validated to be a valid IPv6 CIDR
// with a maximum length of 96 bits. The first specified prefix is then used to
// synthesize AAAA records.
func (p *Proxy) setupDNS64() (err error) {
if !p.Config.UseDNS64 {
return nil
}
// SetNAT64Prefix sets NAT64 prefix
func (p *Proxy) SetNAT64Prefix(prefix []byte) {
if len(prefix) != NAT64PrefixLength {
return
l := len(p.Config.DNS64Prefs)
if l == 0 {
p.dns64Prefs = []netip.Prefix{dns64WellKnownPref}
return nil
}
p.nat64PrefixLock.Lock()
p.nat64Prefix = prefix
p.nat64PrefixLock.Unlock()
for i, pref := range p.Config.DNS64Prefs {
if !pref.Addr().Is6() {
return fmt.Errorf("prefix at index %d: %q is not an IPv6 prefix", i, pref)
}
if pref.Bits() > maxNAT64PrefixBitLen {
return fmt.Errorf("prefix at index %d: %q is too long for DNS64", i, pref)
}
ip := [net.IPv6len]byte{}
copy(ip[:NAT64PrefixLength], prefix)
log.Info("NAT64 prefix: %v", net.IP(ip[:]).String())
p.dns64Prefs = append(p.dns64Prefs, pref.Masked())
}
return nil
}
// createModifiedARequest returns modified question to make A DNS request
func createModifiedARequest(d *dns.Msg) (*dns.Msg, error) {
if d.Question[0].Qtype != dns.TypeAAAA {
return nil, fmt.Errorf("question is not AAAA, do nothing")
// checkDNS64 checks if DNS64 should be performed. It returns a DNS64 request
// to resolve or nil if DNS64 is not desired. It also filters resp to not
// contain any NAT64 excluded addresses in the answer section, if needed. Both
// req and resp must not be nil.
//
// See https://datatracker.ietf.org/doc/html/rfc6147.
func (p *Proxy) checkDNS64(req, resp *dns.Msg) (dns64Req *dns.Msg) {
if len(p.dns64Prefs) == 0 {
return nil
}
q := req.Question[0]
if q.Qtype != dns.TypeAAAA || q.Qclass != dns.ClassINET {
// DNS64 operation for classes other than IN is undefined, and a DNS64
// MUST behave as though no DNS64 function is configured.
return nil
}
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: d.Question[0].Name, Qtype: dns.TypeA, Qclass: dns.ClassINET},
rcode := resp.Rcode
if rcode == dns.RcodeNameError {
// A result with RCODE=3 (Name Error) is handled according to normal DNS
// operation (which is normally to return the error to the client).
return nil
}
return &req, nil
if rcode == dns.RcodeSuccess {
// If resolver receives an answer with at least one AAAA record
// containing an address outside any of the excluded range(s), then it
// by default SHOULD build an answer section for a response including
// only the AAAA record(s) that do not contain any of the addresses
// inside the excluded ranges.
var hasAnswers bool
if resp.Answer, hasAnswers = p.filterNAT64Answers(resp.Answer); hasAnswers {
return nil
}
// Any other RCODE is treated as though the RCODE were 0 and the answer
// section were empty.
}
dns64Req = req.Copy()
dns64Req.Id = dns.Id()
dns64Req.Question[0].Qtype = dns.TypeA
return dns64Req
}
// createDNS64MappedResponse adds a NAT64 mapped answer to the old message
// newAResp is new A response. oldAAAAResp is old *dns.Msg with AAAA request and empty answer
func (p *Proxy) createDNS64MappedResponse(newAResp, oldAAAAResp *dns.Msg) (*dns.Msg, error) {
var nat64Prefix []byte
p.nat64PrefixLock.Lock()
nat64Prefix = p.nat64Prefix
p.nat64PrefixLock.Unlock()
// filterNAT64Answers filters out AAAA records that are within one of NAT64
// exclusion prefixes. hasAnswers is true if the filtered slice contains at
// least a single AAAA answer not within the prefixes or a CNAME.
//
// TODO(e.burkov): Remove prefs from args when old API is removed.
func (p *Proxy) filterNAT64Answers(
rrs []dns.RR,
) (filtered []dns.RR, hasAnswers bool) {
filtered = make([]dns.RR, 0, len(rrs))
for _, ans := range rrs {
switch ans := ans.(type) {
case *dns.AAAA:
addr, err := netutil.IPToAddrNoMapped(ans.AAAA)
if err != nil {
log.Error("proxy: bad aaaa record: %s", err)
continue
}
if len(nat64Prefix) != NAT64PrefixLength {
return nil, errors.Error("cannot create a mapped response, no NAT64 prefix specified")
if p.withinDNS64(addr) {
// Filter the record.
continue
}
filtered, hasAnswers = append(filtered, ans), true
case *dns.CNAME, *dns.DNAME:
// If the response contains a CNAME or a DNAME, then the CNAME or
// DNAME chain is followed until the first terminating A or AAAA
// record is reached.
//
// Just treat CNAME and DNAME responses as passable answers since
// AdGuard Home doesn't follow any of these chains except the
// dnsrewrite-defined ones.
filtered, hasAnswers = append(filtered, ans), true
default:
filtered = append(filtered, ans)
}
}
// check if there are no answers
if len(newAResp.Answer) == 0 {
return nil, fmt.Errorf("no ipv4 answer")
return filtered, hasAnswers
}
// synthDNS64 synthesizes a DNS64 response using the original response as a
// basis and modifying it with data from resp. It returns true if the response
// was actually modified.
func (p *Proxy) synthDNS64(origReq, origResp, resp *dns.Msg) (ok bool) {
if len(resp.Answer) == 0 {
// If there is an empty answer, then the DNS64 responds to the original
// querying client with the answer the DNS64 received to the original
// (initiator's) query.
return false
}
oldAAAAResp.Answer = []dns.RR{}
// add NAT 64 prefix for each ipv4 answer
for _, ans := range newAResp.Answer {
i, ok := ans.(*dns.A)
if !ok {
continue
}
// The Time to Live (TTL) field is set to the minimum of the TTL of the
// original A RR and the SOA RR for the queried domain. If the original
// response contains no SOA records, the minimum of the TTL of the original
// A RR and [maxDNS64SynTTL] should be used. See [maxDNS64SynTTL].
soaTTL := maxDNS64SynTTL
for _, rr := range origResp.Ns {
if hdr := rr.Header(); hdr.Rrtype == dns.TypeSOA && hdr.Name == origReq.Question[0].Name {
soaTTL = hdr.Ttl
// new ip address
mappedAddress := make(net.IP, net.IPv6len)
break
}
}
// add NAT 64 prefix and append ipv4 record
copy(mappedAddress, nat64Prefix)
for index, b := range i.A {
mappedAddress[NAT64PrefixLength+index] = b
newAns := make([]dns.RR, 0, len(resp.Answer))
for _, ans := range resp.Answer {
rr := p.synthRR(ans, soaTTL)
if rr == nil {
// The error should have already been logged.
return false
}
// create new response and fill it
rr := new(dns.AAAA)
rr.Hdr = dns.RR_Header{Name: newAResp.Question[0].Name, Rrtype: dns.TypeAAAA, Ttl: ans.Header().Ttl, Class: dns.ClassINET}
rr.AAAA = mappedAddress
oldAAAAResp.Answer = append(oldAAAAResp.Answer, rr)
newAns = append(newAns, rr)
}
return oldAAAAResp, nil
origResp.Answer = newAns
origResp.Ns = resp.Ns
origResp.Extra = resp.Extra
return true
}
// checkDNS64 is called when there is no answer for AAAA request and a NAT64 prefix is configured.
// this function creates modified A request from oldAAAAReq, exchanges it and returns DNS64 mapped response
// oldAAAAReq is message with AAAA Question. oldAAAAResp is response for oldAAAAReq with empty answer section
func (p *Proxy) checkDNS64(oldAAAAReq, oldAAAAResp *dns.Msg, upstreams []upstream.Upstream) (*dns.Msg, upstream.Upstream, error) {
// Let's create A request to the same hostname
modifiedAReq, err := createModifiedARequest(oldAAAAReq)
// dns64WellKnownPref is the default prefix to use in an algorithmic mapping for
// DNS64. See https://datatracker.ietf.org/doc/html/rfc6052#section-2.1.
var dns64WellKnownPref = netip.MustParsePrefix("64:ff9b::/96")
// withinDNS64 checks if ip is within one of the configured DNS64 prefixes.
//
// TODO(e.burkov): We actually using bytes of only the first prefix from the
// set to construct the answer, so consider using some implementation of a
// prefix set for the rest.
func (p *Proxy) withinDNS64(ip netip.Addr) (ok bool) {
for _, n := range p.dns64Prefs {
if n.Contains(ip) {
return true
}
}
return false
}
// shouldStripDNS64 returns true if DNS64 is enabled and ip has either one of
// custom DNS64 prefixes or the Well-Known one. This is intended to be used
// with PTR requests.
//
// The requirement is to match any Pref64::/n used at the site, and not merely
// the locally configured Pref64::/n. This is because end clients could ask for
// a PTR record matching an address received through a different (site-provided)
// DNS64.
//
// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.3.1.
func (p *Proxy) shouldStripDNS64(ip net.IP) (ok bool) {
if len(p.dns64Prefs) == 0 {
return false
}
addr, err := netutil.IPToAddr(ip, netutil.AddrFamilyIPv6)
if err != nil {
log.Tracef("Failed to create DNS64 mapped request %s", err)
return nil, nil, err
return false
}
switch {
case p.withinDNS64(addr):
log.Debug("proxy: %s is within DNS64 custom prefix set", ip)
case dns64WellKnownPref.Contains(addr):
log.Debug("proxy: %s is within DNS64 well-known prefix", ip)
default:
return false
}
return true
}
// mapDNS64 maps addr to IPv6 address using configured DNS64 prefix. addr must
// be a valid IPv4. It panics, if there are no configured DNS64 prefixes,
// because synthesis should not be performed unless DNS64 function enabled.
//
// TODO(e.burkov): Remove pref from args when old API is removed.
func (p *Proxy) mapDNS64(addr netip.Addr) (mapped net.IP) {
// Don't mask the address here since it should have already been masked on
// initialization stage.
prefData := p.dns64Prefs[0].Addr().As16()
addrData := addr.As4()
mapped = make(net.IP, net.IPv6len)
copy(mapped[:NAT64PrefixLength], prefData[:])
copy(mapped[NAT64PrefixLength:], addrData[:])
return mapped
}
// synthRR synthesizes a DNS64 resource record in compliance with RFC 6147. If
// rr is not an A record, it's returned as is. A records are modified to become
// a DNS64-synthesized AAAA records, and the TTL is set according to the
// original TTL of a record and soaTTL. It returns nil on invalid A records.
func (p *Proxy) synthRR(rr dns.RR, soaTTL uint32) (result dns.RR) {
aResp, ok := rr.(*dns.A)
if !ok {
return rr
}
// Exchange new A request with selected upstreams
newAResp, u, err := p.exchange(modifiedAReq, upstreams)
addr, err := netutil.IPToAddr(aResp.A, netutil.AddrFamilyIPv4)
if err != nil {
log.Tracef("Failed to exchange DNS64 request: %s", err)
return nil, nil, err
log.Error("proxy: bad a record: %s", err)
return nil
}
// Check if oldAAAAResp is nil
if oldAAAAResp == nil {
oldAAAAResp = &dns.Msg{}
oldAAAAResp.Id = oldAAAAReq.Id
oldAAAAResp.RecursionDesired = oldAAAAReq.RecursionDesired
oldAAAAResp.Question = []dns.Question{oldAAAAReq.Question[0]}
aaaa := &dns.AAAA{
Hdr: dns.RR_Header{
Name: aResp.Hdr.Name,
Rrtype: dns.TypeAAAA,
Class: aResp.Hdr.Class,
Ttl: mathutil.Min(aResp.Hdr.Ttl, soaTTL),
},
AAAA: p.mapDNS64(addr),
}
return aaaa
}
// performDNS64 returns the upstream that was used to perform DNS64 request, or
// nil, if the request was not performed.
func (p *Proxy) performDNS64(
origReq *dns.Msg,
origResp *dns.Msg,
upstreams []upstream.Upstream,
) (u upstream.Upstream) {
dns64Req := p.checkDNS64(origReq, origResp)
if dns64Req == nil {
return nil
}
// new A response should be mapped with NAT64 prefix
mappedAAAAResponse, err := p.createDNS64MappedResponse(newAResp, oldAAAAResp)
host := origReq.Question[0].Name
log.Debug("proxy: received an empty aaaa response for %q, checking dns64", host)
dns64Resp, u, err := p.exchange(dns64Req, upstreams)
if err != nil {
log.Tracef("Failed to create DNS64 mapped request %s", err)
return nil, u, err
log.Error("proxy: dns64 request failed: %s", err)
return nil
}
return mappedAAAAResponse, u, nil
if dns64Resp != nil && p.synthDNS64(origReq, origResp, dns64Resp) {
log.Debug("dnsforward: synthesized aaaa response for %q", host)
return u
}
return nil
}
......@@ -2,140 +2,365 @@ package proxy
import (
"net"
"net/netip"
"sync"
"testing"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const ipv4OnlyHost = "ipv4only.arpa"
const ipv4OnlyFqdn = "ipv4.only."
// Valid NAT-64 prefix for 2001:67c:27e4:15::64 server.
var testNAT64Prefix = []byte{32, 1, 6, 124, 39, 228, 16, 100, 0, 0, 0, 0}
func TestProxyWithDNS64(t *testing.T) {
// Create test proxy and manually set NAT64 prefix.
func TestDNS64Race(t *testing.T) {
log.SetLevel(log.DEBUG)
dnsProxy := createTestProxy(t, nil)
dnsProxy.SetNAT64Prefix(testNAT64Prefix)
err := dnsProxy.Start()
require.NoError(t, err)
// Let's create test A request to ipv4OnlyHost and exchange it with the
// test proxy.
req := createHostTestMessage(ipv4OnlyHost)
resp, _, err := dnsProxy.exchange(req, dnsProxy.UpstreamConfig.Upstreams)
require.NoError(t, err)
require.Len(t, resp.Answer, 2)
var mappedIPs []net.IP
for _, rr := range resp.Answer {
a, ok := rr.(*dns.A)
require.True(t, ok)
// Let's manually add NAT64 prefix to IPv4 response.
mappedIP := make(net.IP, net.IPv6len)
copy(mappedIP, testNAT64Prefix)
for index, b := range a.A {
mappedIP[NAT64PrefixLength+index] = b
ans := newRR(t, ipv4OnlyFqdn, dns.TypeA, 3600, net.ParseIP("1.2.3.4"))
ups := upstreamFunc(func(req *dns.Msg) (resp *dns.Msg, err error) {
resp = (&dns.Msg{}).SetReply(req)
if req.Question[0].Qtype == dns.TypeA {
resp.Answer = []dns.RR{dns.Copy(ans)}
}
mappedIPs = append(mappedIPs, mappedIP)
}
return resp, nil
})
// Create test context with AAAA request to ipv4OnlyHost and resolve it.
testDNSContext := createTestDNSContext(ipv4OnlyHost)
err = dnsProxy.Resolve(testDNSContext)
require.NoError(t, err)
dnsProxy.UseDNS64 = true
// Valid NAT-64 prefix for 2001:67c:27e4:15::64 server.
dnsProxy.DNS64Prefs = []netip.Prefix{netip.MustParsePrefix("2001:67c:27e4:1064::/96")}
dnsProxy.UpstreamConfig.Upstreams = []upstream.Upstream{ups}
// Response should be AAAA answer.
res := testDNSContext.Res
require.NotNil(t, res)
for _, rr := range res.Answer {
aaaa, ok := rr.(*dns.AAAA)
require.True(t, ok)
// Compare manually mapped IP with IP that was resolved by dnsproxy
// with calculated NAT64 prefix.
found := false
for _, mappedIP := range mappedIPs {
if aaaa.AAAA.Equal(mappedIP) {
found = true
break
}
}
require.NoError(t, dnsProxy.Start())
testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop)
syncCh := make(chan struct{})
// Send requests.
g := &sync.WaitGroup{}
g.Add(testMessagesCount)
addr := dnsProxy.Addr(ProtoTCP).String()
for i := 0; i < testMessagesCount; i++ {
// The [dns.Conn] isn't safe for concurrent use despite the requirements
// from the [net.Conn] documentation.
conn, err := dns.Dial("tcp", addr)
require.NoError(t, err)
require.True(t, found)
go sendTestAAAAMessageAsync(conn, g, ipv4OnlyFqdn, syncCh)
}
err = dnsProxy.Stop()
require.NoError(t, err)
close(syncCh)
g.Wait()
}
func TestDNS64Race(t *testing.T) {
dnsProxy := createTestProxy(t, nil)
dnsProxy.SetNAT64Prefix(testNAT64Prefix)
dnsProxy.UpstreamConfig.Upstreams = append(dnsProxy.UpstreamConfig.Upstreams, dnsProxy.UpstreamConfig.Upstreams[0])
func sendTestAAAAMessageAsync(conn *dns.Conn, g *sync.WaitGroup, fqdn string, syncCh chan struct{}) {
pt := testutil.PanicT{}
// Start listening.
err := dnsProxy.Start()
require.NoError(t, err)
defer g.Done()
// Create a DNS-over-UDP client connection.
addr := dnsProxy.Addr(ProtoUDP)
conn, err := dns.Dial("udp", addr.String())
require.NoError(t, err)
req := (&dns.Msg{}).SetQuestion(fqdn, dns.TypeAAAA)
<-syncCh
sendTestAAAAMessagesAsync(t, conn)
err := conn.WriteMsg(req)
require.NoError(pt, err)
// Stop the proxy.
err = dnsProxy.Stop()
require.NoError(t, err)
res, err := conn.ReadMsg()
require.NoError(pt, err)
require.Equal(pt, res.Rcode, dns.RcodeSuccess)
require.NotEmpty(pt, res.Answer)
require.IsType(pt, &dns.AAAA{}, res.Answer[0])
}
func sendTestAAAAMessagesAsync(t *testing.T, conn *dns.Conn) {
g := &sync.WaitGroup{}
g.Add(testMessagesCount)
// newRR is a helper that creates a new dns.RR with the given name, qtype,
// ttl and value. It fails the test if the qtype is not supported or the type
// of value doesn't match the qtype.
func newRR(t *testing.T, name string, qtype uint16, ttl uint32, val any) (rr dns.RR) {
t.Helper()
for i := 0; i < testMessagesCount; i++ {
go sendTestAAAAMessageAsync(t, conn, g, ipv4OnlyHost)
switch qtype {
case dns.TypeA:
rr = &dns.A{A: testutil.RequireTypeAssert[net.IP](t, val)}
case dns.TypeAAAA:
rr = &dns.AAAA{AAAA: testutil.RequireTypeAssert[net.IP](t, val)}
case dns.TypeCNAME:
rr = &dns.CNAME{Target: testutil.RequireTypeAssert[string](t, val)}
case dns.TypeSOA:
rr = &dns.SOA{
Ns: "ns." + name,
Mbox: "hostmaster." + name,
Serial: 1,
Refresh: 1,
Retry: 1,
Expire: 1,
Minttl: 1,
}
case dns.TypePTR:
rr = &dns.PTR{Ptr: testutil.RequireTypeAssert[string](t, val)}
default:
t.Fatalf("unsupported qtype: %d", qtype)
}
g.Wait()
*rr.Header() = dns.RR_Header{
Name: name,
Rrtype: qtype,
Class: dns.ClassINET,
Ttl: ttl,
}
return rr
}
func sendTestAAAAMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup, host string) {
defer func() {
g.Done()
}()
// upstreamFunc is a helper type that implements the [upstream.Upstream]
// interface.
type upstreamFunc func(req *dns.Msg) (resp *dns.Msg, err error)
req := createAAAATestMessage(host)
err := conn.WriteMsg(req)
// type check
var _ upstream.Upstream = upstreamFunc(nil)
// Exchange implements the [upstream.Upstream] interface for upstreamFunc.
func (u upstreamFunc) Exchange(req *dns.Msg) (resp *dns.Msg, err error) { return u(req) }
// Address implements the [upstream.Upstream] interface for upstreamFunc.
func (u upstreamFunc) Address() (addr string) { return "func.upstream" }
// Close implements the [upstream.Upstream] interface for upstreamFunc.
func (u upstreamFunc) Close() (err error) { return nil }
func TestProxy_Resolve_dns64(t *testing.T) {
const (
ipv6Domain = "ipv6.only."
soaDomain = "ipv4.soa."
mappedDomain = "filterable.ipv6."
anotherDomain = "another.domain."
pointedDomain = "local1234.ipv4."
globDomain = "real1234.ipv4."
)
someIPv4 := net.IP{1, 2, 3, 4}
someIPv6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
mappedIPv6 := net.ParseIP("64:ff9b::102:304")
ptr64Domain, err := netutil.IPToReversedAddr(mappedIPv6)
require.NoError(t, err)
ptr64Domain = dns.Fqdn(ptr64Domain)
res, err := conn.ReadMsg()
ptrGlobDomain, err := netutil.IPToReversedAddr(someIPv4)
require.NoError(t, err)
require.True(t, len(res.Answer) > 0)
ptrGlobDomain = dns.Fqdn(ptrGlobDomain)
_, ok := res.Answer[0].(*dns.AAAA)
require.True(t, ok)
}
cliIP := &net.TCPAddr{
IP: net.IP{192, 168, 1, 1},
Port: 1234,
}
const (
sectionAnswer = iota
sectionAuthority
sectionAdditional
sectionsNum
)
func createAAAATestMessage(host string) *dns.Msg {
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
name := host + "."
req.Question = []dns.Question{
{Name: name, Qtype: dns.TypeAAAA, Qclass: dns.ClassINET},
// answerMap is a convenience alias for describing the upstream response for
// a given question type.
type answerMap = map[uint16][sectionsNum][]dns.RR
pt := testutil.PanicT{}
newUps := func(answers answerMap) (u upstream.Upstream) {
return upstreamFunc(func(req *dns.Msg) (resp *dns.Msg, err error) {
q := req.Question[0]
require.Contains(pt, answers, q.Qtype)
answer := answers[q.Qtype]
resp = (&dns.Msg{}).SetReply(req)
resp.Answer = answer[sectionAnswer]
resp.Ns = answer[sectionAuthority]
resp.Extra = answer[sectionAdditional]
return resp, nil
})
}
return &req
}
func createTestDNSContext(host string) *DNSContext {
d := DNSContext{}
d.Req = createAAAATestMessage(host)
return &d
localRR := newRR(t, ptr64Domain, dns.TypePTR, 3600, pointedDomain)
localUps := upstreamFunc(func(req *dns.Msg) (resp *dns.Msg, err error) {
require.Equal(pt, req.Question[0].Name, ptr64Domain)
resp = (&dns.Msg{}).SetReply(req)
resp.Answer = []dns.RR{localRR}
return resp, nil
})
testCases := []struct {
name string
qname string
upsAns answerMap
wantAns []dns.RR
qtype uint16
}{{
name: "simple_a",
qname: ipv4OnlyFqdn,
upsAns: answerMap{
dns.TypeA: {
sectionAnswer: {newRR(t, ipv4OnlyFqdn, dns.TypeA, 3600, someIPv4)},
},
dns.TypeAAAA: {},
},
wantAns: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: ipv4OnlyFqdn,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 3600,
},
A: someIPv4,
}},
qtype: dns.TypeA,
}, {
name: "simple_aaaa",
qname: ipv6Domain,
upsAns: answerMap{
dns.TypeA: {},
dns.TypeAAAA: {
sectionAnswer: {newRR(t, ipv6Domain, dns.TypeAAAA, 3600, someIPv6)},
},
},
wantAns: []dns.RR{&dns.AAAA{
Hdr: dns.RR_Header{
Name: ipv6Domain,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: 3600,
},
AAAA: someIPv6,
}},
qtype: dns.TypeAAAA,
}, {
name: "actual_dns64",
qname: ipv4OnlyFqdn,
upsAns: answerMap{
dns.TypeA: {
sectionAnswer: {newRR(t, ipv4OnlyFqdn, dns.TypeA, 3600, someIPv4)},
},
dns.TypeAAAA: {},
},
wantAns: []dns.RR{&dns.AAAA{
Hdr: dns.RR_Header{
Name: ipv4OnlyFqdn,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: maxDNS64SynTTL,
},
AAAA: mappedIPv6,
}},
qtype: dns.TypeAAAA,
}, {
name: "actual_dns64_soattl",
qname: soaDomain,
upsAns: answerMap{
dns.TypeA: {
sectionAnswer: {newRR(t, soaDomain, dns.TypeA, 3600, someIPv4)},
},
dns.TypeAAAA: {
sectionAuthority: {newRR(t, soaDomain, dns.TypeSOA, maxDNS64SynTTL+50, nil)},
},
},
wantAns: []dns.RR{&dns.AAAA{
Hdr: dns.RR_Header{
Name: soaDomain,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: maxDNS64SynTTL + 50,
},
AAAA: mappedIPv6,
}},
qtype: dns.TypeAAAA,
}, {
name: "filtered",
qname: mappedDomain,
upsAns: answerMap{
dns.TypeA: {},
dns.TypeAAAA: {
sectionAnswer: {
newRR(t, mappedDomain, dns.TypeAAAA, 3600, net.ParseIP("64:ff9b::506:708")),
newRR(t, mappedDomain, dns.TypeCNAME, 3600, anotherDomain),
},
},
},
wantAns: []dns.RR{&dns.CNAME{
Hdr: dns.RR_Header{
Name: mappedDomain,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 3600,
},
Target: anotherDomain,
}},
qtype: dns.TypeAAAA,
}, {
name: "ptr",
qname: ptr64Domain,
upsAns: nil,
wantAns: []dns.RR{&dns.PTR{
Hdr: dns.RR_Header{
Name: ptr64Domain,
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: 3600,
},
Ptr: pointedDomain,
}},
qtype: dns.TypePTR,
}, {
name: "ptr_glob",
qname: ptrGlobDomain,
upsAns: answerMap{
dns.TypePTR: {
sectionAnswer: {newRR(t, ptrGlobDomain, dns.TypePTR, 3600, globDomain)},
},
},
wantAns: []dns.RR{&dns.PTR{
Hdr: dns.RR_Header{
Name: ptrGlobDomain,
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: 3600,
},
Ptr: globDomain,
}},
qtype: dns.TypePTR,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := createTestProxy(t, nil)
p.Config.UpstreamConfig.Upstreams = []upstream.Upstream{newUps(tc.upsAns)}
p.Config.PrivateRDNSUpstreamConfig.Upstreams = []upstream.Upstream{localUps}
p.Config.UseDNS64 = true
require.NoError(t, p.Start())
testutil.CleanupAndRequireSuccess(t, p.Stop)
req := (&dns.Msg{}).SetQuestion(tc.qname, tc.qtype)
dctx := &DNSContext{
Req: req,
Addr: cliIP,
}
err := p.Resolve(dctx)
require.NoError(t, err)
res := dctx.Res
require.NotNil(t, res)
assert.Equal(t, tc.wantAns, res.Answer)
})
}
}
......@@ -50,14 +50,15 @@ func (p *Proxy) exchange(req *dns.Msg, upstreams []upstream.Upstream) (reply *dn
func (p *Proxy) getSortedUpstreams(u []upstream.Upstream) []upstream.Upstream {
// clone upstreams list to avoid race conditions
p.rttLock.Lock()
clone := make([]upstream.Upstream, len(u))
copy(clone, u)
p.rttLock.Lock()
defer p.rttLock.Unlock()
sort.Slice(clone, func(i, j int) bool {
return p.upstreamRttStats[clone[i].Address()] < p.upstreamRttStats[clone[j].Address()]
})
p.rttLock.Unlock()
return clone
}
......@@ -89,9 +90,10 @@ func exchangeWithUpstream(u upstream.Upstream, req *dns.Msg) (*dns.Msg, int, err
// updateRtt updates rtt in upstreamRttStats for given address
func (p *Proxy) updateRtt(address string, rtt int) {
p.rttLock.Lock()
defer p.rttLock.Unlock()
if p.upstreamRttStats == nil {
p.upstreamRttStats = map[string]int{}
}
p.upstreamRttStats[address] = (p.upstreamRttStats[address] + rtt) / 2
p.rttLock.Unlock()
}
......@@ -7,6 +7,7 @@ import (
"io"
"net"
"net/http"
"net/netip"
"sync"
"sync/atomic"
"time"
......@@ -88,8 +89,10 @@ type Proxy struct {
// DNS64 (in case dnsproxy works in a NAT64/DNS64 network)
// --
nat64Prefix []byte // NAT 64 prefix
nat64PrefixLock sync.Mutex // Prefix lock
// 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
// --
......@@ -134,8 +137,7 @@ type Proxy struct {
Config // proxy configuration
}
// Init populates fields of p but does not start it. Init must be called before
// calling Start.
// Init populates fields of p but does not start listeners.
func (p *Proxy) Init() (err error) {
p.initCache()
......@@ -176,6 +178,11 @@ func (p *Proxy) Init() (err error) {
p.proxyVerifier = netutil.SliceSubnetSet(trusted)
err = p.setupDNS64()
if err != nil {
return fmt.Errorf("setting up DNS64: %w", err)
}
return nil
}
......@@ -370,32 +377,76 @@ func (p *Proxy) Addr(proto Proto) net.Addr {
}
}
// replyFromUpstream tries to resolve the request.
func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) {
req := d.Req
// needsLocalUpstream returns true if the request should be handled by a private
// upstream servers.
func (p *Proxy) needsLocalUpstream(req *dns.Msg) (ok bool) {
if req.Question[0].Qtype != dns.TypePTR {
return false
}
host := req.Question[0].Name
var upstreams []upstream.Upstream
// Get custom upstreams first. Note that they might be empty.
ip, err := netutil.IPFromReversedAddr(host)
if err != nil {
log.Debug("proxy: failed to parse ip from ptr request: %s", err)
return false
}
return p.shouldStripDNS64(ip)
}
// selectUpstreams returns the upstreams to use for the specified host. It
// firstly considers custom upstreams if those aren't empty and then the
// configured ones. It returns false, if no upstreams are available for current
// request.
func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, ok bool) {
host := d.Req.Question[0].Name
if p.needsLocalUpstream(d.Req) {
if p.PrivateRDNSUpstreamConfig == nil {
return nil, false
}
ip, _ := netutil.IPAndPortFromAddr(d.Addr)
// TODO(e.burkov): Detect against the actual configured subnet set.
// Perhaps, even much earlier.
if !netutil.IsLocallyServed(ip) {
return nil, false
}
return p.PrivateRDNSUpstreamConfig.getUpstreamsForDomain(host), true
}
if d.CustomUpstreamConfig != nil {
upstreams = d.CustomUpstreamConfig.getUpstreamsForDomain(host)
}
// If nothing is found in the custom upstreams, start using the default
// ones.
if upstreams == nil {
upstreams = p.UpstreamConfig.getUpstreamsForDomain(host)
if upstreams != nil {
return upstreams, true
}
return p.UpstreamConfig.getUpstreamsForDomain(host), true
}
// replyFromUpstream tries to resolve the request.
func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) {
req := d.Req
upstreams, ok := p.selectUpstreams(d)
if !ok {
return false, upstream.ErrNoUpstreams
}
start := time.Now()
// Perform the DNS request.
var reply *dns.Msg
var u upstream.Upstream
reply, u, err = p.exchange(req, upstreams)
if p.isNAT64PrefixAvailable() && p.isEmptyAAAAResponse(reply, req) {
log.Tracef("received an empty AAAA response, checking DNS64")
reply, u, err = p.checkDNS64(req, reply, upstreams)
if dns64Ups := p.performDNS64(req, reply, upstreams); dns64Ups != nil {
u = dns64Ups
} else if p.isBogusNXDomain(reply) {
log.Tracef("response ip is contained in bogus-nxdomain list")
reply = p.genWithRCode(reply, dns.RcodeNameError)
reply = p.genWithRCode(req, dns.RcodeNameError)
}
log.Tracef("RTT: %s", time.Since(start))
......@@ -419,8 +470,7 @@ func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) {
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3551.
if len(req.Question) > 0 && len(reply.Question) == 0 {
reply.Question = make([]dns.Question, 1)
reply.Question[0] = req.Question[0]
reply.Question = []dns.Question{req.Question[0]}
}
} else {
reply = p.genServerFailure(req)
......@@ -478,8 +528,8 @@ func (p *Proxy) Resolve(dctx *DNSContext) (err error) {
ok, err = p.replyFromUpstream(dctx)
// Don't cache the responses having CD flag, just like Dnsmasq does. It
// prevents the cache from being poisoned with unvalidated answers which may
// differ from validated ones.
// prevents the cache from being poisoned with unvalidated answers which
// may differ from validated ones.
//
// See https://github.com/imp/dnsmasq/blob/770bce967cfc9967273d0acfb3ea018fb7b17522/src/forward.c#L1169-L1172.
if cacheWorks && ok && !dctx.Res.CheckingDisabled {
......@@ -487,7 +537,11 @@ func (p *Proxy) Resolve(dctx *DNSContext) (err error) {
p.cacheResp(dctx)
}
filterMsg(dctx.Res, dctx.Res, dctx.adBit, dctx.doBit, 0)
// It is possible that the response is nil if the upstream hasn't been
// chosen.
if dctx.Res != nil {
filterMsg(dctx.Res, dctx.Res, dctx.adBit, dctx.doBit, 0)
}
// Complete the response.
dctx.scrub()
......
......@@ -1127,6 +1127,8 @@ func createTestProxy(t *testing.T, tlsConfig *tls.Config) *Proxy {
p.UpstreamConfig = &UpstreamConfig{}
p.UpstreamConfig.Upstreams = append(upstreams, dnsUpstream)
p.PrivateRDNSUpstreamConfig = &UpstreamConfig{}
p.TrustedProxies = []string{"0.0.0.0/0", "::0/0"}
return &p
......
......@@ -200,9 +200,9 @@ func (p *Proxy) genNotImpl(request *dns.Msg) (resp *dns.Msg) {
return resp
}
func (p *Proxy) genWithRCode(r *dns.Msg, code int) (resp *dns.Msg) {
func (p *Proxy) genWithRCode(req *dns.Msg, code int) (resp *dns.Msg) {
resp = &dns.Msg{}
resp.SetRcode(r, code)
resp.SetRcode(req, code)
resp.RecursionAvailable = true
return resp
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.