Add configurable TCP keepalives to TCP listener and dialer

Exposes TCP keepalive configuration via listener and dialer metadata:

  keepalive: true
  keepalive.idle: 30s
  keepalive.interval: 10s
  keepalive.count: 6

When enabled, the OS sends keepalive probes after the idle period and
tears down the socket if the remote does not respond. This causes any
blocked Read() on a dead connection to return promptly, releasing
goroutines and memory — covering both legs of a CONNECT tunnel (the
inbound client connection via the listener, and the outbound upstream
connection via the dialer).

This is the correct alternative to a hard application-level read
deadline (SetReadDeadline) for detecting dead connections, as it
distinguishes truly dead connections from legitimately idle ones.

Relates to: https://github.com/go-gost/x/issues/91
This commit is contained in:
Andrew Beresford
2026-05-08 11:33:54 +01:00
parent 07ca57055a
commit cf70a0cbe1
4 changed files with 67 additions and 0 deletions
+12
View File
@@ -47,6 +47,18 @@ func (d *tcpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOp
conn, err := options.Dialer.Dial(ctx, "tcp", addr) conn, err := options.Dialer.Dial(ctx, "tcp", addr)
if err != nil { if err != nil {
d.logger.Error(err) d.logger.Error(err)
return conn, err
}
if d.md.keepalive {
if tc, ok := conn.(*net.TCPConn); ok {
tc.SetKeepAliveConfig(net.KeepAliveConfig{
Enable: true,
Idle: d.md.keepaliveIdle,
Interval: d.md.keepaliveInterval,
Count: d.md.keepaliveCount,
})
}
} }
conn = proxyproto.WrapClientConn( conn = proxyproto.WrapClientConn(
+10
View File
@@ -4,6 +4,7 @@ import (
"time" "time"
md "github.com/go-gost/core/metadata" md "github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/x/metadata/util"
) )
const ( const (
@@ -16,8 +17,17 @@ const (
type metadata struct { type metadata struct {
dialTimeout time.Duration dialTimeout time.Duration
keepalive bool
keepaliveIdle time.Duration
keepaliveInterval time.Duration
keepaliveCount int
} }
func (d *tcpDialer) parseMetadata(md md.Metadata) (err error) { func (d *tcpDialer) parseMetadata(md md.Metadata) (err error) {
d.md.keepalive = mdutil.GetBool(md, "keepalive")
d.md.keepaliveIdle = mdutil.GetDuration(md, "keepalive.idle")
d.md.keepaliveInterval = mdutil.GetDuration(md, "keepalive.interval")
d.md.keepaliveCount = mdutil.GetInt(md, "keepalive.count")
return return
} }
+33
View File
@@ -61,6 +61,17 @@ func (l *tcpListener) Init(md md.Metadata) (err error) {
return return
} }
if l.md.keepalive {
ln = newKeepaliveListener(ln, net.KeepAliveConfig{
Enable: true,
Idle: l.md.keepaliveIdle,
Interval: l.md.keepaliveInterval,
Count: l.md.keepaliveCount,
})
l.logger.Debugf("tcp keepalive enabled: idle=%v interval=%v count=%d",
l.md.keepaliveIdle, l.md.keepaliveInterval, l.md.keepaliveCount)
}
l.logger.Debugf("pp: %d", l.options.ProxyProtocol) l.logger.Debugf("pp: %d", l.options.ProxyProtocol)
ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second)
@@ -100,3 +111,25 @@ func (l *tcpListener) Addr() net.Addr {
func (l *tcpListener) Close() error { func (l *tcpListener) Close() error {
return l.ln.Close() return l.ln.Close()
} }
// keepaliveListener applies TCP keepalive settings to every accepted connection
// before the connection enters the middleware chain.
type keepaliveListener struct {
net.Listener
cfg net.KeepAliveConfig
}
func newKeepaliveListener(ln net.Listener, cfg net.KeepAliveConfig) net.Listener {
return &keepaliveListener{Listener: ln, cfg: cfg}
}
func (l *keepaliveListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
if tc, ok := conn.(*net.TCPConn); ok {
tc.SetKeepAliveConfig(l.cfg)
}
return conn, nil
}
+12
View File
@@ -1,16 +1,28 @@
package tcp package tcp
import ( import (
"time"
md "github.com/go-gost/core/metadata" md "github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/x/metadata/util" mdutil "github.com/go-gost/x/metadata/util"
) )
type metadata struct { type metadata struct {
mptcp bool mptcp bool
keepalive bool
keepaliveIdle time.Duration
keepaliveInterval time.Duration
keepaliveCount int
} }
func (l *tcpListener) parseMetadata(md md.Metadata) (err error) { func (l *tcpListener) parseMetadata(md md.Metadata) (err error) {
l.md.mptcp = mdutil.GetBool(md, "mptcp") l.md.mptcp = mdutil.GetBool(md, "mptcp")
l.md.keepalive = mdutil.GetBool(md, "keepalive")
l.md.keepaliveIdle = mdutil.GetDuration(md, "keepalive.idle")
l.md.keepaliveInterval = mdutil.GetDuration(md, "keepalive.interval")
l.md.keepaliveCount = mdutil.GetInt(md, "keepalive.count")
return return
} }