From cf70a0cbe1a6aaf3ce7d60f6cdc21ba2484bb749 Mon Sep 17 00:00:00 2001 From: Andrew Beresford Date: Fri, 8 May 2026 11:33:54 +0100 Subject: [PATCH 1/2] Add configurable TCP keepalives to TCP listener and dialer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- dialer/tcp/dialer.go | 12 ++++++++++++ dialer/tcp/metadata.go | 10 ++++++++++ listener/tcp/listener.go | 33 +++++++++++++++++++++++++++++++++ listener/tcp/metadata.go | 12 ++++++++++++ 4 files changed, 67 insertions(+) diff --git a/dialer/tcp/dialer.go b/dialer/tcp/dialer.go index 83af2539..79bbbf99 100644 --- a/dialer/tcp/dialer.go +++ b/dialer/tcp/dialer.go @@ -47,6 +47,18 @@ func (d *tcpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOp conn, err := options.Dialer.Dial(ctx, "tcp", addr) if err != nil { 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( diff --git a/dialer/tcp/metadata.go b/dialer/tcp/metadata.go index 34e37894..03810a74 100644 --- a/dialer/tcp/metadata.go +++ b/dialer/tcp/metadata.go @@ -4,6 +4,7 @@ import ( "time" md "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/x/metadata/util" ) const ( @@ -16,8 +17,17 @@ const ( type metadata struct { dialTimeout time.Duration + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } 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 } diff --git a/listener/tcp/listener.go b/listener/tcp/listener.go index 9d2012b8..c63e3fde 100644 --- a/listener/tcp/listener.go +++ b/listener/tcp/listener.go @@ -61,6 +61,17 @@ func (l *tcpListener) Init(md md.Metadata) (err error) { 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) 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 { 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 +} diff --git a/listener/tcp/metadata.go b/listener/tcp/metadata.go index 28c91886..cf45f327 100644 --- a/listener/tcp/metadata.go +++ b/listener/tcp/metadata.go @@ -1,16 +1,28 @@ package tcp import ( + "time" + md "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/x/metadata/util" ) type metadata struct { mptcp bool + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *tcpListener) parseMetadata(md md.Metadata) (err error) { 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 } From ac99425002c138f05a2c82a0c8816b7b4514503a Mon Sep 17 00:00:00 2001 From: Andrew Beresford Date: Fri, 8 May 2026 14:23:51 +0100 Subject: [PATCH 2/2] feat: extend TCP keepalive support to all TCP-based listeners and dialers Add configurable TCP keepalive (keepalive, keepalive.idle, keepalive.interval, keepalive.count) to all TCP-based transport types: tls, mtls, mtcp, ws/wss, mws/mwss, http2, ssh, sshd listeners and tls, mtls, mtcp, ws/wss, mws/mwss, ssh, sshd dialers. For types that already use "keepalive" for an app-level protocol (grpc listener/dialer, ws/mws dialers, ssh/sshd dialers), the TCP keepalive parameters are exposed under the tcp.keepalive.* prefix to avoid ambiguity. Also extract shared helpers into internal/net/keepalive.go: - WrapKeepaliveListener: applies KeepAliveConfig on every accepted TCPConn - ApplyKeepalive: applies KeepAliveConfig to a single TCPConn Co-Authored-By: Claude Sonnet 4.6 --- dialer/grpc/dialer.go | 10 ++++++++++ dialer/grpc/metadata.go | 10 ++++++++++ dialer/mtcp/dialer.go | 10 ++++++++++ dialer/mtcp/metadata.go | 10 ++++++++++ dialer/mtls/dialer.go | 10 ++++++++++ dialer/mtls/metadata.go | 10 ++++++++++ dialer/mws/dialer.go | 10 ++++++++++ dialer/mws/metadata.go | 10 ++++++++++ dialer/ssh/dialer.go | 9 +++++++++ dialer/ssh/metadata.go | 10 ++++++++++ dialer/sshd/dialer.go | 9 +++++++++ dialer/sshd/metadata.go | 11 +++++++++++ dialer/tls/dialer.go | 10 ++++++++++ dialer/tls/metadata.go | 10 ++++++++++ dialer/ws/dialer.go | 10 ++++++++++ dialer/ws/metadata.go | 10 ++++++++++ internal/net/keepalive.go | 32 ++++++++++++++++++++++++++++++++ listener/grpc/listener.go | 10 ++++++++++ listener/grpc/metadata.go | 11 ++++++++++- listener/http2/listener.go | 10 ++++++++++ listener/http2/metadata.go | 12 ++++++++++++ listener/mtcp/listener.go | 10 ++++++++++ listener/mtcp/metadata.go | 16 +++++++++++++--- listener/mtls/listener.go | 10 ++++++++++ listener/mtls/metadata.go | 17 +++++++++++++---- listener/mws/listener.go | 10 ++++++++++ listener/mws/metadata.go | 10 ++++++++++ listener/ssh/listener.go | 10 ++++++++++ listener/ssh/metadata.go | 11 +++++++++++ listener/sshd/listener.go | 10 ++++++++++ listener/sshd/metadata.go | 11 +++++++++++ listener/tcp/listener.go | 23 +---------------------- listener/tls/listener.go | 10 ++++++++++ listener/tls/metadata.go | 13 +++++++++++-- listener/ws/listener.go | 10 ++++++++++ listener/ws/metadata.go | 10 ++++++++++ 36 files changed, 383 insertions(+), 32 deletions(-) create mode 100644 internal/net/keepalive.go diff --git a/dialer/grpc/dialer.go b/dialer/grpc/dialer.go index aaa4cb18..7d3c7d3d 100644 --- a/dialer/grpc/dialer.go +++ b/dialer/grpc/dialer.go @@ -8,6 +8,7 @@ import ( "github.com/go-gost/core/dialer" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" pb "github.com/go-gost/x/internal/util/grpc/proto" "github.com/go-gost/x/registry" @@ -78,6 +79,15 @@ func (d *grpcDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO return nil, err } + if d.md.tcpKeepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.tcpKeepaliveIdle, + Interval: d.md.tcpKeepaliveInterval, + Count: d.md.tcpKeepaliveCount, + }) + } + conn = proxyproto.WrapClientConn( d.options.ProxyProtocol, xctx.SrcAddrFromContext(ctx), diff --git a/dialer/grpc/metadata.go b/dialer/grpc/metadata.go index c716bd62..a22e1d17 100644 --- a/dialer/grpc/metadata.go +++ b/dialer/grpc/metadata.go @@ -16,6 +16,11 @@ type metadata struct { keepaliveTimeout time.Duration keepalivePermitWithoutStream bool minConnectTimeout time.Duration + + tcpKeepalive bool + tcpKeepaliveIdle time.Duration + tcpKeepaliveInterval time.Duration + tcpKeepaliveCount int } func (d *grpcDialer) parseMetadata(md mdata.Metadata) (err error) { @@ -39,5 +44,10 @@ func (d *grpcDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.minConnectTimeout = 30 * time.Second } + d.md.tcpKeepalive = mdutil.GetBool(md, "tcp.keepalive") + d.md.tcpKeepaliveIdle = mdutil.GetDuration(md, "tcp.keepalive.idle") + d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") + d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + return } diff --git a/dialer/mtcp/dialer.go b/dialer/mtcp/dialer.go index 3c563d56..128dc0fb 100644 --- a/dialer/mtcp/dialer.go +++ b/dialer/mtcp/dialer.go @@ -11,6 +11,7 @@ import ( "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/util/mux" "github.com/go-gost/x/registry" @@ -74,6 +75,15 @@ func (d *mtcpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO return } + if d.md.keepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.keepaliveIdle, + Interval: d.md.keepaliveInterval, + Count: d.md.keepaliveCount, + }) + } + conn = proxyproto.WrapClientConn( d.options.ProxyProtocol, xctx.SrcAddrFromContext(ctx), diff --git a/dialer/mtcp/metadata.go b/dialer/mtcp/metadata.go index 2ca0f0ed..8ac5cc23 100644 --- a/dialer/mtcp/metadata.go +++ b/dialer/mtcp/metadata.go @@ -11,11 +11,21 @@ import ( type metadata struct { handshakeTimeout time.Duration muxCfg *mux.Config + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (d *mtcpDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.handshakeTimeout = mdutil.GetDuration(md, "handshakeTimeout") + 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") + d.md.muxCfg = &mux.Config{ Version: mdutil.GetInt(md, "mux.version"), KeepAliveInterval: mdutil.GetDuration(md, "mux.keepaliveInterval"), diff --git a/dialer/mtls/dialer.go b/dialer/mtls/dialer.go index b19c2f13..5786e7be 100644 --- a/dialer/mtls/dialer.go +++ b/dialer/mtls/dialer.go @@ -12,6 +12,7 @@ import ( "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/util/mux" "github.com/go-gost/x/registry" @@ -75,6 +76,15 @@ func (d *mtlsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO return } + if d.md.keepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.keepaliveIdle, + Interval: d.md.keepaliveInterval, + Count: d.md.keepaliveCount, + }) + } + conn = proxyproto.WrapClientConn( d.options.ProxyProtocol, xctx.SrcAddrFromContext(ctx), diff --git a/dialer/mtls/metadata.go b/dialer/mtls/metadata.go index aad7c04a..d6c1fd61 100644 --- a/dialer/mtls/metadata.go +++ b/dialer/mtls/metadata.go @@ -11,11 +11,21 @@ import ( type metadata struct { handshakeTimeout time.Duration muxCfg *mux.Config + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (d *mtlsDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.handshakeTimeout = mdutil.GetDuration(md, "handshakeTimeout") + 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") + d.md.muxCfg = &mux.Config{ Version: mdutil.GetInt(md, "mux.version"), KeepAliveInterval: mdutil.GetDuration(md, "mux.keepaliveInterval"), diff --git a/dialer/mws/dialer.go b/dialer/mws/dialer.go index a127ef6c..99b5a2d6 100644 --- a/dialer/mws/dialer.go +++ b/dialer/mws/dialer.go @@ -12,6 +12,7 @@ import ( "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/util/mux" ws_util "github.com/go-gost/x/internal/util/ws" @@ -89,6 +90,15 @@ func (d *mwsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOp return } + if d.md.tcpKeepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.tcpKeepaliveIdle, + Interval: d.md.tcpKeepaliveInterval, + Count: d.md.tcpKeepaliveCount, + }) + } + conn = proxyproto.WrapClientConn( d.options.ProxyProtocol, xctx.SrcAddrFromContext(ctx), diff --git a/dialer/mws/metadata.go b/dialer/mws/metadata.go index 10fd3107..c341540c 100644 --- a/dialer/mws/metadata.go +++ b/dialer/mws/metadata.go @@ -27,6 +27,11 @@ type metadata struct { header http.Header keepaliveInterval time.Duration muxCfg *mux.Config + + tcpKeepalive bool + tcpKeepaliveIdle time.Duration + tcpKeepaliveInterval time.Duration + tcpKeepaliveCount int } func (d *mwsDialer) parseMetadata(md mdata.Metadata) (err error) { @@ -67,5 +72,10 @@ func (d *mwsDialer) parseMetadata(md mdata.Metadata) (err error) { } } + d.md.tcpKeepalive = mdutil.GetBool(md, "tcp.keepalive") + d.md.tcpKeepaliveIdle = mdutil.GetDuration(md, "tcp.keepalive.idle") + d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") + d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + return } diff --git a/dialer/ssh/dialer.go b/dialer/ssh/dialer.go index 63a6c1e7..8c8317ae 100644 --- a/dialer/ssh/dialer.go +++ b/dialer/ssh/dialer.go @@ -9,6 +9,7 @@ import ( "github.com/go-gost/core/dialer" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" ssh_util "github.com/go-gost/x/internal/util/ssh" "github.com/go-gost/x/registry" @@ -70,6 +71,14 @@ func (d *sshDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOp if err != nil { return } + if d.md.tcpKeepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.tcpKeepaliveIdle, + Interval: d.md.tcpKeepaliveInterval, + Count: d.md.tcpKeepaliveCount, + }) + } if d.md.handshakeTimeout > 0 { conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout)) defer conn.SetDeadline(time.Time{}) diff --git a/dialer/ssh/metadata.go b/dialer/ssh/metadata.go index feff279c..3be7e76a 100644 --- a/dialer/ssh/metadata.go +++ b/dialer/ssh/metadata.go @@ -19,6 +19,11 @@ type metadata struct { keepaliveInterval time.Duration keepaliveTimeout time.Duration keepaliveRetries int + + tcpKeepalive bool + tcpKeepaliveIdle time.Duration + tcpKeepaliveInterval time.Duration + tcpKeepaliveCount int } func (d *sshDialer) parseMetadata(md mdata.Metadata) (err error) { @@ -66,5 +71,10 @@ func (d *sshDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.keepaliveRetries = mdutil.GetInt(md, "keepalive.retries") } + d.md.tcpKeepalive = mdutil.GetBool(md, "tcp.keepalive") + d.md.tcpKeepaliveIdle = mdutil.GetDuration(md, "tcp.keepalive.idle") + d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") + d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + return } diff --git a/dialer/sshd/dialer.go b/dialer/sshd/dialer.go index 124cc20f..25b0a213 100644 --- a/dialer/sshd/dialer.go +++ b/dialer/sshd/dialer.go @@ -10,6 +10,7 @@ import ( "github.com/go-gost/core/dialer" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" ssh_util "github.com/go-gost/x/internal/util/ssh" "github.com/go-gost/x/registry" @@ -72,6 +73,14 @@ func (d *sshdDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO if err != nil { return } + if d.md.tcpKeepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.tcpKeepaliveIdle, + Interval: d.md.tcpKeepaliveInterval, + Count: d.md.tcpKeepaliveCount, + }) + } if d.md.handshakeTimeout > 0 { conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout)) diff --git a/dialer/sshd/metadata.go b/dialer/sshd/metadata.go index 394e64a3..6b9434f4 100644 --- a/dialer/sshd/metadata.go +++ b/dialer/sshd/metadata.go @@ -19,6 +19,11 @@ type metadata struct { keepaliveInterval time.Duration keepaliveTimeout time.Duration keepaliveRetries int + + tcpKeepalive bool + tcpKeepaliveIdle time.Duration + tcpKeepaliveInterval time.Duration + tcpKeepaliveCount int } func (d *sshdDialer) parseMetadata(md mdata.Metadata) (err error) { @@ -64,5 +69,11 @@ func (d *sshdDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.keepaliveTimeout = mdutil.GetDuration(md, "keepalive.timeout") d.md.keepaliveRetries = mdutil.GetInt(md, "keepalive.retries") } + + d.md.tcpKeepalive = mdutil.GetBool(md, "tcp.keepalive") + d.md.tcpKeepaliveIdle = mdutil.GetDuration(md, "tcp.keepalive.idle") + d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") + d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + return } diff --git a/dialer/tls/dialer.go b/dialer/tls/dialer.go index 07e2c247..35227179 100644 --- a/dialer/tls/dialer.go +++ b/dialer/tls/dialer.go @@ -10,6 +10,7 @@ import ( "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/registry" ) @@ -51,6 +52,15 @@ func (d *tlsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOp d.log.Error(err) } + if d.md.keepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.keepaliveIdle, + Interval: d.md.keepaliveInterval, + Count: d.md.keepaliveCount, + }) + } + conn = proxyproto.WrapClientConn( d.options.ProxyProtocol, xctx.SrcAddrFromContext(ctx), diff --git a/dialer/tls/metadata.go b/dialer/tls/metadata.go index 2fa7f4d3..1b042977 100644 --- a/dialer/tls/metadata.go +++ b/dialer/tls/metadata.go @@ -9,6 +9,11 @@ import ( type metadata struct { handshakeTimeout time.Duration + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (d *tlsDialer) parseMetadata(md mdata.Metadata) (err error) { @@ -18,5 +23,10 @@ func (d *tlsDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) + 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 } diff --git a/dialer/ws/dialer.go b/dialer/ws/dialer.go index a0f1617c..1db6af29 100644 --- a/dialer/ws/dialer.go +++ b/dialer/ws/dialer.go @@ -9,6 +9,7 @@ import ( "github.com/go-gost/core/dialer" md "github.com/go-gost/core/metadata" xctx "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/net/proxyproto" ws_util "github.com/go-gost/x/internal/util/ws" "github.com/go-gost/x/registry" @@ -64,6 +65,15 @@ func (d *wsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOpt d.options.Logger.Error(err) } + if d.md.tcpKeepalive { + xnet.ApplyKeepalive(conn, net.KeepAliveConfig{ + Enable: true, + Idle: d.md.tcpKeepaliveIdle, + Interval: d.md.tcpKeepaliveInterval, + Count: d.md.tcpKeepaliveCount, + }) + } + conn = proxyproto.WrapClientConn( d.options.ProxyProtocol, xctx.SrcAddrFromContext(ctx), diff --git a/dialer/ws/metadata.go b/dialer/ws/metadata.go index 52b96a48..17586108 100644 --- a/dialer/ws/metadata.go +++ b/dialer/ws/metadata.go @@ -25,6 +25,11 @@ type metadata struct { header http.Header keepaliveInterval time.Duration + + tcpKeepalive bool + tcpKeepaliveIdle time.Duration + tcpKeepaliveInterval time.Duration + tcpKeepaliveCount int } func (d *wsDialer) parseMetadata(md mdata.Metadata) (err error) { @@ -56,5 +61,10 @@ func (d *wsDialer) parseMetadata(md mdata.Metadata) (err error) { } } + d.md.tcpKeepalive = mdutil.GetBool(md, "tcp.keepalive") + d.md.tcpKeepaliveIdle = mdutil.GetDuration(md, "tcp.keepalive.idle") + d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") + d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") + return } diff --git a/internal/net/keepalive.go b/internal/net/keepalive.go new file mode 100644 index 00000000..d16dda63 --- /dev/null +++ b/internal/net/keepalive.go @@ -0,0 +1,32 @@ +package net + +import "net" + +// WrapKeepaliveListener wraps ln so that TCP keepalive config is applied to +// every accepted *net.TCPConn before it enters the middleware wrapper chain. +func WrapKeepaliveListener(ln net.Listener, cfg net.KeepAliveConfig) net.Listener { + return &keepaliveListener{Listener: ln, cfg: cfg} +} + +// ApplyKeepalive applies cfg to conn if it is a *net.TCPConn. +func ApplyKeepalive(conn net.Conn, cfg net.KeepAliveConfig) { + if tc, ok := conn.(*net.TCPConn); ok { + tc.SetKeepAliveConfig(cfg) + } +} + +type keepaliveListener struct { + net.Listener + cfg net.KeepAliveConfig +} + +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 +} diff --git a/listener/grpc/listener.go b/listener/grpc/listener.go index 53bcd885..563413d2 100644 --- a/listener/grpc/listener.go +++ b/listener/grpc/listener.go @@ -66,6 +66,16 @@ func (l *grpcListener) Init(md md.Metadata) (err error) { if err != nil { return } + if l.md.tcpKeepalive { + ln = xnet.WrapKeepaliveListener(ln, net.KeepAliveConfig{ + Enable: true, + Idle: l.md.tcpKeepaliveIdle, + Interval: l.md.tcpKeepaliveInterval, + Count: l.md.tcpKeepaliveCount, + }) + l.logger.Debugf("tcp keepalive enabled: idle=%v interval=%v count=%d", + l.md.tcpKeepaliveIdle, l.md.tcpKeepaliveInterval, l.md.tcpKeepaliveCount) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) diff --git a/listener/grpc/metadata.go b/listener/grpc/metadata.go index 3383a357..f5fb87a8 100644 --- a/listener/grpc/metadata.go +++ b/listener/grpc/metadata.go @@ -22,6 +22,11 @@ type metadata struct { keepalivePermitWithoutStream bool keepaliveMaxConnectionIdle time.Duration mptcp bool + + tcpKeepalive bool + tcpKeepaliveIdle time.Duration + tcpKeepaliveInterval time.Duration + tcpKeepaliveCount int } func (l *grpcListener) parseMetadata(md mdata.Metadata) (err error) { @@ -50,8 +55,12 @@ func (l *grpcListener) parseMetadata(md mdata.Metadata) (err error) { l.md.keepalivePermitWithoutStream = mdutil.GetBool(md, "grpc.keepalive.permitWithoutStream", "keepalive.permitWithoutStream") l.md.keepaliveMaxConnectionIdle = mdutil.GetDuration(md, "grpc.keepalive.maxConnectionIdle", "keepalive.maxConnectionIdle") - l.md.mptcp = mdutil.GetBool(md, "mptcp") } + l.md.mptcp = mdutil.GetBool(md, "mptcp") + l.md.tcpKeepalive = mdutil.GetBool(md, "tcp.keepalive") + l.md.tcpKeepaliveIdle = mdutil.GetDuration(md, "tcp.keepalive.idle") + l.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval") + l.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count") return } diff --git a/listener/http2/listener.go b/listener/http2/listener.go index 2bc43bbc..212e0330 100644 --- a/listener/http2/listener.go +++ b/listener/http2/listener.go @@ -93,6 +93,16 @@ func (l *http2Listener) Init(md md.Metadata) (err error) { if err != nil { return err } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(ln, net.KeepAliveConfig{ + Enable: true, + Idle: l.md.keepaliveIdle, + Interval: l.md.keepaliveInterval, + Count: l.md.keepaliveCount, + }) + l.log.Debugf("tcp keepalive enabled: idle=%v interval=%v count=%d", + l.md.keepaliveIdle, l.md.keepaliveInterval, l.md.keepaliveCount) + } l.addr = ln.Addr() ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) diff --git a/listener/http2/metadata.go b/listener/http2/metadata.go index 5e300153..b478d25e 100644 --- a/listener/http2/metadata.go +++ b/listener/http2/metadata.go @@ -1,6 +1,8 @@ package http2 import ( + "time" + mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/x/metadata/util" ) @@ -12,6 +14,11 @@ const ( type metadata struct { backlog int mptcp bool + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *http2Listener) parseMetadata(md mdata.Metadata) (err error) { @@ -25,5 +32,10 @@ func (l *http2Listener) parseMetadata(md mdata.Metadata) (err error) { } 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 } diff --git a/listener/mtcp/listener.go b/listener/mtcp/listener.go index 61f5b230..2cf29557 100644 --- a/listener/mtcp/listener.go +++ b/listener/mtcp/listener.go @@ -63,6 +63,16 @@ func (l *mtcpListener) Init(md md.Metadata) (err error) { if err != nil { return } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(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) diff --git a/listener/mtcp/metadata.go b/listener/mtcp/metadata.go index 8b8db627..82004b65 100644 --- a/listener/mtcp/metadata.go +++ b/listener/mtcp/metadata.go @@ -1,6 +1,8 @@ package mtcp import ( + "time" + md "github.com/go-gost/core/metadata" "github.com/go-gost/x/internal/util/mux" mdutil "github.com/go-gost/x/metadata/util" @@ -11,9 +13,13 @@ const ( ) type metadata struct { - mptcp bool - muxCfg *mux.Config - backlog int + mptcp bool + muxCfg *mux.Config + backlog int + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *mtcpListener) parseMetadata(md md.Metadata) (err error) { @@ -37,5 +43,9 @@ func (l *mtcpListener) parseMetadata(md md.Metadata) (err error) { l.md.backlog = defaultBacklog } + 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 } diff --git a/listener/mtls/listener.go b/listener/mtls/listener.go index 01583fa6..0d84b80c 100644 --- a/listener/mtls/listener.go +++ b/listener/mtls/listener.go @@ -64,6 +64,16 @@ func (l *mtlsListener) Init(md md.Metadata) (err error) { if err != nil { return } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(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) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) diff --git a/listener/mtls/metadata.go b/listener/mtls/metadata.go index 6f6d8432..e9f76fe7 100644 --- a/listener/mtls/metadata.go +++ b/listener/mtls/metadata.go @@ -1,6 +1,8 @@ package mtls import ( + "time" + mdata "github.com/go-gost/core/metadata" "github.com/go-gost/x/internal/util/mux" mdutil "github.com/go-gost/x/metadata/util" @@ -11,9 +13,13 @@ const ( ) type metadata struct { - muxCfg *mux.Config - backlog int - mptcp bool + muxCfg *mux.Config + backlog int + mptcp bool + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *mtlsListener) parseMetadata(md mdata.Metadata) (err error) { @@ -32,6 +38,9 @@ func (l *mtlsListener) parseMetadata(md mdata.Metadata) (err error) { MaxStreamBuffer: mdutil.GetInt(md, "mux.maxStreamBuffer"), } 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 } diff --git a/listener/mws/listener.go b/listener/mws/listener.go index 8324b49e..e9804bfc 100644 --- a/listener/mws/listener.go +++ b/listener/mws/listener.go @@ -109,6 +109,16 @@ func (l *mwsListener) Init(md md.Metadata) (err error) { if err != nil { return } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(ln, net.KeepAliveConfig{ + Enable: true, + Idle: l.md.keepaliveIdle, + Interval: l.md.keepaliveInterval, + Count: l.md.keepaliveCount, + }) + l.log.Debugf("tcp keepalive enabled: idle=%v interval=%v count=%d", + l.md.keepaliveIdle, l.md.keepaliveInterval, l.md.keepaliveCount) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) ln = stats.WrapListener(ln, l.options.Stats) diff --git a/listener/mws/metadata.go b/listener/mws/metadata.go index b2fafab0..1c4ee61a 100644 --- a/listener/mws/metadata.go +++ b/listener/mws/metadata.go @@ -28,6 +28,11 @@ type metadata struct { muxCfg *mux.Config mptcp bool + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *mwsListener) parseMetadata(md mdata.Metadata) (err error) { @@ -67,5 +72,10 @@ func (l *mwsListener) parseMetadata(md mdata.Metadata) (err error) { 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 } diff --git a/listener/ssh/listener.go b/listener/ssh/listener.go index d3acbe8f..dafe679b 100644 --- a/listener/ssh/listener.go +++ b/listener/ssh/listener.go @@ -66,6 +66,16 @@ func (l *sshListener) Init(md md.Metadata) (err error) { if err != nil { return err } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(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) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) diff --git a/listener/ssh/metadata.go b/listener/ssh/metadata.go index ef680b33..92861d72 100644 --- a/listener/ssh/metadata.go +++ b/listener/ssh/metadata.go @@ -2,6 +2,7 @@ package ssh import ( "os" + "time" mdata "github.com/go-gost/core/metadata" ssh_util "github.com/go-gost/x/internal/util/ssh" @@ -19,6 +20,11 @@ type metadata struct { authorizedKeys map[string]bool backlog int mptcp bool + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *sshListener) parseMetadata(md mdata.Metadata) (err error) { @@ -72,5 +78,10 @@ func (l *sshListener) parseMetadata(md mdata.Metadata) (err error) { 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 } diff --git a/listener/sshd/listener.go b/listener/sshd/listener.go index a1f12c03..b76923cb 100644 --- a/listener/sshd/listener.go +++ b/listener/sshd/listener.go @@ -74,6 +74,16 @@ func (l *sshdListener) Init(md md.Metadata) (err error) { if err != nil { return err } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(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) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) diff --git a/listener/sshd/metadata.go b/listener/sshd/metadata.go index 15df0173..1ffff4ba 100644 --- a/listener/sshd/metadata.go +++ b/listener/sshd/metadata.go @@ -3,6 +3,7 @@ package ssh import ( "fmt" "os" + "time" mdata "github.com/go-gost/core/metadata" ssh_util "github.com/go-gost/x/internal/util/ssh" @@ -21,6 +22,11 @@ type metadata struct { authorizedKeys map[string]bool backlog int mptcp bool + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *sshdListener) parseMetadata(md mdata.Metadata) (err error) { @@ -82,5 +88,10 @@ func (l *sshdListener) parseMetadata(md mdata.Metadata) (err error) { 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 } diff --git a/listener/tcp/listener.go b/listener/tcp/listener.go index c63e3fde..77105657 100644 --- a/listener/tcp/listener.go +++ b/listener/tcp/listener.go @@ -62,7 +62,7 @@ func (l *tcpListener) Init(md md.Metadata) (err error) { } if l.md.keepalive { - ln = newKeepaliveListener(ln, net.KeepAliveConfig{ + ln = xnet.WrapKeepaliveListener(ln, net.KeepAliveConfig{ Enable: true, Idle: l.md.keepaliveIdle, Interval: l.md.keepaliveInterval, @@ -112,24 +112,3 @@ func (l *tcpListener) Close() error { 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 -} diff --git a/listener/tls/listener.go b/listener/tls/listener.go index 45512e5a..923d3200 100644 --- a/listener/tls/listener.go +++ b/listener/tls/listener.go @@ -61,6 +61,16 @@ func (l *tlsListener) Init(md md.Metadata) (err error) { if err != nil { return } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(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) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) ln = stats.WrapListener(ln, l.options.Stats) diff --git a/listener/tls/metadata.go b/listener/tls/metadata.go index 9e96e713..c647d2f6 100644 --- a/listener/tls/metadata.go +++ b/listener/tls/metadata.go @@ -1,16 +1,25 @@ package tls import ( + "time" + mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/x/metadata/util" ) type metadata struct { - mptcp bool + mptcp bool + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *tlsListener) parseMetadata(md mdata.Metadata) (err error) { 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 } diff --git a/listener/ws/listener.go b/listener/ws/listener.go index b9841efb..6b70e8fc 100644 --- a/listener/ws/listener.go +++ b/listener/ws/listener.go @@ -104,6 +104,16 @@ func (l *wsListener) Init(md md.Metadata) (err error) { if err != nil { return } + if l.md.keepalive { + ln = xnet.WrapKeepaliveListener(ln, net.KeepAliveConfig{ + Enable: true, + Idle: l.md.keepaliveIdle, + Interval: l.md.keepaliveInterval, + Count: l.md.keepaliveCount, + }) + l.log.Debugf("tcp keepalive enabled: idle=%v interval=%v count=%d", + l.md.keepaliveIdle, l.md.keepaliveInterval, l.md.keepaliveCount) + } ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = metrics.WrapListener(l.options.Service, ln) ln = stats.WrapListener(ln, l.options.Stats) diff --git a/listener/ws/metadata.go b/listener/ws/metadata.go index f6fa1eec..b75d2c9e 100644 --- a/listener/ws/metadata.go +++ b/listener/ws/metadata.go @@ -25,6 +25,11 @@ type metadata struct { header http.Header mptcp bool + + keepalive bool + keepaliveIdle time.Duration + keepaliveInterval time.Duration + keepaliveCount int } func (l *wsListener) parseMetadata(md mdata.Metadata) (err error) { @@ -54,5 +59,10 @@ func (l *wsListener) parseMetadata(md mdata.Metadata) (err error) { 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 }