Skip to content

Commit 6e3408f

Browse files
committed
Fix NS delegation, improve e2e reliability, add preflight tunnel check
- Fix NS delegation: query parent zone authoritative nameservers instead of recursive resolvers (which return NXDOMAIN for subdomain delegations) - Increase e2e timeout 10s → 20s, switch test URL HTTPS → HTTP - Add preflight e2e check: test tunnel via 18 resolvers in parallel before scanning. First success wins — handles blocked resolvers. - Improve error messages for blocked-resolver regions (Iran, etc.)
1 parent 814c2a5 commit 6e3408f

11 files changed

Lines changed: 248 additions & 32 deletions

File tree

GUIDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ findns scan -i doh-resolvers.txt -o results.json \
811811
| `--domain` | دامنه تانل (فعال‌سازی تست tunnel/edns) ||
812812
| `--pubkey` | کلید عمومی سرور DNSTT (فعال‌سازی تست e2e) ||
813813
| `--cert` | مسیر فایل گواهی Slipstream ||
814-
| `--test-url` | آدرسی که از طریق تانل تست شود | `https://httpbin.org/ip` |
814+
| `--test-url` | آدرسی که از طریق تانل تست شود | `http://httpbin.org/ip` |
815815
| `--proxy-auth` | احراز هویت پروکسی SOCKS به صورت `user:pass` (برای تست e2e) ||
816816
| `--doh` | حالت DoH به جای UDP | `false` |
817817
| `--skip-ping` | رد کردن مرحله ping (مفید اگر ICMP مسدود باشد) | `false` |
@@ -1201,7 +1201,7 @@ findns scan -i resolvers.txt -o results.json \
12011201
dnstt-client -udp 8.8.8.8:53 -pubkey YOUR_KEY t.example.com 127.0.0.1:1080 &
12021202

12031203
# صبر کنید 3 ثانیه، بعد:
1204-
curl -x socks5h://127.0.0.1:1080 https://httpbin.org/ip
1204+
curl -x socks5h://127.0.0.1:1080 http://httpbin.org/ip
12051205

12061206
# اگر جواب آمد = تانل کار می‌کند
12071207
# اگر timeout شد = مشکل از سرور یا resolver
@@ -1314,7 +1314,7 @@ https://dns.quad9.net/dns-query
13141314
| `--timeout` | `-t` | تایم‌اوت هر تلاش (ثانیه) | `3` |
13151315
| `--count` | `-c` | تعداد تلاش برای هر IP | `3` |
13161316
| `--workers` || تعداد workerهای موازی | `50` |
1317-
| `--e2e-timeout` || تایم‌اوت تست‌های e2e (ثانیه) | `10` |
1317+
| `--e2e-timeout` || تایم‌اوت تست‌های e2e (ثانیه) | `20` |
13181318
| `--include-failed` || IPهای فیل‌شده از ورودی JSON را هم اسکن کن | `false` |
13191319

13201320
**تنظیم workers:**

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ findns scan -i resolvers.txt -o results.json --domain t.example.com
248248
| `--domain` | Tunnel domain (enables tunnel/e2e steps) ||
249249
| `--pubkey` | DNSTT server public key (enables e2e test) ||
250250
| `--cert` | Slipstream cert path (enables Slipstream e2e) ||
251-
| `--test-url` | URL to fetch through tunnel for e2e test | `https://httpbin.org/ip` |
251+
| `--test-url` | URL to fetch through tunnel for e2e test | `http://httpbin.org/ip` |
252252
| `--proxy-auth` | SOCKS proxy auth as `user:pass` (for e2e tests) ||
253253
| `--doh` | Scan DoH resolvers instead of UDP | `false` |
254254
| `--edns` | Include EDNS payload size check | `false` |
@@ -481,7 +481,7 @@ Step format: `type:key=val,key=val`. Optional params: `count`, `timeout`.
481481
| `--timeout` | `-t` | Timeout per attempt (seconds) | 3 |
482482
| `--count` | `-c` | Attempts per IP/URL | 3 |
483483
| `--workers` | | Concurrent workers | 50 |
484-
| `--e2e-timeout` | | Timeout for e2e tests (seconds) | 10 |
484+
| `--e2e-timeout` | | Timeout for e2e tests (seconds) | 20 |
485485
| `--include-failed` | | Also scan failed entries from JSON input | false |
486486

487487
---
@@ -876,7 +876,7 @@ findns tui
876876
| `--domain` | دامنه تانل (فعال‌سازی تست تانل/e2e) ||
877877
| `--pubkey` | کلید عمومی سرور DNSTT (فعال‌سازی تست e2e) ||
878878
| `--cert` | مسیر گواهی Slipstream (فعال‌سازی تست Slipstream) ||
879-
| `--test-url` | آدرس برای تست اتصال e2e | `https://httpbin.org/ip` |
879+
| `--test-url` | آدرس برای تست اتصال e2e | `http://httpbin.org/ip` |
880880
| `--proxy-auth` | احراز هویت پروکسی SOCKS به صورت `user:pass` (برای تست e2e) ||
881881
| `--doh` | اسکن DoH به جای UDP | `false` |
882882
| `--edns` | فعال‌سازی تست سایز EDNS payload | `false` |
@@ -1164,7 +1164,7 @@ findns chain -i doh-resolvers.txt -o result.json \
11641164
| `--timeout` | `-t` | تایم‌اوت هر تلاش (ثانیه) | 3 |
11651165
| `--count` | `-c` | تعداد تلاش برای هر IP/URL | 3 |
11661166
| `--workers` | | تعداد workerهای موازی | 50 |
1167-
| `--e2e-timeout` | | تایم‌اوت تست‌های e2e (ثانیه) | 10 |
1167+
| `--e2e-timeout` | | تایم‌اوت تست‌های e2e (ثانیه) | 20 |
11681168
| `--include-failed` | | اسکن IPهای فیل‌شده از ورودی JSON | false |
11691169

11701170
---

cmd/chain.go

Lines changed: 3 additions & 3 deletions
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-
testURL := "https://httpbin.org/ip"
103+
testURL := "http://httpbin.org/ip"
104104
if v, ok := cfg.params["test-url"]; ok {
105105
testURL = v
106106
}
@@ -113,7 +113,7 @@ func buildStep(cfg stepConfig, defaultTimeout, defaultCount int, ports chan int,
113113
return scanner.Step{}, fmt.Errorf("step %q: missing required param 'domain'", cfg.name)
114114
}
115115
cert := cfg.params["cert"]
116-
testURL := "https://httpbin.org/ip"
116+
testURL := "http://httpbin.org/ip"
117117
if v, ok := cfg.params["test-url"]; ok {
118118
testURL = v
119119
}
@@ -153,7 +153,7 @@ func buildStep(cfg stepConfig, defaultTimeout, defaultCount int, ports chan int,
153153
if !ok || pubkey == "" {
154154
return scanner.Step{}, fmt.Errorf("step %q: missing required param 'pubkey'", cfg.name)
155155
}
156-
testURL := "https://httpbin.org/ip"
156+
testURL := "http://httpbin.org/ip"
157157
if v, ok := cfg.params["test-url"]; ok {
158158
testURL = v
159159
}

cmd/doh_e2e.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var dohE2ECmd = &cobra.Command{
2020
func init() {
2121
dohE2ECmd.Flags().String("domain", "", "DNSTT tunnel domain")
2222
dohE2ECmd.Flags().String("pubkey", "", "DNSTT server public key")
23-
dohE2ECmd.Flags().String("test-url", "https://httpbin.org/ip", "URL to fetch through tunnel")
23+
dohE2ECmd.Flags().String("test-url", "http://httpbin.org/ip", "URL to fetch through tunnel")
2424
dohE2ECmd.Flags().String("proxy-auth", "", "SOCKS proxy auth as user:pass")
2525
dohE2ECmd.MarkFlagRequired("domain")
2626
dohE2ECmd.MarkFlagRequired("pubkey")

cmd/e2e_dnstt.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var e2eDnsttCmd = &cobra.Command{
2020
func init() {
2121
e2eDnsttCmd.Flags().String("domain", "", "DNSTT tunnel domain")
2222
e2eDnsttCmd.Flags().String("pubkey", "", "DNSTT server public key")
23-
e2eDnsttCmd.Flags().String("test-url", "https://httpbin.org/ip", "URL to fetch through tunnel")
23+
e2eDnsttCmd.Flags().String("test-url", "http://httpbin.org/ip", "URL to fetch through tunnel")
2424
e2eDnsttCmd.Flags().String("proxy-auth", "", "SOCKS proxy auth as user:pass")
2525
e2eDnsttCmd.MarkFlagRequired("domain")
2626
e2eDnsttCmd.MarkFlagRequired("pubkey")

cmd/e2e_slipstream.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var e2eSlipstreamCmd = &cobra.Command{
2020
func init() {
2121
e2eSlipstreamCmd.Flags().String("domain", "", "Slipstream tunnel domain")
2222
e2eSlipstreamCmd.Flags().String("cert", "", "path to Slipstream certificate for cert pinning (optional)")
23-
e2eSlipstreamCmd.Flags().String("test-url", "https://httpbin.org/ip", "URL to fetch through tunnel")
23+
e2eSlipstreamCmd.Flags().String("test-url", "http://httpbin.org/ip", "URL to fetch through tunnel")
2424
e2eSlipstreamCmd.Flags().String("proxy-auth", "", "SOCKS proxy auth as user:pass")
2525
e2eSlipstreamCmd.MarkFlagRequired("domain")
2626
e2eCmd.AddCommand(e2eSlipstreamCmd)

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func init() {
4545
rootCmd.PersistentFlags().IntVar(&workers, "workers", 50, "concurrent workers")
4646
rootCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", 3, "timeout per attempt in seconds")
4747
rootCmd.PersistentFlags().IntVarP(&count, "count", "c", 3, "number of attempts per IP for ping/resolve checks")
48-
rootCmd.PersistentFlags().IntVar(&e2eTimeout, "e2e-timeout", 10, "timeout for e2e tunnel tests in seconds")
48+
rootCmd.PersistentFlags().IntVar(&e2eTimeout, "e2e-timeout", 20, "timeout for e2e tunnel tests in seconds")
4949
rootCmd.SilenceUsage = true
5050
}
5151

cmd/scan.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func init() {
5858
scanCmd.Flags().String("domain", "", "tunnel domain (required for tunnel/edns/e2e steps)")
5959
scanCmd.Flags().String("pubkey", "", "DNSTT public key (enables e2e test)")
6060
scanCmd.Flags().String("cert", "", "Slipstream cert path (enables slipstream e2e test)")
61-
scanCmd.Flags().String("test-url", "https://httpbin.org/ip", "URL to test through tunnel")
61+
scanCmd.Flags().String("test-url", "http://httpbin.org/ip", "URL to test through tunnel")
6262
scanCmd.Flags().String("proxy-auth", "", "SOCKS proxy auth as user:pass (for e2e tests)")
6363
scanCmd.Flags().Bool("doh", false, "scan DoH resolvers instead of UDP")
6464
scanCmd.Flags().Bool("skip-ping", false, "skip ICMP ping step")
@@ -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
}
@@ -242,7 +242,22 @@ func printPreFlight(ipCount int, domain, dnsttBin, slipstreamBin string, steps [
242242
fmt.Fprintf(w, " %s\u2718%s Domain: %s%s%s — %sNS delegation NOT found!%s\n",
243243
colorRed, colorReset, colorCyan, domain, colorReset, colorRed, colorReset)
244244
fmt.Fprintf(w, " %sTunnel/e2e steps will likely fail. Verify your DNS setup:%s\n", colorDim, colorReset)
245-
fmt.Fprintf(w, " %snslookup -type=NS %s 8.8.8.8%s\n", colorDim, domain, colorReset)
245+
fmt.Fprintf(w, " %sdig NS %s @8.8.8.8 (or check your registrar/Cloudflare dashboard)%s\n", colorDim, domain, colorReset)
246+
}
247+
}
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+
preflightTimeout := time.Duration(e2eTimeout) * time.Second
252+
result := scanner.PreflightE2E(dnsttBin, domain, pubkey, testURL, proxyAuth, preflightTimeout)
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)
246261
}
247262
}
248263
fmt.Fprintf(w, "\n")

internal/scanner/dns.go

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scanner
33
import (
44
"context"
55
"net"
6+
"strings"
67
"time"
78

89
"github.com/miekg/dns"
@@ -152,27 +153,102 @@ func QueryNSMulti(domain string, timeout time.Duration) ([]string, bool) {
152153
}
153154

154155
func QueryNS(resolver, domain string, timeout time.Duration) ([]string, bool) {
156+
// Strategy 1: direct NS query — works when the recursive resolver returns
157+
// the delegation NS in Answer or Authority.
155158
r, ok := queryRaw(resolver, domain, dns.TypeNS, timeout)
156-
if !ok {
159+
if ok {
160+
var hosts []string
161+
for _, ans := range r.Answer {
162+
if ns, ok := ans.(*dns.NS); ok {
163+
hosts = append(hosts, ns.Ns)
164+
}
165+
}
166+
if len(hosts) == 0 {
167+
for _, ans := range r.Ns {
168+
if ns, ok := ans.(*dns.NS); ok {
169+
hosts = append(hosts, ns.Ns)
170+
}
171+
}
172+
}
173+
if len(hosts) > 0 {
174+
return hosts, true
175+
}
176+
}
177+
178+
// Strategy 2: query the parent zone's authoritative nameservers directly.
179+
// For "t.example.com", find NS of "example.com", then ask those servers
180+
// for NS of "t.example.com". This is how subdomain delegation actually
181+
// works in the DNS hierarchy.
182+
parent := parentZone(domain)
183+
if parent == "" {
157184
return nil, false
158185
}
159-
var hosts []string
160-
// Check Answer section first
161-
for _, ans := range r.Answer {
186+
// Get parent zone NS from the resolver
187+
pr, pok := queryRaw(resolver, parent, dns.TypeNS, timeout)
188+
if !pok {
189+
return nil, false
190+
}
191+
var parentNS []string
192+
for _, ans := range pr.Answer {
162193
if ns, ok := ans.(*dns.NS); ok {
163-
hosts = append(hosts, ns.Ns)
194+
parentNS = append(parentNS, ns.Ns)
164195
}
165196
}
166-
// For subdomain delegations, NS records are often in the Authority section
167-
if len(hosts) == 0 {
168-
for _, ans := range r.Ns {
197+
if len(parentNS) == 0 {
198+
return nil, false
199+
}
200+
201+
// Resolve the first parent NS to an IP and query it directly
202+
for _, nsHost := range parentNS {
203+
nsHost = strings.TrimSuffix(nsHost, ".")
204+
// Resolve the NS hostname to an IP via the same resolver
205+
ar, aok := queryRaw(resolver, nsHost, dns.TypeA, timeout)
206+
if !aok {
207+
continue
208+
}
209+
var nsIP string
210+
for _, ans := range ar.Answer {
211+
if a, ok := ans.(*dns.A); ok {
212+
nsIP = a.A.String()
213+
break
214+
}
215+
}
216+
if nsIP == "" {
217+
continue
218+
}
219+
// Ask the parent's authoritative NS for the subdomain's NS records
220+
dr, dok := queryRaw(nsIP, domain, dns.TypeNS, timeout)
221+
if !dok {
222+
continue
223+
}
224+
var hosts []string
225+
for _, ans := range dr.Answer {
169226
if ns, ok := ans.(*dns.NS); ok {
170227
hosts = append(hosts, ns.Ns)
171228
}
172229
}
230+
if len(hosts) == 0 {
231+
for _, ans := range dr.Ns {
232+
if ns, ok := ans.(*dns.NS); ok {
233+
hosts = append(hosts, ns.Ns)
234+
}
235+
}
236+
}
237+
if len(hosts) > 0 {
238+
return hosts, true
239+
}
173240
}
174-
if len(hosts) == 0 {
175-
return nil, false
241+
return nil, false
242+
}
243+
244+
// parentZone returns the parent zone of a domain.
245+
// e.g. "t.example.com" → "example.com", "example.com" → "com"
246+
// Returns "" if the domain has no parent (is a TLD or empty).
247+
func parentZone(domain string) string {
248+
domain = strings.TrimSuffix(domain, ".")
249+
parts := strings.SplitN(domain, ".", 2)
250+
if len(parts) < 2 || parts[1] == "" {
251+
return ""
176252
}
177-
return hosts, true
253+
return parts[1]
178254
}

0 commit comments

Comments
 (0)