Skip to content

Commit f5f8c49

Browse files
committed
Fix e2e: real SOCKS5 CONNECT through tunnel, not local port check
- Old DnsttSOCKSCheckBin only checked if TCP port opened — dnstt-client opens the port immediately before Noise handshake, giving false positives - Old waitAndTestSOCKS5Auth only did SOCKS5 auth which is handled locally by dnstt-client, never touching the DNS tunnel New waitAndTestSOCKS5Connect sends a SOCKS5 CONNECT request to example.com:80 through the tunnel. The request travels: client → dnstt-client → DNS tunnel → resolver → dnstt-server → reply Any SOCKS5 reply (even failure) proves bidirectional tunnel data flow. Also: remove test-url, proxy-auth, curl deps from TUI/CLI/guide, update GUIDE.md with worker recommendations (5-10 for e2e), add --batch/--resume for large scans.
1 parent 8e73df2 commit f5f8c49

7 files changed

Lines changed: 220 additions & 178 deletions

File tree

GUIDE.md

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ TUI شما را قدم به قدم راهنمایی می‌کند:
199199

200200
**بخش E2E (اختیاری):**
201201
- **E2E Testing** — به صورت پیش‌فرض خاموش است. وقتی روشن کنید:
202-
- وضعیت باینری‌ها را نشان می‌دهد (✔ یا ✘ برای `dnstt-client`، `slipstream-client`، `curl`)
203-
- فیلدهای Pubkey، Cert Path، Test URL، Proxy Auth و E2E Timeout ظاهر می‌شوند
202+
- وضعیت باینری‌ها را نشان می‌دهد (✔ یا ✘ برای `dnstt-client` و `slipstream-client`)
203+
- فیلدهای Pubkey، Cert Path، Query Size و E2E Timeout ظاهر می‌شوند
204204
- بدون باینری‌ها اسکن شروع نمی‌شود — ابتدا آن‌ها را نصب کنید
205205

206206
**توضیح فیلدهای E2E:**
@@ -216,16 +216,18 @@ TUI شما را قدم به قدم راهنمایی می‌کند:
216216
- باید یک بار از سرور به سیستم خود کپی کنید (مثلاً با `scp`) و مسیر لوکالش را وارد کنید
217217
- مثال: `/home/user/cert.pem`
218218

219-
- **Query Size** — حداکثر سایز query DNS که dnstt-client می‌فرستد (بایت). پیش‌فرض: خالی (حداکثر ممکن).
220-
- اگر در ایران همه e2e فیل می‌شوند، مقدار 50 تا 80 را امتحان کنید
221-
- بعضی فیلترها query‌های بزرگ را بلاک می‌کنند
219+
- **Query Size** — حداکثر سایز query DNS که dnstt-client می‌فرستد (بایت). پیش‌فرض: ۵۰.
220+
- مقدار ۵۰ بهترین عملکرد را روی شبکه‌های فیلتر شده ایران دارد
221+
- بعضی فیلترها query‌های بزرگ را بلاک می‌کنند — مقدار ۵۰ تا ۸۰ بهترین رنج است
222+
- مقدار ۰ = حداکثر ممکن (فقط برای شبکه‌های بدون فیلتر)
222223

223-
- **Test URL** — آدرس سایتی که از طریق تانل تست می‌شود. پیش‌فرض: `http://httpbin.org/ip`
224-
- معمولاً نیازی به تغییر ندارد
224+
- **E2E Timeout** — حداکثر زمان انتظار برای هر تست e2e (ثانیه). پیش‌فرض: ۱۵ ثانیه.
225225

226-
- **Proxy Auth** — احراز هویت SOCKS به فرمت `user:pass`. فقط اگر سرور شما رمز دارد.
227-
228-
- **E2E Timeout** — حداکثر زمان انتظار برای هر تست e2e (ثانیه). پیش‌فرض: 20 ثانیه.
226+
**نکته مهم درباره Workers برای e2e:**
227+
- برای تست e2e، تعداد worker بالا (مثلاً ۵۰) می‌تواند سرور dnstt شما را overload کند
228+
- پیشنهاد: **۵ تا ۱۰ worker** برای e2e (مخصوصاً روی شبکه‌های فیلتر شده ایران)
229+
- هر تست e2e واقعاً یک تانل dnstt باز می‌کند و Noise handshake رمزنگاری‌شده انجام می‌دهد — سنگین‌تر از تست‌های DNS ساده است
230+
- سرور ضعیف: `--workers 5` | سرور قوی: `--workers 10`
229231

230232
هر فیلد یک توضیح در پایین صفحه نشان می‌دهد.
231233

@@ -797,8 +799,9 @@ findns scan -i resolvers.txt -o results.json \
797799

798800
> با `--edns`: `ping -> nxdomain -> edns -> resolve/tunnel -> e2e/dnstt`
799801
800-
نیازمند: `dnstt-client` و `curl` در PATH. این مرحله واقعاً dnstt-client را اجرا می‌کند، یک تانل SOCKS می‌سازد و با curl از طریق آن تانل یک صفحه وب را باز می‌کند.
801-
- متریک: `e2e_ms` (کل زمان از شروع تا اتصال موفق)
802+
نیازمند: `dnstt-client` در PATH. این مرحله واقعاً dnstt-client را اجرا می‌کند و Noise handshake رمزنگاری‌شده را از طریق هر resolver تست می‌کند. اگر handshake موفق شود = resolver برای تانل کار می‌کند.
803+
- متریک: `socks_ms` (زمان تا تکمیل Noise handshake)
804+
- پیشنهاد: `--workers 5` تا `--workers 10` برای e2e (تعداد بالا سرور را overload می‌کند)
802805

803806
### اسکن با تست واقعی Slipstream (اختیاری)
804807

@@ -847,8 +850,6 @@ findns scan -i doh-resolvers.txt -o results.json \
847850
| `--domain` | دامنه تانل (فعال‌سازی تست tunnel/edns) ||
848851
| `--pubkey` | کلید عمومی سرور DNSTT (فعال‌سازی تست e2e) ||
849852
| `--cert` | مسیر فایل گواهی Slipstream ||
850-
| `--test-url` | آدرسی که از طریق تانل تست شود | `http://httpbin.org/ip` |
851-
| `--proxy-auth` | احراز هویت پروکسی SOCKS به صورت `user:pass` (برای تست e2e) ||
852853
| `--doh` | حالت DoH به جای UDP | `false` |
853854
| `--skip-ping` | رد کردن مرحله ping (مفید اگر ICMP مسدود باشد) | `false` |
854855
| `--edns` | فعال‌سازی تست سایز EDNS payload (اختیاری) | `false` |
@@ -1192,7 +1193,6 @@ findns scan -i resolvers.txt -o results.json --skip-nxdomain
11921193
# بررسی:
11931194
which dnstt-client # برای DNSTT
11941195
which slipstream-client # برای Slipstream
1195-
which curl # برای هر دو
11961196

11971197
# اگر نیست، نصب کنید (بخش 1 را ببینید)
11981198
# یا باینری را در همان فولدر findns بگذارید
@@ -1237,13 +1237,18 @@ ss -ulnp | grep :53
12371237
</div>
12381238

12391239
```bash
1240-
# پیش‌فرض 10 ثانیه — برای شبکه کند بیشتر کنید:
1240+
# پیش‌فرض 15 ثانیه — برای شبکه کند بیشتر کنید:
12411241
findns scan -i resolvers.txt -o results.json \
12421242
--domain t.example.com --pubkey abc123... --e2e-timeout 20
12431243
```
12441244

12451245
<div dir="rtl">
12461246

1247+
**۴.۵ تعداد worker زیاد:**
1248+
- اگر همه e2e تایم‌اوت می‌شوند ولی تست تکی کار می‌کند: **workerها زیادند**
1249+
- هر تست e2e یک تانل واقعی باز می‌کند — ۵۰ تانل همزمان سرور را overload می‌کند
1250+
- `--workers 5` یا `--workers 10` برای e2e استفاده کنید
1251+
12471252
**۵. pubkey اشتباه:**
12481253
- pubkey باید دقیقاً همان کلیدی باشد که سرور DNSTT با آن اجرا شده
12491254
- اگر pubkey اشتباه باشد، dnstt-client بدون پیام خطا فیل می‌شود
@@ -1371,13 +1376,14 @@ https://dns.quad9.net/dns-query
13711376
| `--timeout` | `-t` | تایم‌اوت هر تلاش (ثانیه) | `3` |
13721377
| `--count` | `-c` | تعداد تلاش برای هر IP | `3` |
13731378
| `--workers` || تعداد workerهای موازی | `50` |
1374-
| `--e2e-timeout` || تایم‌اوت تست‌های e2e (ثانیه) | `20` |
1379+
| `--e2e-timeout` || تایم‌اوت تست‌های e2e (ثانیه) | `15` |
13751380
| `--include-failed` || IPهای فیل‌شده از ورودی JSON را هم اسکن کن | `false` |
13761381

13771382
**تنظیم workers:**
1378-
- سرور ضعیف یا اینترنت کند: `--workers 20`
1379-
- سرور قوی: `--workers 100`
1380-
- پیش‌فرض 50 برای اکثر سرورها مناسب است
1383+
- بدون e2e (فقط DNS): `--workers 50` پیش‌فرض خوبه، تا `--workers 100` هم مشکلی نداره
1384+
- **با e2e:** حتماً کمتر کنید! `--workers 5` تا `--workers 10` پیشنهاد می‌شود
1385+
- هر تست e2e یک تانل واقعی dnstt باز می‌کند — ۱۰ تانل همزمان بار زیادی روی سرور dnstt می‌گذارد
1386+
- بیشتر از ۱۰ worker ممکن است باعث timeout شود (سرور نمی‌تواند همه handshake‌ها را همزمان جواب دهد)
13811387

13821388
**تنظیم timeout:**
13831389
- شبکه ایران (resolverهای کند): `-t 5`

cmd/chain.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func buildStep(cfg stepConfig, defaultTimeout, defaultCount int, ports chan int,
100100
if !ok || pubkey == "" {
101101
return scanner.Step{}, fmt.Errorf("step %q: missing required param 'pubkey'", cfg.name)
102102
}
103-
return scanner.Step{Name: "e2e/dnstt", Timeout: dur, Check: scanner.DnsttSOCKSCheckBin(binPaths["dnstt-client"], domain, pubkey, ports), SortBy: "socks_ms"}, nil
103+
return scanner.Step{Name: "e2e/dnstt", Timeout: dur, Check: scanner.DnsttCheckBin(binPaths["dnstt-client"], domain, pubkey, ports), SortBy: "socks_ms"}, nil
104104

105105
case "e2e/slipstream":
106106
domain, ok := cfg.params["domain"]

cmd/scan.go

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ func init() {
6868
scanCmd.Flags().StringSlice("cidr", nil, "CIDR range(s) to scan (e.g. --cidr 5.52.0.0/16)")
6969
scanCmd.Flags().String("cidr-file", "", "text file with one CIDR range per line to scan")
7070
scanCmd.Flags().String("output-ips", "", "write plain IP list (one per line) to this file")
71-
scanCmd.Flags().Int("top", 10, "number of top results to display")
71+
scanCmd.Flags().Int("top", 10, "number of top results to display")
72+
scanCmd.Flags().Int("batch", 0, "scan N resolvers at a time, saving after each batch (0 = all at once)")
73+
scanCmd.Flags().Bool("resume", false, "skip IPs already in the output file (resume a previous scan)")
7274
rootCmd.AddCommand(scanCmd)
7375
}
7476

@@ -81,7 +83,9 @@ func runScan(cmd *cobra.Command, args []string) error {
8183
skipNXD, _ := cmd.Flags().GetBool("skip-nxdomain")
8284
ednsMode, _ := cmd.Flags().GetBool("edns")
8385
topN, _ := cmd.Flags().GetInt("top")
84-
outputIPs, _ := cmd.Flags().GetString("output-ips")
86+
outputIPs, _ := cmd.Flags().GetString("output-ips")
87+
batchSize, _ := cmd.Flags().GetInt("batch")
88+
resume, _ := cmd.Flags().GetBool("resume")
8589

8690
ednsSize, _ := cmd.Flags().GetInt("edns-size")
8791
querySize, _ := cmd.Flags().GetInt("query-size")
@@ -243,7 +247,7 @@ outputIPs, _ := cmd.Flags().GetString("output-ips")
243247
// handshake succeeds — proving bidirectional tunnel data flow.
244248
steps = append(steps, scanner.Step{
245249
Name: "e2e/dnstt", Timeout: time.Duration(e2eTimeout) * time.Second,
246-
Check: scanner.DnsttSOCKSCheckBin(dnsttBin, domain, pubkey, ports), SortBy: "socks_ms",
250+
Check: scanner.DnsttCheckBin(dnsttBin, domain, pubkey, ports), SortBy: "socks_ms",
247251
})
248252
}
249253
if domain != "" && certPath != "" {
@@ -258,6 +262,52 @@ outputIPs, _ := cmd.Flags().GetString("output-ips")
258262
return fmt.Errorf("no scan steps configured")
259263
}
260264

265+
// --resume: load existing results and skip already-scanned IPs
266+
var allPassed []scanner.IPRecord
267+
if resume {
268+
if existing, err := scanner.LoadChainReport(outputFile); err == nil {
269+
seen := make(map[string]bool, len(existing.Passed)+len(existing.Failed))
270+
for _, r := range existing.Passed {
271+
seen[r.IP] = true
272+
allPassed = append(allPassed, r)
273+
}
274+
for _, r := range existing.Failed {
275+
seen[r.IP] = true
276+
}
277+
filtered := ips[:0]
278+
for _, ip := range ips {
279+
if !seen[ip] {
280+
filtered = append(filtered, ip)
281+
}
282+
}
283+
skipped := len(ips) - len(filtered)
284+
ips = filtered
285+
fmt.Fprintf(os.Stderr, " %s✔ Resume: skipping %d already-scanned IPs, %d remaining%s\n",
286+
colorGreen, skipped, len(ips), colorReset)
287+
if len(ips) == 0 {
288+
fmt.Fprintf(os.Stderr, " %s✔ All IPs already scanned%s\n", colorGreen, colorReset)
289+
return nil
290+
}
291+
}
292+
}
293+
294+
if outputIPs == "" {
295+
outputIPs = strings.TrimSuffix(outputFile, ".json") + "_ips.txt"
296+
}
297+
298+
// saveResults writes current results to JSON + IP list
299+
saveResults := func(report scanner.ChainReport) {
300+
// Merge with previously loaded results from --resume
301+
merged := report
302+
if len(allPassed) > 0 {
303+
merged.Passed = append(allPassed, report.Passed...)
304+
}
305+
scanner.WriteChainReport(merged, outputFile)
306+
if len(merged.Passed) > 0 {
307+
scanner.WriteIPList(merged.Passed, outputIPs)
308+
}
309+
}
310+
261311
printBanner(len(ips), dohMode, domain, steps)
262312
printPreFlight(len(ips), domain, dnsttBin, slipstreamBin, steps)
263313

@@ -266,6 +316,61 @@ outputIPs, _ := cmd.Flags().GetString("output-ips")
266316

267317
scanner.ResetE2EDiagnostic()
268318
scanStart := time.Now()
319+
320+
// --batch: split IPs into chunks, save after each batch
321+
if batchSize > 0 && len(ips) > batchSize {
322+
var combinedReport scanner.ChainReport
323+
totalBatches := (len(ips) + batchSize - 1) / batchSize
324+
for i := 0; i < len(ips); i += batchSize {
325+
if ctx.Err() != nil {
326+
break
327+
}
328+
end := i + batchSize
329+
if end > len(ips) {
330+
end = len(ips)
331+
}
332+
batchNum := i/batchSize + 1
333+
chunk := ips[i:end]
334+
fmt.Fprintf(os.Stderr, "\n %s━━━ Batch %d/%d (%d IPs) ━━━%s\n\n",
335+
colorCyan, batchNum, totalBatches, len(chunk), colorReset)
336+
337+
report := scanner.RunChainQuietCtx(ctx, chunk, workers, steps,
338+
newScanProgressFactory(len(steps), stepDescriptions))
339+
340+
// Merge into combined report
341+
combinedReport.Passed = append(combinedReport.Passed, report.Passed...)
342+
combinedReport.Failed = append(combinedReport.Failed, report.Failed...)
343+
if len(combinedReport.Steps) == 0 {
344+
combinedReport.Steps = report.Steps
345+
} else {
346+
for j := range combinedReport.Steps {
347+
if j < len(report.Steps) {
348+
combinedReport.Steps[j].Tested += report.Steps[j].Tested
349+
combinedReport.Steps[j].Passed += report.Steps[j].Passed
350+
combinedReport.Steps[j].Failed += report.Steps[j].Failed
351+
combinedReport.Steps[j].Seconds += report.Steps[j].Seconds
352+
}
353+
}
354+
}
355+
356+
// Save after each batch
357+
saveResults(combinedReport)
358+
totalPassed := len(allPassed) + len(combinedReport.Passed)
359+
fmt.Fprintf(os.Stderr, " %s✔ Batch %d done — %d passed so far — saved to %s%s\n",
360+
colorGreen, batchNum, totalPassed, outputFile, colorReset)
361+
}
362+
363+
totalTime := time.Since(scanStart)
364+
if ctx.Err() != nil {
365+
fmt.Fprintf(os.Stderr, "\n %s⚠ Interrupted — partial results saved to %s%s\n", colorYellow, outputFile, colorReset)
366+
}
367+
printSummary(combinedReport, topN, totalTime, domain)
368+
totalPassed := len(allPassed) + len(combinedReport.Passed)
369+
fmt.Fprintf(os.Stderr, " %s✔ IP list written to %s (%d IPs)%s\n", colorGreen, outputIPs, totalPassed, colorReset)
370+
return nil
371+
}
372+
373+
// No batching — scan all at once
269374
report := scanner.RunChainQuietCtx(ctx, ips, workers, steps, newScanProgressFactory(len(steps), stepDescriptions))
270375
totalTime := time.Since(scanStart)
271376

@@ -274,19 +379,11 @@ outputIPs, _ := cmd.Flags().GetString("output-ips")
274379
}
275380

276381
printSummary(report, topN, totalTime, domain)
382+
saveResults(report)
277383

278-
if err := scanner.WriteChainReport(report, outputFile); err != nil {
279-
return err
280-
}
281-
// Auto-generate _ips.txt alongside JSON (same as TUI behavior)
282-
if outputIPs == "" && len(report.Passed) > 0 {
283-
outputIPs = strings.TrimSuffix(outputFile, ".json") + "_ips.txt"
284-
}
285-
if outputIPs != "" && len(report.Passed) > 0 {
286-
if err := scanner.WriteIPList(report.Passed, outputIPs); err != nil {
287-
return fmt.Errorf("writing IP list: %w", err)
288-
}
289-
fmt.Fprintf(os.Stderr, " %s✔ IP list written to %s (%d IPs)%s\n", colorGreen, outputIPs, len(report.Passed), colorReset)
384+
totalPassed := len(allPassed) + len(report.Passed)
385+
if totalPassed > 0 {
386+
fmt.Fprintf(os.Stderr, " %s✔ IP list written to %s (%d IPs)%s\n", colorGreen, outputIPs, totalPassed, colorReset)
290387
}
291388
return nil
292389
}

internal/scanner/chain.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,16 @@ func WriteChainReport(report ChainReport, path string) error {
165165
}
166166
return os.WriteFile(path, data, 0644)
167167
}
168+
169+
// LoadChainReport reads a previously saved ChainReport from disk.
170+
func LoadChainReport(path string) (ChainReport, error) {
171+
raw, err := os.ReadFile(path)
172+
if err != nil {
173+
return ChainReport{}, err
174+
}
175+
var report ChainReport
176+
if err := json.Unmarshal(raw, &report); err != nil {
177+
return ChainReport{}, err
178+
}
179+
return report, nil
180+
}

internal/scanner/doh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func dohDnsttCheck(bin, domain, pubkey string, ports chan int) CheckFunc {
237237
ports <- port
238238
}()
239239

240-
if !waitAndTestSOCKS5Auth(ctx, port, exited) {
240+
if !waitAndTestSOCKS5Connect(ctx, port, exited) {
241241
if diagOnce.CompareAndSwap(false, true) {
242242
processExitedEarly := false
243243
select {

0 commit comments

Comments
 (0)