@@ -15,15 +15,19 @@ package zdns
1515
1616import (
1717 "context"
18+ "encoding/binary"
1819 "encoding/hex"
1920 "fmt"
21+ "io"
2022 "math/rand"
2123 "net"
2224 "reflect"
2325 "regexp"
2426 "slices"
2527 "sort"
28+ "strconv"
2629 "strings"
30+ "sync/atomic"
2731 "testing"
2832 "time"
2933
@@ -2117,3 +2121,131 @@ func verifyCombinedResult(t *testing.T, records map[string][]ExtendedResult, exp
21172121 t .Errorf ("Combined result not matching, expected %v, found %v" , expectedRecords , records )
21182122 }
21192123}
2124+
2125+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2126+ // TCP Connection Poison Tests
2127+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2128+
2129+ // startAlternatingDelayTCPServer starts a TCP DNS server on a random port. Odd-numbered
2130+ // queries (globally, across all connections) are answered only after delay, causing the
2131+ // client to time out. Even-numbered queries are answered immediately. The global counter
2132+ // is atomic so concurrent connection goroutines are safe.
2133+ func startAlternatingDelayTCPServer (t * testing.T , delay time.Duration ) (addr string ) {
2134+ t .Helper ()
2135+ ln , err := net .Listen ("tcp" , "127.0.0.1:0" )
2136+ require .NoError (t , err )
2137+ t .Cleanup (func () { ln .Close () })
2138+
2139+ var queryCount atomic.Int32
2140+ go func () {
2141+ for {
2142+ conn , err := ln .Accept ()
2143+ if err != nil {
2144+ return
2145+ }
2146+ go serveAlternatingDelayConn (conn , & queryCount , delay )
2147+ }
2148+ }()
2149+ return ln .Addr ().String ()
2150+ }
2151+
2152+ func serveAlternatingDelayConn (conn net.Conn , queryCount * atomic.Int32 , delay time.Duration ) {
2153+ defer conn .Close ()
2154+ for {
2155+ var msgLen uint16
2156+ if err := binary .Read (conn , binary .BigEndian , & msgLen ); err != nil {
2157+ return
2158+ }
2159+ buf := make ([]byte , msgLen )
2160+ if _ , err := io .ReadFull (conn , buf ); err != nil {
2161+ return
2162+ }
2163+ req := new (dns.Msg )
2164+ if err := req .Unpack (buf ); err != nil {
2165+ return
2166+ }
2167+
2168+ n := queryCount .Add (1 )
2169+ if n % 2 == 1 {
2170+ // Odd query: sleep past the client's timeout so the reply arrives late.
2171+ time .Sleep (delay )
2172+ }
2173+
2174+ resp := new (dns.Msg )
2175+ resp .SetReply (req )
2176+ resp .Answer = []dns.RR {
2177+ & dns.A {
2178+ Hdr : dns.RR_Header {
2179+ Name : req .Question [0 ].Name ,
2180+ Rrtype : dns .TypeA ,
2181+ Class : dns .ClassINET ,
2182+ Ttl : 60 ,
2183+ },
2184+ A : net .ParseIP ("192.0.2.1" ),
2185+ },
2186+ }
2187+ packed , err := resp .Pack ()
2188+ if err != nil {
2189+ return
2190+ }
2191+ var lenBuf [2 ]byte
2192+ binary .BigEndian .PutUint16 (lenBuf [:], uint16 (len (packed )))
2193+ if _ , err := conn .Write (append (lenBuf [:], packed ... )); err != nil {
2194+ return
2195+ }
2196+ }
2197+ }
2198+
2199+ // TestTCPConnNotPoisonedOnTimeout verifies that a TCP connection is not reused after a
2200+ // timeout (GitHub issue #602). Without the fix, the server's late reply to a timed-out
2201+ // query stays buffered on the recycled connection and is mis-read as the reply to the
2202+ // next query, producing StatusError instead of StatusNoError.
2203+ //
2204+ // The test server alternates: odd queries (1, 3) delay past the client timeout, even
2205+ // queries (2, 4) respond immediately. With socket recycling enabled, the pre-fix code
2206+ // would recycle the poisoned connection and the even queries would get ID-mismatched
2207+ // replies. The fix closes the connection on any timeout so the next query reconnects.
2208+ func TestTCPConnNotPoisonedOnTimeout (t * testing.T ) {
2209+ const networkTimeout = 200 * time .Millisecond
2210+ const serverDelay = 2 * networkTimeout
2211+
2212+ addr := startAlternatingDelayTCPServer (t , serverDelay )
2213+
2214+ ip , portStr , err := net .SplitHostPort (addr )
2215+ require .NoError (t , err )
2216+ port , err := strconv .Atoi (portStr )
2217+ require .NoError (t , err )
2218+ ns := NameServer {IP : net .ParseIP (ip ), Port : uint16 (port )}
2219+
2220+ config := NewResolverConfig ()
2221+ config .TransportMode = TCPOnly
2222+ config .ShouldRecycleSockets = true
2223+ config .NetworkTimeout = networkTimeout
2224+ config .Timeout = 5 * time .Second
2225+ config .Retries = 0
2226+ config .IPVersionMode = IPv4Only
2227+ config .LocalAddrsV4 = []net.IP {net .ParseIP ("127.0.0.1" )}
2228+ config .ExternalNameServersV4 = []NameServer {ns }
2229+ config .RootNameServersV4 = []NameServer {ns }
2230+ config .LookupClient = LookupClient {}
2231+
2232+ resolver , err := InitResolver (config )
2233+ require .NoError (t , err )
2234+ defer resolver .Close ()
2235+
2236+ names := []string {"a.example" , "b.example" , "c.example" , "d.example" }
2237+ statuses := make ([]Status , len (names ))
2238+ for i , name := range names {
2239+ q := Question {Name : name , Type : dns .TypeA , Class : dns .ClassINET }
2240+ _ , _ , statuses [i ], _ = resolver .ExternalLookup (context .Background (), & q , & ns )
2241+ }
2242+
2243+ require .Equal (t , StatusTimeout , statuses [0 ], "query 1 (odd): expected timeout" )
2244+ require .Equal (t , StatusNoError , statuses [1 ], "query 2 (even): expected success, not poisoned by query 1 timeout" )
2245+ require .Equal (t , StatusTimeout , statuses [2 ], "query 3 (odd): expected timeout" )
2246+ require .Equal (t , StatusNoError , statuses [3 ], "query 4 (even): expected success, not poisoned by query 3 timeout" )
2247+ }
2248+
2249+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2250+ // END TCP Connection Poison Tests
2251+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0 commit comments