@@ -362,33 +362,46 @@ func truncate(s string, maxLen int) string {
362362 return s
363363}
364364
365- // preflightResolvers are tried in order for the e2e preflight check.
366- // Uses the same list as nsResolvers in dns.go for maximum coverage.
367- var preflightResolvers = []string {
368- "8.8.8.8" , // Google
369- "1.1.1.1" , // Cloudflare
370- "9.9.9.9" , // Quad9
371- "208.67.222.222" , // OpenDNS
372- "76.76.2.0" , // ControlD
373- "94.140.14.14" , // AdGuard
374- "185.228.168.9" , // CleanBrowsing
375- "76.76.19.19" , // Alternate DNS
376- "149.112.112.112" , // Quad9 secondary
377- "8.26.56.26" , // Comodo Secure
378- "156.154.70.1" , // Neustar/UltraDNS
379- "178.22.122.100" , // Shecan (Iran)
380- "185.51.200.2" , // DNS.sb (anycast)
381- "195.175.39.39" , // Turk Telekom (Turkey)
382- "80.80.80.80" , // Freenom/Level3 (Turkey/EU)
383- "217.218.127.127" , // TCI (Iran)
384- "85.132.75.12" , // AzOnline (Azerbaijan)
385- "213.42.20.20" , // Etisalat DNS (UAE)
365+ // preflightResolver describes a single resolver for the preflight check.
366+ type preflightResolver struct {
367+ addr string // IP for UDP, URL for DoH
368+ doh bool // true = use -doh flag instead of -udp
369+ name string // human-readable label
370+ }
371+
372+ // preflightResolvers: UDP resolvers tried first (fast path), then DoH fallbacks.
373+ // DoH goes over HTTPS (port 443) which is almost never blocked, even in Iran.
374+ var preflightResolvers = []preflightResolver {
375+ // UDP resolvers — fast path
376+ {"8.8.8.8" , false , "Google" },
377+ {"1.1.1.1" , false , "Cloudflare" },
378+ {"9.9.9.9" , false , "Quad9" },
379+ {"208.67.222.222" , false , "OpenDNS" },
380+ {"76.76.2.0" , false , "ControlD" },
381+ {"94.140.14.14" , false , "AdGuard" },
382+ {"185.228.168.9" , false , "CleanBrowsing" },
383+ {"76.76.19.19" , false , "Alternate DNS" },
384+ {"149.112.112.112" , false , "Quad9 secondary" },
385+ {"8.26.56.26" , false , "Comodo Secure" },
386+ {"156.154.70.1" , false , "Neustar/UltraDNS" },
387+ {"178.22.122.100" , false , "Shecan (Iran)" },
388+ {"185.51.200.2" , false , "DNS.sb (anycast)" },
389+ {"195.175.39.39" , false , "Turk Telekom" },
390+ {"80.80.80.80" , false , "Freenom/Level3" },
391+ {"217.218.127.127" , false , "TCI (Iran)" },
392+ {"85.132.75.12" , false , "AzOnline (Azerbaijan)" },
393+ {"213.42.20.20" , false , "Etisalat DNS (UAE)" },
394+ // DoH fallbacks — bypass UDP blocking entirely (port 443)
395+ {"https://dns.google/dns-query" , true , "Google DoH" },
396+ {"https://cloudflare-dns.com/dns-query" , true , "Cloudflare DoH" },
397+ {"https://dns.sb/dns-query" , true , "DNS.sb DoH" },
386398}
387399
388400// PreflightE2EResult holds the outcome of a preflight e2e test.
389401type PreflightE2EResult struct {
390402 OK bool
391403 Resolver string // which resolver worked (or last tried)
404+ DoH bool // true if connected via DoH
392405 Stderr string // dnstt-client stderr on failure
393406 Err string // human-readable error
394407}
@@ -409,16 +422,17 @@ func PreflightE2EContext(parent context.Context, bin, domain, pubkey, testURL, p
409422
410423 // Each parallel test needs its own port
411424 basePort := 29900
412- results := make (chan PreflightE2EResult , len (preflightResolvers ))
425+ total := len (preflightResolvers )
426+ results := make (chan PreflightE2EResult , total )
413427
414428 ctx , cancel := context .WithTimeout (parent , timeout )
415429 defer cancel ()
416430
417- for i , resolver := range preflightResolvers {
418- go func (res string , port int ) {
419- r := preflightSingle (ctx , bin , res , domain , pubkey , testURL , proxyAuth , port , timeout )
420- results <- r
421- }(resolver , basePort + i )
431+ for i , res := range preflightResolvers {
432+ go func (r preflightResolver , port int ) {
433+ result := preflightSingle (ctx , bin , r , domain , pubkey , testURL , proxyAuth , port , timeout )
434+ results <- result
435+ }(res , basePort + i )
422436 }
423437
424438 // Wait for first success or all failures
@@ -431,10 +445,10 @@ func PreflightE2EContext(parent context.Context, bin, domain, pubkey, testURL, p
431445 return r
432446 }
433447 failures ++
434- if failures >= len ( preflightResolvers ) {
448+ if failures >= total {
435449 return PreflightE2EResult {
436450 OK : false ,
437- Err : "tunnel test failed via all resolvers — dnstt-server may not be running, or all resolvers are blocked in your region " ,
451+ Err : "tunnel test failed via all resolvers (UDP + DoH) — dnstt-server may not be running, or your network blocks all DNS paths " ,
438452 }
439453 }
440454 case <- ctx .Done ():
@@ -446,21 +460,30 @@ func PreflightE2EContext(parent context.Context, bin, domain, pubkey, testURL, p
446460 }
447461}
448462
449- func preflightSingle (parent context.Context , bin , resolver , domain , pubkey , testURL , proxyAuth string , port int , timeout time.Duration ) PreflightE2EResult {
463+ func preflightSingle (parent context.Context , bin string , res preflightResolver , domain , pubkey , testURL , proxyAuth string , port int , timeout time.Duration ) PreflightE2EResult {
450464 ctx , cancel := context .WithTimeout (parent , timeout )
451465 defer cancel ()
452466
467+ label := res .name
468+ if label == "" {
469+ label = res .addr
470+ }
471+
472+ // Build dnstt-client args: -doh URL or -udp IP:53
473+ var args []string
474+ if res .doh {
475+ args = []string {"-doh" , res .addr , "-pubkey" , pubkey , domain , fmt .Sprintf ("127.0.0.1:%d" , port )}
476+ } else {
477+ args = []string {"-udp" , net .JoinHostPort (res .addr , "53" ), "-pubkey" , pubkey , domain , fmt .Sprintf ("127.0.0.1:%d" , port )}
478+ }
479+
453480 var stderrBuf bytes.Buffer
454- cmd := execCommandContext (ctx , bin ,
455- "-udp" , net .JoinHostPort (resolver , "53" ),
456- "-pubkey" , pubkey ,
457- domain ,
458- fmt .Sprintf ("127.0.0.1:%d" , port ))
481+ cmd := execCommandContext (ctx , bin , args ... )
459482 cmd .Stdout = io .Discard
460483 cmd .Stderr = & stderrBuf
461484
462485 if err := cmd .Start (); err != nil {
463- return PreflightE2EResult {Resolver : resolver , Err : fmt .Sprintf ("cannot start %s: %v" , bin , err )}
486+ return PreflightE2EResult {Resolver : label , DoH : res . doh , Err : fmt .Sprintf ("cannot start %s: %v" , bin , err )}
464487 }
465488
466489 exited := make (chan struct {})
@@ -480,14 +503,14 @@ func preflightSingle(parent context.Context, bin, resolver, domain, pubkey, test
480503
481504 if waitAndTestSOCKS (ctx , port , testURL , proxyAuth , exited , timeout ) {
482505 cleanup ()
483- return PreflightE2EResult {OK : true , Resolver : resolver }
506+ return PreflightE2EResult {OK : true , Resolver : label , DoH : res . doh }
484507 }
485508
486509 // Kill and wait to safely read stderr
487510 cleanup ()
488511 stderr := strings .TrimSpace (stderrBuf .String ())
489512 if stderr != "" {
490- return PreflightE2EResult {Resolver : resolver , Stderr : truncate (stderr , 300 ), Err : "dnstt-client error: " + truncate (stderr , 200 )}
513+ return PreflightE2EResult {Resolver : label , DoH : res . doh , Stderr : truncate (stderr , 300 ), Err : "dnstt-client error: " + truncate (stderr , 200 )}
491514 }
492- return PreflightE2EResult {Resolver : resolver , Err : fmt .Sprintf ("tunnel via %s: no HTTP response within %v" , resolver , timeout )}
515+ return PreflightE2EResult {Resolver : label , DoH : res . doh , Err : fmt .Sprintf ("tunnel via %s: no HTTP response within %v" , label , timeout )}
493516}
0 commit comments