@@ -361,3 +361,104 @@ func truncate(s string, maxLen int) string {
361361 }
362362 return s
363363}
364+
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)
386+ }
387+
388+ // PreflightE2EResult holds the outcome of a preflight e2e test.
389+ type PreflightE2EResult struct {
390+ OK bool
391+ Resolver string // which resolver worked (or last tried)
392+ Stderr string // dnstt-client stderr on failure
393+ Err string // human-readable error
394+ }
395+
396+ // PreflightE2E runs a single e2e tunnel test against known-good resolvers
397+ // to verify that the dnstt-server is reachable before scanning thousands of IPs.
398+ // It tries preflightResolvers in order and returns on the first success.
399+ func PreflightE2E (bin , domain , pubkey , testURL , proxyAuth string , timeout time.Duration ) PreflightE2EResult {
400+ if testURL == "" {
401+ testURL = defaultTestURL
402+ }
403+ port := 29999 // dedicated port for preflight, outside normal pool range
404+
405+ for _ , resolver := range preflightResolvers {
406+ result := preflightSingle (bin , resolver , domain , pubkey , testURL , proxyAuth , port , timeout )
407+ if result .OK {
408+ return result
409+ }
410+ }
411+
412+ return PreflightE2EResult {
413+ OK : false ,
414+ Err : fmt .Sprintf ("tunnel test failed via all preflight resolvers (%v) — dnstt-server may not be running or is misconfigured" , preflightResolvers ),
415+ }
416+ }
417+
418+ func preflightSingle (bin , resolver , domain , pubkey , testURL , proxyAuth string , port int , timeout time.Duration ) PreflightE2EResult {
419+ ctx , cancel := context .WithTimeout (context .Background (), timeout )
420+ defer cancel ()
421+
422+ var stderrBuf bytes.Buffer
423+ cmd := execCommandContext (ctx , bin ,
424+ "-udp" , net .JoinHostPort (resolver , "53" ),
425+ "-pubkey" , pubkey ,
426+ domain ,
427+ fmt .Sprintf ("127.0.0.1:%d" , port ))
428+ cmd .Stdout = io .Discard
429+ cmd .Stderr = & stderrBuf
430+
431+ if err := cmd .Start (); err != nil {
432+ return PreflightE2EResult {Resolver : resolver , Err : fmt .Sprintf ("cannot start %s: %v" , bin , err )}
433+ }
434+
435+ exited := make (chan struct {})
436+ go func () {
437+ cmd .Wait ()
438+ close (exited )
439+ }()
440+
441+ defer func () {
442+ cmd .Process .Kill ()
443+ select {
444+ case <- exited :
445+ case <- time .After (2 * time .Second ):
446+ }
447+ }()
448+
449+ if waitAndTestSOCKS (ctx , port , testURL , proxyAuth , exited , timeout ) {
450+ return PreflightE2EResult {OK : true , Resolver : resolver }
451+ }
452+
453+ // Kill and wait to safely read stderr
454+ cmd .Process .Kill ()
455+ select {
456+ case <- exited :
457+ case <- time .After (2 * time .Second ):
458+ }
459+ stderr := strings .TrimSpace (stderrBuf .String ())
460+ if stderr != "" {
461+ return PreflightE2EResult {Resolver : resolver , Stderr : truncate (stderr , 300 ), Err : "dnstt-client error: " + truncate (stderr , 200 )}
462+ }
463+ return PreflightE2EResult {Resolver : resolver , Err : fmt .Sprintf ("tunnel via %s: no HTTP response within %v" , resolver , timeout )}
464+ }
0 commit comments