Skip to content

Commit 7c3bf0d

Browse files
committed
Remove preflight and NS checks — scan is the real test
Preflight e2e and NS delegation checks used external resolvers (8.8.8.8, 1.1.1.1, etc.) that are all blocked in some areas. They always failed, showed misleading warnings, and added delay. The scan itself tests every resolver directly. Removed 372 lines of dead code and updated docs to match.
1 parent d06a680 commit 7c3bf0d

6 files changed

Lines changed: 14 additions & 372 deletions

File tree

GUIDE.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ findns scan --domain t.example.com
732732

733733
مراحل: `ping -> nxdomain -> resolve/tunnel`
734734

735-
> **نکته مهم:** وقتی `--domain` تنظیم شود، مرحله `resolve` ساده (رکورد A برای google.com) رد می‌شود — دامنه‌های تانل رکورد A ندارند. findns مستقیم به `resolve/tunnel` (بررسی NS delegation) می‌رود.
735+
> **نکته مهم:** وقتی `--domain` تنظیم شود، مرحله `resolve` ساده (رکورد A برای google.com) رد می‌شود — دامنه‌های تانل رکورد A ندارند. findns مستقیم به `resolve/tunnel` می‌رود.
736736
737737
> برای اضافه کردن تست EDNS payload size از فلگ `--edns` استفاده کنید. با این فلگ: `ping -> nxdomain -> edns -> resolve/tunnel`
738738
@@ -1200,11 +1200,7 @@ findns scan -i resolvers.txt -o results.json \
12001200
- pubkey باید دقیقاً همان کلیدی باشد که سرور DNSTT با آن اجرا شده
12011201
- اگر pubkey اشتباه باشد، dnstt-client بدون پیام خطا فیل می‌شود
12021202

1203-
**۶. preflight e2e و DoH fallback:**
1204-
1205-
findns قبل از شروع تست e2e، یک "preflight" انجام می‌دهد — با یک resolver عمومی (Google, Cloudflare و ...) تست می‌کند تانل کار می‌کند. اگر DNS معمولی (UDP) مسدود باشد، به صورت خودکار از DoH fallback (آدرس‌های IP-based مثل `https://8.8.8.8/dns-query`) استفاده می‌کند. این یعنی حتی اگر تمام DNS مسدود باشد، preflight e2e همچنان کار می‌کند.
1206-
1207-
**۷. تست دستی:**
1203+
**۶. تست دستی:**
12081204

12091205
</div>
12101206

@@ -1672,7 +1668,6 @@ findns به صورت کامل آفلاین کار می‌کند:
16721668
- بدون `-o`: نتایج در `results.json` ذخیره می‌شود
16731669
- فایل `_ips.txt` خودکار ساخته می‌شود
16741670
- `fetch` اگر دانلود شکست بخورد، خودکار از لیست داخلی استفاده می‌کند
1675-
- تست e2e preflight از DoH fallback (IP-based) استفاده می‌کند — حتی اگر DNS مسدود باشد
16761671

16771672
ساده‌ترین دستور ممکن: `findns scan --domain t.example.com`
16781673

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ Supports both **UDP** and **DoH (DNS-over-HTTPS)** resolvers with end-to-end tun
2626
| 🌐 **CIDR Input** | Accept IP ranges like `185.51.200.0/24` — auto-expanded to individual hosts |
2727
| 🖥️ **Interactive TUI** | Full terminal UI with guided setup — no flags to remember |
2828
| 🔌 **Fully Offline** | Zero-config: auto-loads bundled resolvers, no `-i` or `-o` needed |
29-
| 🛡️ **DoH Preflight** | E2E preflight uses DoH fallback (IP-based URLs) to bypass UDP DNS blocking |
3029

3130
---
3231

@@ -263,7 +262,7 @@ findns scan --domain t.example.com
263262
**UDP mode pipeline:** `ping → nxdomain → resolve/tunnel → e2e` (add `--edns` for EDNS payload check)
264263
**DoH mode pipeline:** `doh/resolve/tunnel → doh/e2e`
265264

266-
> When `--domain` is set, the basic `resolve` step (A record for google.com) is skipped — tunnel domains have no A record, so findns goes straight to `resolve/tunnel` (NS delegation check).
265+
> When `--domain` is set, the basic `resolve` step (A record for google.com) is skipped — tunnel domains have no A record, so findns goes straight to `resolve/tunnel`.
267266
268267
| Flag | Description | Default |
269268
|------|-------------|---------|
@@ -635,7 +634,6 @@ MIT
635634
| 🌐 **ورودی CIDR** | رنج آی‌پی مثل `185.51.200.0/24` را می‌خواند و به صورت خودکار باز می‌کند |
636635
| 🖥️ **رابط کاربری ترمینال (TUI)** | رابط تعاملی کامل — بدون نیاز به حفظ فلگ‌ها |
637636
| 🔌 **کاملاً آفلاین** | بدون تنظیم: resolverهای داخلی خودکار بارگذاری می‌شوند، نیازی به `-i` یا `-o` نیست |
638-
| 🛡️ **DoH Preflight** | تست e2e از DoH fallback (آدرس‌های IP-based) برای دور زدن مسدودسازی DNS استفاده می‌کند |
639637

640638
---
641639

@@ -917,7 +915,7 @@ findns tui
917915
**حالت UDP:** `ping → nxdomain → resolve/tunnel → e2e` (با `--edns` مرحله EDNS اضافه می‌شود)
918916
**حالت DoH:** `doh/resolve/tunnel → doh/e2e`
919917

920-
> وقتی `--domain` تنظیم شود، مرحله `resolve` ساده (رکورد A برای google.com) رد می‌شود — دامنه‌های تانل رکورد A ندارند، بنابراین findns مستقیم به `resolve/tunnel` (بررسی NS delegation) می‌رود.
918+
> وقتی `--domain` تنظیم شود، مرحله `resolve` ساده (رکورد A برای google.com) رد می‌شود — دامنه‌های تانل رکورد A ندارند، بنابراین findns مستقیم به `resolve/tunnel` می‌رود.
921919
922920
| فلگ | توضیح | پیش‌فرض |
923921
|-----|-------|---------|

cmd/scan.go

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func runScan(cmd *cobra.Command, args []string) error {
198198
}
199199

200200
printBanner(len(ips), dohMode, domain, steps)
201-
printPreFlight(len(ips), domain, pubkey, testURL, proxyAuth, dnsttBin, slipstreamBin, steps)
201+
printPreFlight(len(ips), domain, dnsttBin, slipstreamBin, steps)
202202

203203
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
204204
defer stop()
@@ -241,53 +241,23 @@ func hline(left, fill, right string, width int) string {
241241
return left + strings.Repeat(fill, width) + right
242242
}
243243

244-
func printPreFlight(ipCount int, domain, pubkey, testURL, proxyAuth, dnsttBin, slipstreamBin string, steps []scanner.Step) {
244+
func printPreFlight(ipCount int, domain, dnsttBin, slipstreamBin string, steps []scanner.Step) {
245245
if !isTTY() {
246246
return
247247
}
248248
w := os.Stderr
249249
fmt.Fprintf(w, " %sPre-flight:%s\n", colorBold, colorReset)
250-
fmt.Fprintf(w, " %s\u2714%s %d resolvers loaded\n", colorGreen, colorReset, ipCount)
251-
fmt.Fprintf(w, " %s\u2714%s %d workers\n", colorGreen, colorReset, workers)
252-
fmt.Fprintf(w, " %s\u2714%s %d scan steps configured\n", colorGreen, colorReset, len(steps))
250+
fmt.Fprintf(w, " %s%s %d resolvers loaded\n", colorGreen, colorReset, ipCount)
251+
fmt.Fprintf(w, " %s%s %d workers\n", colorGreen, colorReset, workers)
252+
fmt.Fprintf(w, " %s%s %d scan steps configured\n", colorGreen, colorReset, len(steps))
253253
if dnsttBin != "" {
254-
fmt.Fprintf(w, " %s\u2714%s dnstt-client: %s%s%s\n", colorGreen, colorReset, colorDim, dnsttBin, colorReset)
254+
fmt.Fprintf(w, " %s%s dnstt-client: %s%s%s\n", colorGreen, colorReset, colorDim, dnsttBin, colorReset)
255255
}
256256
if slipstreamBin != "" {
257-
fmt.Fprintf(w, " %s\u2714%s slipstream-client: %s%s%s\n", colorGreen, colorReset, colorDim, slipstreamBin, colorReset)
257+
fmt.Fprintf(w, " %s%s slipstream-client: %s%s%s\n", colorGreen, colorReset, colorDim, slipstreamBin, colorReset)
258258
}
259259
if domain != "" {
260-
nsHosts, nsOK := scanner.QueryNSMulti(domain, 5*time.Second)
261-
if nsOK && len(nsHosts) > 0 {
262-
fmt.Fprintf(w, " %s\u2714%s Domain: %s%s%s — NS delegation verified\n",
263-
colorGreen, colorReset, colorCyan, domain, colorReset)
264-
for _, ns := range nsHosts {
265-
fmt.Fprintf(w, " %s%s%s\n", colorDim, ns, colorReset)
266-
}
267-
} else {
268-
fmt.Fprintf(w, " %s⚠%s Domain: %s%s%s — %scould not verify NS delegation (DNS may be blocked)%s\n",
269-
colorYellow, colorReset, colorCyan, domain, colorReset, colorYellow, colorReset)
270-
fmt.Fprintf(w, " %sThis is normal on filtered networks — scan will continue anyway%s\n", colorDim, colorReset)
271-
}
272-
}
273-
// Preflight e2e: test tunnel connectivity with a known-good resolver
274-
if dnsttBin != "" && domain != "" && pubkey != "" {
275-
fmt.Fprintf(w, " %s…%s Tunnel preflight: testing dnstt-server connectivity...\n", colorDim, colorReset)
276-
preflightTimeout := time.Duration(e2eTimeout) * time.Second
277-
result := scanner.PreflightE2E(dnsttBin, domain, pubkey, testURL, proxyAuth, preflightTimeout)
278-
if result.OK {
279-
via := result.Resolver
280-
if result.DoH {
281-
via += " (DoH)"
282-
}
283-
fmt.Fprintf(w, "\r\033[2K\033[A\033[2K %s\u2714%s Tunnel preflight: %sconnected via %s%s\n",
284-
colorGreen, colorReset, colorGreen, via, colorReset)
285-
} else {
286-
fmt.Fprintf(w, "\r\033[2K\033[A\033[2K %s\u2718%s Tunnel preflight: %sFAILED — %s%s\n",
287-
colorRed, colorReset, colorRed, result.Err, colorReset)
288-
fmt.Fprintf(w, " %sYour dnstt-server may not be running or is misconfigured.%s\n", colorDim, colorReset)
289-
fmt.Fprintf(w, " %sThe e2e step will likely produce 0 results.%s\n", colorDim, colorReset)
290-
}
260+
fmt.Fprintf(w, " %s✔%s Domain: %s%s%s\n", colorGreen, colorReset, colorCyan, domain, colorReset)
291261
}
292262
fmt.Fprintf(w, "\n")
293263
}

internal/scanner/dns.go

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -114,57 +114,6 @@ func QueryA(resolver, domain string, timeout time.Duration) bool {
114114
return len(r.Answer) > 0
115115
}
116116

117-
// nsResolvers is the list of public DNS resolvers used for NS delegation checks.
118-
// Multiple are needed because some may be blocked or unreachable in certain regions.
119-
var nsResolvers = []string{
120-
// Global providers
121-
"8.8.8.8", // Google
122-
"1.1.1.1", // Cloudflare
123-
"9.9.9.9", // Quad9
124-
"208.67.222.222", // OpenDNS
125-
"76.76.2.0", // ControlD
126-
"94.140.14.14", // AdGuard
127-
"185.228.168.9", // CleanBrowsing
128-
"76.76.19.19", // Alternate DNS
129-
"149.112.112.112", // Quad9 secondary
130-
"8.26.56.26", // Comodo Secure
131-
"156.154.70.1", // Neustar/UltraDNS
132-
// Regional (Middle East / Central Asia)
133-
"178.22.122.100", // Shecan (Iran)
134-
"185.51.200.2", // DNS.sb (anycast, good in ME)
135-
"195.175.39.39", // Turk Telekom (Turkey)
136-
"80.80.80.80", // Freenom/Level3 (Turkey/EU)
137-
"217.218.127.127", // TCI (Iran)
138-
// Regional (Caucasus / nearby)
139-
"85.132.75.12", // AzOnline (Azerbaijan)
140-
"213.42.20.20", // Etisalat DNS (UAE)
141-
}
142-
143-
// QueryNSMulti tries all resolvers in parallel and returns the first successful result.
144-
// Overall deadline is the per-resolver timeout (first responder wins).
145-
func QueryNSMulti(domain string, timeout time.Duration) ([]string, bool) {
146-
type nsResult struct {
147-
hosts []string
148-
ok bool
149-
}
150-
ch := make(chan nsResult, len(nsResolvers))
151-
for _, resolver := range nsResolvers {
152-
go func(r string) {
153-
hosts, ok := QueryNS(r, domain, timeout)
154-
ch <- nsResult{hosts, ok && len(hosts) > 0}
155-
}(resolver)
156-
}
157-
failures := 0
158-
for range nsResolvers {
159-
res := <-ch
160-
if res.ok {
161-
return res.hosts, true
162-
}
163-
failures++
164-
}
165-
return nil, false
166-
}
167-
168117
func QueryNS(resolver, domain string, timeout time.Duration) ([]string, bool) {
169118
// Strategy 1: direct NS query — works when the recursive resolver returns
170119
// the delegation NS in Answer or Authority.

internal/scanner/e2e.go

Lines changed: 0 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -362,156 +362,3 @@ func truncate(s string, maxLen int) string {
362362
return s
363363
}
364364

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-
// IP-based URLs avoid needing DNS to resolve the DoH server itself
396-
{"https://8.8.8.8/dns-query", true, "Google DoH"},
397-
{"https://1.1.1.1/dns-query", true, "Cloudflare DoH"},
398-
{"https://9.9.9.9:5053/dns-query", true, "Quad9 DoH"},
399-
}
400-
401-
// PreflightE2EResult holds the outcome of a preflight e2e test.
402-
type PreflightE2EResult struct {
403-
OK bool
404-
Resolver string // which resolver worked (or last tried)
405-
DoH bool // true if connected via DoH
406-
Stderr string // dnstt-client stderr on failure
407-
Err string // human-readable error
408-
}
409-
410-
// PreflightE2E runs e2e tunnel tests against multiple resolvers in parallel.
411-
// Returns as soon as any one resolver succeeds. If all fail within the timeout,
412-
// returns an error. This handles blocked resolvers (e.g. Google in Iran) by
413-
// racing them — whichever resolver is reachable responds first.
414-
func PreflightE2E(bin, domain, pubkey, testURL, proxyAuth string, timeout time.Duration) PreflightE2EResult {
415-
return PreflightE2EContext(context.Background(), bin, domain, pubkey, testURL, proxyAuth, timeout)
416-
}
417-
418-
// PreflightE2EContext is like PreflightE2E but accepts a parent context for cancellation.
419-
func PreflightE2EContext(parent context.Context, bin, domain, pubkey, testURL, proxyAuth string, timeout time.Duration) PreflightE2EResult {
420-
if testURL == "" {
421-
testURL = defaultTestURL
422-
}
423-
424-
// Each parallel test needs its own port
425-
basePort := 29900
426-
total := len(preflightResolvers)
427-
results := make(chan PreflightE2EResult, total)
428-
429-
ctx, cancel := context.WithTimeout(parent, timeout)
430-
defer cancel()
431-
432-
for i, res := range preflightResolvers {
433-
go func(r preflightResolver, port int) {
434-
result := preflightSingle(ctx, bin, r, domain, pubkey, testURL, proxyAuth, port, timeout)
435-
results <- result
436-
}(res, basePort+i)
437-
}
438-
439-
// Wait for first success or all failures
440-
failures := 0
441-
for {
442-
select {
443-
case r := <-results:
444-
if r.OK {
445-
cancel() // stop remaining goroutines
446-
return r
447-
}
448-
failures++
449-
if failures >= total {
450-
return PreflightE2EResult{
451-
OK: false,
452-
Err: "tunnel test failed via all resolvers (UDP + DoH) — dnstt-server may not be running, or your network blocks all DNS paths",
453-
}
454-
}
455-
case <-ctx.Done():
456-
return PreflightE2EResult{
457-
OK: false,
458-
Err: "tunnel preflight timed out — dnstt-server may not be running, or resolvers are blocked in your region",
459-
}
460-
}
461-
}
462-
}
463-
464-
func preflightSingle(parent context.Context, bin string, res preflightResolver, domain, pubkey, testURL, proxyAuth string, port int, timeout time.Duration) PreflightE2EResult {
465-
ctx, cancel := context.WithTimeout(parent, timeout)
466-
defer cancel()
467-
468-
label := res.name
469-
if label == "" {
470-
label = res.addr
471-
}
472-
473-
// Build dnstt-client args: -doh URL or -udp IP:53
474-
var args []string
475-
if res.doh {
476-
args = []string{"-doh", res.addr, "-pubkey", pubkey, domain, fmt.Sprintf("127.0.0.1:%d", port)}
477-
} else {
478-
args = []string{"-udp", net.JoinHostPort(res.addr, "53"), "-pubkey", pubkey, domain, fmt.Sprintf("127.0.0.1:%d", port)}
479-
}
480-
481-
var stderrBuf bytes.Buffer
482-
cmd := execCommandContext(ctx, bin, args...)
483-
cmd.Stdout = io.Discard
484-
cmd.Stderr = &stderrBuf
485-
486-
if err := cmd.Start(); err != nil {
487-
return PreflightE2EResult{Resolver: label, DoH: res.doh, Err: fmt.Sprintf("cannot start %s: %v", bin, err)}
488-
}
489-
490-
exited := make(chan struct{})
491-
go func() {
492-
cmd.Wait()
493-
close(exited)
494-
}()
495-
496-
// cleanup: kill process and wait for exit before returning
497-
cleanup := func() {
498-
cmd.Process.Kill()
499-
select {
500-
case <-exited:
501-
case <-time.After(2 * time.Second):
502-
}
503-
}
504-
505-
if waitAndTestSOCKS(ctx, port, testURL, proxyAuth, exited, timeout) {
506-
cleanup()
507-
return PreflightE2EResult{OK: true, Resolver: label, DoH: res.doh}
508-
}
509-
510-
// Kill and wait to safely read stderr
511-
cleanup()
512-
stderr := strings.TrimSpace(stderrBuf.String())
513-
if stderr != "" {
514-
return PreflightE2EResult{Resolver: label, DoH: res.doh, Stderr: truncate(stderr, 300), Err: "dnstt-client error: " + truncate(stderr, 200)}
515-
}
516-
return PreflightE2EResult{Resolver: label, DoH: res.doh, Err: fmt.Sprintf("tunnel via %s: no HTTP response within %v", label, timeout)}
517-
}

0 commit comments

Comments
 (0)