From 1dfc1c886da42e8753176ba7fe9dfabd375cbd0d Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Tue, 26 Mar 2019 12:48:31 +0300 Subject: [PATCH 1/9] [feature] proxy, main: add DNS64/NAT64 support --- main.go | 16 +++ proxy/dns64.go | 262 ++++++++++++++++++++++++++++++++++++++++++++ proxy/dns64_test.go | 125 +++++++++++++++++++++ proxy/proxy.go | 17 ++- 4 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 proxy/dns64.go create mode 100644 proxy/dns64_test.go diff --git a/main.go b/main.go index e913e06e2..b81c00e66 100644 --- a/main.go +++ b/main.go @@ -60,6 +60,9 @@ type Options struct { // DNS upstreams Upstreams []string `short:"u" long:"upstream" description:"An upstream to be used (can be specified multiple times)" required:"true"` + // DNS64 upstream + DNS64Upstreams []string `short:"d" long:"dns64-upstream" description:"DNS64 upstream server to be used (can be specified multiple times)"` + // Fallback DNS resolver Fallbacks []string `short:"f" long:"fallback" description:"Fallback resolvers to use when regular ones are unavailable, can be specified multiple times"` @@ -157,6 +160,19 @@ func createProxyConfig(options Options) proxy.Config { AllServers: options.AllServers, } + if len(options.DNS64Upstreams) != 0 { + dns64Upstreams := []upstream.Upstream{} + for i, u := range options.DNS64Upstreams { + dns64Upstream, err := upstream.AddressToUpstream(u, upstream.Options{Timeout: defaultTimeout}) + if err != nil { + log.Tracef("Failed to create dns64Upstream %s: %s", options.DNS64Upstreams, err) + } + dns64Upstreams = append(dns64Upstreams, dns64Upstream) + log.Printf("System server %d: %s", i, dns64Upstream.Address()) + } + config.DNS64Upstreams = dns64Upstreams + } + if options.Fallbacks != nil { fallbacks := []upstream.Upstream{} for i, f := range options.Fallbacks { diff --git a/proxy/dns64.go b/proxy/dns64.go new file mode 100644 index 000000000..a83bfe3ef --- /dev/null +++ b/proxy/dns64.go @@ -0,0 +1,262 @@ +package proxy + +import ( + "fmt" + "net" + + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" + "github.com/miekg/dns" +) + +// Byte representation of IPv4 addresses we are looking for after NAT64 prefix while dns response parsing +// It's two "well-known IPv4" addresses defined for Pref64::/n +// https://tools.ietf.org/html/rfc7050#section-2.2 +var wellKnownIpv4First = []byte{192, 0, 0, 171} //nolint +var wellKnownIpv4Second = []byte{192, 0, 0, 170} //nolint + +// createIpv4ArpaMessage creates AAAA request for the "Well-Known IPv4-only Name" +// this request should be exchanged with DNS64 upstreams. +func createIpv4ArpaMessage() *dns.Msg { + req := dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: "ipv4only.arpa.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, + } + return &req +} + +// getNAT64PrefixFromResponse parses a response for NAT64 prefix +// valid answer should contains the following AAAA record: +// +// - 16 bytes record +// - first 12 bytes is ipv6 prefix +// - last 4 bytes are required Ipv4: wellKnownIpv4First or wellKnownIpv4Second +// we use simplified algorithm and consider the first matched record to be valid +func getNAT64PrefixFromResponse(r *dns.Msg) ([]byte, error) { + var prefix []byte + for _, reply := range r.Answer { + a, ok := reply.(*dns.AAAA) + if !ok { + log.Tracef("Answer is not AAAA record") + continue + } + ip := a.AAAA + + // Let's separate IPv4 part from NAT64 prefix + ipv4 := ip[12:] + if len(ipv4) != net.IPv4len { + continue + } + + // Compare bytes in IPv4 part to wellKnownIpv4First and wellKnownIpv4Second + valid := true + for i, b := range ipv4 { + // Compare + if b != wellKnownIpv4First[i] && b != wellKnownIpv4Second[i] { + valid = false + break + } + } + + if !valid { + continue + } + + // Set NAT64 prefix and break loop + fmt.Printf("got prefix from response. answer is: %s\n", ip.String()) + prefix = ip[:12] + break + } + + if len(prefix) == 0 { + return nil, fmt.Errorf("no NAT64 prefix in answers") + } + return prefix, nil +} + +// isIpv6ResponseEmpty checks AAAA answer to be empty +// returns true if NAT64 prefix already calculated and there are no answers for AAAA question +func (p *Proxy) isIpv6ResponseEmpty(resp, req *dns.Msg) bool { + return p.isNAT64PrefixAvailable() && req.Question[0].Qtype == dns.TypeAAAA && (resp == nil || len(resp.Answer) == 0) +} + +// isNAT64PrefixAvailable returns true if nat64 prefix was calculated +func (p *Proxy) isNAT64PrefixAvailable() bool { + p.nat64Lock.Lock() + prefixSize := len(p.nat64Prefix) + p.nat64Lock.Unlock() + return prefixSize == 12 +} + +// 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") + } + + 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}, + } + return &req, nil +} + +// nat64Result is a result of NAT64 prefix calculation +type nat64Result struct { + prefix []byte + upstream upstream.Upstream + err error +} + +// getNAT64PrefixAsync sends result of getNAT64PrefixWithUpstream to the channel +func getNAT64PrefixAsync(req *dns.Msg, u upstream.Upstream, ch chan nat64Result) { + ch <- getNAT64PrefixWithUpstream(req, u) +} + +// getNAT64PrefixWithUpstream returns result of NAT64 prefix calculation with one upstream +func getNAT64PrefixWithUpstream(req *dns.Msg, u upstream.Upstream) nat64Result { + resp, err := u.Exchange(req) + if err != nil { + return nat64Result{upstream: u, err: err} + } + + prefix, err := getNAT64PrefixFromResponse(resp) + if err != nil { + return nat64Result{upstream: u, err: err} + } + + return nat64Result{prefix: prefix, upstream: u} +} + +// getNAT64PrefixParallel starts parallel NAT64 prefix calculation with all available upstreams +func (p *Proxy) getNAT64PrefixParallel() nat64Result { + size := len(p.DNS64Upstreams) + req := createIpv4ArpaMessage() + if size == 1 { + return getNAT64PrefixWithUpstream(req, p.DNS64Upstreams[0]) + } + + errs := []error{} + ch := make(chan nat64Result, size) + for _, u := range p.DNS64Upstreams { + go getNAT64PrefixAsync(req, u, ch) + } + + for { + select { + case rep := <-ch: + if rep.err != nil { + errs = append(errs, rep.err) + } + + if len(errs) == size { + return nat64Result{err: errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...)} + } + + if len(rep.prefix) == 12 && rep.err == nil { + return rep + } + } + } +} + +// getNAT64Prefix exchanges ipv4 arpa request with DNS64 upstreams and sets NAT64 prefix to the proxy +func (p *Proxy) getNAT64Prefix() { + // First check if no DNS64 upstreams specified + if len(p.DNS64Upstreams) == 0 { + log.Tracef("no DNS64 upstream specified") + return + } + + // Do nothing if NAT64 prefix was not calculated + if p.isNAT64PrefixAvailable() { + return + } + + res := p.getNAT64PrefixParallel() + if res.err != nil { + log.Tracef("Failed to calculate NAT64 prefix: %s", res.err) + return + } + + log.Tracef("Use %s server NAT64 prefix", res.upstream.Address()) + p.nat64Lock.Lock() + p.nat64Prefix = res.prefix + p.nat64Lock.Unlock() +} + +// createDNS64MappedResponse adds NAT 64 mapped answer to the old message +// res is new A response. req is old AAAA request +func (p *Proxy) createDNS64MappedResponse(res, req *dns.Msg) (*dns.Msg, error) { + // do nothing if prefix is not valid + if !p.isNAT64PrefixAvailable() { + return nil, fmt.Errorf("can not create DNS64 mapped response: NAT64 prefix was not calculated") + } + + // check if there are no answers + if len(res.Answer) == 0 { + return nil, fmt.Errorf("no ipv4 answer") + } + + // add NAT 64 prefix for each ipv4 answer + for _, ans := range res.Answer { + i, ok := ans.(*dns.A) + if !ok { + continue + } + + // new ip address + mappedAddress := make(net.IP, net.IPv6len) + + // add NAT 64 prefix and append ipv4 record + p.nat64Lock.Lock() + copy(mappedAddress, p.nat64Prefix) + p.nat64Lock.Unlock() + for index, b := range i.A { + mappedAddress[12+index] = b + } + + // check if answer length not equals to IPv6len + if len(mappedAddress) != net.IPv6len { + return nil, fmt.Errorf("wrong count of bytes in the answer after DNS64 mapping: %d", len(mappedAddress)) + } + + // create new response and fill it + rr := new(dns.AAAA) + rr.Hdr = dns.RR_Header{Name: res.Question[0].Name, Rrtype: dns.TypeAAAA, Ttl: ans.Header().Ttl, Class: dns.ClassINET} + rr.AAAA = mappedAddress + req.Answer = append(req.Answer, rr) + } + return req, nil +} + +// checkDNS64 is called when there is no answer for AAAA request and NAT64 prefix available. +// this function creates modified A request, exchanges it and returns DNS64 mapped response +func (p *Proxy) checkDNS64(oldReq, oldResp *dns.Msg, upstreams []upstream.Upstream) (*dns.Msg, upstream.Upstream, error) { + // Let's create A request to the same hostname + req, err := createModifiedARequest(oldReq) + if err != nil { + log.Tracef("Failed to create DNS64 mapped request %s", err) + return oldReq, nil, err + } + + // Exchange new A request with selected upstreams + resp, u, err := p.exchange(req, upstreams) + if err != nil { + log.Tracef("Failed to exchange DNS64 request: %s", err) + return oldReq, nil, err + } + + // A response should be mapped with NAT64 prefix + response, err := p.createDNS64MappedResponse(resp, oldResp) + if err != nil { + log.Tracef("Failed to create DNS64 mapped request %s", err) + return oldReq, u, err + } + return response, u, nil +} diff --git a/proxy/dns64_test.go b/proxy/dns64_test.go new file mode 100644 index 000000000..fbe6bbba4 --- /dev/null +++ b/proxy/dns64_test.go @@ -0,0 +1,125 @@ +package proxy + +import ( + "net" + "testing" + "time" + + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/miekg/dns" +) + +const dns64Upstream = "2001:67c:27e4:15::64" +const ipv4OnlyHost = "and.ru" + +// TestNAT64Prefix calculates nat64 prefix +func TestNAT64Prefix(t *testing.T) { + arpa := createIpv4ArpaMessage() + u, err := upstream.AddressToUpstream(dns64Upstream, upstream.Options{Timeout: defaultTimeout}) + if err != nil { + t.Fatalf("Failed to create upstream to %s", dns64Upstream) + } + + resp, err := u.Exchange(arpa) + if err != nil { + t.Fatalf("Can not exchange ipv4Arpa message: %s", err) + } + + prefix, err := getNAT64PrefixFromResponse(resp) + if err != nil { + t.Fatalf("Can not get NAT64 prefix from response") + } + + if l := len(prefix); l != 12 { + t.Fatalf("Wrong prefix length: %d", l) + } +} + +func TestProxyWithDNS64(t *testing.T) { + d := createTestProxy(t, nil) + d.DNS64Upstreams = []upstream.Upstream{} + dns64 := []string{dns64Upstream, "8.8.8.8"} + for _, up := range dns64 { + u, err := upstream.AddressToUpstream(up, upstream.Options{Timeout: time.Second}) + if err != nil { + t.Fatalf("Failed to create upstream to %s", up) + } + + d.DNS64Upstreams = append(d.DNS64Upstreams, u) + } + + err := d.Start() + if err != nil { + t.Fatalf("Failed to start dns proxy") + } + + // Wait for DNS64 upstream timeout. NAT64 prefix should be already calculated + time.Sleep(time.Second) + if !d.isNAT64PrefixAvailable() { + t.Fatalf("Failed to calculate NAT64 prefix") + } + + // Let's create test A request to ipv4OnlyHost and exchange it with test proxy + req := createHostTestMessage(ipv4OnlyHost) + resp, _, err := d.exchange(req, d.DNS64Upstreams) + if err != nil { + t.Fatalf("Can not exchange test message for %s cause: %s", ipv4OnlyHost, err) + } + + a, ok := resp.Answer[0].(*dns.A) + if !ok { + t.Fatalf("Answer for %s is not A record!", ipv4OnlyHost) + } + + // Let's manually add NAT64 prefix to IPv4 response + mappedIP := make(net.IP, net.IPv6len) + copy(mappedIP, d.nat64Prefix) + for index, b := range a.A { + mappedIP[12+index] = b + } + + // Create test context with AAAA request to ipv4OnlyHost and resolve it + testDNSContext := createTestDNSContext(ipv4OnlyHost) + err = d.Resolve(testDNSContext) + if err != nil { + t.Fatalf("Error whilr DNSContext resolve: %s", err) + } + + // Response should be AAAA answer + res := testDNSContext.Res + if res == nil { + t.Fatalf("No response") + } + + ans, ok := res.Answer[0].(*dns.AAAA) + if !ok { + t.Fatalf("Mapped answer for %s is not AAAA record", ipv4OnlyHost) + } + + // Compare manually mapped IP with IP that was resolved by dnsproxy with calculated NAT64 prefix + if !ans.AAAA.Equal(mappedIP) { + t.Fatalf("Manually mapped IP %s not equlas to repsonse %s", mappedIP.String(), ans.AAAA.String()) + } + + err = d.Stop() + if err != nil { + t.Fatalf("Failed to stop dns proxy") + } +} + +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}, + } + return &req +} + +func createTestDNSContext(host string) *DNSContext { + d := DNSContext{} + d.Req = createAAAATestMessage(host) + return &d +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 645184b60..cdf6583ac 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -57,6 +57,9 @@ type Proxy struct { upstreamRttStats map[string]int // Map of upstream addresses and their rtt. Used to sort upstreams "from fast to slow" rttLock sync.Mutex // Synchronizes access to the upstreamRttStats map + nat64Prefix []byte // NAT 64 prefix + nat64Lock sync.Mutex // Prefix lock + ratelimitBuckets *gocache.Cache // where the ratelimiters are stored, per IP ratelimitLock sync.Mutex // Synchronizes access to ratelimitBuckets @@ -84,9 +87,11 @@ type Config struct { CacheEnabled bool // cache status CacheSize int // number of cached elements. Default size: 1000 - Upstreams []upstream.Upstream // list of upstreams - Fallbacks []upstream.Upstream // list of fallback resolvers (which will be used if regular upstream failed to answer) - Handler Handler // custom middleware (optional) + Upstreams []upstream.Upstream // list of upstreams + Fallbacks []upstream.Upstream // list of fallback resolvers (which will be used if regular upstream failed to answer) + DNS64Upstreams []upstream.Upstream // list of DNS 64 upstreams (system servers) + + Handler Handler // custom middleware (optional) DomainsReservedUpstreams map[string][]upstream.Upstream // map of domains and lists of corresponding upstreams } @@ -198,6 +203,8 @@ func (p *Proxy) Start() error { return err } + go p.getNAT64Prefix() + p.started = true return nil } @@ -332,6 +339,10 @@ func (p *Proxy) Resolve(d *DNSContext) error { // execute the DNS request startTime := time.Now() reply, u, err := p.exchange(d.Req, upstreams) + if p.isIpv6ResponseEmpty(reply, d.Req) { + reply, u, err = p.checkDNS64(d.Req, reply, upstreams) + } + rtt := int(time.Since(startTime) / time.Millisecond) log.Tracef("RTT: %d ms", rtt) From b54d829d6f518349fe36aaacbf5680e992d9b2b2 Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Wed, 27 Mar 2019 19:33:44 +0300 Subject: [PATCH 2/9] [change] proxy, mobile, readme: add DNS64Upstreams to mobile api, fix issues from review --- README.md | 33 ++++++++-------- main.go | 4 +- mobile/mobile.go | 36 ++++++++++++++---- proxy/dns64.go | 55 +++++++++++---------------- proxy/dns64_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 154 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index a8d9fa591..f533d6fb9 100644 --- a/README.md +++ b/README.md @@ -25,22 +25,23 @@ Usage: dnsproxy [OPTIONS] Application Options: - -v, --verbose Verbose output (optional) - -o, --output= Path to the log file. If not set, write to stdout. - -l, --listen= Listen address (default: 0.0.0.0) - -p, --port= Listen port. Zero value disables TCP and UDP listeners (default: 53) - -h, --https-port= Listen port for DNS-over-HTTPS (default: 0) - -t, --tls-port= Listen port for DNS-over-TLS (default: 0) - -c, --tls-crt= Path to a file with the certificate chain - -k, --tls-key= Path to a file with the private key - -b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53) - -r, --ratelimit= Ratelimit (requests per second) (default: 0) - -z, --cache If specified, DNS cache is enabled - -e --cache-size= Maximum number of elements in the cache. Default size: 1000 - -a, --refuse-any If specified, refuse ANY requests - -u, --upstream= An upstream to be used (can be specified multiple times) - -f, --fallback= Fallback resolvers to use when regular ones are unavailable, can be specified multiple times - -s, --all-servers Use parallel queries to speed up resolving by querying all upstream servers simultaneously + -v, --verbose Verbose output (optional) + -o, --output= Path to the log file. If not set, write to stdout. + -l, --listen= Listen address (default: 0.0.0.0) + -p, --port= Listen port. Zero value disables TCP and UDP listeners (default: 53) + -h, --https-port= Listen port for DNS-over-HTTPS (default: 0) + -t, --tls-port= Listen port for DNS-over-TLS (default: 0) + -c, --tls-crt= Path to a file with the certificate chain + -k, --tls-key= Path to a file with the private key + -b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53) + -r, --ratelimit= Ratelimit (requests per second) (default: 0) + -z, --cache If specified, DNS cache is enabled + -e --cache-size= Maximum number of elements in the cache. Default size: 1000 + -a, --refuse-any If specified, refuse ANY requests + -u, --upstream= An upstream to be used (can be specified multiple times) + -d, --dns64-upstream= DNS64 upstream server which prefix will be used in ipv6-only networks (can be specified multiple times) + -f, --fallback= Fallback resolvers to use when regular ones are unavailable, can be specified multiple times + -s, --all-servers Use parallel queries to speed up resolving by querying all upstream servers simultaneously Help Options: -h, --help Show this help message diff --git a/main.go b/main.go index b81c00e66..722b8ecb2 100644 --- a/main.go +++ b/main.go @@ -61,7 +61,7 @@ type Options struct { Upstreams []string `short:"u" long:"upstream" description:"An upstream to be used (can be specified multiple times)" required:"true"` // DNS64 upstream - DNS64Upstreams []string `short:"d" long:"dns64-upstream" description:"DNS64 upstream server to be used (can be specified multiple times)"` + DNS64Upstreams []string `short:"d" long:"dns64-upstream" description:"DNS64 upstream server which prefix will be used in ipv6-only networks (can be specified multiple times)"` // Fallback DNS resolver Fallbacks []string `short:"f" long:"fallback" description:"Fallback resolvers to use when regular ones are unavailable, can be specified multiple times"` @@ -168,7 +168,7 @@ func createProxyConfig(options Options) proxy.Config { log.Tracef("Failed to create dns64Upstream %s: %s", options.DNS64Upstreams, err) } dns64Upstreams = append(dns64Upstreams, dns64Upstream) - log.Printf("System server %d: %s", i, dns64Upstream.Address()) + log.Printf("DNS64 Upstream %d: %s", i, dns64Upstream.Address()) } config.DNS64Upstreams = dns64Upstreams } diff --git a/mobile/mobile.go b/mobile/mobile.go index 9aa292a95..134a00244 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -27,14 +27,15 @@ type DNSProxy struct { // Config is the DNS proxy configuration which uses only the subset of types that is supported by gomobile // In Java API this structure becomes an object that needs to be configured and setted as field of DNSProxy type Config struct { - ListenAddr string // IP address to listen to - ListenPort int // Port to listen to - BootstrapDNS string // A list of bootstrap DNS (i.e. 8.8.8.8:53 each on a new line) - Fallbacks string // A list of fallback resolvers that will be used if the main one is not available (i.e. 1.1.1.1:53 each on a new line) - Upstreams string // A list of upstream resolvers (each on a new line) - Timeout int // Default timeout for all resolvers (milliseconds) - CacheSize int // Maximum number of elements in the cache. Default size: 1000 - AllServers bool // If true, parallel queries to all configured upstream servers are enabled + ListenAddr string // IP address to listen to + ListenPort int // Port to listen to + BootstrapDNS string // A list of bootstrap DNS (i.e. 8.8.8.8:53 each on a new line) + Fallbacks string // A list of fallback resolvers that will be used if the main one is not available (i.e. 1.1.1.1:53 each on a new line) + Upstreams string // A list of upstream resolvers (each on a new line) + DNS64Upstream string // A list of DNS64 upstreams for ipv6-only network (each on new line) + Timeout int // Default timeout for all resolvers (milliseconds) + CacheSize int // Maximum number of elements in the cache. Default size: 1000 + AllServers bool // If true, parallel queries to all configured upstream servers are enabled } // Start starts the DNS proxy @@ -136,6 +137,25 @@ func createConfig(config *Config) (*proxy.Config, error) { CacheSize: config.CacheSize, } + if config.DNS64Upstream != "" { + dns64Upstreams := []upstream.Upstream{} + lines = strings.Split(config.DNS64Upstream, "\n") + for i, line := range lines { + if line == "" { + continue + } + + dns64Upstream, err := upstream.AddressToUpstream(line, upstream.Options{Timeout: timeout}) + if err != nil { + return nil, fmt.Errorf("cannot parse the DNS64 upstream %s : %s", line, err) + } + + log.Printf("DNS64 Upstream %d: %s", i, dns64Upstream.Address()) + dns64Upstreams = append(dns64Upstreams, dns64Upstream) + } + proxyConfig.DNS64Upstreams = dns64Upstreams + } + if config.Fallbacks != "" { fallbacks := []upstream.Upstream{} lines = strings.Split(config.Fallbacks, "\n") diff --git a/proxy/dns64.go b/proxy/dns64.go index a83bfe3ef..a8a85cfe6 100644 --- a/proxy/dns64.go +++ b/proxy/dns64.go @@ -13,8 +13,8 @@ import ( // Byte representation of IPv4 addresses we are looking for after NAT64 prefix while dns response parsing // It's two "well-known IPv4" addresses defined for Pref64::/n // https://tools.ietf.org/html/rfc7050#section-2.2 -var wellKnownIpv4First = []byte{192, 0, 0, 171} //nolint -var wellKnownIpv4Second = []byte{192, 0, 0, 170} //nolint +var wellKnownIPv4First = []byte{192, 0, 0, 171} //nolint +var wellKnownIPv4Second = []byte{192, 0, 0, 170} //nolint // createIpv4ArpaMessage creates AAAA request for the "Well-Known IPv4-only Name" // this request should be exchanged with DNS64 upstreams. @@ -32,8 +32,8 @@ func createIpv4ArpaMessage() *dns.Msg { // valid answer should contains the following AAAA record: // // - 16 bytes record -// - first 12 bytes is ipv6 prefix -// - last 4 bytes are required Ipv4: wellKnownIpv4First or wellKnownIpv4Second +// - first 12 bytes is NAT64 prefix +// - last 4 bytes are required IPv4: wellKnownIpv4First or wellKnownIpv4Second // we use simplified algorithm and consider the first matched record to be valid func getNAT64PrefixFromResponse(r *dns.Msg) ([]byte, error) { var prefix []byte @@ -51,22 +51,13 @@ func getNAT64PrefixFromResponse(r *dns.Msg) ([]byte, error) { continue } - // Compare bytes in IPv4 part to wellKnownIpv4First and wellKnownIpv4Second - valid := true - for i, b := range ipv4 { - // Compare - if b != wellKnownIpv4First[i] && b != wellKnownIpv4Second[i] { - valid = false - break - } - } - - if !valid { + // Compare IPv4 part to wellKnownIPv4First and wellKnownIPv4Second + if !ipv4.Equal(wellKnownIPv4First) && !ipv4.Equal(wellKnownIPv4Second) { continue } - // Set NAT64 prefix and break loop - fmt.Printf("got prefix from response. answer is: %s\n", ip.String()) + // Set NAT64 prefix and break the loop + log.Tracef("NAT64 prefix was obtained from response. Answer is: %s", ip.String()) prefix = ip[:12] break } @@ -83,7 +74,7 @@ func (p *Proxy) isIpv6ResponseEmpty(resp, req *dns.Msg) bool { return p.isNAT64PrefixAvailable() && req.Question[0].Qtype == dns.TypeAAAA && (resp == nil || len(resp.Answer) == 0) } -// isNAT64PrefixAvailable returns true if nat64 prefix was calculated +// isNAT64PrefixAvailable returns true if NAT64 prefix was calculated func (p *Proxy) isNAT64PrefixAvailable() bool { p.nat64Lock.Lock() prefixSize := len(p.nat64Prefix) @@ -122,12 +113,12 @@ func getNAT64PrefixAsync(req *dns.Msg, u upstream.Upstream, ch chan nat64Result) func getNAT64PrefixWithUpstream(req *dns.Msg, u upstream.Upstream) nat64Result { resp, err := u.Exchange(req) if err != nil { - return nat64Result{upstream: u, err: err} + return nat64Result{err: err} } prefix, err := getNAT64PrefixFromResponse(resp) if err != nil { - return nat64Result{upstream: u, err: err} + return nat64Result{err: err} } return nat64Result{prefix: prefix, upstream: u} @@ -152,13 +143,10 @@ func (p *Proxy) getNAT64PrefixParallel() nat64Result { case rep := <-ch: if rep.err != nil { errs = append(errs, rep.err) - } - - if len(errs) == size { - return nat64Result{err: errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...)} - } - - if len(rep.prefix) == 12 && rep.err == nil { + if len(errs) == size { + return nat64Result{err: errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...)} + } + } else { return rep } } @@ -203,6 +191,12 @@ func (p *Proxy) createDNS64MappedResponse(res, req *dns.Msg) (*dns.Msg, error) { return nil, fmt.Errorf("no ipv4 answer") } + // this bug occurs only ones + if req == nil { + return nil, fmt.Errorf("request is nil") + } + + req.Answer = []dns.RR{} // add NAT 64 prefix for each ipv4 answer for _, ans := range res.Answer { i, ok := ans.(*dns.A) @@ -214,18 +208,11 @@ func (p *Proxy) createDNS64MappedResponse(res, req *dns.Msg) (*dns.Msg, error) { mappedAddress := make(net.IP, net.IPv6len) // add NAT 64 prefix and append ipv4 record - p.nat64Lock.Lock() copy(mappedAddress, p.nat64Prefix) - p.nat64Lock.Unlock() for index, b := range i.A { mappedAddress[12+index] = b } - // check if answer length not equals to IPv6len - if len(mappedAddress) != net.IPv6len { - return nil, fmt.Errorf("wrong count of bytes in the answer after DNS64 mapping: %d", len(mappedAddress)) - } - // create new response and fill it rr := new(dns.AAAA) rr.Hdr = dns.RR_Header{Name: res.Question[0].Name, Rrtype: dns.TypeAAAA, Ttl: ans.Header().Ttl, Class: dns.ClassINET} diff --git a/proxy/dns64_test.go b/proxy/dns64_test.go index fbe6bbba4..eb825d72f 100644 --- a/proxy/dns64_test.go +++ b/proxy/dns64_test.go @@ -2,6 +2,7 @@ package proxy import ( "net" + "sync" "testing" "time" @@ -11,11 +12,12 @@ import ( const dns64Upstream = "2001:67c:27e4:15::64" const ipv4OnlyHost = "and.ru" +const dns64Timeout = time.Second // TestNAT64Prefix calculates nat64 prefix func TestNAT64Prefix(t *testing.T) { arpa := createIpv4ArpaMessage() - u, err := upstream.AddressToUpstream(dns64Upstream, upstream.Options{Timeout: defaultTimeout}) + u, err := upstream.AddressToUpstream(dns64Upstream, upstream.Options{Timeout: dns64Timeout}) if err != nil { t.Fatalf("Failed to create upstream to %s", dns64Upstream) } @@ -38,9 +40,9 @@ func TestNAT64Prefix(t *testing.T) { func TestProxyWithDNS64(t *testing.T) { d := createTestProxy(t, nil) d.DNS64Upstreams = []upstream.Upstream{} - dns64 := []string{dns64Upstream, "8.8.8.8"} + dns64 := []string{dns64Upstream} for _, up := range dns64 { - u, err := upstream.AddressToUpstream(up, upstream.Options{Timeout: time.Second}) + u, err := upstream.AddressToUpstream(up, upstream.Options{Timeout: dns64Timeout}) if err != nil { t.Fatalf("Failed to create upstream to %s", up) } @@ -54,7 +56,7 @@ func TestProxyWithDNS64(t *testing.T) { } // Wait for DNS64 upstream timeout. NAT64 prefix should be already calculated - time.Sleep(time.Second) + time.Sleep(dns64Timeout) if !d.isNAT64PrefixAvailable() { t.Fatalf("Failed to calculate NAT64 prefix") } @@ -93,7 +95,7 @@ func TestProxyWithDNS64(t *testing.T) { ans, ok := res.Answer[0].(*dns.AAAA) if !ok { - t.Fatalf("Mapped answer for %s is not AAAA record", ipv4OnlyHost) + t.Fatalf("Answer for %s is not AAAA record", ipv4OnlyHost) } // Compare manually mapped IP with IP that was resolved by dnsproxy with calculated NAT64 prefix @@ -107,6 +109,85 @@ func TestProxyWithDNS64(t *testing.T) { } } +func TestDNS64Race(t *testing.T) { + dnsProxy := createTestProxy(t, nil) + + // Add multiple DNS64 upstreams + dnsProxy.DNS64Upstreams = []upstream.Upstream{} + dns64 := []string{dns64Upstream, dns64Upstream, upstreamAddr, upstreamAddr} + for _, up := range dns64 { + u, err := upstream.AddressToUpstream(up, upstream.Options{Timeout: time.Second}) + if err != nil { + t.Fatalf("Failed to create upstream to %s", up) + } + + dnsProxy.DNS64Upstreams = append(dnsProxy.DNS64Upstreams, u) + } + + dnsProxy.Upstreams = append(dnsProxy.Upstreams, dnsProxy.Upstreams[0]) + + // Start listening + err := dnsProxy.Start() + if err != nil { + t.Fatalf("cannot start the DNS proxy: %s", err) + } + + // Wait for NAT64 prefix calculation + time.Sleep(dns64Timeout) + + // Create a DNS-over-UDP client connection + addr := dnsProxy.Addr(ProtoUDP) + conn, err := dns.Dial("udp", addr.String()) + if err != nil { + t.Fatalf("cannot connect to the proxy: %s", err) + } + + sendTestAAAAMessagesAsync(t, conn) + + // Stop the proxy + err = dnsProxy.Stop() + if err != nil { + t.Fatalf("cannot stop the DNS proxy: %s", err) + } +} + +func sendTestAAAAMessagesAsync(t *testing.T, conn *dns.Conn) { + g := &sync.WaitGroup{} + g.Add(testMessagesCount) + + for i := 0; i < testMessagesCount; i++ { + go sendTestAAAAMessageAsync(t, conn, g, ipv4OnlyHost) + } + + g.Wait() +} + +func sendTestAAAAMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup, host string) { + defer func() { + g.Done() + }() + + req := createAAAATestMessage(host) + err := conn.WriteMsg(req) + if err != nil { + t.Fatalf("cannot write message: %s", err) + } + + res, err := conn.ReadMsg() + if err != nil { + t.Fatalf("cannot read response to message: %s", err) + } + + if len(res.Answer) == 0 { + t.Fatalf("No answers!") + } + + _, ok := res.Answer[0].(*dns.AAAA) + if !ok { + t.Fatalf("Answer for %s is not AAAA record!", host) + } +} + func createAAAATestMessage(host string) *dns.Msg { req := dns.Msg{} req.Id = dns.Id() From 5f08e4bfe65186ebcfa5f0c3f0ed80946cec48fb Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Fri, 29 Mar 2019 18:49:23 +0300 Subject: [PATCH 3/9] [change] proxy, mobile: move NAT64 prefix calculation to mobile --- README.md | 33 +++--- main.go | 16 --- mobile/mobile.go | 244 ++++++++++++++++++++++++++++++++++++++---- mobile/mobile_test.go | 138 +++++++++++++++++++----- proxy/dns64.go | 159 ++++----------------------- proxy/dns64_test.go | 77 +++---------- proxy/proxy.go | 10 +- 7 files changed, 387 insertions(+), 290 deletions(-) diff --git a/README.md b/README.md index f533d6fb9..a8d9fa591 100644 --- a/README.md +++ b/README.md @@ -25,23 +25,22 @@ Usage: dnsproxy [OPTIONS] Application Options: - -v, --verbose Verbose output (optional) - -o, --output= Path to the log file. If not set, write to stdout. - -l, --listen= Listen address (default: 0.0.0.0) - -p, --port= Listen port. Zero value disables TCP and UDP listeners (default: 53) - -h, --https-port= Listen port for DNS-over-HTTPS (default: 0) - -t, --tls-port= Listen port for DNS-over-TLS (default: 0) - -c, --tls-crt= Path to a file with the certificate chain - -k, --tls-key= Path to a file with the private key - -b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53) - -r, --ratelimit= Ratelimit (requests per second) (default: 0) - -z, --cache If specified, DNS cache is enabled - -e --cache-size= Maximum number of elements in the cache. Default size: 1000 - -a, --refuse-any If specified, refuse ANY requests - -u, --upstream= An upstream to be used (can be specified multiple times) - -d, --dns64-upstream= DNS64 upstream server which prefix will be used in ipv6-only networks (can be specified multiple times) - -f, --fallback= Fallback resolvers to use when regular ones are unavailable, can be specified multiple times - -s, --all-servers Use parallel queries to speed up resolving by querying all upstream servers simultaneously + -v, --verbose Verbose output (optional) + -o, --output= Path to the log file. If not set, write to stdout. + -l, --listen= Listen address (default: 0.0.0.0) + -p, --port= Listen port. Zero value disables TCP and UDP listeners (default: 53) + -h, --https-port= Listen port for DNS-over-HTTPS (default: 0) + -t, --tls-port= Listen port for DNS-over-TLS (default: 0) + -c, --tls-crt= Path to a file with the certificate chain + -k, --tls-key= Path to a file with the private key + -b, --bootstrap= Bootstrap DNS for DoH and DoT, can be specified multiple times (default: 8.8.8.8:53) + -r, --ratelimit= Ratelimit (requests per second) (default: 0) + -z, --cache If specified, DNS cache is enabled + -e --cache-size= Maximum number of elements in the cache. Default size: 1000 + -a, --refuse-any If specified, refuse ANY requests + -u, --upstream= An upstream to be used (can be specified multiple times) + -f, --fallback= Fallback resolvers to use when regular ones are unavailable, can be specified multiple times + -s, --all-servers Use parallel queries to speed up resolving by querying all upstream servers simultaneously Help Options: -h, --help Show this help message diff --git a/main.go b/main.go index 722b8ecb2..e913e06e2 100644 --- a/main.go +++ b/main.go @@ -60,9 +60,6 @@ type Options struct { // DNS upstreams Upstreams []string `short:"u" long:"upstream" description:"An upstream to be used (can be specified multiple times)" required:"true"` - // DNS64 upstream - DNS64Upstreams []string `short:"d" long:"dns64-upstream" description:"DNS64 upstream server which prefix will be used in ipv6-only networks (can be specified multiple times)"` - // Fallback DNS resolver Fallbacks []string `short:"f" long:"fallback" description:"Fallback resolvers to use when regular ones are unavailable, can be specified multiple times"` @@ -160,19 +157,6 @@ func createProxyConfig(options Options) proxy.Config { AllServers: options.AllServers, } - if len(options.DNS64Upstreams) != 0 { - dns64Upstreams := []upstream.Upstream{} - for i, u := range options.DNS64Upstreams { - dns64Upstream, err := upstream.AddressToUpstream(u, upstream.Options{Timeout: defaultTimeout}) - if err != nil { - log.Tracef("Failed to create dns64Upstream %s: %s", options.DNS64Upstreams, err) - } - dns64Upstreams = append(dns64Upstreams, dns64Upstream) - log.Printf("DNS64 Upstream %d: %s", i, dns64Upstream.Address()) - } - config.DNS64Upstreams = dns64Upstreams - } - if options.Fallbacks != nil { fallbacks := []upstream.Upstream{} for i, f := range options.Fallbacks { diff --git a/mobile/mobile.go b/mobile/mobile.go index 134a00244..448bd6e12 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net" + "os" "strings" "sync" "time" @@ -14,8 +15,18 @@ import ( "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" + "github.com/miekg/dns" ) +// Byte representation of IPv4 addresses we are looking for after NAT64 prefix while dns response parsing +// It's two "well-known IPv4" addresses defined for Pref64::/n +// https://tools.ietf.org/html/rfc7050#section-2.2 +var wellKnownIPv4First = []byte{192, 0, 0, 171} //nolint +var wellKnownIPv4Second = []byte{192, 0, 0, 170} //nolint + +const resolverTimeout = 5 * time.Second + // DNSProxy represents a proxy with it's configuration type DNSProxy struct { Config *Config // Proxy configuration @@ -41,18 +52,26 @@ type Config struct { // Start starts the DNS proxy func (d *DNSProxy) Start() error { d.Lock() - defer d.Unlock() if d.dnsProxy != nil { + d.Unlock() return errors.New("DNS proxy is already started") } c, err := createConfig(d.Config) if err != nil { + d.Unlock() return fmt.Errorf("cannot start the DNS proxy: %s", err) } d.dnsProxy = &proxy.Proxy{Config: *c} + // defer called here 'cause otherwise d.dnsProxy may be null + defer func() { + log.Tracef("CALL DEFER!") + d.Unlock() + go calculateNAT64Prefix(d.dnsProxy, d.Config.DNS64Upstream) + }() + // Start the proxy return d.dnsProxy.Start() } @@ -137,25 +156,6 @@ func createConfig(config *Config) (*proxy.Config, error) { CacheSize: config.CacheSize, } - if config.DNS64Upstream != "" { - dns64Upstreams := []upstream.Upstream{} - lines = strings.Split(config.DNS64Upstream, "\n") - for i, line := range lines { - if line == "" { - continue - } - - dns64Upstream, err := upstream.AddressToUpstream(line, upstream.Options{Timeout: timeout}) - if err != nil { - return nil, fmt.Errorf("cannot parse the DNS64 upstream %s : %s", line, err) - } - - log.Printf("DNS64 Upstream %d: %s", i, dns64Upstream.Address()) - dns64Upstreams = append(dns64Upstreams, dns64Upstream) - } - proxyConfig.DNS64Upstreams = dns64Upstreams - } - if config.Fallbacks != "" { fallbacks := []upstream.Upstream{} lines = strings.Split(config.Fallbacks, "\n") @@ -177,3 +177,207 @@ func createConfig(config *Config) (*proxy.Config, error) { return &proxyConfig, nil } + +// getImportantError looks for errors that may occurs on network change: network is unreachable or client timeout +// if errs contains one of this errors we should try to exchange ipv4only.arpa again +func getImportantError(errs []error) error { + for _, err := range errs { + // Timeout + if os.IsTimeout(err) { + return nil + } + + // Let's put out error syscall + if e, ok := err.(*net.OpError); ok { + if er, ok := e.Err.(*os.SyscallError); ok { + // No connection, let,s try again + if er.Syscall == "connect" { + return nil + } + } + } + } + + // No important errors in errs slice + return errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...) +} + +// validateIPv6Addresses returns only valid ipv6 addresses +func validateIPv6Addresses(dns64 string) []string { + addresses := []string{} + lines := strings.Split(dns64, "\n") + for _, address := range lines { + if address == "" { + continue + } + + // DNS64 upstream is just a plain DNS host:port + // First let's check port + _, _, err := net.SplitHostPort(address) + if err != nil { + // Doesn't have port, add default one + address = net.JoinHostPort(address, "53") + } + + // Separate ip from port. It should be IPv6 address + host, _, err := net.SplitHostPort(address) + if err != nil { + continue + } + + // ParseIP func may return IPv6 address with zero 12-bytes prefix + ip := net.ParseIP(host) + if len(ip) != net.IPv6len || ip.To4() != nil { + continue + } + + // Add address to slice after validation + addresses = append(addresses, address) + } + + return addresses +} + +// calculateNAT64Prefix should be called inside the goroutine. +// This func validates dns64 addresses and starts ticker for prefix calculation +// Each tryout starts after resolverTimeout. If getNAT64PrefixParallel returns an error it breaks the loop +// It also breaks the loop and set prefix to proxy after successfully calculation +func calculateNAT64Prefix(p *proxy.Proxy, dns64 string) { + addresses := validateIPv6Addresses(dns64) + if len(addresses) == 0 { + log.Tracef("no dns64 upstreams specified") + return + } + + count := 1 + var prefix []byte + ticker := time.NewTicker(resolverTimeout) + for range ticker.C { + log.Tracef("%d tryout of NAT64 prefix calculation", count) + res := getNAT64PrefixParallel(addresses) + + if res.err != nil { + log.Tracef("Failed to lookup for ipv4only.arpa: %s", res.err) + break + } + + // Non-zero prefix. Break the loop + if res.prefix != nil { + prefix = res.prefix + break + } + + // Five tryouts + if count == 5 { + break + } + count++ + } + + if len(prefix) != 12 { + log.Tracef("Failed to calculate NAT64 prefix") + } + + p.SetNAT64Prefix(prefix) +} + +// createIpv4ArpaMessage creates AAAA request for the "Well-Known IPv4-only Name" +// this request should be exchanged with DNS64 upstreams. +func createIpv4ArpaMessage() *dns.Msg { + req := dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: "ipv4only.arpa.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, + } + return &req +} + +// getNAT64PrefixFromResponse parses a response for NAT64 prefix +// valid answer should contains the following AAAA record: +// +// - 16 bytes record +// - first 12 bytes is NAT64 prefix +// - last 4 bytes are required IPv4: wellKnownIpv4First or wellKnownIpv4Second +// we use simplified algorithm and consider the first matched record to be valid +func getNAT64PrefixFromDNSResponse(r *dns.Msg) ([]byte, error) { + var prefix []byte + for _, reply := range r.Answer { + a, ok := reply.(*dns.AAAA) + if !ok { + log.Tracef("Answer is not AAAA record") + continue + } + ip := a.AAAA + + // Let's separate IPv4 part from NAT64 prefix + ipv4 := ip[12:] + if len(ipv4) != net.IPv4len { + continue + } + + // Compare IPv4 part to wellKnownIPv4First and wellKnownIPv4Second + if !ipv4.Equal(wellKnownIPv4First) && !ipv4.Equal(wellKnownIPv4Second) { + continue + } + + // Set NAT64 prefix and break the loop + log.Tracef("NAT64 prefix was obtained from response. Answer is: %s", ip.String()) + prefix = ip[:12] + break + } + + if len(prefix) == 0 { + return nil, fmt.Errorf("no NAT64 prefix in answers") + } + + return prefix, nil +} + +// nat64Result is a result of NAT64 prefix calculation +type nat64Result struct { + prefix []byte + err error +} + +// getNAT64PrefixParallel starts parallel NAT64 prefix calculation with all available dns64 upstreams +func getNAT64PrefixParallel(dns64 []string) nat64Result { + ch := make(chan nat64Result, len(dns64)) + for _, d := range dns64 { + go getNAT64PrefixAsync(d, ch) + } + + errs := []error{} + for { + select { + case rep := <-ch: + if rep.err != nil { + errs = append(errs, rep.err) + if len(errs) == len(dns64) { + return nat64Result{err: getImportantError(errs)} + } + } else { + return rep + } + } + } +} + +// getNAT64PrefixWithClient sends ipv4only.arpa AAAA request to dns64 address via dns.Client +// In case of successfully exchange it returns result of getNAT64PrefixFromDNSResponse +func getNAT64PrefixWithClient(dns64 string) nat64Result { + req := createIpv4ArpaMessage() + tcpClient := dns.Client{Net: "tcp", Timeout: resolverTimeout} + reply, _, tcpErr := tcpClient.Exchange(req, dns64) + if tcpErr != nil { + return nat64Result{err: tcpErr} + } + + prefix, err := getNAT64PrefixFromDNSResponse(reply) + return nat64Result{prefix, err} +} + +// getNAT64PrefixAsync sends result of getNAT64PrefixWithClient into the channel +func getNAT64PrefixAsync(dns64 string, ch chan nat64Result) { + ch <- getNAT64PrefixWithClient(dns64) +} diff --git a/mobile/mobile_test.go b/mobile/mobile_test.go index 9b56b1d80..f02e0a729 100644 --- a/mobile/mobile_test.go +++ b/mobile/mobile_test.go @@ -4,28 +4,13 @@ import ( "net" "strings" "testing" + "time" "github.com/miekg/dns" ) func TestMobileApi(t *testing.T) { - upstreams := []string{ - "tls://dns.adguard.com", - "https://dns.adguard.com/dns-query", - // AdGuard DNS (DNSCrypt) - "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", - } - upstreamsStr := strings.Join(upstreams, "\n") - - config := &Config{ - ListenAddr: "127.0.0.1", - ListenPort: 0, // Specify 0 to start listening on a random free port - BootstrapDNS: "8.8.8.8:53\n1.1.1.1:53", - Fallbacks: "8.8.8.8:53\n1.1.1.1:53", - Timeout: 5000, - Upstreams: upstreamsStr, - } - + config := createTestConfig() proxy := DNSProxy{Config: config} err := proxy.Start() if err != nil { @@ -37,15 +22,9 @@ func TestMobileApi(t *testing.T) { // // Create a test DNS message - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } - + req := createTestMessage("google-public-dns-a.google.com.", dns.TypeA) addr := proxy.Addr() - reply, err := dns.Exchange(&req, addr) + reply, err := dns.Exchange(req, addr) if err != nil { t.Fatalf("Couldn't talk to upstream %s: %s", addr, err) } @@ -65,3 +44,112 @@ func TestMobileApi(t *testing.T) { t.Fatalf("cannot stop the mobile proxy: %s", err) } } + +func TestMobileApiDNS64(t *testing.T) { + config := createTestConfig() + config.DNS64Upstream = "2001:67c:27e4:15::64" + proxy := DNSProxy{Config: config} + err := proxy.Start() + if err != nil { + t.Fatalf("cannot start the mobile proxy: %s", err) + } + + // Wait for NAT64 prefix calculation + time.Sleep(6 * time.Second) + + // + // Test that it resolves IPv4 only host with AAAA request type + // + + // Create a test DNS message + req := createTestMessage("and.ru.", dns.TypeAAAA) + addr := proxy.Addr() + reply, err := dns.Exchange(req, addr) + if err != nil { + t.Fatalf("Couldn't talk to upstream %s: %s", addr, err) + } + if len(reply.Answer) != 1 { + t.Fatalf("DNS upstream %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) + } + + if len(reply.Answer) == 0 { + t.Fatalf("No answers") + } + + if _, ok := reply.Answer[0].(*dns.AAAA); !ok { + t.Fatalf("DNS upstream %s returned wrong answer type instead of AAAA: %v", addr, reply.Answer[0]) + } + + err = proxy.Stop() + if err != nil { + t.Fatalf("cannot stop the mobile proxy: %s", err) + } +} + +func TestDNS64AddressValidation(t *testing.T) { + dns64 := "1.1.1.1\n1.1.1.1:53\nhttps://dns.adguard.com\n[2001:67c:27e4:15::64]:53\n2001:67c:27e4:15::64" + addresses := validateIPv6Addresses(dns64) + if len(addresses) != 2 { + t.Fatalf("Wrong count of addresses: %d", len(addresses)) + } + if addresses[0] != addresses[1] { + t.Fatalf("Wrong addresses. Expected: [2001:67c:27e4:15::64]:53, actual: %s, %s", addresses[0], addresses[1]) + } + +} + +func TestExchangeWithClient(t *testing.T) { + res := getNAT64PrefixWithClient("1.1.1.1:53") + if res.err == nil { + t.Fatalf("1.1.1.1:53 is not DNS64 server") + } + + res = getNAT64PrefixWithClient("[2001:67c:27e4:15::64]:53") + if res.err != nil { + t.Fatalf("Error while ipv4only.arpa exchange: %s", res.err) + } + + if len(res.prefix) != 12 { + t.Fatalf("Wrong prefix format: %v", res.prefix) + } +} + +func TestParallelExchange(t *testing.T) { + dns64 := []string{"1.1.1.1:53", "[2001:67c:27e4:15::64]:53", "8.8.8.8"} + res := getNAT64PrefixParallel(dns64) + if res.err != nil { + t.Fatalf("Error while NAT64 prefix calculation: %s", res.err) + } + + if len(res.prefix) != 12 { + t.Fatalf("Invalid prefix: %v", res.prefix) + } +} + +func createTestMessage(name string, dnsType uint16) *dns.Msg { + req := &dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: name, Qtype: dnsType, Qclass: dns.ClassINET}, + } + return req +} + +func createTestConfig() *Config { + upstreams := []string{ + "tls://dns.adguard.com", + "https://dns.adguard.com/dns-query", + // AdGuard DNS (DNSCrypt) + "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", + } + upstreamsStr := strings.Join(upstreams, "\n") + return &Config{ + ListenAddr: "127.0.0.1", + ListenPort: 0, // Specify 0 to start listening on a random free port + BootstrapDNS: "8.8.8.8:53\n1.1.1.1:53", + Fallbacks: "8.8.8.8:53\n1.1.1.1:53", + Timeout: 5000, + Upstreams: upstreamsStr, + } +} diff --git a/proxy/dns64.go b/proxy/dns64.go index a8a85cfe6..4ad5f39e5 100644 --- a/proxy/dns64.go +++ b/proxy/dns64.go @@ -6,68 +6,9 @@ import ( "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" - "github.com/joomcode/errorx" "github.com/miekg/dns" ) -// Byte representation of IPv4 addresses we are looking for after NAT64 prefix while dns response parsing -// It's two "well-known IPv4" addresses defined for Pref64::/n -// https://tools.ietf.org/html/rfc7050#section-2.2 -var wellKnownIPv4First = []byte{192, 0, 0, 171} //nolint -var wellKnownIPv4Second = []byte{192, 0, 0, 170} //nolint - -// createIpv4ArpaMessage creates AAAA request for the "Well-Known IPv4-only Name" -// this request should be exchanged with DNS64 upstreams. -func createIpv4ArpaMessage() *dns.Msg { - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "ipv4only.arpa.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, - } - return &req -} - -// getNAT64PrefixFromResponse parses a response for NAT64 prefix -// valid answer should contains the following AAAA record: -// -// - 16 bytes record -// - first 12 bytes is NAT64 prefix -// - last 4 bytes are required IPv4: wellKnownIpv4First or wellKnownIpv4Second -// we use simplified algorithm and consider the first matched record to be valid -func getNAT64PrefixFromResponse(r *dns.Msg) ([]byte, error) { - var prefix []byte - for _, reply := range r.Answer { - a, ok := reply.(*dns.AAAA) - if !ok { - log.Tracef("Answer is not AAAA record") - continue - } - ip := a.AAAA - - // Let's separate IPv4 part from NAT64 prefix - ipv4 := ip[12:] - if len(ipv4) != net.IPv4len { - continue - } - - // Compare IPv4 part to wellKnownIPv4First and wellKnownIPv4Second - if !ipv4.Equal(wellKnownIPv4First) && !ipv4.Equal(wellKnownIPv4Second) { - continue - } - - // Set NAT64 prefix and break the loop - log.Tracef("NAT64 prefix was obtained from response. Answer is: %s", ip.String()) - prefix = ip[:12] - break - } - - if len(prefix) == 0 { - return nil, fmt.Errorf("no NAT64 prefix in answers") - } - return prefix, nil -} - // isIpv6ResponseEmpty checks AAAA answer to be empty // returns true if NAT64 prefix already calculated and there are no answers for AAAA question func (p *Proxy) isIpv6ResponseEmpty(resp, req *dns.Msg) bool { @@ -82,6 +23,25 @@ func (p *Proxy) isNAT64PrefixAvailable() bool { return prefixSize == 12 } +// SetNAT64Prefix sets NAT64 prefix +func (p *Proxy) SetNAT64Prefix(prefix []byte) { + if len(prefix) != 12 { + return + } + + // Check if proxy is started and has no prefix yet + p.nat64Lock.Lock() + if len(p.nat64Prefix) == 0 { + p.RLock() + if p.started { + p.nat64Prefix = prefix + log.Printf("NAT64 prefix: %v", prefix) + } + p.RUnlock() + } + p.nat64Lock.Unlock() +} + // createModifiedARequest returns modified question to make A DNS request func createModifiedARequest(d *dns.Msg) (*dns.Msg, error) { if d.Question[0].Qtype != dns.TypeAAAA { @@ -97,87 +57,6 @@ func createModifiedARequest(d *dns.Msg) (*dns.Msg, error) { return &req, nil } -// nat64Result is a result of NAT64 prefix calculation -type nat64Result struct { - prefix []byte - upstream upstream.Upstream - err error -} - -// getNAT64PrefixAsync sends result of getNAT64PrefixWithUpstream to the channel -func getNAT64PrefixAsync(req *dns.Msg, u upstream.Upstream, ch chan nat64Result) { - ch <- getNAT64PrefixWithUpstream(req, u) -} - -// getNAT64PrefixWithUpstream returns result of NAT64 prefix calculation with one upstream -func getNAT64PrefixWithUpstream(req *dns.Msg, u upstream.Upstream) nat64Result { - resp, err := u.Exchange(req) - if err != nil { - return nat64Result{err: err} - } - - prefix, err := getNAT64PrefixFromResponse(resp) - if err != nil { - return nat64Result{err: err} - } - - return nat64Result{prefix: prefix, upstream: u} -} - -// getNAT64PrefixParallel starts parallel NAT64 prefix calculation with all available upstreams -func (p *Proxy) getNAT64PrefixParallel() nat64Result { - size := len(p.DNS64Upstreams) - req := createIpv4ArpaMessage() - if size == 1 { - return getNAT64PrefixWithUpstream(req, p.DNS64Upstreams[0]) - } - - errs := []error{} - ch := make(chan nat64Result, size) - for _, u := range p.DNS64Upstreams { - go getNAT64PrefixAsync(req, u, ch) - } - - for { - select { - case rep := <-ch: - if rep.err != nil { - errs = append(errs, rep.err) - if len(errs) == size { - return nat64Result{err: errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...)} - } - } else { - return rep - } - } - } -} - -// getNAT64Prefix exchanges ipv4 arpa request with DNS64 upstreams and sets NAT64 prefix to the proxy -func (p *Proxy) getNAT64Prefix() { - // First check if no DNS64 upstreams specified - if len(p.DNS64Upstreams) == 0 { - log.Tracef("no DNS64 upstream specified") - return - } - - // Do nothing if NAT64 prefix was not calculated - if p.isNAT64PrefixAvailable() { - return - } - - res := p.getNAT64PrefixParallel() - if res.err != nil { - log.Tracef("Failed to calculate NAT64 prefix: %s", res.err) - return - } - - log.Tracef("Use %s server NAT64 prefix", res.upstream.Address()) - p.nat64Lock.Lock() - p.nat64Prefix = res.prefix - p.nat64Lock.Unlock() -} - // createDNS64MappedResponse adds NAT 64 mapped answer to the old message // res is new A response. req is old AAAA request func (p *Proxy) createDNS64MappedResponse(res, req *dns.Msg) (*dns.Msg, error) { diff --git a/proxy/dns64_test.go b/proxy/dns64_test.go index eb825d72f..9ae3a2718 100644 --- a/proxy/dns64_test.go +++ b/proxy/dns64_test.go @@ -4,66 +4,28 @@ import ( "net" "sync" "testing" - "time" - "github.com/AdguardTeam/dnsproxy/upstream" "github.com/miekg/dns" ) -const dns64Upstream = "2001:67c:27e4:15::64" const ipv4OnlyHost = "and.ru" -const dns64Timeout = time.Second -// TestNAT64Prefix calculates nat64 prefix -func TestNAT64Prefix(t *testing.T) { - arpa := createIpv4ArpaMessage() - u, err := upstream.AddressToUpstream(dns64Upstream, upstream.Options{Timeout: dns64Timeout}) - if err != nil { - t.Fatalf("Failed to create upstream to %s", dns64Upstream) - } - - resp, err := u.Exchange(arpa) - if err != nil { - t.Fatalf("Can not exchange ipv4Arpa message: %s", err) - } - - prefix, err := getNAT64PrefixFromResponse(resp) - if err != nil { - t.Fatalf("Can not get NAT64 prefix from response") - } - - if l := len(prefix); l != 12 { - t.Fatalf("Wrong prefix length: %d", l) - } -} +// Valid NAT-64 prefix for 2001:67c:27e4:15::64 server +var prefix = []byte{32, 1, 6, 124, 39, 228, 16, 100, 0, 0, 0, 0} //nolint func TestProxyWithDNS64(t *testing.T) { - d := createTestProxy(t, nil) - d.DNS64Upstreams = []upstream.Upstream{} - dns64 := []string{dns64Upstream} - for _, up := range dns64 { - u, err := upstream.AddressToUpstream(up, upstream.Options{Timeout: dns64Timeout}) - if err != nil { - t.Fatalf("Failed to create upstream to %s", up) - } - - d.DNS64Upstreams = append(d.DNS64Upstreams, u) - } + // Create test proxy and manually set NAT64 prefix + dnsProxy := createTestProxy(t, nil) + dnsProxy.nat64Prefix = prefix - err := d.Start() + err := dnsProxy.Start() if err != nil { t.Fatalf("Failed to start dns proxy") } - // Wait for DNS64 upstream timeout. NAT64 prefix should be already calculated - time.Sleep(dns64Timeout) - if !d.isNAT64PrefixAvailable() { - t.Fatalf("Failed to calculate NAT64 prefix") - } - // Let's create test A request to ipv4OnlyHost and exchange it with test proxy req := createHostTestMessage(ipv4OnlyHost) - resp, _, err := d.exchange(req, d.DNS64Upstreams) + resp, _, err := dnsProxy.exchange(req, dnsProxy.Upstreams) if err != nil { t.Fatalf("Can not exchange test message for %s cause: %s", ipv4OnlyHost, err) } @@ -75,16 +37,16 @@ func TestProxyWithDNS64(t *testing.T) { // Let's manually add NAT64 prefix to IPv4 response mappedIP := make(net.IP, net.IPv6len) - copy(mappedIP, d.nat64Prefix) + copy(mappedIP, dnsProxy.nat64Prefix) for index, b := range a.A { mappedIP[12+index] = b } // Create test context with AAAA request to ipv4OnlyHost and resolve it testDNSContext := createTestDNSContext(ipv4OnlyHost) - err = d.Resolve(testDNSContext) + err = dnsProxy.Resolve(testDNSContext) if err != nil { - t.Fatalf("Error whilr DNSContext resolve: %s", err) + t.Fatalf("Error while DNSContext resolve: %s", err) } // Response should be AAAA answer @@ -103,7 +65,7 @@ func TestProxyWithDNS64(t *testing.T) { t.Fatalf("Manually mapped IP %s not equlas to repsonse %s", mappedIP.String(), ans.AAAA.String()) } - err = d.Stop() + err = dnsProxy.Stop() if err != nil { t.Fatalf("Failed to stop dns proxy") } @@ -111,19 +73,7 @@ func TestProxyWithDNS64(t *testing.T) { func TestDNS64Race(t *testing.T) { dnsProxy := createTestProxy(t, nil) - - // Add multiple DNS64 upstreams - dnsProxy.DNS64Upstreams = []upstream.Upstream{} - dns64 := []string{dns64Upstream, dns64Upstream, upstreamAddr, upstreamAddr} - for _, up := range dns64 { - u, err := upstream.AddressToUpstream(up, upstream.Options{Timeout: time.Second}) - if err != nil { - t.Fatalf("Failed to create upstream to %s", up) - } - - dnsProxy.DNS64Upstreams = append(dnsProxy.DNS64Upstreams, u) - } - + dnsProxy.nat64Prefix = prefix dnsProxy.Upstreams = append(dnsProxy.Upstreams, dnsProxy.Upstreams[0]) // Start listening @@ -132,9 +82,6 @@ func TestDNS64Race(t *testing.T) { t.Fatalf("cannot start the DNS proxy: %s", err) } - // Wait for NAT64 prefix calculation - time.Sleep(dns64Timeout) - // Create a DNS-over-UDP client connection addr := dnsProxy.Addr(ProtoUDP) conn, err := dns.Dial("udp", addr.String()) diff --git a/proxy/proxy.go b/proxy/proxy.go index cdf6583ac..bbed55fea 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -87,11 +87,9 @@ type Config struct { CacheEnabled bool // cache status CacheSize int // number of cached elements. Default size: 1000 - Upstreams []upstream.Upstream // list of upstreams - Fallbacks []upstream.Upstream // list of fallback resolvers (which will be used if regular upstream failed to answer) - DNS64Upstreams []upstream.Upstream // list of DNS 64 upstreams (system servers) - - Handler Handler // custom middleware (optional) + Upstreams []upstream.Upstream // list of upstreams + Fallbacks []upstream.Upstream // list of fallback resolvers (which will be used if regular upstream failed to answer) + Handler Handler // custom middleware (optional) DomainsReservedUpstreams map[string][]upstream.Upstream // map of domains and lists of corresponding upstreams } @@ -203,8 +201,6 @@ func (p *Proxy) Start() error { return err } - go p.getNAT64Prefix() - p.started = true return nil } From ae4515be3467266ce41d711abd5b78584ba703b1 Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Fri, 29 Mar 2019 19:25:50 +0300 Subject: [PATCH 4/9] [fix] mobile: remove unuseful comment --- mobile/mobile.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/mobile.go b/mobile/mobile.go index 448bd6e12..9e1282b39 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -67,7 +67,6 @@ func (d *DNSProxy) Start() error { // defer called here 'cause otherwise d.dnsProxy may be null defer func() { - log.Tracef("CALL DEFER!") d.Unlock() go calculateNAT64Prefix(d.dnsProxy, d.Config.DNS64Upstream) }() From 37c56e14c775b9433aafbba14294907968bc9a59 Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Fri, 5 Apr 2019 12:26:01 +0300 Subject: [PATCH 5/9] * mobile, proxy: remove unuseful lock and defer --- mobile/mobile.go | 13 ++++++------- proxy/dns64.go | 2 -- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/mobile/mobile.go b/mobile/mobile.go index 9e1282b39..2284d5ec6 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -65,14 +65,13 @@ func (d *DNSProxy) Start() error { } d.dnsProxy = &proxy.Proxy{Config: *c} - // defer called here 'cause otherwise d.dnsProxy may be null - defer func() { - d.Unlock() - go calculateNAT64Prefix(d.dnsProxy, d.Config.DNS64Upstream) - }() - // Start the proxy - return d.dnsProxy.Start() + err = d.dnsProxy.Start() + d.Unlock() + if err == nil { + go calculateNAT64Prefix(d.dnsProxy, d.Config.DNS64Upstream) + } + return err } // Stop stops the DNS proxy diff --git a/proxy/dns64.go b/proxy/dns64.go index 4ad5f39e5..6b0378888 100644 --- a/proxy/dns64.go +++ b/proxy/dns64.go @@ -32,12 +32,10 @@ func (p *Proxy) SetNAT64Prefix(prefix []byte) { // Check if proxy is started and has no prefix yet p.nat64Lock.Lock() if len(p.nat64Prefix) == 0 { - p.RLock() if p.started { p.nat64Prefix = prefix log.Printf("NAT64 prefix: %v", prefix) } - p.RUnlock() } p.nat64Lock.Unlock() } From fd0d430e1f00731e78d3b5f42e5758b09f3c1b5a Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Thu, 11 Apr 2019 16:06:16 +0300 Subject: [PATCH 6/9] * mobile: move dns64 fuctions to separate file --- mobile/dns64.go | 243 +++++++++++++++++++++++++++++++++++++++++++++++ mobile/mobile.go | 221 +----------------------------------------- 2 files changed, 245 insertions(+), 219 deletions(-) create mode 100644 mobile/dns64.go diff --git a/mobile/dns64.go b/mobile/dns64.go new file mode 100644 index 000000000..9e815d225 --- /dev/null +++ b/mobile/dns64.go @@ -0,0 +1,243 @@ +// Utility functions for NAT64 prefix calculation (see https://tools.ietf.org/html/rfc7050). +// We called calculateNAT64Prefix in the separate goroutine after proxy starts. +// - Checks if dns64 addresses are IPv6. +// - Starts ticker with resolverTimeout. It's necessary to make several calculation tryouts (5 tryouts with a break of resolverTimeout). +// - getNAT64PrefixParallel starts parallel prefix calculation with all available dns64 upstreams. +// - getNAT64PrefixWithClient returns result of ipv4only.arpa AAAA request exchange via dns.Client. +// - getNAT64PrefixFromDNSResponse parses AAAA response for NAT64 prefix. Valid answer is: +// * First 12 bytes are NAT64 prefix +// * Last 4 bytes are required "well-known IPv4" addresses: wellKnownIpv4First or wellKnownIpv4Second +// - NAT64 prefix is set to proxy after successful validation. +// - getImportantError called if all dns64 upstreams failed to calculate NAT64 prefix. +// - When the network changes, the following errors may occur: +// * Timeout +// * SyscallError with "connect" message +// - There is no real error in this case and we should retry to calculate NAT64 prefix one more time +// - The calculation ends after it's success or after 5 unsuccessful attempts. + +package mobile + +import ( + "fmt" + "net" + "os" + "strings" + "time" + + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" + "github.com/miekg/dns" +) + +// Byte representation of IPv4 addresses we are looking for after NAT64 prefix while dns response parsing +// It's two "well-known IPv4" addresses defined for Pref64::/n +// https://tools.ietf.org/html/rfc7050#section-2.2 +var wellKnownIPv4First = []byte{192, 0, 0, 171} //nolint +var wellKnownIPv4Second = []byte{192, 0, 0, 170} //nolint + +const resolverTimeout = 5 * time.Second + +// getImportantError looks for errors that may occurs on network change: network is unreachable or client timeout +// if errs contains one of this errors we should try to exchange ipv4only.arpa again +func getImportantError(errs []error) error { + for _, err := range errs { + // Timeout + if os.IsTimeout(err) { + return nil + } + + // Let's put out error syscall + if e, ok := err.(*net.OpError); ok { + if er, ok := e.Err.(*os.SyscallError); ok { + // No connection, let,s try again + if er.Syscall == "connect" { + return nil + } + } + } + } + + // No important errors in errs slice + return errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...) +} + +// validateIPv6Addresses returns slice of valid ipv6 addresses. Param dns64 is a list of system dns upstreams (each on new line) +func validateIPv6Addresses(dns64 string) []string { + addresses := []string{} + lines := strings.Split(dns64, "\n") + for _, address := range lines { + if address == "" { + continue + } + + // DNS64 upstream is just a plain DNS host:port + // First let's check port + _, _, err := net.SplitHostPort(address) + if err != nil { + // Doesn't have a port, we should add default one + address = net.JoinHostPort(address, "53") + } + + // Separate ip from port. It should be IPv6 address + host, _, err := net.SplitHostPort(address) + if err != nil { + continue + } + + // ParseIP func may return IPv6 address with zero 12-bytes prefix + ip := net.ParseIP(host) + if len(ip) != net.IPv6len || ip.To4() != nil { + continue + } + + // Add address to slice after validation + addresses = append(addresses, address) + } + + return addresses +} + +// calculateNAT64Prefix should be called inside the goroutine. +// This func validates dns64 addresses and starts ticker for prefix calculation +// Each tryout starts after resolverTimeout. If getNAT64PrefixParallel returns an error it breaks the loop +// It also breaks the loop and set prefix to proxy after successfully calculation +func calculateNAT64Prefix(p *proxy.Proxy, dns64 string) { + addresses := validateIPv6Addresses(dns64) + if len(addresses) == 0 { + log.Tracef("no dns64 upstreams specified") + return + } + + count := 1 + var prefix []byte + ticker := time.NewTicker(resolverTimeout) + for range ticker.C { + log.Tracef("%d tryout of NAT64 prefix calculation", count) + res := getNAT64PrefixParallel(addresses) + + if res.err != nil { + log.Tracef("Failed to lookup for ipv4only.arpa: %s", res.err) + break + } + + // Non-zero prefix. Break the loop + if res.prefix != nil { + prefix = res.prefix + break + } + + // Five tryouts + if count == 5 { + break + } + count++ + } + + if len(prefix) != 12 { + log.Tracef("Failed to calculate NAT64 prefix") + } + + p.SetNAT64Prefix(prefix) +} + +// createIpv4ArpaMessage creates AAAA request for the "Well-Known IPv4-only Name" +// this request should be exchanged with DNS64 upstreams. +func createIpv4ArpaMessage() *dns.Msg { + req := dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: "ipv4only.arpa.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, + } + return &req +} + +// getNAT64PrefixFromResponse parses a response for NAT64 prefix +// valid answer should contains the following AAAA record: +// +// - 16 bytes record +// - first 12 bytes is NAT64 prefix +// - last 4 bytes are required IPv4: wellKnownIpv4First or wellKnownIpv4Second +// we use simplified algorithm and consider the first matched record to be valid +func getNAT64PrefixFromDNSResponse(r *dns.Msg) ([]byte, error) { + var prefix []byte + for _, reply := range r.Answer { + a, ok := reply.(*dns.AAAA) + if !ok { + log.Tracef("Answer is not AAAA record") + continue + } + ip := a.AAAA + + // Let's separate IPv4 part from NAT64 prefix + ipv4 := ip[12:] + if len(ipv4) != net.IPv4len { + continue + } + + // Compare IPv4 part to wellKnownIPv4First and wellKnownIPv4Second + if !ipv4.Equal(wellKnownIPv4First) && !ipv4.Equal(wellKnownIPv4Second) { + continue + } + + // Set NAT64 prefix and break the loop + log.Tracef("NAT64 prefix was obtained from response. Answer is: %s", ip.String()) + prefix = ip[:12] + break + } + + if len(prefix) == 0 { + return nil, fmt.Errorf("no NAT64 prefix in answers") + } + + return prefix, nil +} + +// nat64Result is a result of NAT64 prefix calculation +type nat64Result struct { + prefix []byte + err error +} + +// getNAT64PrefixParallel starts parallel NAT64 prefix calculation with all available dns64 upstreams +func getNAT64PrefixParallel(dns64 []string) nat64Result { + ch := make(chan nat64Result, len(dns64)) + for _, d := range dns64 { + go getNAT64PrefixAsync(d, ch) + } + + errs := []error{} + for { + select { + case rep := <-ch: + if rep.err != nil { + errs = append(errs, rep.err) + if len(errs) == len(dns64) { + return nat64Result{err: getImportantError(errs)} + } + } else { + return rep + } + } + } +} + +// getNAT64PrefixWithClient sends ipv4only.arpa AAAA request to dns64 address via dns.Client +// In case of successfully exchange it returns result of getNAT64PrefixFromDNSResponse +func getNAT64PrefixWithClient(dns64 string) nat64Result { + req := createIpv4ArpaMessage() + tcpClient := dns.Client{Net: "tcp", Timeout: resolverTimeout} + reply, _, tcpErr := tcpClient.Exchange(req, dns64) + if tcpErr != nil { + return nat64Result{err: tcpErr} + } + + prefix, err := getNAT64PrefixFromDNSResponse(reply) + return nat64Result{prefix, err} +} + +// getNAT64PrefixAsync sends result of getNAT64PrefixWithClient into the channel +func getNAT64PrefixAsync(dns64 string, ch chan nat64Result) { + ch <- getNAT64PrefixWithClient(dns64) +} diff --git a/mobile/mobile.go b/mobile/mobile.go index 2284d5ec6..559b261c5 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "net" - "os" "strings" "sync" "time" @@ -15,18 +14,8 @@ import ( "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" - "github.com/joomcode/errorx" - "github.com/miekg/dns" ) -// Byte representation of IPv4 addresses we are looking for after NAT64 prefix while dns response parsing -// It's two "well-known IPv4" addresses defined for Pref64::/n -// https://tools.ietf.org/html/rfc7050#section-2.2 -var wellKnownIPv4First = []byte{192, 0, 0, 171} //nolint -var wellKnownIPv4Second = []byte{192, 0, 0, 170} //nolint - -const resolverTimeout = 5 * time.Second - // DNSProxy represents a proxy with it's configuration type DNSProxy struct { Config *Config // Proxy configuration @@ -43,7 +32,7 @@ type Config struct { BootstrapDNS string // A list of bootstrap DNS (i.e. 8.8.8.8:53 each on a new line) Fallbacks string // A list of fallback resolvers that will be used if the main one is not available (i.e. 1.1.1.1:53 each on a new line) Upstreams string // A list of upstream resolvers (each on a new line) - DNS64Upstream string // A list of DNS64 upstreams for ipv6-only network (each on new line) + DNS64Upstream string // A list of DNS64 upstreams for ipv6-only network (each on new line). We need to specify it to use dns.Client instead of net.Resolver Timeout int // Default timeout for all resolvers (milliseconds) CacheSize int // Maximum number of elements in the cache. Default size: 1000 AllServers bool // If true, parallel queries to all configured upstream servers are enabled @@ -52,22 +41,20 @@ type Config struct { // Start starts the DNS proxy func (d *DNSProxy) Start() error { d.Lock() + defer d.Unlock() if d.dnsProxy != nil { - d.Unlock() return errors.New("DNS proxy is already started") } c, err := createConfig(d.Config) if err != nil { - d.Unlock() return fmt.Errorf("cannot start the DNS proxy: %s", err) } d.dnsProxy = &proxy.Proxy{Config: *c} // Start the proxy err = d.dnsProxy.Start() - d.Unlock() if err == nil { go calculateNAT64Prefix(d.dnsProxy, d.Config.DNS64Upstream) } @@ -175,207 +162,3 @@ func createConfig(config *Config) (*proxy.Config, error) { return &proxyConfig, nil } - -// getImportantError looks for errors that may occurs on network change: network is unreachable or client timeout -// if errs contains one of this errors we should try to exchange ipv4only.arpa again -func getImportantError(errs []error) error { - for _, err := range errs { - // Timeout - if os.IsTimeout(err) { - return nil - } - - // Let's put out error syscall - if e, ok := err.(*net.OpError); ok { - if er, ok := e.Err.(*os.SyscallError); ok { - // No connection, let,s try again - if er.Syscall == "connect" { - return nil - } - } - } - } - - // No important errors in errs slice - return errorx.DecorateMany("Failed to get NAT64 prefix with all upstreams:", errs...) -} - -// validateIPv6Addresses returns only valid ipv6 addresses -func validateIPv6Addresses(dns64 string) []string { - addresses := []string{} - lines := strings.Split(dns64, "\n") - for _, address := range lines { - if address == "" { - continue - } - - // DNS64 upstream is just a plain DNS host:port - // First let's check port - _, _, err := net.SplitHostPort(address) - if err != nil { - // Doesn't have port, add default one - address = net.JoinHostPort(address, "53") - } - - // Separate ip from port. It should be IPv6 address - host, _, err := net.SplitHostPort(address) - if err != nil { - continue - } - - // ParseIP func may return IPv6 address with zero 12-bytes prefix - ip := net.ParseIP(host) - if len(ip) != net.IPv6len || ip.To4() != nil { - continue - } - - // Add address to slice after validation - addresses = append(addresses, address) - } - - return addresses -} - -// calculateNAT64Prefix should be called inside the goroutine. -// This func validates dns64 addresses and starts ticker for prefix calculation -// Each tryout starts after resolverTimeout. If getNAT64PrefixParallel returns an error it breaks the loop -// It also breaks the loop and set prefix to proxy after successfully calculation -func calculateNAT64Prefix(p *proxy.Proxy, dns64 string) { - addresses := validateIPv6Addresses(dns64) - if len(addresses) == 0 { - log.Tracef("no dns64 upstreams specified") - return - } - - count := 1 - var prefix []byte - ticker := time.NewTicker(resolverTimeout) - for range ticker.C { - log.Tracef("%d tryout of NAT64 prefix calculation", count) - res := getNAT64PrefixParallel(addresses) - - if res.err != nil { - log.Tracef("Failed to lookup for ipv4only.arpa: %s", res.err) - break - } - - // Non-zero prefix. Break the loop - if res.prefix != nil { - prefix = res.prefix - break - } - - // Five tryouts - if count == 5 { - break - } - count++ - } - - if len(prefix) != 12 { - log.Tracef("Failed to calculate NAT64 prefix") - } - - p.SetNAT64Prefix(prefix) -} - -// createIpv4ArpaMessage creates AAAA request for the "Well-Known IPv4-only Name" -// this request should be exchanged with DNS64 upstreams. -func createIpv4ArpaMessage() *dns.Msg { - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "ipv4only.arpa.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, - } - return &req -} - -// getNAT64PrefixFromResponse parses a response for NAT64 prefix -// valid answer should contains the following AAAA record: -// -// - 16 bytes record -// - first 12 bytes is NAT64 prefix -// - last 4 bytes are required IPv4: wellKnownIpv4First or wellKnownIpv4Second -// we use simplified algorithm and consider the first matched record to be valid -func getNAT64PrefixFromDNSResponse(r *dns.Msg) ([]byte, error) { - var prefix []byte - for _, reply := range r.Answer { - a, ok := reply.(*dns.AAAA) - if !ok { - log.Tracef("Answer is not AAAA record") - continue - } - ip := a.AAAA - - // Let's separate IPv4 part from NAT64 prefix - ipv4 := ip[12:] - if len(ipv4) != net.IPv4len { - continue - } - - // Compare IPv4 part to wellKnownIPv4First and wellKnownIPv4Second - if !ipv4.Equal(wellKnownIPv4First) && !ipv4.Equal(wellKnownIPv4Second) { - continue - } - - // Set NAT64 prefix and break the loop - log.Tracef("NAT64 prefix was obtained from response. Answer is: %s", ip.String()) - prefix = ip[:12] - break - } - - if len(prefix) == 0 { - return nil, fmt.Errorf("no NAT64 prefix in answers") - } - - return prefix, nil -} - -// nat64Result is a result of NAT64 prefix calculation -type nat64Result struct { - prefix []byte - err error -} - -// getNAT64PrefixParallel starts parallel NAT64 prefix calculation with all available dns64 upstreams -func getNAT64PrefixParallel(dns64 []string) nat64Result { - ch := make(chan nat64Result, len(dns64)) - for _, d := range dns64 { - go getNAT64PrefixAsync(d, ch) - } - - errs := []error{} - for { - select { - case rep := <-ch: - if rep.err != nil { - errs = append(errs, rep.err) - if len(errs) == len(dns64) { - return nat64Result{err: getImportantError(errs)} - } - } else { - return rep - } - } - } -} - -// getNAT64PrefixWithClient sends ipv4only.arpa AAAA request to dns64 address via dns.Client -// In case of successfully exchange it returns result of getNAT64PrefixFromDNSResponse -func getNAT64PrefixWithClient(dns64 string) nat64Result { - req := createIpv4ArpaMessage() - tcpClient := dns.Client{Net: "tcp", Timeout: resolverTimeout} - reply, _, tcpErr := tcpClient.Exchange(req, dns64) - if tcpErr != nil { - return nat64Result{err: tcpErr} - } - - prefix, err := getNAT64PrefixFromDNSResponse(reply) - return nat64Result{prefix, err} -} - -// getNAT64PrefixAsync sends result of getNAT64PrefixWithClient into the channel -func getNAT64PrefixAsync(dns64 string, ch chan nat64Result) { - ch <- getNAT64PrefixWithClient(dns64) -} From a3f74a20b84d6f2562c5ecf9eb448d52983e4a28 Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Thu, 11 Apr 2019 16:43:06 +0300 Subject: [PATCH 7/9] * mobile: fix merge issues --- mobile/callback.go | 4 +- mobile/mobile.go | 8 +- mobile/mobile_test.go | 194 ++++++++++++++++++++---------------------- 3 files changed, 97 insertions(+), 109 deletions(-) diff --git a/mobile/callback.go b/mobile/callback.go index 80f702dcd..ea3b474ca 100644 --- a/mobile/callback.go +++ b/mobile/callback.go @@ -13,7 +13,7 @@ import ( var dnsRequestProcessedListener DNSRequestProcessedListener // nolint var dnsRequestProcessedListenerGuard sync.Mutex // nolint -// DNSRequestProcessedEvent +// DNSRequestProcessedEvent represents DNS processed event type DNSRequestProcessedEvent struct { Domain string // Queried domain name Type string // Query type @@ -34,7 +34,7 @@ type DNSRequestProcessedListener interface { DNSRequestProcessed(e *DNSRequestProcessedEvent) } -// Configures a global listener for the DNSRequestProcessedEvent events +// ConfigureDNSRequestProcessedListener configures a global listener for the DNSRequestProcessedEvent events func ConfigureDNSRequestProcessedListener(l DNSRequestProcessedListener) { dnsRequestProcessedListenerGuard.Lock() dnsRequestProcessedListener = l diff --git a/mobile/mobile.go b/mobile/mobile.go index f4d5d971f..1f877e238 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -12,13 +12,11 @@ import ( "sync" "time" - "github.com/joomcode/errorx" - - "github.com/miekg/dns" - "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" + "github.com/miekg/dns" ) //nolint @@ -54,7 +52,7 @@ type Config struct { CacheSize int // Maximum number of elements in the cache. Default size: 1000 AllServers bool // If true, parallel queries to all configured upstream servers are enabled MaxGoroutines int // Maximum number of parallel goroutines that process the requests - DNS64Upstream string // A list of DNS64 upstreams for ipv6-only network (each on new line). We need to specify it to use dns.Client instead of net.Resolver + DNS64Upstream string // A list of DNS64 upstreams for ipv6-only network (each on new line). We need to specify it to use dns.Client instead of default net.Resolver } // Start starts the DNS proxy diff --git a/mobile/mobile_test.go b/mobile/mobile_test.go index 2e3549e97..221b45e70 100644 --- a/mobile/mobile_test.go +++ b/mobile/mobile_test.go @@ -9,10 +9,9 @@ import ( "time" "github.com/AdguardTeam/dnsproxy/proxy" - "github.com/shirou/gopsutil/process" - "github.com/AdguardTeam/golibs/log" "github.com/miekg/dns" + "github.com/shirou/gopsutil/process" ) const ( @@ -229,92 +228,28 @@ func TestMobileApiMultipleQueries(t *testing.T) { } } -func sendTestMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup) { - defer func() { - g.Done() - }() - - req := createTestMessage() - err := conn.WriteMsg(req) - if err != nil { - t.Fatalf("cannot write message: %s", err) - } - - res, err := conn.ReadMsg() - if err != nil { - t.Fatalf("cannot read response to message: %s", err) - } - assertResponse(t, res) -} - -// sendTestMessagesAsync sends messages in parallel -// so that we could find race issues -func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) { - g := &sync.WaitGroup{} - g.Add(testMessagesCount) - - for i := 0; i < testMessagesCount; i++ { - go sendTestMessageAsync(t, conn, g) - } - - g.Wait() -} - -func createTestMessage() *dns.Msg { - return createHostTestMessage("google-public-dns-a.google.com") -} - -func createHostTestMessage(host string) *dns.Msg { - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - name := host + "." - req.Question = []dns.Question{ - {Name: name, Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } - return &req -} - -func assertResponse(t *testing.T, reply *dns.Msg) { - if len(reply.Answer) != 1 { - t.Fatalf("DNS upstream returned reply with wrong number of answers - %d", len(reply.Answer)) - } - if a, ok := reply.Answer[0].(*dns.A); ok { - if !net.IPv4(8, 8, 8, 8).Equal(a.A) { - t.Fatalf("DNS upstream returned wrong answer instead of 8.8.8.8: %v", a.A) - } - } else { - t.Fatalf("DNS upstream returned wrong answer type instead of A: %v", reply.Answer[0]) - } -} - -func getRSS() uint64 { - proc, err := process.NewProcess(int32(os.Getpid())) - if err != nil { - panic(err) +func TestMobileApiDNS64(t *testing.T) { + upstreams := []string{ + "tls://dns.adguard.com", + "https://dns.adguard.com/dns-query", + // AdGuard DNS (DNSCrypt) + "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", } - minfo, err := proc.MemoryInfo() - if err != nil { - panic(err) + upstreamsStr := strings.Join(upstreams, "\n") + config := &Config{ + ListenAddr: "127.0.0.1", + ListenPort: 0, // Specify 0 to start listening on a random free port + BootstrapDNS: "8.8.8.8:53\n1.1.1.1:53", + Fallbacks: "8.8.8.8:53\n1.1.1.1:53", + Timeout: 5000, + Upstreams: upstreamsStr, } - return minfo.RSS -} - -type testDNSRequestProcessedListener struct { - e []DNSRequestProcessedEvent -} -func (l *testDNSRequestProcessedListener) DNSRequestProcessed(e *DNSRequestProcessedEvent) { - l.e = append(l.e, *e) -} - -func TestMobileApiDNS64(t *testing.T) { - config := createTestConfig() config.DNS64Upstream = "2001:67c:27e4:15::64" - proxy := DNSProxy{Config: config} - err := proxy.Start() + dnsProxy := DNSProxy{Config: config} + err := dnsProxy.Start() if err != nil { - t.Fatalf("cannot start the mobile proxy: %s", err) + t.Fatalf("cannot start the mobile dnsProxy: %s", err) } // Wait for NAT64 prefix calculation @@ -325,8 +260,8 @@ func TestMobileApiDNS64(t *testing.T) { // // Create a test DNS message - req := createTestMessage("and.ru.", dns.TypeAAAA) - addr := proxy.Addr() + req := createHostTestMessageWithType("and.ru", dns.TypeAAAA) + addr := dnsProxy.Addr() reply, err := dns.Exchange(req, addr) if err != nil { t.Fatalf("Couldn't talk to upstream %s: %s", addr, err) @@ -343,9 +278,9 @@ func TestMobileApiDNS64(t *testing.T) { t.Fatalf("DNS upstream %s returned wrong answer type instead of AAAA: %v", addr, reply.Answer[0]) } - err = proxy.Stop() + err = dnsProxy.Stop() if err != nil { - t.Fatalf("cannot stop the mobile proxy: %s", err) + t.Fatalf("cannot stop the mobile dnsProxy: %s", err) } } @@ -389,30 +324,85 @@ func TestParallelExchange(t *testing.T) { } } -func createTestMessage(name string, dnsType uint16) *dns.Msg { +func sendTestMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup) { + defer func() { + g.Done() + }() + + req := createTestMessage() + err := conn.WriteMsg(req) + if err != nil { + t.Fatalf("cannot write message: %s", err) + } + + res, err := conn.ReadMsg() + if err != nil { + t.Fatalf("cannot read response to message: %s", err) + } + assertResponse(t, res) +} + +// sendTestMessagesAsync sends messages in parallel +// so that we could find race issues +func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) { + g := &sync.WaitGroup{} + g.Add(testMessagesCount) + + for i := 0; i < testMessagesCount; i++ { + go sendTestMessageAsync(t, conn, g) + } + + g.Wait() +} + +func createTestMessage() *dns.Msg { + return createHostTestMessage("google-public-dns-a.google.com") +} + +func createHostTestMessage(host string) *dns.Msg { + return createHostTestMessageWithType(host, dns.TypeA) +} + +func createHostTestMessageWithType(host string, dnsType uint16) *dns.Msg { req := &dns.Msg{} req.Id = dns.Id() req.RecursionDesired = true + name := host + "." req.Question = []dns.Question{ {Name: name, Qtype: dnsType, Qclass: dns.ClassINET}, } return req } -func createTestConfig() *Config { - upstreams := []string{ - "tls://dns.adguard.com", - "https://dns.adguard.com/dns-query", - // AdGuard DNS (DNSCrypt) - "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", +func assertResponse(t *testing.T, reply *dns.Msg) { + if len(reply.Answer) != 1 { + t.Fatalf("DNS upstream returned reply with wrong number of answers - %d", len(reply.Answer)) } - upstreamsStr := strings.Join(upstreams, "\n") - return &Config{ - ListenAddr: "127.0.0.1", - ListenPort: 0, // Specify 0 to start listening on a random free port - BootstrapDNS: "8.8.8.8:53\n1.1.1.1:53", - Fallbacks: "8.8.8.8:53\n1.1.1.1:53", - Timeout: 5000, - Upstreams: upstreamsStr, + if a, ok := reply.Answer[0].(*dns.A); ok { + if !net.IPv4(8, 8, 8, 8).Equal(a.A) { + t.Fatalf("DNS upstream returned wrong answer instead of 8.8.8.8: %v", a.A) + } + } else { + t.Fatalf("DNS upstream returned wrong answer type instead of A: %v", reply.Answer[0]) } } + +func getRSS() uint64 { + proc, err := process.NewProcess(int32(os.Getpid())) + if err != nil { + panic(err) + } + minfo, err := proc.MemoryInfo() + if err != nil { + panic(err) + } + return minfo.RSS +} + +type testDNSRequestProcessedListener struct { + e []DNSRequestProcessedEvent +} + +func (l *testDNSRequestProcessedListener) DNSRequestProcessed(e *DNSRequestProcessedEvent) { + l.e = append(l.e, *e) +} From 47a49cdfe217742b26609bdf010380823c19ab25 Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Thu, 11 Apr 2019 19:41:06 +0300 Subject: [PATCH 8/9] * mobile: rename field, add DetectDNS64Prefix flag --- mobile/mobile.go | 25 +++++++++++++------------ mobile/mobile_test.go | 3 ++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mobile/mobile.go b/mobile/mobile.go index 1f877e238..4c7c54f16 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -43,16 +43,17 @@ type DNSProxy struct { // Config is the DNS proxy configuration which uses only the subset of types that is supported by gomobile // In Java API this structure becomes an object that needs to be configured and setted as field of DNSProxy type Config struct { - ListenAddr string // IP address to listen to - ListenPort int // Port to listen to - BootstrapDNS string // A list of bootstrap DNS (i.e. 8.8.8.8:53 each on a new line) - Fallbacks string // A list of fallback resolvers that will be used if the main one is not available (i.e. 1.1.1.1:53 each on a new line) - Upstreams string // A list of upstream resolvers (each on a new line) - Timeout int // Default timeout for all resolvers (milliseconds) - CacheSize int // Maximum number of elements in the cache. Default size: 1000 - AllServers bool // If true, parallel queries to all configured upstream servers are enabled - MaxGoroutines int // Maximum number of parallel goroutines that process the requests - DNS64Upstream string // A list of DNS64 upstreams for ipv6-only network (each on new line). We need to specify it to use dns.Client instead of default net.Resolver + ListenAddr string // IP address to listen to + ListenPort int // Port to listen to + BootstrapDNS string // A list of bootstrap DNS (i.e. 8.8.8.8:53 each on a new line) + Fallbacks string // A list of fallback resolvers that will be used if the main one is not available (i.e. 1.1.1.1:53 each on a new line) + Upstreams string // A list of upstream resolvers (each on a new line) + Timeout int // Default timeout for all resolvers (milliseconds) + CacheSize int // Maximum number of elements in the cache. Default size: 1000 + AllServers bool // If true, parallel queries to all configured upstream servers are enabled + MaxGoroutines int // Maximum number of parallel goroutines that process the requests + SystemResolvers string // A list of system resolvers for ipv6-only network (each on new line). We need to specify it to use dns.Client instead of default net.Resolver + DetectDNS64Prefix bool // If true, DNS64 prefix detection is enabled } // Start starts the DNS proxy @@ -72,8 +73,8 @@ func (d *DNSProxy) Start() error { // Start the proxy err = d.dnsProxy.Start() - if err == nil { - go calculateNAT64Prefix(d.dnsProxy, d.Config.DNS64Upstream) + if err == nil && d.Config.DetectDNS64Prefix { + go calculateNAT64Prefix(d.dnsProxy, d.Config.SystemResolvers) } return err } diff --git a/mobile/mobile_test.go b/mobile/mobile_test.go index 221b45e70..5d3a4e09d 100644 --- a/mobile/mobile_test.go +++ b/mobile/mobile_test.go @@ -245,7 +245,8 @@ func TestMobileApiDNS64(t *testing.T) { Upstreams: upstreamsStr, } - config.DNS64Upstream = "2001:67c:27e4:15::64" + config.SystemResolvers = "2001:67c:27e4:15::64" + config.DetectDNS64Prefix = true dnsProxy := DNSProxy{Config: config} err := dnsProxy.Start() if err != nil { From 40558e8a1cb3ce801a319f2aae2b8ccb35f02101 Mon Sep 17 00:00:00 2001 From: Aleksey Dmitrevskiy Date: Mon, 15 Apr 2019 18:15:33 +0300 Subject: [PATCH 9/9] * proxy: update naming, fix NPE --- proxy/dns64.go | 54 +++++++++++++++++++++++++++----------------------- proxy/proxy.go | 2 +- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/proxy/dns64.go b/proxy/dns64.go index 6b0378888..7b7f620c9 100644 --- a/proxy/dns64.go +++ b/proxy/dns64.go @@ -9,9 +9,9 @@ import ( "github.com/miekg/dns" ) -// isIpv6ResponseEmpty checks AAAA answer to be empty +// isEmptyAAAAResponse checks AAAA answer to be empty // returns true if NAT64 prefix already calculated and there are no answers for AAAA question -func (p *Proxy) isIpv6ResponseEmpty(resp, req *dns.Msg) bool { +func (p *Proxy) isEmptyAAAAResponse(resp, req *dns.Msg) bool { return p.isNAT64PrefixAvailable() && req.Question[0].Qtype == dns.TypeAAAA && (resp == nil || len(resp.Answer) == 0) } @@ -56,26 +56,21 @@ func createModifiedARequest(d *dns.Msg) (*dns.Msg, error) { } // createDNS64MappedResponse adds NAT 64 mapped answer to the old message -// res is new A response. req is old AAAA request -func (p *Proxy) createDNS64MappedResponse(res, req *dns.Msg) (*dns.Msg, error) { +// 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) { // do nothing if prefix is not valid if !p.isNAT64PrefixAvailable() { return nil, fmt.Errorf("can not create DNS64 mapped response: NAT64 prefix was not calculated") } // check if there are no answers - if len(res.Answer) == 0 { + if len(newAResp.Answer) == 0 { return nil, fmt.Errorf("no ipv4 answer") } - // this bug occurs only ones - if req == nil { - return nil, fmt.Errorf("request is nil") - } - - req.Answer = []dns.RR{} + oldAAAAResp.Answer = []dns.RR{} // add NAT 64 prefix for each ipv4 answer - for _, ans := range res.Answer { + for _, ans := range newAResp.Answer { i, ok := ans.(*dns.A) if !ok { continue @@ -92,35 +87,44 @@ func (p *Proxy) createDNS64MappedResponse(res, req *dns.Msg) (*dns.Msg, error) { // create new response and fill it rr := new(dns.AAAA) - rr.Hdr = dns.RR_Header{Name: res.Question[0].Name, Rrtype: dns.TypeAAAA, Ttl: ans.Header().Ttl, Class: dns.ClassINET} + rr.Hdr = dns.RR_Header{Name: newAResp.Question[0].Name, Rrtype: dns.TypeAAAA, Ttl: ans.Header().Ttl, Class: dns.ClassINET} rr.AAAA = mappedAddress - req.Answer = append(req.Answer, rr) + oldAAAAResp.Answer = append(oldAAAAResp.Answer, rr) } - return req, nil + return oldAAAAResp, nil } // checkDNS64 is called when there is no answer for AAAA request and NAT64 prefix available. -// this function creates modified A request, exchanges it and returns DNS64 mapped response -func (p *Proxy) checkDNS64(oldReq, oldResp *dns.Msg, upstreams []upstream.Upstream) (*dns.Msg, upstream.Upstream, error) { +// 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 - req, err := createModifiedARequest(oldReq) + modifiedAReq, err := createModifiedARequest(oldAAAAReq) if err != nil { log.Tracef("Failed to create DNS64 mapped request %s", err) - return oldReq, nil, err + return nil, nil, err } // Exchange new A request with selected upstreams - resp, u, err := p.exchange(req, upstreams) + newAResp, u, err := p.exchange(modifiedAReq, upstreams) if err != nil { log.Tracef("Failed to exchange DNS64 request: %s", err) - return oldReq, nil, err + return nil, nil, err + } + + // 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]} } - // A response should be mapped with NAT64 prefix - response, err := p.createDNS64MappedResponse(resp, oldResp) + // new A response should be mapped with NAT64 prefix + mappedAAAAResponse, err := p.createDNS64MappedResponse(newAResp, oldAAAAResp) if err != nil { log.Tracef("Failed to create DNS64 mapped request %s", err) - return oldReq, u, err + return nil, u, err } - return response, u, nil + return mappedAAAAResponse, u, nil } diff --git a/proxy/proxy.go b/proxy/proxy.go index b8dc197dc..387bdff34 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -357,7 +357,7 @@ func (p *Proxy) Resolve(d *DNSContext) error { // execute the DNS request startTime := time.Now() reply, u, err := p.exchange(d.Req, upstreams) - if p.isIpv6ResponseEmpty(reply, d.Req) { + if p.isEmptyAAAAResponse(reply, d.Req) { reply, u, err = p.checkDNS64(d.Req, reply, upstreams) }