Skip to content

Commit ee4bc57

Browse files
committed
Fix EDNS0 causing false NXDOMAIN and fix 12 scanner bugs
Root cause fix: queryRaw() unconditionally sets EDNS0 OPT on all DNS queries. Some servers (e.g. dnstm) return NXDOMAIN instead of FORMERR when they don't understand EDNS0. Refactored retry logic to strip EDNS0 on ANY non-success Rcode, with full fallback chain: UDP+EDNS0 → UDP-EDNS0 → TCP+EDNS0 → TCP-EDNS0 → TCP ednsRetry. Other fixes: - cmd/scan.go: add --edns flag (opt-in), improve 0% diagnostic hints - cmd/doh_tunnel.go: fix subcommand registration (was dohResolveCmd) - check.go: fix PingCheck deadline calc, remove maxConsecFail early exit - doh.go: fix port pool leak on Start() failure, add HTTP client timeouts - e2e.go: fix port pool leak, consolidate defer (kill→wait→sleep→return) - edns.go: handle FORMERR with EDNS0 retry - input.go: deduplicate IPs, strip inline comments - nxdomain.go: fix hijack detection, return metrics on both paths - worker.go: buffer results channel to prevent goroutine leaks
1 parent 475be78 commit ee4bc57

10 files changed

Lines changed: 132 additions & 73 deletions

File tree

cmd/doh_tunnel.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var dohTunnelCmd = &cobra.Command{
2020
func init() {
2121
dohTunnelCmd.Flags().String("domain", "", "tunnel domain to check NS for")
2222
dohTunnelCmd.MarkFlagRequired("domain")
23-
dohResolveCmd.AddCommand(dohTunnelCmd)
23+
dohCmd.AddCommand(dohTunnelCmd)
2424
}
2525

2626
func runDoHTunnel(cmd *cobra.Command, args []string) error {

cmd/scan.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var stepDescriptions = map[string]string{
3939

4040
var scanCmd = &cobra.Command{
4141
Use: "scan",
42-
Short: "Full scan pipeline: ping -> resolve -> nxdomain -> tunnel -> e2e",
42+
Short: "Full scan pipeline: ping -> resolve -> nxdomain -> tunnel -> e2e (use --edns to add EDNS check)",
4343
Long: `Run a complete resolver scan with all checks in sequence.
4444
This is the recommended way to find working resolvers for DNS tunneling.
4545
@@ -63,6 +63,7 @@ func init() {
6363
scanCmd.Flags().Bool("doh", false, "scan DoH resolvers instead of UDP")
6464
scanCmd.Flags().Bool("skip-ping", false, "skip ICMP ping step")
6565
scanCmd.Flags().Bool("skip-nxdomain", false, "skip NXDOMAIN hijack check")
66+
scanCmd.Flags().Bool("edns", false, "include EDNS payload size check (filters resolvers that don't support EDNS)")
6667
scanCmd.Flags().Int("top", 10, "number of top results to display")
6768
rootCmd.AddCommand(scanCmd)
6869
}
@@ -76,6 +77,7 @@ func runScan(cmd *cobra.Command, args []string) error {
7677
dohMode, _ := cmd.Flags().GetBool("doh")
7778
skipPing, _ := cmd.Flags().GetBool("skip-ping")
7879
skipNXD, _ := cmd.Flags().GetBool("skip-nxdomain")
80+
ednsMode, _ := cmd.Flags().GetBool("edns")
7981
topN, _ := cmd.Flags().GetInt("top")
8082

8183
if outputFile == "" {
@@ -153,10 +155,12 @@ func runScan(cmd *cobra.Command, args []string) error {
153155
})
154156
}
155157
if domain != "" {
156-
steps = append(steps, scanner.Step{
157-
Name: "edns", Timeout: dur,
158-
Check: scanner.EDNSCheck(domain, count), SortBy: "edns_max",
159-
})
158+
if ednsMode {
159+
steps = append(steps, scanner.Step{
160+
Name: "edns", Timeout: dur,
161+
Check: scanner.EDNSCheck(domain, count), SortBy: "edns_max",
162+
})
163+
}
160164
steps = append(steps, scanner.Step{
161165
Name: "resolve/tunnel", Timeout: dur,
162166
Check: scanner.TunnelCheck(domain, count), SortBy: "resolve_ms",
@@ -320,9 +324,11 @@ func printSummary(report scanner.ChainReport, topN int, totalTime time.Duration,
320324
switch step.Name {
321325
case "resolve/tunnel", "doh/resolve/tunnel":
322326
fmt.Fprintf(w, "\n %s\u26a0 Hint: resolve/tunnel had 0%% pass rate.%s\n", colorYellow, colorReset)
323-
fmt.Fprintf(w, " %sThis usually means your tunnel domain's NS delegation is not set up correctly.%s\n", colorDim, colorReset)
324-
fmt.Fprintf(w, " %sVerify with: nslookup -type=NS <your-domain> 8.8.8.8%s\n", colorDim, colorReset)
325-
fmt.Fprintf(w, " %sYou need NS + glue A records pointing to your DNSTT server.%s\n", colorDim, colorReset)
327+
fmt.Fprintf(w, " %sPossible causes:%s\n", colorDim, colorReset)
328+
fmt.Fprintf(w, " %s 1. NS delegation not set up: nslookup -type=NS <your-domain> 8.8.8.8%s\n", colorDim, colorReset)
329+
fmt.Fprintf(w, " %s You need NS + glue A records pointing to your server.%s\n", colorDim, colorReset)
330+
fmt.Fprintf(w, " %s 2. Server returns NXDOMAIN: delegation works but dnstt-server/dnstm is misconfigured.%s\n", colorDim, colorReset)
331+
fmt.Fprintf(w, " %s Check: cat /etc/dnstm/config.json | journalctl -u dnstm-dnsrouter -n 20%s\n", colorDim, colorReset)
326332
fmt.Fprintf(w, " %sSee: https://github.com/SamNet-dev/findns/blob/main/GUIDE.md#-تنظیم-دامنه-تانل-مهم--قبل-از-اسکن-بخوانید%s\n", colorDim, colorReset)
327333
case "ping":
328334
fmt.Fprintf(w, "\n %s\u26a0 Hint: ping had 0%% pass rate. Try --skip-ping (ICMP may be blocked).%s\n", colorYellow, colorReset)

internal/scanner/check.go

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
"time"
1212
)
1313

14-
const maxConsecFail = 3
15-
1614
// Regex for Linux/macOS: "rtt min/avg/max/mdev = 4.123/5.456/6.789/..."
1715
var pingAvgRegex = regexp.MustCompile(`= [\d.]+/([\d.]+)/`)
1816

@@ -59,7 +57,7 @@ func PingCheck(count int) CheckFunc {
5957
if secs < 1 {
6058
secs = 1
6159
}
62-
deadline := count + secs
60+
deadline := count*secs + 2
6361
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deadline+2)*time.Second)
6462
defer cancel()
6563

@@ -77,19 +75,12 @@ func PingCheck(count int) CheckFunc {
7775
func ResolveCheck(domain string, count int) CheckFunc {
7876
return func(ip string, timeout time.Duration) (bool, Metrics) {
7977
var successes []float64
80-
var consecFail int
8178

8279
for i := 0; i < count; i++ {
8380
start := time.Now()
8481
if QueryA(ip, domain, timeout) {
8582
ms := float64(time.Since(start).Microseconds()) / 1000.0
8683
successes = append(successes, ms)
87-
consecFail = 0
88-
} else {
89-
consecFail++
90-
if consecFail >= maxConsecFail {
91-
return false, nil
92-
}
9384
}
9485
}
9586

@@ -108,34 +99,24 @@ func ResolveCheck(domain string, count int) CheckFunc {
10899
func TunnelCheck(domain string, count int) CheckFunc {
109100
return func(ip string, timeout time.Duration) (bool, Metrics) {
110101
var successes []float64
111-
var consecFail int
112102

113103
for i := 0; i < count; i++ {
114104
start := time.Now()
115105

116106
// Step 1: Query NS for the tunnel domain
117107
hosts, ok := QueryNS(ip, domain, timeout)
118108
if !ok || len(hosts) == 0 {
119-
consecFail++
120-
if consecFail >= maxConsecFail {
121-
return false, nil
122-
}
123109
continue
124110
}
125111

126112
// Step 2: Resolve the first NS hostname to verify glue record
127113
nsHost := strings.TrimRight(hosts[0], ".")
128114
if !QueryA(ip, nsHost, timeout) {
129-
consecFail++
130-
if consecFail >= maxConsecFail {
131-
return false, nil
132-
}
133115
continue
134116
}
135117

136118
ms := float64(time.Since(start).Microseconds()) / 1000.0
137119
successes = append(successes, ms)
138-
consecFail = 0
139120
}
140121

141122
if len(successes) == 0 {

internal/scanner/dns.go

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,73 @@ func queryRaw(resolver, domain string, qtype uint16, timeout time.Duration) (*dn
2222
c.Net = "udp"
2323
c.Timeout = timeout
2424

25-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
26-
defer cancel()
25+
// Use a deadline so all retries share a generous overall budget
26+
deadline := time.Now().Add(timeout * 2)
2727

28+
remaining := func() time.Duration {
29+
d := time.Until(deadline)
30+
if d < 500*time.Millisecond {
31+
return 500 * time.Millisecond
32+
}
33+
return d
34+
}
35+
36+
ctx, cancel := context.WithTimeout(context.Background(), remaining())
2837
r, _, err := c.ExchangeContext(ctx, m, addr)
38+
cancel()
2939

30-
// If EDNS0 caused FORMERR, retry without it
31-
if err == nil && r != nil && r.Rcode == dns.RcodeFormatError {
32-
m.Extra = nil // strip EDNS0 OPT record
33-
r, _, err = c.ExchangeContext(ctx, m, addr)
40+
// ednsRetry strips the EDNS0 OPT record and retries the query.
41+
// Returns true if the retry produced a better response.
42+
ednsRetry := func() bool {
43+
savedExtra := m.Extra
44+
m.Extra = nil
45+
ctx, cancel = context.WithTimeout(context.Background(), remaining())
46+
r2, _, err2 := c.ExchangeContext(ctx, m, addr)
47+
cancel()
48+
if err2 == nil && r2 != nil {
49+
r, err = r2, nil
50+
return true // EDNS0 was the problem; keep it stripped
51+
}
52+
m.Extra = savedExtra // retry didn't help; restore
53+
return false
54+
}
55+
56+
// If EDNS0 caused an error response, retry without it.
57+
// Some servers (e.g. dnstm) return NXDOMAIN instead of FORMERR
58+
// when they don't understand the OPT record.
59+
if err == nil && r != nil && r.Rcode != dns.RcodeSuccess {
60+
ednsRetry()
3461
}
3562

3663
// If UDP failed entirely, try TCP before giving up
3764
if err != nil || r == nil {
3865
c.Net = "tcp"
66+
ctx, cancel = context.WithTimeout(context.Background(), remaining())
3967
r, _, err = c.ExchangeContext(ctx, m, addr)
68+
cancel()
4069
if err != nil || r == nil {
41-
return nil, false
70+
// TCP with EDNS0 also failed; last resort: TCP without EDNS0
71+
m.Extra = nil
72+
ctx, cancel = context.WithTimeout(context.Background(), remaining())
73+
r, _, err = c.ExchangeContext(ctx, m, addr)
74+
cancel()
75+
if err != nil || r == nil {
76+
return nil, false
77+
}
78+
}
79+
// TCP succeeded but got error Rcode; try without EDNS0
80+
// Skip if EDNS0 was already stripped (m.Extra is nil from line 71)
81+
if r != nil && r.Rcode != dns.RcodeSuccess && len(m.Extra) > 0 {
82+
ednsRetry()
4283
}
4384
}
4485

4586
// Retry over TCP if response was truncated
4687
if r.Truncated {
4788
c.Net = "tcp"
89+
ctx, cancel = context.WithTimeout(context.Background(), remaining())
4890
r, _, err = c.ExchangeContext(ctx, m, addr)
91+
cancel()
4992
if err != nil || r == nil {
5093
return nil, false
5194
}

internal/scanner/doh.go

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ import (
1414

1515
var dohHTTPClient = &http.Client{
1616
Transport: &http.Transport{
17-
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
18-
MaxIdleConnsPerHost: 2,
19-
IdleConnTimeout: 30 * time.Second,
17+
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
18+
MaxIdleConnsPerHost: 10,
19+
MaxConnsPerHost: 10,
20+
IdleConnTimeout: 30 * time.Second,
21+
TLSHandshakeTimeout: 10 * time.Second,
22+
ResponseHeaderTimeout: 10 * time.Second,
2023
},
2124
}
2225

@@ -114,19 +117,12 @@ func QueryDoHNS(resolverURL, domain string, timeout time.Duration) ([]string, bo
114117
func DoHResolveCheck(domain string, count int) CheckFunc {
115118
return func(url string, timeout time.Duration) (bool, Metrics) {
116119
var successes []float64
117-
var consecFail int
118120

119121
for i := 0; i < count; i++ {
120122
start := time.Now()
121123
if QueryDoHA(url, domain, timeout) {
122124
ms := float64(time.Since(start).Microseconds()) / 1000.0
123125
successes = append(successes, ms)
124-
consecFail = 0
125-
} else {
126-
consecFail++
127-
if consecFail >= maxConsecFail {
128-
return false, nil
129-
}
130126
}
131127
}
132128

@@ -146,17 +142,12 @@ func DoHResolveCheck(domain string, count int) CheckFunc {
146142
func DoHTunnelCheck(domain string, count int) CheckFunc {
147143
return func(url string, timeout time.Duration) (bool, Metrics) {
148144
var successes []float64
149-
var consecFail int
150145

151146
for i := 0; i < count; i++ {
152147
start := time.Now()
153148

154149
hosts, ok := QueryDoHNS(url, domain, timeout)
155150
if !ok || len(hosts) == 0 {
156-
consecFail++
157-
if consecFail >= maxConsecFail {
158-
return false, nil
159-
}
160151
continue
161152
}
162153

@@ -166,16 +157,11 @@ func DoHTunnelCheck(domain string, count int) CheckFunc {
166157
nsHost = nsHost[:last]
167158
}
168159
if !QueryDoHA(url, nsHost, timeout) {
169-
consecFail++
170-
if consecFail >= maxConsecFail {
171-
return false, nil
172-
}
173160
continue
174161
}
175162

176163
ms := float64(time.Since(start).Microseconds()) / 1000.0
177164
successes = append(successes, ms)
178-
consecFail = 0
179165
}
180166

181167
if len(successes) == 0 {
@@ -211,7 +197,6 @@ func dohDnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan in
211197
case <-ctx.Done():
212198
return false, nil
213199
}
214-
defer func() { ports <- port }()
215200

216201
start := time.Now()
217202

@@ -223,12 +208,15 @@ func dohDnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan in
223208
cmd.Stdout = io.Discard
224209
cmd.Stderr = io.Discard
225210
if err := cmd.Start(); err != nil {
211+
ports <- port
226212
return false, nil
227213
}
214+
// Kill process and wait for cleanup BEFORE returning port
228215
defer func() {
229216
cmd.Process.Kill()
230217
cmd.Wait()
231-
time.Sleep(100 * time.Millisecond)
218+
time.Sleep(300 * time.Millisecond)
219+
ports <- port
232220
}()
233221

234222
// Wait for subprocess to start, but cap at 1/3 of timeout

internal/scanner/e2e.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ func dnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan int)
4343
case <-ctx.Done():
4444
return false, nil
4545
}
46-
defer func() { ports <- port }()
4746

4847
start := time.Now()
4948

@@ -55,13 +54,15 @@ func dnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan int)
5554
cmd.Stdout = io.Discard
5655
cmd.Stderr = io.Discard
5756
if err := cmd.Start(); err != nil {
57+
ports <- port
5858
return false, nil
5959
}
60+
// Kill process and wait for cleanup BEFORE returning port
6061
defer func() {
6162
cmd.Process.Kill()
6263
cmd.Wait()
63-
// Brief pause so OS can release the port before it's reused
64-
time.Sleep(100 * time.Millisecond)
64+
time.Sleep(300 * time.Millisecond)
65+
ports <- port
6566
}()
6667

6768
// Wait for subprocess to start, but cap at 1/3 of timeout
@@ -103,7 +104,6 @@ func slipstreamCheck(bin, domain, certPath, testURL, proxyAuth string, ports cha
103104
case <-ctx.Done():
104105
return false, nil
105106
}
106-
defer func() { ports <- port }()
107107

108108
start := time.Now()
109109

@@ -119,12 +119,15 @@ func slipstreamCheck(bin, domain, certPath, testURL, proxyAuth string, ports cha
119119
cmd.Stdout = io.Discard
120120
cmd.Stderr = io.Discard
121121
if err := cmd.Start(); err != nil {
122+
ports <- port
122123
return false, nil
123124
}
125+
// Kill process and wait for cleanup BEFORE returning port
124126
defer func() {
125127
cmd.Process.Kill()
126128
cmd.Wait()
127-
time.Sleep(100 * time.Millisecond)
129+
time.Sleep(300 * time.Millisecond)
130+
ports <- port
128131
}()
129132

130133
// Wait for subprocess to start, but cap at 1/3 of timeout

internal/scanner/edns.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,26 @@ func testEDNSPayload(resolver, domain string, payload uint16, timeout time.Durat
5555
c.Net = "udp"
5656
c.Timeout = timeout
5757

58+
addr := net.JoinHostPort(resolver, "53")
5859
ctx, cancel := context.WithTimeout(context.Background(), timeout)
59-
r, _, err := c.ExchangeContext(ctx, m, net.JoinHostPort(resolver, "53"))
60-
cancel()
60+
r, _, err := c.ExchangeContext(ctx, m, addr)
6161

6262
if err != nil || r == nil {
63+
cancel()
6364
continue
6465
}
6566

67+
// If EDNS0 caused FORMERR, retry without it
68+
if r.Rcode == dns.RcodeFormatError {
69+
m.Extra = nil
70+
r, _, err = c.ExchangeContext(ctx, m, addr)
71+
if err != nil || r == nil {
72+
cancel()
73+
continue
74+
}
75+
}
76+
cancel()
77+
6678
// NOERROR or NXDOMAIN both count as "resolver handled it"
6779
if r.Rcode == dns.RcodeSuccess || r.Rcode == dns.RcodeNameError {
6880
successes++

0 commit comments

Comments
 (0)