From eb564936c760504ea879d819a26fd401f4c72947 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 7 Apr 2026 08:01:51 +0200 Subject: [PATCH] Add separate handshake timeout This PR adds a new setting to the config: `network.timeout`. This setting defines a time period during which all handshake procedures and ceremonies must be completed. If not - connection is aborted. This should help in situations when connection is established but client cannot continue for some reason (for example, RST sent by some middle box). --- example.config.toml | 4 +-- internal/cli/run_proxy.go | 1 + internal/config/config.go | 7 ++--- internal/config/parse.go | 7 ++--- mtglib/init.go | 4 +++ mtglib/internal/tls/fake/client_side.go | 5 ---- .../tls/fake/client_side_snapshot_test.go | 6 ----- mtglib/internal/tls/fake/client_side_test.go | 26 +------------------ mtglib/internal/tls/fake/init.go | 11 +------- mtglib/proxy.go | 12 +++++++++ mtglib/proxy_opts.go | 14 ++++++++++ 11 files changed, 42 insertions(+), 55 deletions(-) diff --git a/example.config.toml b/example.config.toml index 38cd940..73f5683 100644 --- a/example.config.toml +++ b/example.config.toml @@ -205,13 +205,11 @@ proxies = [ # means a timeout on pumping data between sockset when nothing is # happening. # -# please be noticed that handshakes have no timeouts intentionally. You can -# find a reasoning here: -# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf [network.timeout] tcp = "5s" http = "10s" idle = "5m" +handshake = "10s" # mtg has to mimic real websites. It does not mean domain fronting, it also # means that traffic characteristics should be similar to real world traffic. diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 8eabc3d..de8b206 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -263,6 +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), + HandshakeTimeout: conf.Network.Timeout.Handshake.Get(mtglib.DefaultHandshakeTimeout), DoppelGangerURLs: doppelGangerURLs, DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid), diff --git a/internal/config/config.go b/internal/config/config.go index dcc3186..2fb79ee 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,9 +60,10 @@ type Config struct { } `json:"defense"` Network struct { Timeout struct { - TCP TypeDuration `json:"tcp"` - HTTP TypeDuration `json:"http"` - Idle TypeDuration `json:"idle"` + TCP TypeDuration `json:"tcp"` + HTTP TypeDuration `json:"http"` + Idle TypeDuration `json:"idle"` + Handshake TypeDuration `json:"handshake"` } `json:"timeout"` DOHIP TypeIP `json:"dohIp"` DNS TypeDNSURI `json:"dns"` diff --git a/internal/config/parse.go b/internal/config/parse.go index 6bd16f7..6136588 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -55,9 +55,10 @@ type tomlConfig struct { } `toml:"defense" json:"defense,omitempty"` Network struct { Timeout struct { - TCP string `toml:"tcp" json:"tcp,omitempty"` - HTTP string `toml:"http" json:"http,omitempty"` - Idle string `toml:"idle" json:"idle,omitempty"` + TCP string `toml:"tcp" json:"tcp,omitempty"` + HTTP string `toml:"http" json:"http,omitempty"` + Idle string `toml:"idle" json:"idle,omitempty"` + Handshake string `toml:"handshake" json:"handshake,omitempty"` } `toml:"timeout" json:"timeout,omitempty"` DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` DNS string `toml:"dns" json:"dns,omitempty"` diff --git a/mtglib/init.go b/mtglib/init.go index 327539a..dccd5ac 100644 --- a/mtglib/init.go +++ b/mtglib/init.go @@ -81,6 +81,10 @@ const ( // avoid racing with MTProto ping_delay_disconnect (~60s interval). DefaultIdleTimeout = 5 * time.Minute + // DefaultHandshakeTimeout defines a time period during which the + // all handshake ceremonies must be completed. + DefaultHandshakeTimeout = 10 * time.Second + // DefaultTolerateTimeSkewness is a default timeout for time skewness on a // faketls timeout verification. DefaultTolerateTimeSkewness = 3 * time.Second diff --git a/mtglib/internal/tls/fake/client_side.go b/mtglib/internal/tls/fake/client_side.go index 66542bc..f79642f 100644 --- a/mtglib/internal/tls/fake/client_side.go +++ b/mtglib/internal/tls/fake/client_side.go @@ -40,11 +40,6 @@ func ReadClientHello( hostname string, tolerateTimeSkewness time.Duration, ) (*ClientHello, error) { - if err := conn.SetReadDeadline(time.Now().Add(ClientHelloReadTimeout)); err != nil { - return nil, fmt.Errorf("cannot set read deadline: %w", err) - } - defer conn.SetReadDeadline(resetDeadline) //nolint: errcheck - // This is how FakeTLS is organized: // 1. We create sha256 HMAC with a given secret // 2. We dump there a whole TLS frame except of the fact that random diff --git a/mtglib/internal/tls/fake/client_side_snapshot_test.go b/mtglib/internal/tls/fake/client_side_snapshot_test.go index e4e0480..ab44ea0 100644 --- a/mtglib/internal/tls/fake/client_side_snapshot_test.go +++ b/mtglib/internal/tls/fake/client_side_snapshot_test.go @@ -12,7 +12,6 @@ import ( "github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/mtglib/internal/tls/fake" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -71,11 +70,6 @@ func (suite *ParseClientHelloSnapshotTestSuite) makeConn(data []byte) *parseClie readBuf: readBuf, } - connMock. - On("SetReadDeadline", mock.AnythingOfType("time.Time")). - Twice(). - Return(nil) - return connMock } diff --git a/mtglib/internal/tls/fake/client_side_test.go b/mtglib/internal/tls/fake/client_side_test.go index bf23409..d27de03 100644 --- a/mtglib/internal/tls/fake/client_side_test.go +++ b/mtglib/internal/tls/fake/client_side_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/binary" "encoding/json" - "errors" "io" "os" "testing" @@ -14,7 +13,6 @@ import ( "github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/mtglib/internal/tls" "github.com/9seconds/mtg/v2/mtglib/internal/tls/fake" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -53,11 +51,6 @@ func (suite *ParseClientHelloTestSuite) SetupTest() { suite.connMock = &parseClientHelloConnMock{ readBuf: suite.readBuf, } - - suite.connMock. - On("SetReadDeadline", mock.AnythingOfType("time.Time")). - Twice(). - Return(nil) } func (suite *ParseClientHelloTestSuite) TearDownTest() { @@ -69,23 +62,11 @@ type ParseClientHello_TLSHeaderTestSuite struct { } func (suite *ParseClientHello_TLSHeaderTestSuite) TestEmpty() { - suite.connMock.ExpectedCalls = []*mock.Call{} - suite.connMock. - On("SetReadDeadline", mock.AnythingOfType("time.Time")). - Once(). - Return(errors.New("fail")) - _, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime) - suite.ErrorContains(err, "fail") + suite.ErrorContains(err, "cannot read client hello") } func (suite *ParseClientHello_TLSHeaderTestSuite) TestNothing() { - suite.connMock.ExpectedCalls = []*mock.Call{} - suite.connMock. - On("SetReadDeadline", mock.AnythingOfType("time.Time")). - Twice(). - Return(nil) - _, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime) suite.ErrorIs(err, io.EOF) } @@ -478,11 +459,6 @@ func (s *ParseClientHelloFragmentedTestSuite) makeConn(data []byte) *parseClient readBuf: readBuf, } - connMock. - On("SetReadDeadline", mock.AnythingOfType("time.Time")). - Twice(). - Return(nil) - return connMock } diff --git a/mtglib/internal/tls/fake/init.go b/mtglib/internal/tls/fake/init.go index 77bbe6b..0e60876 100644 --- a/mtglib/internal/tls/fake/init.go +++ b/mtglib/internal/tls/fake/init.go @@ -2,15 +2,6 @@ package fake import ( "errors" - "time" ) -const ( - ClientHelloReadTimeout = 5 * time.Second -) - -var ( - resetDeadline time.Time - - ErrBadDigest = errors.New("incorrect client random") -) +var ErrBadDigest = errors.New("incorrect client random") diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 7acf8b7..a89e18c 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -28,6 +28,7 @@ type Proxy struct { allowFallbackOnUnknownDC bool tolerateTimeSkewness time.Duration idleTimeout time.Duration + handshakeTimeout time.Duration domainFrontingPort int domainFrontingIP string domainFrontingProxyProtocol bool @@ -66,6 +67,11 @@ func (p *Proxy) ServeConn(conn essentials.Conn) { ctx := newStreamContext(p.ctx, p.logger, conn) defer ctx.Close() + if err := ctx.clientConn.SetDeadline(time.Now().Add(p.handshakeTimeout)); err != nil { + ctx.logger.WarningError("cannot set handshake timeout", err) + return + } + stop := context.AfterFunc(ctx, func() { ctx.Close() }) @@ -97,6 +103,11 @@ func (p *Proxy) ServeConn(conn essentials.Conn) { return } + if err := ctx.clientConn.SetDeadline(time.Time{}); err != nil { + ctx.logger.WarningError("cannot set deadline", err) + return + } + if err := p.doTelegramCall(ctx); err != nil { ctx.logger.WarningError("cannot dial to telegram", err) return @@ -346,6 +357,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { domainFrontingIP: opts.DomainFrontingIP, tolerateTimeSkewness: opts.getTolerateTimeSkewness(), idleTimeout: opts.getIdleTimeout(), + handshakeTimeout: opts.getHandshakeTimeout(), allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, telegram: tg, doppelGanger: doppel.NewGanger( diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index c4516c8..8dc69a5 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -70,6 +70,12 @@ type ProxyOpts struct { // This is an optional setting. IdleTimeout time.Duration + // HandshakeTimeout is a timeout during which all handshake ceremonies must + // be completed, otherwise this process will be aborted + // + // This is an optional setting. + HandshakeTimeout time.Duration + // TolerateTimeSkewness is a time boundary that defines a time range where // faketls timestamp is acceptable. // @@ -215,6 +221,14 @@ func (p ProxyOpts) getPreferIP() string { return p.PreferIP } +func (p ProxyOpts) getHandshakeTimeout() time.Duration { + if p.HandshakeTimeout == 0 { + return DefaultHandshakeTimeout + } + + return p.HandshakeTimeout +} + func (p ProxyOpts) getIdleTimeout() time.Duration { if p.IdleTimeout == 0 { return DefaultIdleTimeout