Skip to content

Commit 35b1731

Browse files
committed
Add DoH fallback to preflight e2e — bypass UDP DNS blocking in Iran
1 parent 52f047d commit 35b1731

3 files changed

Lines changed: 72 additions & 41 deletions

File tree

cmd/scan.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,12 @@ func printPreFlight(ipCount int, domain, pubkey, testURL, proxyAuth, dnsttBin, s
262262
preflightTimeout := time.Duration(e2eTimeout) * time.Second
263263
result := scanner.PreflightE2E(dnsttBin, domain, pubkey, testURL, proxyAuth, preflightTimeout)
264264
if result.OK {
265+
via := result.Resolver
266+
if result.DoH {
267+
via += " (DoH)"
268+
}
265269
fmt.Fprintf(w, "\r\033[2K\033[A\033[2K %s\u2714%s Tunnel preflight: %sconnected via %s%s\n",
266-
colorGreen, colorReset, colorGreen, result.Resolver, colorReset)
270+
colorGreen, colorReset, colorGreen, via, colorReset)
267271
} else {
268272
fmt.Fprintf(w, "\r\033[2K\033[A\033[2K %s\u2718%s Tunnel preflight: %sFAILED — %s%s\n",
269273
colorRed, colorReset, colorRed, result.Err, colorReset)

internal/scanner/e2e.go

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
389401
type 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
}

internal/tui/screen_config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,11 @@ func binaryStatus(domain, pubkey, testURL, proxyAuth string) string {
497497
r := e2eCache.result
498498
e2eCache.mu.Unlock()
499499
if r.OK {
500-
b.WriteString(fmt.Sprintf(" %s %s\n", greenStyle.Render("✔"), dimStyle.Render("Tunnel preflight → connected via "+r.Resolver)))
500+
via := r.Resolver
501+
if r.DoH {
502+
via += " (DoH)"
503+
}
504+
b.WriteString(fmt.Sprintf(" %s %s\n", greenStyle.Render("✔"), dimStyle.Render("Tunnel preflight → connected via "+via)))
501505
} else {
502506
b.WriteString(fmt.Sprintf(" %s %s\n", redStyle.Render("✘"), redStyle.Render("Tunnel preflight FAILED")))
503507
}

0 commit comments

Comments
 (0)