diff --git a/internal/netutil/conn.go b/internal/netutil/conn.go index 3e2c4b61..29bbe256 100644 --- a/internal/netutil/conn.go +++ b/internal/netutil/conn.go @@ -232,6 +232,14 @@ func (b *BufferedConn) Peek(n int) ([]byte, error) { return b.r.Peek(n) } +// DefaultTunnelIdleTimeout bounds how long an established TCP tunnel may sit with +// no traffic in either direction before it is torn down. Without it, a half-dead +// connection (peer gone with no FIN/RST — common behind NAT/flaky links) keeps its +// two copy goroutines and two fds alive forever, so they accumulate over long +// uptime and drive CPU up. Activity in either direction resets the timer, so this +// only reaps genuinely-silent tunnels. Mirrors the existing UDPIdleTimeout. +const DefaultTunnelIdleTimeout = 300 * time.Second + // IdleTimeoutConn wraps a net.Conn to extend the deadline on every Read/Write call. // This is useful for sessions which should stay alive as long as there is activity. type IdleTimeoutConn struct { diff --git a/internal/server/http/handler_http.go b/internal/server/http/handler_http.go index e307dc7a..07ad2291 100644 --- a/internal/server/http/handler_http.go +++ b/internal/server/http/handler_http.go @@ -56,9 +56,14 @@ func (h *HTTPHandler) HandleRequest( ctx, cancel := context.WithCancel(ctx) defer cancel() + // See handler_https.go: bound the tunnel so a half-dead peer can't leak its + // copy goroutines and fds forever. Activity in either direction resets it. + lIdle := netutil.NewIdleTimeoutConn(lConn, netutil.DefaultTunnelIdleTimeout) + rIdle := netutil.NewIdleTimeoutConn(rConn, netutil.DefaultTunnelIdleTimeout) + startedAt := time.Now() - go netutil.TunnelConns(ctx, resCh, lConn, rConn, netutil.TunnelDirOut) - go netutil.TunnelConns(ctx, resCh, rConn, lConn, netutil.TunnelDirIn) + go netutil.TunnelConns(ctx, resCh, lIdle, rIdle, netutil.TunnelDirOut) + go netutil.TunnelConns(ctx, resCh, rIdle, lIdle, netutil.TunnelDirIn) return netutil.WaitForTunnelCompletion( ctx, diff --git a/internal/server/http/handler_https.go b/internal/server/http/handler_https.go index 21587d78..b987fdc8 100644 --- a/internal/server/http/handler_https.go +++ b/internal/server/http/handler_https.go @@ -102,9 +102,16 @@ func (h *HTTPSHandler) HandleRequest( ctx, cancel := context.WithCancel(ctx) defer cancel() + // Bound the established tunnel: a half-dead peer (gone with no FIN/RST) would + // otherwise leak both copy goroutines and both fds forever, accumulating over + // long uptime. IdleTimeoutConn resets on activity in either direction, so only + // fully-silent tunnels are reaped (the resulting timeout is treated as benign). + lIdle := netutil.NewIdleTimeoutConn(lConn, netutil.DefaultTunnelIdleTimeout) + rIdle := netutil.NewIdleTimeoutConn(rConn, netutil.DefaultTunnelIdleTimeout) + startedAt := time.Now() - go netutil.TunnelConns(ctx, resCh, lConn, rConn, netutil.TunnelDirOut) - go netutil.TunnelConns(ctx, resCh, rConn, lConn, netutil.TunnelDirIn) + go netutil.TunnelConns(ctx, resCh, lIdle, rIdle, netutil.TunnelDirOut) + go netutil.TunnelConns(ctx, resCh, rIdle, lIdle, netutil.TunnelDirIn) handleErrs := func(errs []error) error { if len(errs) == 0 {