From 34dde845dda7b80d2d1fbc4d3fa89e4744b68213 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Thu, 1 Aug 2024 12:01:24 +0300 Subject: [PATCH 1/3] concurrent DNS bootstrap: implement CLI --- main.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++------- resolver.go | 3 ++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index e42dca0..14a99b6 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "crypto/tls" "crypto/x509" @@ -8,6 +9,7 @@ import ( "errors" "flag" "fmt" + "io" "io/ioutil" "log" "net" @@ -43,6 +45,37 @@ func arg_fail(msg string) { os.Exit(2) } +type CSVArg struct { + values []string +} + +func (a *CSVArg) String() string { + if len(a.values) == 0 { + return "" + } + buf := new(bytes.Buffer) + wr := csv.NewWriter(buf) + wr.Write(a.values) + wr.Flush() + return strings.TrimRight(buf.String(), "\n") +} + +func (a *CSVArg) Set(line string) error { + rd := csv.NewReader(strings.NewReader(line)) + rd.FieldsPerRecord = -1 + rd.TrimLeadingSpace = true + values, err := rd.Read() + if err == io.EOF { + a.values = nil + return nil + } + if err != nil { + return fmt.Errorf("unable to parse comma-separated argument: %w", err) + } + a.values = values + return nil +} + type CLIArgs struct { country string listCountries bool @@ -55,15 +88,26 @@ type CLIArgs struct { apiLogin string apiPassword string apiAddress string - bootstrapDNS string + bootstrapDNS *CSVArg refresh time.Duration refreshRetry time.Duration certChainWorkaround bool caFile string } -func parse_args() CLIArgs { - var args CLIArgs +func parse_args() *CLIArgs { + args := &CLIArgs{ + bootstrapDNS: &CSVArg{ + values: []string{ + "https://1.1.1.3/dns-query", + "https://security.cloudflare-dns.com/dns-query", + "https://wikimedia-dns.org/dns-query", + "https://dns.adguard-dns.com/dns-query", + "https://dns.quad9.net/dns-query", + "https://doh.cleanbrowsing.org/doh/adult-filter/", + }, + }, + } flag.StringVar(&args.country, "country", "EU", "desired proxy location") flag.BoolVar(&args.listCountries, "list-countries", false, "list available countries and exit") flag.BoolVar(&args.listProxies, "list-proxies", false, "output proxy list and exit") @@ -78,10 +122,10 @@ func parse_args() CLIArgs { flag.StringVar(&args.apiLogin, "api-login", "se0316", "SurfEasy API login") flag.StringVar(&args.apiPassword, "api-password", "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II", "SurfEasy API password") flag.StringVar(&args.apiAddress, "api-address", "", fmt.Sprintf("override IP address of %s", API_DOMAIN)) - flag.StringVar(&args.bootstrapDNS, "bootstrap-dns", "https://1.1.1.3/dns-query", - "DNS/DoH/DoT/DoQ resolver for initial discovering of SurfEasy API address. "+ + flag.Var(args.bootstrapDNS, "bootstrap-dns", + "comma-separated list of DNS/DoH/DoT/DoQ resolvers for initial discovery of SurfEasy API address. "+ "See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. "+ - "Examples: https://1.1.1.1/dns-query, quic://dns.adguard.com") + "Examples: https://1.1.1.1/dns-query,quic://dns.adguard.com") flag.DurationVar(&args.refresh, "refresh", 4*time.Hour, "login refresh interval") flag.DurationVar(&args.refreshRetry, "refresh-retry", 5*time.Second, "login refresh retry interval") flag.BoolVar(&args.certChainWorkaround, "certchain-workaround", true, @@ -147,13 +191,13 @@ func run() int { } seclientDialer := dialer - if args.apiAddress != "" || args.bootstrapDNS != "" { + if args.apiAddress != "" || len(args.bootstrapDNS.values) > 0 { var apiAddress string if args.apiAddress != "" { apiAddress = args.apiAddress mainLogger.Info("Using fixed API host IP address = %s", apiAddress) } else { - resolver, err := NewResolver(args.bootstrapDNS, args.timeout) + resolver, err := NewResolver(args.bootstrapDNS.values[0], args.timeout) if err != nil { mainLogger.Critical("Unable to instantiate DNS resolver: %v", err) return 4 diff --git a/resolver.go b/resolver.go index 5889900..9f2318e 100644 --- a/resolver.go +++ b/resolver.go @@ -1,9 +1,10 @@ package main import ( + "time" + "github.com/AdguardTeam/dnsproxy/upstream" "github.com/miekg/dns" - "time" ) type Resolver struct { From 9125854fa9bb0904a88286fd18517ebcc783b9af Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Thu, 1 Aug 2024 12:44:23 +0300 Subject: [PATCH 2/3] implement concurrent bootstrap DNS resolver --- main.go | 15 +++++++-- resolver.go | 87 +++++++++++++---------------------------------------- 2 files changed, 33 insertions(+), 69 deletions(-) diff --git a/main.go b/main.go index 14a99b6..b3930e1 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "log" "net" "net/http" + "net/netip" "net/url" "os" "strings" @@ -197,20 +198,28 @@ func run() int { apiAddress = args.apiAddress mainLogger.Info("Using fixed API host IP address = %s", apiAddress) } else { - resolver, err := NewResolver(args.bootstrapDNS.values[0], args.timeout) + resolver, err := NewResolver(args.bootstrapDNS.values, args.timeout) if err != nil { mainLogger.Critical("Unable to instantiate DNS resolver: %v", err) return 4 } mainLogger.Info("Discovering API IP address...") - addrs := resolver.ResolveA(API_DOMAIN) + addrs, err := func() ([]netip.Addr, error) { + ctx, cancel := context.WithTimeout(context.Background(), args.timeout) + defer cancel() + return resolver.LookupNetIP(ctx, "ip4", API_DOMAIN) + }() + if err != nil { + mainLogger.Critical("Unable to resolve API server address: %v", err) + return 14 + } if len(addrs) == 0 { mainLogger.Critical("Unable to resolve %s with specified bootstrap DNS", API_DOMAIN) return 14 } - apiAddress = addrs[0] + apiAddress = addrs[0].String() mainLogger.Info("Discovered address of API host = %s", apiAddress) } seclientDialer = NewFixedDialer(apiAddress, dialer) diff --git a/resolver.go b/resolver.go index 9f2318e..f460e4e 100644 --- a/resolver.go +++ b/resolver.go @@ -1,83 +1,38 @@ package main import ( + "context" + "fmt" + "net/netip" "time" "github.com/AdguardTeam/dnsproxy/upstream" - "github.com/miekg/dns" ) type Resolver struct { - upstream upstream.Upstream + resolvers upstream.ParallelResolver + timeout time.Duration } -const DOT = 0x2e - -func NewResolver(address string, timeout time.Duration) (*Resolver, error) { - opts := &upstream.Options{Timeout: timeout} - u, err := upstream.AddressToUpstream(address, opts) - if err != nil { - return nil, err +func NewResolver(addresses []string, timeout time.Duration) (*Resolver, error) { + resolvers := make([]upstream.Resolver, 0, len(addresses)) + opts := &upstream.Options{ + Timeout: timeout, } - return &Resolver{upstream: u}, nil -} - -func (r *Resolver) ResolveA(domain string) []string { - res := make([]string, 0) - if len(domain) == 0 { - return res - } - if domain[len(domain)-1] != DOT { - domain = domain + "." - } - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: domain, Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } - reply, err := r.upstream.Exchange(&req) - if err != nil { - return res - } - for _, rr := range reply.Answer { - if a, ok := rr.(*dns.A); ok { - res = append(res, a.A.String()) + for _, addr := range addresses { + u, err := upstream.AddressToUpstream(addr, opts) + if err != nil { + return nil, fmt.Errorf("unable to construct upstream resolver from string %q: %w", + addr, err) } + resolvers = append(resolvers, &upstream.UpstreamResolver{Upstream: u}) } - return res + return &Resolver{ + resolvers: resolvers, + timeout: timeout, + }, nil } -func (r *Resolver) ResolveAAAA(domain string) []string { - res := make([]string, 0) - if len(domain) == 0 { - return res - } - if domain[len(domain)-1] != DOT { - domain = domain + "." - } - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: domain, Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, - } - reply, err := r.upstream.Exchange(&req) - if err != nil { - return res - } - for _, rr := range reply.Answer { - if a, ok := rr.(*dns.AAAA); ok { - res = append(res, a.AAAA.String()) - } - } - return res -} - -func (r *Resolver) Resolve(domain string) []string { - res := r.ResolveA(domain) - if len(res) == 0 { - res = r.ResolveAAAA(domain) - } - return res +func (r *Resolver) LookupNetIP(ctx context.Context, network string, host string) (addrs []netip.Addr, err error) { + return r.resolvers.LookupNetIP(ctx, network, host) } From 7d3173576b1b109b95ed983f7d5754def4c94611 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Thu, 1 Aug 2024 12:49:41 +0300 Subject: [PATCH 3/3] add google DoH as well --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index b3930e1..d4c3565 100644 --- a/main.go +++ b/main.go @@ -101,6 +101,8 @@ func parse_args() *CLIArgs { bootstrapDNS: &CSVArg{ values: []string{ "https://1.1.1.3/dns-query", + "https://8.8.8.8/dns-query", + "https://dns.google/dns-query", "https://security.cloudflare-dns.com/dns-query", "https://wikimedia-dns.org/dns-query", "https://dns.adguard-dns.com/dns-query",