From 836090ebdf743da4c11882cc7a86809e56497bbb Mon Sep 17 00:00:00 2001 From: Alexey Dolotov Date: Sat, 28 Mar 2026 22:47:39 +0300 Subject: [PATCH 1/4] fix: apply idle timeout to domain fronting relay connections Domain fronting relay (for non-Telegram traffic) had no idle timeout, causing worker pool exhaustion under traffic spikes. The ProxyOpts.IdleTimeout field existed but was never wired into the proxy. Now domain fronting connections are wrapped with per-read/write deadlines reset to the configured idle timeout (default 1m), so stale or slowloris-style connections are reaped promptly. Fixes #378 --- internal/cli/run_proxy.go | 1 + mtglib/conns.go | 19 +++++++++++++++++++ mtglib/proxy.go | 6 ++++-- mtglib/proxy_opts.go | 8 ++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 643f645..8eabc3d 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -262,6 +262,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, + IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout), DoppelGangerURLs: doppelGangerURLs, DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid), diff --git a/mtglib/conns.go b/mtglib/conns.go index 8219b75..e3b60e8 100644 --- a/mtglib/conns.go +++ b/mtglib/conns.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "time" "github.com/9seconds/mtg/v2/essentials" "github.com/pires/go-proxyproto" @@ -95,3 +96,21 @@ func newConnProxyProtocol(source, target essentials.Conn) *connProxyProtocol { sourceAddr: source.RemoteAddr(), } } + +type connIdleTimeout struct { + essentials.Conn + + timeout time.Duration +} + +func (c connIdleTimeout) Read(b []byte) (int, error) { + c.Conn.SetReadDeadline(time.Now().Add(c.timeout)) //nolint: errcheck + + return c.Conn.Read(b) //nolint: wrapcheck +} + +func (c connIdleTimeout) Write(b []byte) (int, error) { + c.Conn.SetWriteDeadline(time.Now().Add(c.timeout)) //nolint: errcheck + + return c.Conn.Write(b) //nolint: wrapcheck +} diff --git a/mtglib/proxy.go b/mtglib/proxy.go index be05194..65f52ad 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -27,6 +27,7 @@ type Proxy struct { allowFallbackOnUnknownDC bool tolerateTimeSkewness time.Duration + idleTimeout time.Duration domainFrontingPort int domainFrontingIP string domainFrontingProxyProtocol bool @@ -306,8 +307,8 @@ func (p *Proxy) doDomainFronting(ctx *streamContext, conn *connRewind) { relay.Relay( ctx, ctx.logger.Named("domain-fronting"), - frontConn, - conn, + connIdleTimeout{Conn: frontConn, timeout: p.idleTimeout}, + connIdleTimeout{Conn: conn, timeout: p.idleTimeout}, ) } @@ -339,6 +340,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { domainFrontingPort: opts.getDomainFrontingPort(), domainFrontingIP: opts.DomainFrontingIP, tolerateTimeSkewness: opts.getTolerateTimeSkewness(), + idleTimeout: opts.getIdleTimeout(), allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, telegram: tg, doppelGanger: doppel.NewGanger( diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 3d07fbd..beea1c6 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -216,6 +216,14 @@ func (p ProxyOpts) getPreferIP() string { return p.PreferIP } +func (p ProxyOpts) getIdleTimeout() time.Duration { + if p.IdleTimeout == 0 { + return DefaultIdleTimeout + } + + return p.IdleTimeout +} + func (p ProxyOpts) getLogger(name string) Logger { return p.Logger.Named(name) } From 289bb283b1d479e93512d6a795791fb62de1e8a1 Mon Sep 17 00:00:00 2001 From: Alexey Dolotov Date: Sat, 28 Mar 2026 22:52:39 +0300 Subject: [PATCH 2/4] fix: close connection on worker pool overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the worker pool rejected a connection (ErrPoolOverload), the accepted net.Conn was never closed — leaking a file descriptor and TCP socket per rejected connection. Under sustained traffic spikes this compounds the problem: leaked descriptors reduce the capacity for new dials (including to the fronting domain), accelerating the failure cascade described in #378. --- mtglib/proxy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 65f52ad..cb252ed 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -152,6 +152,7 @@ func (p *Proxy) Serve(listener net.Listener) error { case errors.Is(err, ants.ErrPoolClosed): return nil case errors.Is(err, ants.ErrPoolOverload): + conn.Close() //nolint: errcheck logger.Info("connection was concurrency limited") p.eventStream.Send(p.ctx, NewEventConcurrencyLimited()) } From f355512aa698df5334a253a3b1d8376dc0555c92 Mon Sep 17 00:00:00 2001 From: Alexey Dolotov Date: Sat, 28 Mar 2026 22:59:32 +0300 Subject: [PATCH 3/4] fix: address staticcheck lint issues - avoid deprecated DefaultIdleTimeout, use time.Minute directly - simplify embedded field selectors (QF1008) --- internal/cli/run_proxy.go | 3 ++- mtglib/conns.go | 4 ++-- mtglib/proxy_opts.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 8eabc3d..c57374b 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "os" + "time" "github.com/9seconds/mtg/v2/antireplay" "github.com/9seconds/mtg/v2/events" @@ -262,7 +263,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, - IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout), + IdleTimeout: conf.Network.Timeout.Idle.Get(time.Minute), DoppelGangerURLs: doppelGangerURLs, DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid), diff --git a/mtglib/conns.go b/mtglib/conns.go index e3b60e8..12f19d1 100644 --- a/mtglib/conns.go +++ b/mtglib/conns.go @@ -104,13 +104,13 @@ type connIdleTimeout struct { } func (c connIdleTimeout) Read(b []byte) (int, error) { - c.Conn.SetReadDeadline(time.Now().Add(c.timeout)) //nolint: errcheck + c.SetReadDeadline(time.Now().Add(c.timeout)) //nolint: errcheck return c.Conn.Read(b) //nolint: wrapcheck } func (c connIdleTimeout) Write(b []byte) (int, error) { - c.Conn.SetWriteDeadline(time.Now().Add(c.timeout)) //nolint: errcheck + c.SetWriteDeadline(time.Now().Add(c.timeout)) //nolint: errcheck return c.Conn.Write(b) //nolint: wrapcheck } diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index beea1c6..102b279 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -218,7 +218,7 @@ func (p ProxyOpts) getPreferIP() string { func (p ProxyOpts) getIdleTimeout() time.Duration { if p.IdleTimeout == 0 { - return DefaultIdleTimeout + return time.Minute } return p.IdleTimeout From 46c33f15322e2f40c8b6a1df6bfbad926e104678 Mon Sep 17 00:00:00 2001 From: Alexey Dolotov Date: Sat, 28 Mar 2026 23:04:33 +0300 Subject: [PATCH 4/4] ci: retrigger (flaky antireplay test)