Skip to content

Commit ad908a8

Browse files
committed
Add e2e preflight check: test dnstt-server before scanning
Before scanning thousands of resolvers, test tunnel connectivity via known-good resolvers (all 18 from NS resolver list). If dnstt-server is unreachable, warn immediately instead of scanning for 0 results. Shows clear pass/fail in CLI pre-flight: ✔ Tunnel preflight: connected via 8.8.8.8 ✘ Tunnel preflight: FAILED — dnstt-server may not be running
1 parent 66619f7 commit ad908a8

2 files changed

Lines changed: 118 additions & 2 deletions

File tree

cmd/scan.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ func runScan(cmd *cobra.Command, args []string) error {
185185
}
186186

187187
printBanner(len(ips), dohMode, domain, steps)
188-
printPreFlight(len(ips), domain, dnsttBin, slipstreamBin, steps)
188+
printPreFlight(len(ips), domain, pubkey, testURL, proxyAuth, dnsttBin, slipstreamBin, steps)
189189

190190
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
191191
defer stop()
@@ -215,7 +215,7 @@ func hline(left, fill, right string, width int) string {
215215
return left + strings.Repeat(fill, width) + right
216216
}
217217

218-
func printPreFlight(ipCount int, domain, dnsttBin, slipstreamBin string, steps []scanner.Step) {
218+
func printPreFlight(ipCount int, domain, pubkey, testURL, proxyAuth, dnsttBin, slipstreamBin string, steps []scanner.Step) {
219219
if !isTTY() {
220220
return
221221
}
@@ -245,6 +245,21 @@ func printPreFlight(ipCount int, domain, dnsttBin, slipstreamBin string, steps [
245245
fmt.Fprintf(w, " %sdig NS %s @8.8.8.8 (or check your registrar/Cloudflare dashboard)%s\n", colorDim, domain, colorReset)
246246
}
247247
}
248+
// Preflight e2e: test tunnel connectivity with a known-good resolver
249+
if dnsttBin != "" && domain != "" && pubkey != "" {
250+
fmt.Fprintf(w, " %s…%s Tunnel preflight: testing dnstt-server connectivity...\n", colorDim, colorReset)
251+
e2eTimeout := time.Duration(e2eTimeout) * time.Second
252+
result := scanner.PreflightE2E(dnsttBin, domain, pubkey, testURL, proxyAuth, e2eTimeout)
253+
if result.OK {
254+
fmt.Fprintf(w, "\r\033[2K\033[A\033[2K %s\u2714%s Tunnel preflight: %sconnected via %s%s\n",
255+
colorGreen, colorReset, colorGreen, result.Resolver, colorReset)
256+
} else {
257+
fmt.Fprintf(w, "\r\033[2K\033[A\033[2K %s\u2718%s Tunnel preflight: %sFAILED — %s%s\n",
258+
colorRed, colorReset, colorRed, result.Err, colorReset)
259+
fmt.Fprintf(w, " %sYour dnstt-server may not be running or is misconfigured.%s\n", colorDim, colorReset)
260+
fmt.Fprintf(w, " %sThe e2e step will likely produce 0 results.%s\n", colorDim, colorReset)
261+
}
262+
}
248263
fmt.Fprintf(w, "\n")
249264
}
250265

internal/scanner/e2e.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)