Browse Source

Merge upstream: handshake timeout, GREASE cipher fix, no-default-TLS-cipher

Merges upstream commits:
- bec321d Fix DPI detection: skip GREASE cipher suite in ClientHello parsing
- eb56493 Add separate handshake timeout
- 39ab557 Small refactoring
- 5bf218f Do not use default TLS cipher

Conflict resolution:
- Kept ReadClientHelloMulti for multi-secret support
- Preserved Secrets/Throttle/APIBindTo features from dolonet fork
- Removed per-connection SetReadDeadline from ReadClientHelloMulti
  (superseded by proxy-level handshake timeout via SetDeadline)
pull/450/head
dolonet 4 weeks ago
parent
commit
e84b6ba25d

+ 1
- 3
example.config.toml View File

@@ -220,13 +220,11 @@ proxies = [
220 220
 # means a timeout on pumping data between sockset when nothing is
221 221
 # happening.
222 222
 #
223
-# please be noticed that handshakes have no timeouts intentionally. You can
224
-# find a reasoning here:
225
-# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf
226 223
 [network.timeout]
227 224
 tcp = "5s"
228 225
 http = "10s"
229 226
 idle = "5m"
227
+handshake = "10s"
230 228
 
231 229
 # mtg has to mimic real websites. It does not mean domain fronting, it also
232 230
 # means that traffic characteristics should be similar to real world traffic.

+ 1
- 0
internal/cli/run_proxy.go View File

@@ -264,6 +264,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
264 264
 		AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
265 265
 		TolerateTimeSkewness:     conf.TolerateTimeSkewness.Value,
266 266
 		IdleTimeout:              conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout),
267
+		HandshakeTimeout:         conf.Network.Timeout.Handshake.Get(mtglib.DefaultHandshakeTimeout),
267 268
 
268 269
 		DoppelGangerURLs:    doppelGangerURLs,
269 270
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),

+ 4
- 3
internal/config/config.go View File

@@ -61,9 +61,10 @@ type Config struct {
61 61
 	} `json:"defense"`
62 62
 	Network struct {
63 63
 		Timeout struct {
64
-			TCP  TypeDuration `json:"tcp"`
65
-			HTTP TypeDuration `json:"http"`
66
-			Idle TypeDuration `json:"idle"`
64
+			TCP       TypeDuration `json:"tcp"`
65
+			HTTP      TypeDuration `json:"http"`
66
+			Idle      TypeDuration `json:"idle"`
67
+			Handshake TypeDuration `json:"handshake"`
67 68
 		} `json:"timeout"`
68 69
 		DOHIP   TypeIP         `json:"dohIp"`
69 70
 		DNS     TypeDNSURI     `json:"dns"`

+ 4
- 3
internal/config/parse.go View File

@@ -56,9 +56,10 @@ type tomlConfig struct {
56 56
 	} `toml:"defense" json:"defense,omitempty"`
57 57
 	Network struct {
58 58
 		Timeout struct {
59
-			TCP  string `toml:"tcp" json:"tcp,omitempty"`
60
-			HTTP string `toml:"http" json:"http,omitempty"`
61
-			Idle string `toml:"idle" json:"idle,omitempty"`
59
+			TCP       string `toml:"tcp" json:"tcp,omitempty"`
60
+			HTTP      string `toml:"http" json:"http,omitempty"`
61
+			Idle      string `toml:"idle" json:"idle,omitempty"`
62
+			Handshake string `toml:"handshake" json:"handshake,omitempty"`
62 63
 		} `toml:"timeout" json:"timeout,omitempty"`
63 64
 		DOHIP   string   `toml:"doh-ip" json:"dohIp,omitempty"`
64 65
 		DNS     string   `toml:"dns" json:"dns,omitempty"`

+ 4
- 0
mtglib/init.go View File

@@ -81,6 +81,10 @@ const (
81 81
 	// avoid racing with MTProto ping_delay_disconnect (~60s interval).
82 82
 	DefaultIdleTimeout = 5 * time.Minute
83 83
 
84
+	// DefaultHandshakeTimeout defines a time period during which the
85
+	// all handshake ceremonies must be completed.
86
+	DefaultHandshakeTimeout = 10 * time.Second
87
+
84 88
 	// DefaultTolerateTimeSkewness is a default timeout for time skewness on a
85 89
 	// faketls timeout verification.
86 90
 	DefaultTolerateTimeSkewness = 3 * time.Second

+ 27
- 13
mtglib/internal/tls/fake/client_side.go View File

@@ -6,6 +6,7 @@ import (
6 6
 	"crypto/sha256"
7 7
 	"crypto/subtle"
8 8
 	"encoding/binary"
9
+	"errors"
9 10
 	"fmt"
10 11
 	"io"
11 12
 	"net"
@@ -20,12 +21,19 @@ const (
20 21
 	// record_type(1) + version(2) + size(2) + handshake_type(1) + uint24_length(3) + client_version(2)
21 22
 	RandomOffset = 1 + 2 + 2 + 1 + 3 + 2
22 23
 
24
+	// https://datatracker.ietf.org/doc/html/rfc8701#name-grease-values
25
+	// https://medium.com/asecuritysite-when-bob-met-alice/in-cybersecurity-what-is-grease-9f8850558dea
26
+	GreaseMask      = 0x0f0f
27
+	GreaseValueType = 0x0a0a
28
+
23 29
 	sniDNSNamesListType = 0
24 30
 )
25 31
 
26 32
 var (
27 33
 	emptyRandom = [RandomLen]byte{}
28 34
 	extTypeSNI  = [2]byte{}
35
+
36
+	ErrCannotFindCipher = errors.New("cannot find a cipher")
29 37
 )
30 38
 
31 39
 type ClientHello struct {
@@ -64,11 +72,6 @@ func ReadClientHelloMulti(
64 72
 	hostname string,
65 73
 	tolerateTimeSkewness time.Duration,
66 74
 ) (*ReadClientHelloResult, error) {
67
-	if err := conn.SetReadDeadline(time.Now().Add(ClientHelloReadTimeout)); err != nil {
68
-		return nil, fmt.Errorf("cannot set read deadline: %w", err)
69
-	}
70
-	defer conn.SetReadDeadline(resetDeadline) //nolint: errcheck
71
-
72 75
 	clientHelloCopy, handshakeReader, err := parseClientHello(conn)
73 76
 	if err != nil {
74 77
 		return nil, fmt.Errorf("cannot read client hello: %w", err)
@@ -155,16 +158,27 @@ func parseHandshake(r io.Reader) (*ClientHello, error) {
155 158
 
156 159
 	cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
157 160
 
158
-	// we do not care about picking up any cipher. we pick the first one,
159
-	// so it is always should be present.
160
-	if _, err := io.ReadFull(r, header[:]); err != nil {
161
-		return nil, fmt.Errorf("cannot read first cipher suite: %w", err)
162
-	}
161
+	// Pick the first non-GREASE cipher suite from the list.
162
+	// Real TLS servers never select GREASE values (RFC 8701, pattern 0x?a?a),
163
+	// so echoing them back is a trivial DPI fingerprint.
164
+	// cipherSuiteLen is in bytes; each cipher suite is 2 bytes.
165
+	for range cipherSuiteLen / 2 {
166
+		if _, err := io.ReadFull(r, header[:]); err != nil {
167
+			return nil, fmt.Errorf("cannot read cipher suite: %w", err)
168
+		}
169
+
170
+		if hello.CipherSuite != 0 {
171
+			// do not forget we have to scan until the end
172
+			continue
173
+		}
163 174
 
164
-	hello.CipherSuite = binary.BigEndian.Uint16(header[:])
175
+		if cs := binary.BigEndian.Uint16(header[:]); cs&GreaseMask != GreaseValueType {
176
+			hello.CipherSuite = cs
177
+		}
178
+	}
165 179
 
166
-	if _, err := io.CopyN(io.Discard, r, cipherSuiteLen-2); err != nil {
167
-		return nil, fmt.Errorf("cannot skip remaining cipher suites: %w", err)
180
+	if hello.CipherSuite == 0 {
181
+		return nil, ErrCannotFindCipher
168 182
 	}
169 183
 
170 184
 	if _, err := io.ReadFull(r, header[:1]); err != nil {

+ 0
- 6
mtglib/internal/tls/fake/client_side_snapshot_test.go View File

@@ -12,7 +12,6 @@ import (
12 12
 	"github.com/dolonet/mtg-multi/mtglib"
13 13
 	"github.com/dolonet/mtg-multi/mtglib/internal/tls/fake"
14 14
 	"github.com/stretchr/testify/assert"
15
-	"github.com/stretchr/testify/mock"
16 15
 	"github.com/stretchr/testify/require"
17 16
 	"github.com/stretchr/testify/suite"
18 17
 )
@@ -71,11 +70,6 @@ func (suite *ParseClientHelloSnapshotTestSuite) makeConn(data []byte) *parseClie
71 70
 		readBuf: readBuf,
72 71
 	}
73 72
 
74
-	connMock.
75
-		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
76
-		Twice().
77
-		Return(nil)
78
-
79 73
 	return connMock
80 74
 }
81 75
 

+ 23
- 28
mtglib/internal/tls/fake/client_side_test.go View File

@@ -2,9 +2,9 @@ package fake_test
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	cryptotls "crypto/tls"
5 6
 	"encoding/binary"
6 7
 	"encoding/json"
7
-	"errors"
8 8
 	"io"
9 9
 	"os"
10 10
 	"testing"
@@ -14,7 +14,6 @@ import (
14 14
 	"github.com/dolonet/mtg-multi/mtglib"
15 15
 	"github.com/dolonet/mtg-multi/mtglib/internal/tls"
16 16
 	"github.com/dolonet/mtg-multi/mtglib/internal/tls/fake"
17
-	"github.com/stretchr/testify/mock"
18 17
 	"github.com/stretchr/testify/require"
19 18
 	"github.com/stretchr/testify/suite"
20 19
 )
@@ -53,11 +52,6 @@ func (suite *ParseClientHelloTestSuite) SetupTest() {
53 52
 	suite.connMock = &parseClientHelloConnMock{
54 53
 		readBuf: suite.readBuf,
55 54
 	}
56
-
57
-	suite.connMock.
58
-		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
59
-		Twice().
60
-		Return(nil)
61 55
 }
62 56
 
63 57
 func (suite *ParseClientHelloTestSuite) TearDownTest() {
@@ -69,23 +63,11 @@ type ParseClientHello_TLSHeaderTestSuite struct {
69 63
 }
70 64
 
71 65
 func (suite *ParseClientHello_TLSHeaderTestSuite) TestEmpty() {
72
-	suite.connMock.ExpectedCalls = []*mock.Call{}
73
-	suite.connMock.
74
-		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
75
-		Once().
76
-		Return(errors.New("fail"))
77
-
78 66
 	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
79
-	suite.ErrorContains(err, "fail")
67
+	suite.ErrorContains(err, "cannot read client hello")
80 68
 }
81 69
 
82 70
 func (suite *ParseClientHello_TLSHeaderTestSuite) TestNothing() {
83
-	suite.connMock.ExpectedCalls = []*mock.Call{}
84
-	suite.connMock.
85
-		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
86
-		Twice().
87
-		Return(nil)
88
-
89 71
 	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
90 72
 	suite.ErrorIs(err, io.EOF)
91 73
 }
@@ -234,12 +216,13 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCipherSuiteLe
234 216
 }
235 217
 
236 218
 func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadFirstCipherSuite() {
237
-	body := make([]byte, 2+fake.RandomLen+1+2)
219
+	body := make([]byte, 2+fake.RandomLen+1+2+1) // cipherSuiteLen=2 but only 1 byte available
220
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
238 221
 
239 222
 	suite.writeBody(body)
240 223
 
241 224
 	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
242
-	suite.ErrorContains(err, "cannot read first cipher suite")
225
+	suite.ErrorContains(err, "cannot read cipher suite")
243 226
 }
244 227
 
245 228
 func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCipherSuites() {
@@ -249,12 +232,27 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCiph
249 232
 	suite.writeBody(body)
250 233
 
251 234
 	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
252
-	suite.ErrorContains(err, "cannot skip remaining cipher suites")
235
+	suite.ErrorContains(err, "cannot read cipher suite")
236
+}
237
+
238
+func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotFindCipher() {
239
+	// All cipher suites are GREASE values — must return ErrCannotFindCipher.
240
+	body := make([]byte, 2+fake.RandomLen+1+2+4+1)
241
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 4)
242
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2:], 0x0a0a)
243
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2+2:], 0x1a1a)
244
+	body[2+fake.RandomLen+1+2+4] = 1
245
+
246
+	suite.writeBody(body)
247
+
248
+	_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
249
+	suite.ErrorIs(err, fake.ErrCannotFindCipher)
253 250
 }
254 251
 
255 252
 func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() {
256 253
 	body := make([]byte, 2+fake.RandomLen+1+2+2)
257 254
 	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
255
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2:], cryptotls.TLS_AES_128_GCM_SHA256)
258 256
 
259 257
 	suite.writeBody(body)
260 258
 
@@ -265,6 +263,7 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMe
265 263
 func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipCompressionMethods() {
266 264
 	body := make([]byte, 2+fake.RandomLen+1+2+2+1)
267 265
 	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
266
+	binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2:], cryptotls.TLS_AES_128_GCM_SHA256)
268 267
 	body[2+fake.RandomLen+1+2+2] = 1
269 268
 
270 269
 	suite.writeBody(body)
@@ -300,6 +299,7 @@ func (suite *ParseClientHelloSNITestSuite) writeExtensions(extensions []byte) {
300 299
 	// cipherSuite(2) + compressionLen(1) + compression(1) = 41
301 300
 	body := make([]byte, 41)
302 301
 	binary.BigEndian.PutUint16(body[35:], 2)
302
+	binary.BigEndian.PutUint16(body[37:], cryptotls.TLS_AES_128_GCM_SHA256)
303 303
 	body[39] = 1
304 304
 
305 305
 	suite.readBuf.Write(body)
@@ -478,11 +478,6 @@ func (s *ParseClientHelloFragmentedTestSuite) makeConn(data []byte) *parseClient
478 478
 		readBuf: readBuf,
479 479
 	}
480 480
 
481
-	connMock.
482
-		On("SetReadDeadline", mock.AnythingOfType("time.Time")).
483
-		Twice().
484
-		Return(nil)
485
-
486 481
 	return connMock
487 482
 }
488 483
 

+ 2
- 13
mtglib/internal/tls/fake/init.go View File

@@ -1,16 +1,5 @@
1 1
 package fake
2 2
 
3
-import (
4
-	"errors"
5
-	"time"
6
-)
3
+import "errors"
7 4
 
8
-const (
9
-	ClientHelloReadTimeout = 5 * time.Second
10
-)
11
-
12
-var (
13
-	resetDeadline time.Time
14
-
15
-	ErrBadDigest = errors.New("incorrect client random")
16
-)
5
+var ErrBadDigest = errors.New("incorrect client random")

+ 1
- 1
mtglib/internal/tls/fake/server_side_test.go View File

@@ -58,7 +58,7 @@ func (suite *SendServerHelloTestSuite) TestRecordStructure() {
58 58
 	recordType, length, err := tls.ReadRecord(suite.buf, &rec)
59 59
 	suite.NoError(err)
60 60
 	suite.Equal(byte(tls.TypeApplicationData), recordType)
61
-	suite.Greater(length, int64(2500))
61
+	suite.GreaterOrEqual(length, int64(2500))
62 62
 
63 63
 	suite.Empty(suite.buf.Bytes())
64 64
 }

+ 8
- 0
mtglib/internal/tls/fake/testdata/client-hello-ok-grease-first.json View File

@@ -0,0 +1,8 @@
1
+{
2
+  "time": 1617181365,
3
+  "random": "w4TaDfYg/aUKdx1oi68vxMKvHJczRNvtRRppLETzeNE=",
4
+  "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
5
+  "host": "storage.googleapis.com",
6
+  "cipherSuite": 4867,
7
+  "full": "FgMBAgIBAAH+AwPDhNoN9iD9pQp3HWiLry/Ewq8clzNE2+1FGmksRPN40SBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIwKrgA2WloTAxMBEwLALMArwCTAI8AKwAnMqcAwwC/AKMAnwBTAE8yoAJ0AnAA9ADwANQAvwAjAEgAKAQABf/8BAAEAAAAAGwAZAAAWc3RvcmFnZS5nb29nbGVhcGlzLmNvbQAXAAAADQAYABYEAwgEBAEFAwIDCAUIBQUBCAYGAQIBAAUABQEAAAAAM3QAAAASAAAAEAAwAC4CaDIFaDItMTYFaDItMTUFaDItMTQIc3BkeS8zLjEGc3BkeS8zCGh0dHAvMS4xAAsAAgEAADMAJgAkAB0AIAf+6C8fSRJSAC7CyUvdR9kDclNR9KLCsCFHpVZ3bC8iAC0AAgEBACsACQgDBAMDAwIDAQAKAAoACAAdABcAGAAZABUAoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
8
+}

+ 12
- 0
mtglib/proxy.go View File

@@ -29,6 +29,7 @@ type Proxy struct {
29 29
 	allowFallbackOnUnknownDC    bool
30 30
 	tolerateTimeSkewness        time.Duration
31 31
 	idleTimeout                 time.Duration
32
+	handshakeTimeout            time.Duration
32 33
 	domainFrontingPort          int
33 34
 	domainFrontingIP            string
34 35
 	domainFrontingProxyProtocol bool
@@ -70,6 +71,11 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
70 71
 	ctx := newStreamContext(p.ctx, p.logger, conn)
71 72
 	defer ctx.Close()
72 73
 
74
+	if err := ctx.clientConn.SetDeadline(time.Now().Add(p.handshakeTimeout)); err != nil {
75
+		ctx.logger.WarningError("cannot set handshake timeout", err)
76
+		return
77
+	}
78
+
73 79
 	stop := context.AfterFunc(ctx, func() {
74 80
 		ctx.Close()
75 81
 	})
@@ -113,6 +119,11 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
113 119
 		return
114 120
 	}
115 121
 
122
+	if err := ctx.clientConn.SetDeadline(time.Time{}); err != nil {
123
+		ctx.logger.WarningError("cannot set deadline", err)
124
+		return
125
+	}
126
+
116 127
 	if err := p.doTelegramCall(ctx); err != nil {
117 128
 		ctx.logger.WarningError("cannot dial to telegram", err)
118 129
 		return
@@ -409,6 +420,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
409 420
 		domainFrontingIP:         opts.DomainFrontingIP,
410 421
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
411 422
 		idleTimeout:              opts.getIdleTimeout(),
423
+		handshakeTimeout:         opts.getHandshakeTimeout(),
412 424
 		allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
413 425
 		telegram:                 tg,
414 426
 		doppelGanger: doppel.NewGanger(

+ 14
- 0
mtglib/proxy_opts.go View File

@@ -79,6 +79,12 @@ type ProxyOpts struct {
79 79
 	// This is an optional setting.
80 80
 	IdleTimeout time.Duration
81 81
 
82
+	// HandshakeTimeout is a timeout during which all handshake ceremonies must
83
+	// be completed, otherwise this process will be aborted
84
+	//
85
+	// This is an optional setting.
86
+	HandshakeTimeout time.Duration
87
+
82 88
 	// TolerateTimeSkewness is a time boundary that defines a time range where
83 89
 	// faketls timestamp is acceptable.
84 90
 	//
@@ -275,6 +281,14 @@ func (p ProxyOpts) getPreferIP() string {
275 281
 	return p.PreferIP
276 282
 }
277 283
 
284
+func (p ProxyOpts) getHandshakeTimeout() time.Duration {
285
+	if p.HandshakeTimeout == 0 {
286
+		return DefaultHandshakeTimeout
287
+	}
288
+
289
+	return p.HandshakeTimeout
290
+}
291
+
278 292
 func (p ProxyOpts) getIdleTimeout() time.Duration {
279 293
 	if p.IdleTimeout == 0 {
280 294
 		return DefaultIdleTimeout

Loading…
Cancel
Save