Skip to content

Commit

Permalink
Merge pull request #40 in DNS/dnsproxy from feature/40 to master
Browse files Browse the repository at this point in the history
* commit '40558e8a1cb3ce801a319f2aae2b8ccb35f02101':
  * proxy: update naming, fix NPE
  * mobile: rename field, add DetectDNS64Prefix flag
  * mobile: fix merge issues
  * mobile: move dns64 fuctions to separate file
  * mobile, proxy: remove unuseful lock and defer
  [fix] mobile: remove unuseful comment
  [change] proxy, mobile: move NAT64 prefix calculation to mobile
  [change] proxy, mobile, readme: add DNS64Upstreams to mobile api, fix issues from review
  [feature] proxy, main: add DNS64/NAT64 support
  • Loading branch information
ameshkov committed Apr 15, 2019
2 parents 73a63a8 + 40558e8 commit 625bf10
Show file tree
Hide file tree
Showing 7 changed files with 659 additions and 21 deletions.
4 changes: 2 additions & 2 deletions mobile/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
243 changes: 243 additions & 0 deletions mobile/dns64.go
Original file line number Diff line number Diff line change
@@ -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)
}
32 changes: 18 additions & 14 deletions mobile/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,15 +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
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
Expand All @@ -72,7 +72,11 @@ func (d *DNSProxy) Start() error {
d.dnsProxy = &proxy.Proxy{Config: *c}

// Start the proxy
return d.dnsProxy.Start()
err = d.dnsProxy.Start()
if err == nil && d.Config.DetectDNS64Prefix {
go calculateNAT64Prefix(d.dnsProxy, d.Config.SystemResolvers)
}
return err
}

// Stop stops the DNS proxy
Expand Down
Loading

0 comments on commit 625bf10

Please sign in to comment.