Skip to content

Commit f01110c

Browse files
Close TCP Conn on error (#605)
* close conn on timeout on tcp conns to prevent poison * move close conn to all errors on TCP conn * added cache poison test * add end comment * guard the conn close with a nil check --------- Co-authored-by: Zakir Durumeric <zakird@gmail.com>
1 parent d59a213 commit f01110c

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

src/zdns/lookup.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,12 @@ func wireLookupTCP(ctx context.Context, connInfo *ConnectionInfo, q Question, na
11481148
}
11491149
if err != nil || r == nil {
11501150
if nerr, ok := err.(net.Error); ok {
1151+
// Close the TCP connection on Error to prevent latent replies poisoning the connection.
1152+
// See github.com/zmap/zdns/issues/602
1153+
if connInfo != nil && connInfo.tcpConn != nil {
1154+
connInfo.tcpConn.Close()
1155+
connInfo.tcpConn = nil
1156+
}
11511157
if nerr.Timeout() {
11521158
return &res, r, StatusTimeout, nil
11531159
}

src/zdns/lookup_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ package zdns
1515

1616
import (
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

Comments
 (0)