Merge pull request #447 from 9seconds/handshake-timeout

Add separate handshake timeout
This commit is contained in:
Sergei Arkhipov
2026-04-07 08:29:23 +02:00
committed by GitHub
11 changed files with 42 additions and 55 deletions
+1 -3
View File
@@ -205,13 +205,11 @@ proxies = [
# means a timeout on pumping data between sockset when nothing is # means a timeout on pumping data between sockset when nothing is
# happening. # 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] [network.timeout]
tcp = "5s" tcp = "5s"
http = "10s" http = "10s"
idle = "5m" idle = "5m"
handshake = "10s"
# mtg has to mimic real websites. It does not mean domain fronting, it also # 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. # means that traffic characteristics should be similar to real world traffic.
+1
View File
@@ -263,6 +263,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout), IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout),
HandshakeTimeout: conf.Network.Timeout.Handshake.Get(mtglib.DefaultHandshakeTimeout),
DoppelGangerURLs: doppelGangerURLs, DoppelGangerURLs: doppelGangerURLs,
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid), DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
+1
View File
@@ -63,6 +63,7 @@ type Config struct {
TCP TypeDuration `json:"tcp"` TCP TypeDuration `json:"tcp"`
HTTP TypeDuration `json:"http"` HTTP TypeDuration `json:"http"`
Idle TypeDuration `json:"idle"` Idle TypeDuration `json:"idle"`
Handshake TypeDuration `json:"handshake"`
} `json:"timeout"` } `json:"timeout"`
DOHIP TypeIP `json:"dohIp"` DOHIP TypeIP `json:"dohIp"`
DNS TypeDNSURI `json:"dns"` DNS TypeDNSURI `json:"dns"`
+1
View File
@@ -58,6 +58,7 @@ type tomlConfig struct {
TCP string `toml:"tcp" json:"tcp,omitempty"` TCP string `toml:"tcp" json:"tcp,omitempty"`
HTTP string `toml:"http" json:"http,omitempty"` HTTP string `toml:"http" json:"http,omitempty"`
Idle string `toml:"idle" json:"idle,omitempty"` Idle string `toml:"idle" json:"idle,omitempty"`
Handshake string `toml:"handshake" json:"handshake,omitempty"`
} `toml:"timeout" json:"timeout,omitempty"` } `toml:"timeout" json:"timeout,omitempty"`
DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"`
DNS string `toml:"dns" json:"dns,omitempty"` DNS string `toml:"dns" json:"dns,omitempty"`
+4
View File
@@ -81,6 +81,10 @@ const (
// avoid racing with MTProto ping_delay_disconnect (~60s interval). // avoid racing with MTProto ping_delay_disconnect (~60s interval).
DefaultIdleTimeout = 5 * time.Minute 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 // DefaultTolerateTimeSkewness is a default timeout for time skewness on a
// faketls timeout verification. // faketls timeout verification.
DefaultTolerateTimeSkewness = 3 * time.Second DefaultTolerateTimeSkewness = 3 * time.Second
-5
View File
@@ -47,11 +47,6 @@ func ReadClientHello(
hostname string, hostname string,
tolerateTimeSkewness time.Duration, tolerateTimeSkewness time.Duration,
) (*ClientHello, error) { ) (*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: // This is how FakeTLS is organized:
// 1. We create sha256 HMAC with a given secret // 1. We create sha256 HMAC with a given secret
// 2. We dump there a whole TLS frame except of the fact that random // 2. We dump there a whole TLS frame except of the fact that random
@@ -12,7 +12,6 @@ import (
"github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake" "github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@@ -71,11 +70,6 @@ func (suite *ParseClientHelloSnapshotTestSuite) makeConn(data []byte) *parseClie
readBuf: readBuf, readBuf: readBuf,
} }
connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
return connMock return connMock
} }
+1 -25
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors"
"io" "io"
"os" "os"
"testing" "testing"
@@ -14,7 +13,6 @@ import (
"github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls" "github.com/9seconds/mtg/v2/mtglib/internal/tls"
"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake" "github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@@ -53,11 +51,6 @@ func (suite *ParseClientHelloTestSuite) SetupTest() {
suite.connMock = &parseClientHelloConnMock{ suite.connMock = &parseClientHelloConnMock{
readBuf: suite.readBuf, readBuf: suite.readBuf,
} }
suite.connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
} }
func (suite *ParseClientHelloTestSuite) TearDownTest() { func (suite *ParseClientHelloTestSuite) TearDownTest() {
@@ -69,23 +62,11 @@ type ParseClientHello_TLSHeaderTestSuite struct {
} }
func (suite *ParseClientHello_TLSHeaderTestSuite) TestEmpty() { 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) _, 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() { 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) _, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
suite.ErrorIs(err, io.EOF) suite.ErrorIs(err, io.EOF)
} }
@@ -479,11 +460,6 @@ func (s *ParseClientHelloFragmentedTestSuite) makeConn(data []byte) *parseClient
readBuf: readBuf, readBuf: readBuf,
} }
connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
return connMock return connMock
} }
+1 -10
View File
@@ -2,15 +2,6 @@ package fake
import ( import (
"errors" "errors"
"time"
) )
const ( var ErrBadDigest = errors.New("incorrect client random")
ClientHelloReadTimeout = 5 * time.Second
)
var (
resetDeadline time.Time
ErrBadDigest = errors.New("incorrect client random")
)
+12
View File
@@ -28,6 +28,7 @@ type Proxy struct {
allowFallbackOnUnknownDC bool allowFallbackOnUnknownDC bool
tolerateTimeSkewness time.Duration tolerateTimeSkewness time.Duration
idleTimeout time.Duration idleTimeout time.Duration
handshakeTimeout time.Duration
domainFrontingPort int domainFrontingPort int
domainFrontingIP string domainFrontingIP string
domainFrontingProxyProtocol bool domainFrontingProxyProtocol bool
@@ -66,6 +67,11 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
ctx := newStreamContext(p.ctx, p.logger, conn) ctx := newStreamContext(p.ctx, p.logger, conn)
defer ctx.Close() 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() { stop := context.AfterFunc(ctx, func() {
ctx.Close() ctx.Close()
}) })
@@ -97,6 +103,11 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
return 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 { if err := p.doTelegramCall(ctx); err != nil {
ctx.logger.WarningError("cannot dial to telegram", err) ctx.logger.WarningError("cannot dial to telegram", err)
return return
@@ -346,6 +357,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
domainFrontingIP: opts.DomainFrontingIP, domainFrontingIP: opts.DomainFrontingIP,
tolerateTimeSkewness: opts.getTolerateTimeSkewness(), tolerateTimeSkewness: opts.getTolerateTimeSkewness(),
idleTimeout: opts.getIdleTimeout(), idleTimeout: opts.getIdleTimeout(),
handshakeTimeout: opts.getHandshakeTimeout(),
allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
telegram: tg, telegram: tg,
doppelGanger: doppel.NewGanger( doppelGanger: doppel.NewGanger(
+14
View File
@@ -70,6 +70,12 @@ type ProxyOpts struct {
// This is an optional setting. // This is an optional setting.
IdleTimeout time.Duration 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 // TolerateTimeSkewness is a time boundary that defines a time range where
// faketls timestamp is acceptable. // faketls timestamp is acceptable.
// //
@@ -215,6 +221,14 @@ func (p ProxyOpts) getPreferIP() string {
return p.PreferIP return p.PreferIP
} }
func (p ProxyOpts) getHandshakeTimeout() time.Duration {
if p.HandshakeTimeout == 0 {
return DefaultHandshakeTimeout
}
return p.HandshakeTimeout
}
func (p ProxyOpts) getIdleTimeout() time.Duration { func (p ProxyOpts) getIdleTimeout() time.Duration {
if p.IdleTimeout == 0 { if p.IdleTimeout == 0 {
return DefaultIdleTimeout return DefaultIdleTimeout