@@ -80,7 +80,6 @@ func DnsttCheck(domain, pubkey, testURL string, ports chan int) CheckFunc {
8080}
8181
8282func dnsttCheck (bin , domain , pubkey , testURL , proxyAuth string , ports chan int ) CheckFunc {
83- testURL = effectiveTestURL (testURL )
8483 var diagOnce atomic.Bool
8584
8685 return func (ip string , timeout time.Duration ) (bool , Metrics ) {
@@ -132,16 +131,17 @@ func dnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan int)
132131 ports <- port
133132 }()
134133
135- if ! waitAndTestSOCKS (ctx , port , testURL , proxyAuth , exited , timeout ) {
134+ // Wait for SOCKS port to open, then do a SOCKS5 handshake through
135+ // the tunnel. This is much faster than spawning curl — we just need
136+ // to verify that data flows bidirectionally through the DNS tunnel.
137+ if ! waitAndTestSOCKS5Auth (ctx , port , exited ) {
136138 if diagOnce .CompareAndSwap (false , true ) {
137- // Check if process exited on its own before we kill it
138139 processExitedEarly := false
139140 select {
140141 case <- exited :
141142 processExitedEarly = true
142143 default :
143144 }
144- // Kill and wait so stderr pipe is fully closed before reading
145145 cmd .Process .Kill ()
146146 select {
147147 case <- exited :
@@ -153,7 +153,7 @@ func dnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan int)
153153 } else if processExitedEarly {
154154 setDiag ("e2e/dnstt first failure (ip=%s): dnstt-client exited early with no stderr" , ip )
155155 } else {
156- setDiag ("e2e/dnstt first failure (ip=%s): curl could not get HTTP 200 through SOCKS within %v (test-url=%s) " , ip , timeout , testURL )
156+ setDiag ("e2e/dnstt first failure (ip=%s): SOCKS5 handshake through tunnel timed out within %v" , ip , timeout )
157157 }
158158 }
159159 return false , nil
@@ -163,6 +163,64 @@ func dnsttCheck(bin, domain, pubkey, testURL, proxyAuth string, ports chan int)
163163 }
164164}
165165
166+ // waitAndTestSOCKS5Auth waits for the SOCKS port to open, then performs a
167+ // SOCKS5 auth handshake. In dnstt, the SOCKS protocol is handled by a proxy
168+ // on the server side — so the auth bytes travel through the DNS tunnel and
169+ // the reply comes back through it. Getting the 2-byte auth reply proves
170+ // bidirectional data flow through the DNS tunnel. This is the minimum
171+ // possible test: 3 bytes up, 2 bytes back, one tunnel round-trip.
172+ func waitAndTestSOCKS5Auth (ctx context.Context , port int , exited <- chan struct {}) bool {
173+ addr := fmt .Sprintf ("127.0.0.1:%d" , port )
174+
175+ // Wait for SOCKS port to start listening.
176+ for {
177+ select {
178+ case <- ctx .Done ():
179+ return false
180+ case <- exited :
181+ return false
182+ default :
183+ }
184+ conn , err := net .DialTimeout ("tcp" , addr , 500 * time .Millisecond )
185+ if err == nil {
186+ conn .Close ()
187+ break
188+ }
189+ select {
190+ case <- ctx .Done ():
191+ return false
192+ case <- exited :
193+ return false
194+ case <- time .After (300 * time .Millisecond ):
195+ }
196+ }
197+
198+ // Send SOCKS5 auth and wait for reply through the tunnel.
199+ // Single attempt — the DNS tunnel round-trip at MTU 50 can take
200+ // 5-10 seconds, so retrying wastes the timeout budget.
201+ d := net.Dialer {}
202+ conn , err := d .DialContext (ctx , "tcp" , addr )
203+ if err != nil {
204+ return false
205+ }
206+ defer conn .Close ()
207+
208+ if deadline , ok := ctx .Deadline (); ok {
209+ conn .SetDeadline (deadline )
210+ }
211+
212+ // SOCKS5 auth: version=5, 1 method, no-auth(0x00)
213+ if _ , err := conn .Write ([]byte {0x05 , 0x01 , 0x00 }); err != nil {
214+ return false
215+ }
216+ authResp := make ([]byte , 2 )
217+ if _ , err := io .ReadFull (conn , authResp ); err != nil {
218+ return false
219+ }
220+ // Any valid SOCKS5 reply (0x05, 0x00) proves the tunnel works.
221+ return authResp [0 ] == 0x05
222+ }
223+
166224// SlipstreamCheckBin is like SlipstreamCheck but uses an explicit binary path.
167225func SlipstreamCheckBin (bin , domain , certPath , testURL , proxyAuth string , ports chan int ) CheckFunc {
168226 return slipstreamCheck (bin , domain , certPath , testURL , proxyAuth , ports )
@@ -372,24 +430,20 @@ func nullDevice() string {
372430func waitAndTestSOCKS (ctx context.Context , port int , testURL , proxyAuth string , exited <- chan struct {}, totalTimeout time.Duration ) bool {
373431 addr := fmt .Sprintf ("127.0.0.1:%d" , port )
374432
375- // Compute per-attempt curl timeout: aim for 3 attempts minimum.
376- // Reserve ~2s for Phase 1, then divide the rest by 3 .
433+ // Compute per-attempt curl timeout. DNS tunnels are slow — give each
434+ // attempt generous time. Aim for 2 attempts; reserve ~3s for Phase 1 .
377435 totalSec := int (totalTimeout .Seconds ())
378- curlMaxTime := (totalSec - 2 ) / 3
379- if curlMaxTime < 3 {
380- curlMaxTime = 3
381- }
382- if curlMaxTime > 8 {
383- curlMaxTime = 8
436+ curlMaxTime := (totalSec - 3 ) / 2
437+ if curlMaxTime < 5 {
438+ curlMaxTime = 5
384439 }
385- // Never exceed the total timeout budget
386- if curlMaxTime > totalSec {
387- curlMaxTime = totalSec
440+ if curlMaxTime > totalSec - 2 {
441+ curlMaxTime = totalSec - 2
388442 }
389443 // connect-timeout should be less than max-time
390- curlConnTimeout := curlMaxTime - 1
391- if curlConnTimeout < 2 {
392- curlConnTimeout = 2
444+ curlConnTimeout := curlMaxTime - 2
445+ if curlConnTimeout < 3 {
446+ curlConnTimeout = 3
393447 }
394448
395449 // Phase 1: wait for SOCKS port to start listening (poll every 300ms).
0 commit comments