fix(dot): validate connections before reuse to prevent io.EOF errors

Add connection health check in getConn to validate TLS connections
before reusing them from the pool. This prevents io.EOF errors when
reusing connections that were closed by the server (e.g., due to idle
timeout).
This commit is contained in:
Cuong Manh Le
2026-01-28 17:54:01 +07:00
committed by Cuong Manh Le
parent 209c9211b9
commit da3ea05763
+29 -11
View File
@@ -48,7 +48,7 @@ type dotConnPool struct {
} }
type dotConn struct { type dotConn struct {
conn net.Conn conn *tls.Conn
lastUsed time.Time lastUsed time.Time
refCount int refCount int
mu sync.Mutex mu sync.Mutex
@@ -105,13 +105,6 @@ func (p *dotConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
return nil, wrapCertificateVerificationError(err) return nil, wrapCertificateVerificationError(err)
} }
// Set deadline
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(5 * time.Second)
}
_ = conn.SetDeadline(deadline)
client := dns.Client{Net: "tcp-tls"} client := dns.Client{Net: "tcp-tls"}
answer, _, err := client.ExchangeWithConnContext(ctx, msg, &dns.Conn{Conn: conn}) answer, _, err := client.ExchangeWithConnContext(ctx, msg, &dns.Conn{Conn: conn})
isGood := err == nil isGood := err == nil
@@ -136,7 +129,7 @@ func (p *dotConnPool) getConn(ctx context.Context) (net.Conn, string, error) {
// Try to reuse an existing connection // Try to reuse an existing connection
for addr, dotConn := range p.conns { for addr, dotConn := range p.conns {
dotConn.mu.Lock() dotConn.mu.Lock()
if dotConn.refCount == 0 && dotConn.conn != nil { if dotConn.refCount == 0 && dotConn.conn != nil && isAlive(dotConn.conn) {
dotConn.refCount++ dotConn.refCount++
dotConn.lastUsed = time.Now() dotConn.lastUsed = time.Now()
conn := dotConn.conn conn := dotConn.conn
@@ -193,7 +186,7 @@ func (p *dotConnPool) putConn(addr string, conn net.Conn, isGood bool) {
} }
// dialConn creates a new TCP/TLS connection. // dialConn creates a new TCP/TLS connection.
func (p *dotConnPool) dialConn(ctx context.Context) (string, net.Conn, error) { func (p *dotConnPool) dialConn(ctx context.Context) (string, *tls.Conn, error) {
logger := ProxyLogger.Load() logger := ProxyLogger.Load()
var endpoint string var endpoint string
@@ -215,7 +208,7 @@ func (p *dotConnPool) dialConn(ctx context.Context) (string, net.Conn, error) {
// Try bootstrap IPs in parallel // Try bootstrap IPs in parallel
if len(p.addrs) > 0 { if len(p.addrs) > 0 {
type result struct { type result struct {
conn net.Conn conn *tls.Conn
addr string addr string
err error err error
} }
@@ -307,3 +300,28 @@ func (p *dotConnPool) CloseIdleConnections() {
delete(p.conns, addr) delete(p.conns, addr)
} }
} }
func isAlive(c *tls.Conn) bool {
// Set a very short deadline for the read
c.SetReadDeadline(time.Now().Add(1 * time.Millisecond))
// Try to read 1 byte without consuming it (using a small buffer)
one := make([]byte, 1)
_, err := c.Read(one)
// Reset the deadline for future operations
c.SetReadDeadline(time.Time{})
if err == io.EOF {
return false // Connection is definitely closed
}
// If we get a timeout, it means no data is waiting,
// but the connection is likely still "up."
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
return err == nil
}