Bläddra i källkod

Add Linux DPI desync mode for FakeTLS

pull/572/head
Gzzle 1 vecka sedan
förälder
incheckning
9c49759382

+ 7
- 0
README.md Visa fil

@@ -369,6 +369,7 @@ Flags:
369 369
   -n, --doh-ip=1.1.1.1                 IP address of DNS-over-HTTP to use.
370 370
   -t, --timeout=10s                    Network timeout to use
371 371
   -a, --antireplay-cache-size="1MB"    A size of anti-replay cache to use.
372
+      --dpi-desync                     Enable Linux IPv4 DPI desync for fake TLS handshakes.
372 373
 ```
373 374
 
374 375
 So, if you want to startup a proxy with CLI only, you can do something like
@@ -401,6 +402,10 @@ bind-to = "0.0.0.0:443"
401 402
 This is enough to run the whole application. All other
402 403
 options already have sensible defaults for the app at almost any scale.
403 404
 
405
+`dpi-desync = true` enables Linux IPv4-only DPI desync for FakeTLS
406
+handshakes. It opens a raw packet socket, so run mtg as root or grant
407
+`CAP_NET_RAW`.
408
+
404 409
 ### Run a proxy
405 410
 
406 411
 Put a binary and a config into your webserver. Just for example,
@@ -422,6 +427,8 @@ RestartSec=3
422 427
 DynamicUser=true
423 428
 LimitNOFILE=65536
424 429
 AmbientCapabilities=CAP_NET_BIND_SERVICE
430
+# If dpi-desync = true, also add CAP_NET_RAW:
431
+# AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW
425 432
 
426 433
 [Install]
427 434
 WantedBy=multi-user.target

+ 16
- 0
essentials/conns.go Visa fil

@@ -1,8 +1,10 @@
1 1
 package essentials
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"io"
5 6
 	"net"
7
+	"syscall"
6 8
 )
7 9
 
8 10
 // CloseableReader is an [io.Reader] interface that can close its reading end.
@@ -49,3 +51,17 @@ func (n netConnWrapper) CloseWrite() error {
49 51
 func WrapNetConn(conn net.Conn) Conn {
50 52
 	return netConnWrapper{conn}
51 53
 }
54
+
55
+func SetTCPWindowClamp(conn net.Conn, value int) error {
56
+	sysConn, ok := conn.(syscall.Conn)
57
+	if !ok {
58
+		return nil
59
+	}
60
+
61
+	rawConn, err := sysConn.SyscallConn()
62
+	if err != nil {
63
+		return fmt.Errorf("cannot get raw connection: %w", err)
64
+	}
65
+
66
+	return SetRawTCPWindowClamp(rawConn, value)
67
+}

+ 25
- 0
essentials/tcp_options_linux.go Visa fil

@@ -0,0 +1,25 @@
1
+//go:build linux
2
+
3
+package essentials
4
+
5
+import (
6
+	"fmt"
7
+	"syscall"
8
+
9
+	"golang.org/x/sys/unix"
10
+)
11
+
12
+func SetRawTCPWindowClamp(conn syscall.RawConn, value int) error {
13
+	var err error
14
+
15
+	if controlErr := conn.Control(func(fd uintptr) {
16
+		err = unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_WINDOW_CLAMP, value)
17
+	}); controlErr != nil && err == nil {
18
+		return fmt.Errorf("cannot access TCP socket: %w", controlErr)
19
+	}
20
+	if err != nil {
21
+		return fmt.Errorf("cannot set TCP_WINDOW_CLAMP: %w", err)
22
+	}
23
+
24
+	return nil
25
+}

+ 9
- 0
essentials/tcp_options_other.go Visa fil

@@ -0,0 +1,9 @@
1
+//go:build !linux
2
+
3
+package essentials
4
+
5
+import "syscall"
6
+
7
+func SetRawTCPWindowClamp(_ syscall.RawConn, _ int) error {
8
+	return nil
9
+}

+ 6
- 0
example.config.toml Visa fil

@@ -37,6 +37,12 @@ bind-to = "0.0.0.0:3128"
37 37
 # default value is false.
38 38
 # proxy-protocol-listener = false
39 39
 
40
+# Enable Linux IPv4 DPI desync for FakeTLS handshakes. This opens a raw packet
41
+# socket, so the process needs root or CAP_NET_RAW. The option is ignored on
42
+# non-Linux builds.
43
+# default value is false.
44
+# dpi-desync = false
45
+
40 46
 # Defines how many concurrent connections are allowed to this proxy.
41 47
 # All other incoming connections are going to be dropped.
42 48
 concurrency = 8192

+ 25
- 3
internal/cli/run_proxy.go Visa fil

@@ -11,6 +11,7 @@ import (
11 11
 	"github.com/9seconds/mtg/v2/antireplay"
12 12
 	"github.com/9seconds/mtg/v2/events"
13 13
 	"github.com/9seconds/mtg/v2/internal/config"
14
+	"github.com/9seconds/mtg/v2/internal/desync"
14 15
 	"github.com/9seconds/mtg/v2/internal/proxyprotocol"
15 16
 	"github.com/9seconds/mtg/v2/internal/utils"
16 17
 	"github.com/9seconds/mtg/v2/ipblocklist"
@@ -179,7 +180,8 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr
179 180
 			conf.Stats.StatsD.Address.Get(""),
180 181
 			logger.Named("statsd"),
181 182
 			conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix),
182
-			conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat))
183
+			conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat),
184
+		)
183 185
 		if err != nil {
184 186
 			return nil, fmt.Errorf("cannot build statsd observer: %w", err)
185 187
 		}
@@ -254,6 +256,8 @@ func warnDeprecatedDomainFronting(conf *config.Config, log mtglib.Logger) {
254 256
 	}
255 257
 }
256 258
 
259
+const dpiDesyncHandshakeWindowClamp = 256
260
+
257 261
 func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop
258 262
 	logger := makeLogger(conf)
259 263
 
@@ -279,7 +283,8 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
279 283
 		ntw,
280 284
 		func(ctx context.Context, size int) {
281 285
 			eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
282
-		})
286
+		},
287
+	)
283 288
 	if err != nil {
284 289
 		return fmt.Errorf("cannot build ip blocklist: %w", err)
285 290
 	}
@@ -296,6 +301,13 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
296 301
 		return fmt.Errorf("cannot build ip allowlist: %w", err)
297 302
 	}
298 303
 
304
+	windowClamp := 0
305
+	if conf.DPIDesync.Get(false) {
306
+		// Empirically chosen: small enough for Linux IPv4 DPI desync, but still
307
+		// large enough for Telegram media after the post-handshake clamp restore.
308
+		windowClamp = dpiDesyncHandshakeWindowClamp
309
+	}
310
+
299 311
 	doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
300 312
 	for i, v := range conf.Defense.Doppelganger.URLs {
301 313
 		doppelGangerURLs[i] = v.String()
@@ -326,6 +338,8 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
326 338
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
327 339
 		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
328 340
 		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
341
+
342
+		DPIDesync: windowClamp > 0,
329 343
 	}
330 344
 
331 345
 	proxy, err := mtglib.NewProxy(opts)
@@ -333,7 +347,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
333 347
 		return fmt.Errorf("cannot create a proxy: %w", err)
334 348
 	}
335 349
 
336
-	listener, err := utils.NewListener(conf.BindTo.Get(""), 0)
350
+	listener, err := utils.NewListener(conf.BindTo.Get(""), windowClamp)
337 351
 	if err != nil {
338 352
 		return fmt.Errorf("cannot start proxy: %w", err)
339 353
 	}
@@ -348,6 +362,14 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
348 362
 
349 363
 	ctx := utils.RootContext()
350 364
 
365
+	if windowClamp > 0 {
366
+		desyncSvc, err := desync.Start(int(conf.BindTo.Port))
367
+		if err != nil {
368
+			return fmt.Errorf("cannot start raw desync: %w", err)
369
+		}
370
+		defer desyncSvc.Close() //nolint: errcheck
371
+	}
372
+
351 373
 	go proxy.Serve(listener) //nolint: errcheck
352 374
 
353 375
 	<-ctx.Done()

+ 14
- 11
internal/cli/simple_run.go Visa fil

@@ -13,17 +13,19 @@ type SimpleRun struct {
13 13
 	BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
14 14
 	Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`
15 15
 
16
-	Debug               bool          `kong:"name='debug',short='d',help='Run in debug mode.'"`                                                                        //nolint: lll
17
-	Concurrency         uint64        `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"`                        //nolint: lll
18
-	TCPBuffer           string        `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"`                                                 //nolint: lll
19
-	PreferIP            string        `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` //nolint: lll
20
-	DomainFrontingPort  uint64        `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"`                                                                  //nolint: lll
21
-	DomainFrontingHost  string        `kong:"name='domain-fronting-host',help='Hostname or IP to dial for domain fronting instead of resolving the secret hostname.'"`                                           //nolint: lll
22
-	DomainFrontingIP    string        `kong:"name='domain-fronting-ip',help='Deprecated: use --domain-fronting-host. Setting this flag logs a warning at startup and the value is ignored.'"`                    //nolint: lll
23
-	DOHIP               net.IP        `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"`                                    //nolint: lll
24
-	Timeout             time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"`                                                    //nolint: lll
25
-	Socks5Proxies       []string      `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"`                                          //nolint: lll
26
-	AntiReplayCacheSize string        `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"`                         //nolint: lll
16
+	Debug               bool          `kong:"name='debug',short='d',help='Run in debug mode.'"`                                                                                               //nolint: lll
17
+	Concurrency         uint64        `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"`                                               //nolint: lll
18
+	TCPBuffer           string        `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"`                                                                        //nolint: lll
19
+	PreferIP            string        `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"`                        //nolint: lll
20
+	DomainFrontingPort  uint64        `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"`                                               //nolint: lll
21
+	DomainFrontingHost  string        `kong:"name='domain-fronting-host',help='Hostname or IP to dial for domain fronting instead of resolving the secret hostname.'"`                        //nolint: lll
22
+	DomainFrontingIP    string        `kong:"name='domain-fronting-ip',help='Deprecated: use --domain-fronting-host. Setting this flag logs a warning at startup and the value is ignored.'"` //nolint: lll
23
+	DOHIP               net.IP        `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"`                                                           //nolint: lll
24
+	Timeout             time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"`                                                                           //nolint: lll
25
+	Socks5Proxies       []string      `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"`                                                                 //nolint: lll
26
+	AntiReplayCacheSize string        `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"`                                                //nolint: lll
27
+
28
+	DPIDesync bool `kong:"name='dpi-desync',help='Enable Linux IPv4 DPI desync for fake TLS handshakes.'"` //nolint: lll
27 29
 
28 30
 	ProxyProtocolListener bool `kong:"name='proxy-protocol-listener',help='Expect PROXY protocol (v1 or v2) headers on the listener. Use when mtg sits behind HAProxy, nginx stream, or similar.'"` //nolint: lll
29 31
 }
@@ -99,6 +101,7 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funle
99 101
 	conf.Debug.Value = s.Debug
100 102
 	conf.AllowFallbackOnUnknownDC.Value = true
101 103
 	conf.Defense.AntiReplay.Enabled.Value = true
104
+	conf.DPIDesync.Value = s.DPIDesync
102 105
 	conf.ProxyProtocolListener.Value = s.ProxyProtocolListener
103 106
 
104 107
 	if err := conf.Validate(); err != nil {

+ 5
- 4
internal/config/config.go Visa fil

@@ -27,6 +27,7 @@ type Config struct {
27 27
 	Secret                      mtglib.Secret   `json:"secret"`
28 28
 	BindTo                      TypeHostPort    `json:"bindTo"`
29 29
 	ProxyProtocolListener       TypeBool        `json:"proxyProtocolListener"`
30
+	DPIDesync                   TypeBool        `json:"dpiDesync"`
30 31
 	PreferIP                    TypePreferIP    `json:"preferIp"`
31 32
 	AutoUpdate                  TypeBool        `json:"autoUpdate"`
32 33
 	DomainFrontingPort          TypePort        `json:"domainFrontingPort"`
@@ -71,10 +72,10 @@ type Config struct {
71 72
 			Interval TypeDuration    `json:"interval"`
72 73
 			Count    TypeConcurrency `json:"count"`
73 74
 		} `json:"keepAlive"`
74
-		DOHIP            TypeIP         `json:"dohIp"`
75
-		DNS              TypeDNSURI     `json:"dns"`
76
-		Proxies          []TypeProxyURL `json:"proxies"`
77
-		TCPNotSentLowat  TypeBytes      `json:"tcpNotSentLowat"`
75
+		DOHIP           TypeIP         `json:"dohIp"`
76
+		DNS             TypeDNSURI     `json:"dns"`
77
+		Proxies         []TypeProxyURL `json:"proxies"`
78
+		TCPNotSentLowat TypeBytes      `json:"tcpNotSentLowat"`
78 79
 	} `json:"network"`
79 80
 	Stats struct {
80 81
 		StatsD struct {

+ 6
- 0
internal/config/config_test.go Visa fil

@@ -68,6 +68,12 @@ func (suite *ConfigTestSuite) TestParsePublicIPNotSet() {
68 68
 	suite.Nil(conf.PublicIPv6.Get(nil))
69 69
 }
70 70
 
71
+func (suite *ConfigTestSuite) TestParseDPIDesync() {
72
+	conf, err := config.Parse(suite.ReadConfig("dpi_desync.toml"))
73
+	suite.NoError(err)
74
+	suite.True(conf.DPIDesync.Get(false))
75
+}
76
+
71 77
 func (suite *ConfigTestSuite) TestString() {
72 78
 	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
73 79
 	suite.NoError(err)

+ 1
- 0
internal/config/parse.go Visa fil

@@ -14,6 +14,7 @@ type tomlConfig struct {
14 14
 	Secret                      string `toml:"secret" json:"secret"`
15 15
 	BindTo                      string `toml:"bind-to" json:"bindTo"`
16 16
 	ProxyProtocolListener       bool   `toml:"proxy-protocol-listener" json:"proxyProtocolListener"`
17
+	DPIDesync                   bool   `toml:"dpi-desync" json:"dpiDesync,omitempty"`
17 18
 	PreferIP                    string `toml:"prefer-ip" json:"preferIp,omitempty"`
18 19
 	AutoUpdate                  bool   `toml:"auto-update" json:"autoUpdate,omitempty"`
19 20
 	DomainFrontingPort          uint   `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"`

+ 3
- 0
internal/config/testdata/dpi_desync.toml Visa fil

@@ -0,0 +1,3 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+dpi-desync = true

+ 372
- 0
internal/desync/service_linux.go Visa fil

@@ -0,0 +1,372 @@
1
+//go:build linux
2
+
3
+package desync
4
+
5
+import (
6
+	"encoding/binary"
7
+	"fmt"
8
+	"io"
9
+	"sync"
10
+	"time"
11
+
12
+	"golang.org/x/sys/unix"
13
+)
14
+
15
+const (
16
+	tcpFlagFin = 0x01
17
+	tcpFlagSyn = 0x02
18
+	tcpFlagPsh = 0x08
19
+	tcpFlagAck = 0x10
20
+
21
+	desyncStateTTL     = 30 * time.Second
22
+	desyncCleanupEvery = time.Second
23
+	ethernetHeaderLen  = 14
24
+	vlanHeaderLen      = 4
25
+	ipv4HeaderLen      = 20
26
+	tcpHeaderLen       = 20
27
+)
28
+
29
+var fakeTLSAlert = []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x28}
30
+
31
+type runner struct {
32
+	port      uint16
33
+	packetFD  int
34
+	rawFD     int
35
+	wg        sync.WaitGroup
36
+	closeOnce sync.Once
37
+	state     map[connKey]*connState
38
+	ident     uint16
39
+	cleanup   time.Time
40
+}
41
+
42
+type connKey struct {
43
+	clientIP   [4]byte
44
+	localIP    [4]byte
45
+	clientPort uint16
46
+}
47
+
48
+type connState struct {
49
+	clientSeq uint32
50
+	serverSeq uint32
51
+	expiresAt time.Time
52
+	sentMask  uint8
53
+}
54
+
55
+type tcpPacket struct {
56
+	srcIP   [4]byte
57
+	dstIP   [4]byte
58
+	srcPort uint16
59
+	dstPort uint16
60
+	seq     uint32
61
+	ack     uint32
62
+	flags   byte
63
+}
64
+
65
+func Start(port int) (io.Closer, error) {
66
+	if port <= 0 || port > 65535 {
67
+		return nil, fmt.Errorf("invalid desync port: %d", port)
68
+	}
69
+
70
+	packetFD, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW|unix.SOCK_CLOEXEC, int(htons(unix.ETH_P_IP)))
71
+	if err != nil {
72
+		return nil, fmt.Errorf("cannot open packet socket: %w", err)
73
+	}
74
+
75
+	rawFD, err := unix.Socket(unix.AF_INET, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.IPPROTO_RAW)
76
+	if err != nil {
77
+		unix.Close(packetFD) //nolint: errcheck
78
+
79
+		return nil, fmt.Errorf("cannot open raw ipv4 socket: %w", err)
80
+	}
81
+	if err := unix.SetsockoptInt(rawFD, unix.IPPROTO_IP, unix.IP_HDRINCL, 1); err != nil {
82
+		unix.Close(packetFD) //nolint: errcheck
83
+		unix.Close(rawFD)    //nolint: errcheck
84
+
85
+		return nil, fmt.Errorf("cannot enable IP_HDRINCL: %w", err)
86
+	}
87
+
88
+	r := &runner{
89
+		port:     uint16(port),
90
+		packetFD: packetFD,
91
+		rawFD:    rawFD,
92
+		state:    map[connKey]*connState{},
93
+	}
94
+
95
+	r.wg.Add(1)
96
+	go r.loop()
97
+
98
+	return r, nil
99
+}
100
+
101
+func (r *runner) Close() error {
102
+	r.closeOnce.Do(func() {
103
+		unix.Close(r.packetFD) //nolint: errcheck
104
+		unix.Close(r.rawFD)    //nolint: errcheck
105
+		r.wg.Wait()
106
+	})
107
+
108
+	return nil
109
+}
110
+
111
+func (r *runner) loop() {
112
+	defer r.wg.Done()
113
+
114
+	buf := make([]byte, 64*1024)
115
+	for {
116
+		n, _, err := unix.Recvfrom(r.packetFD, buf, 0)
117
+		if err != nil {
118
+			if err == unix.EBADF || err == unix.EINVAL {
119
+				return
120
+			}
121
+
122
+			continue
123
+		}
124
+
125
+		packet, ok := parseIPv4TCP(buf[:n])
126
+		if !ok {
127
+			continue
128
+		}
129
+
130
+		switch {
131
+		case packet.dstPort == r.port:
132
+			r.handleInbound(packet)
133
+		case packet.srcPort == r.port:
134
+			r.handleOutbound(packet)
135
+		}
136
+	}
137
+}
138
+
139
+func (r *runner) handleInbound(packet tcpPacket) {
140
+	key := connKey{
141
+		clientIP:   packet.srcIP,
142
+		localIP:    packet.dstIP,
143
+		clientPort: packet.srcPort,
144
+	}
145
+
146
+	now := time.Now()
147
+
148
+	r.cleanupExpired(now)
149
+
150
+	state := r.state[key]
151
+	if packet.flags&tcpFlagSyn != 0 && packet.flags&tcpFlagAck == 0 {
152
+		if state == nil {
153
+			state = &connState{}
154
+			r.state[key] = state
155
+		}
156
+		state.clientSeq = packet.seq
157
+		state.expiresAt = now.Add(desyncStateTTL)
158
+
159
+		return
160
+	}
161
+
162
+	if state == nil {
163
+		return
164
+	}
165
+
166
+	state.expiresAt = now.Add(desyncStateTTL)
167
+
168
+	if packet.flags&tcpFlagFin != 0 {
169
+		delete(r.state, key)
170
+
171
+		return
172
+	}
173
+
174
+	if packet.flags&tcpFlagAck != 0 &&
175
+		state.clientSeq != 0 &&
176
+		state.serverSeq != 0 &&
177
+		packet.seq == state.clientSeq+1 &&
178
+		packet.ack == state.serverSeq+1 {
179
+		r.sendFake(key, state, 1)
180
+		delete(r.state, key)
181
+	}
182
+}
183
+
184
+func (r *runner) handleOutbound(packet tcpPacket) {
185
+	if packet.flags&tcpFlagSyn == 0 || packet.flags&tcpFlagAck == 0 {
186
+		return
187
+	}
188
+
189
+	key := connKey{
190
+		clientIP:   packet.dstIP,
191
+		localIP:    packet.srcIP,
192
+		clientPort: packet.dstPort,
193
+	}
194
+
195
+	now := time.Now()
196
+
197
+	r.cleanupExpired(now)
198
+
199
+	state := r.state[key]
200
+	if state == nil {
201
+		state = &connState{}
202
+		r.state[key] = state
203
+	}
204
+
205
+	state.serverSeq = packet.seq
206
+	state.expiresAt = now.Add(desyncStateTTL)
207
+
208
+	if state.clientSeq != 0 {
209
+		r.sendFake(key, state, 0)
210
+	}
211
+}
212
+
213
+func (r *runner) sendFake(key connKey, state *connState, phase uint8) {
214
+	mask := uint8(1) << phase
215
+	if state.sentMask&mask != 0 {
216
+		return
217
+	}
218
+	state.sentMask |= mask
219
+	r.ident++
220
+
221
+	packet := buildIPv4TCPPacket(key.localIP, key.clientIP,
222
+		r.port, key.clientPort, state.serverSeq+1, state.clientSeq+1, r.ident)
223
+
224
+	unix.Sendto(r.rawFD, packet, 0, &unix.SockaddrInet4{Addr: key.clientIP}) //nolint: errcheck
225
+}
226
+
227
+func (r *runner) cleanupExpired(now time.Time) {
228
+	if now.Before(r.cleanup) {
229
+		return
230
+	}
231
+	r.cleanup = now.Add(desyncCleanupEvery)
232
+
233
+	for key, state := range r.state {
234
+		if now.After(state.expiresAt) {
235
+			delete(r.state, key)
236
+		}
237
+	}
238
+}
239
+
240
+func parseIPv4TCP(frame []byte) (tcpPacket, bool) {
241
+	if len(frame) < ethernetHeaderLen {
242
+		return tcpPacket{}, false
243
+	}
244
+
245
+	etherType := binary.BigEndian.Uint16(frame[12:14])
246
+	ipOffset := ethernetHeaderLen
247
+	if etherType == 0x8100 || etherType == 0x88a8 {
248
+		if len(frame) < ethernetHeaderLen+vlanHeaderLen {
249
+			return tcpPacket{}, false
250
+		}
251
+		etherType = binary.BigEndian.Uint16(frame[16:18])
252
+		ipOffset += vlanHeaderLen
253
+	}
254
+	if etherType != unix.ETH_P_IP {
255
+		return tcpPacket{}, false
256
+	}
257
+	if len(frame) < ipOffset+ipv4HeaderLen {
258
+		return tcpPacket{}, false
259
+	}
260
+
261
+	ip := frame[ipOffset:]
262
+	ihl := int(ip[0]&0x0f) * 4
263
+	if ihl < ipv4HeaderLen || len(ip) < ihl+tcpHeaderLen {
264
+		return tcpPacket{}, false
265
+	}
266
+	if ip[0]>>4 != 4 || ip[9] != unix.IPPROTO_TCP {
267
+		return tcpPacket{}, false
268
+	}
269
+
270
+	fragment := binary.BigEndian.Uint16(ip[6:8])
271
+	if fragment&0x1fff != 0 {
272
+		return tcpPacket{}, false
273
+	}
274
+
275
+	totalLen := int(binary.BigEndian.Uint16(ip[2:4]))
276
+	if totalLen < ihl+tcpHeaderLen || totalLen > len(ip) {
277
+		return tcpPacket{}, false
278
+	}
279
+
280
+	tcp := ip[ihl:totalLen]
281
+	dataOffset := int(tcp[12]>>4) * 4
282
+	if dataOffset < tcpHeaderLen || len(tcp) < dataOffset {
283
+		return tcpPacket{}, false
284
+	}
285
+
286
+	var packet tcpPacket
287
+	copy(packet.srcIP[:], ip[12:16])
288
+	copy(packet.dstIP[:], ip[16:20])
289
+	packet.srcPort = binary.BigEndian.Uint16(tcp[0:2])
290
+	packet.dstPort = binary.BigEndian.Uint16(tcp[2:4])
291
+	packet.seq = binary.BigEndian.Uint32(tcp[4:8])
292
+	packet.ack = binary.BigEndian.Uint32(tcp[8:12])
293
+	packet.flags = tcp[13]
294
+
295
+	return packet, true
296
+}
297
+
298
+func buildIPv4TCPPacket(
299
+	srcIP [4]byte,
300
+	dstIP [4]byte,
301
+	srcPort uint16,
302
+	dstPort uint16,
303
+	seq uint32,
304
+	ack uint32,
305
+	ident uint16,
306
+) []byte {
307
+	packet := make([]byte, ipv4HeaderLen+tcpHeaderLen+len(fakeTLSAlert))
308
+	ip := packet[:ipv4HeaderLen]
309
+	tcp := packet[ipv4HeaderLen : ipv4HeaderLen+tcpHeaderLen]
310
+
311
+	ip[0] = 0x45
312
+	binary.BigEndian.PutUint16(ip[2:4], uint16(len(packet)))
313
+	binary.BigEndian.PutUint16(ip[4:6], ident)
314
+	binary.BigEndian.PutUint16(ip[6:8], 0x4000)
315
+	ip[8] = 64
316
+	ip[9] = unix.IPPROTO_TCP
317
+	copy(ip[12:16], srcIP[:])
318
+	copy(ip[16:20], dstIP[:])
319
+	binary.BigEndian.PutUint16(ip[10:12], checksum(ip))
320
+
321
+	binary.BigEndian.PutUint16(tcp[0:2], srcPort)
322
+	binary.BigEndian.PutUint16(tcp[2:4], dstPort)
323
+	binary.BigEndian.PutUint32(tcp[4:8], seq)
324
+	binary.BigEndian.PutUint32(tcp[8:12], ack)
325
+	tcp[12] = 5 << 4
326
+	tcp[13] = tcpFlagPsh | tcpFlagAck
327
+	binary.BigEndian.PutUint16(tcp[14:16], 65535)
328
+	copy(packet[ipv4HeaderLen+tcpHeaderLen:], fakeTLSAlert)
329
+
330
+	// The checksum is deliberately invalid: DPI can still inspect the fake TLS
331
+	// alert, but the client TCP stack should drop it.
332
+	sum := tcpChecksum(srcIP, dstIP, tcp) ^ 0xffff
333
+	if sum == 0 {
334
+		sum = 0xffff
335
+	}
336
+	binary.BigEndian.PutUint16(tcp[16:18], sum)
337
+
338
+	return packet
339
+}
340
+
341
+func tcpChecksum(srcIP, dstIP [4]byte, tcp []byte) uint16 {
342
+	pseudo := make([]byte, 12+len(tcp)+len(fakeTLSAlert))
343
+	copy(pseudo[0:4], srcIP[:])
344
+	copy(pseudo[4:8], dstIP[:])
345
+	pseudo[9] = unix.IPPROTO_TCP
346
+	binary.BigEndian.PutUint16(pseudo[10:12], uint16(len(tcp)+len(fakeTLSAlert)))
347
+	copy(pseudo[12:], tcp)
348
+	copy(pseudo[12+len(tcp):], fakeTLSAlert)
349
+
350
+	return checksum(pseudo)
351
+}
352
+
353
+func checksum(data []byte) uint16 {
354
+	var sum uint32
355
+	for len(data) >= 2 {
356
+		sum += uint32(binary.BigEndian.Uint16(data[:2]))
357
+		data = data[2:]
358
+	}
359
+	if len(data) == 1 {
360
+		sum += uint32(data[0]) << 8
361
+	}
362
+
363
+	for sum>>16 != 0 {
364
+		sum = (sum & 0xffff) + (sum >> 16)
365
+	}
366
+
367
+	return ^uint16(sum)
368
+}
369
+
370
+func htons(value uint16) uint16 {
371
+	return (value << 8) | (value >> 8)
372
+}

+ 48
- 0
internal/desync/service_linux_test.go Visa fil

@@ -0,0 +1,48 @@
1
+//go:build linux
2
+
3
+package desync
4
+
5
+import (
6
+	"encoding/binary"
7
+	"testing"
8
+
9
+	"github.com/stretchr/testify/require"
10
+	"golang.org/x/sys/unix"
11
+)
12
+
13
+func TestBuildAndParseIPv4TCPPacket(t *testing.T) {
14
+	src := [4]byte{192, 0, 2, 10}
15
+	dst := [4]byte{198, 51, 100, 20}
16
+
17
+	ipPacket := buildIPv4TCPPacket(src, dst, 12345, 9595, 100, 200, 1)
18
+
19
+	require.Len(t, ipPacket, 20+20+len(fakeTLSAlert))
20
+	require.Equal(t, byte(64), ipPacket[8])
21
+	require.Equal(t, byte(unix.IPPROTO_TCP), ipPacket[9])
22
+	require.Equal(t, uint16(12345), binary.BigEndian.Uint16(ipPacket[20:22]))
23
+	require.Equal(t, uint16(9595), binary.BigEndian.Uint16(ipPacket[22:24]))
24
+	require.Equal(t, uint32(100), binary.BigEndian.Uint32(ipPacket[24:28]))
25
+	require.Equal(t, uint32(200), binary.BigEndian.Uint32(ipPacket[28:32]))
26
+	require.Equal(t, byte(tcpFlagPsh|tcpFlagAck), ipPacket[33])
27
+
28
+	wireSum := binary.BigEndian.Uint16(ipPacket[36:38])
29
+	tcp := append([]byte(nil), ipPacket[20:40]...)
30
+	binary.BigEndian.PutUint16(tcp[16:18], 0)
31
+	validSum := tcpChecksum(src, dst, tcp)
32
+	require.Equal(t, validSum^0xffff, wireSum)
33
+
34
+	frame := make([]byte, 14+len(ipPacket))
35
+	binary.BigEndian.PutUint16(frame[12:14], unix.ETH_P_IP)
36
+	copy(frame[14:], ipPacket)
37
+
38
+	packet, ok := parseIPv4TCP(frame)
39
+
40
+	require.True(t, ok)
41
+	require.Equal(t, [4]byte{192, 0, 2, 10}, packet.srcIP)
42
+	require.Equal(t, [4]byte{198, 51, 100, 20}, packet.dstIP)
43
+	require.Equal(t, uint16(12345), packet.srcPort)
44
+	require.Equal(t, uint16(9595), packet.dstPort)
45
+	require.Equal(t, uint32(100), packet.seq)
46
+	require.Equal(t, uint32(200), packet.ack)
47
+	require.Equal(t, byte(tcpFlagPsh|tcpFlagAck), packet.flags)
48
+}

+ 12
- 0
internal/desync/service_other.go Visa fil

@@ -0,0 +1,12 @@
1
+//go:build !linux
2
+
3
+package desync
4
+
5
+import (
6
+	"errors"
7
+	"io"
8
+)
9
+
10
+func Start(_ int) (io.Closer, error) {
11
+	return nil, errors.New("raw TCP desync is supported only on linux")
12
+}

+ 15
- 1
internal/proxyprotocol/conn.go Visa fil

@@ -1,6 +1,11 @@
1 1
 package proxyprotocol
2 2
 
3
-import "github.com/pires/go-proxyproto"
3
+import (
4
+	"errors"
5
+	"syscall"
6
+
7
+	"github.com/pires/go-proxyproto"
8
+)
4 9
 
5 10
 type connWrapper struct {
6 11
 	*proxyproto.Conn
@@ -23,3 +28,12 @@ func (c connWrapper) CloseWrite() error {
23 28
 
24 29
 	return tcpConn.CloseWrite()
25 30
 }
31
+
32
+func (c connWrapper) SyscallConn() (syscall.RawConn, error) {
33
+	tcpConn, ok := c.TCPConn()
34
+	if !ok {
35
+		return nil, errors.New("proxy protocol connection is not TCP")
36
+	}
37
+
38
+	return tcpConn.SyscallConn()
39
+}

+ 48
- 0
internal/proxyprotocol/conn_test.go Visa fil

@@ -0,0 +1,48 @@
1
+package proxyprotocol
2
+
3
+import (
4
+	"net"
5
+	"syscall"
6
+	"testing"
7
+
8
+	"github.com/pires/go-proxyproto"
9
+	"github.com/stretchr/testify/require"
10
+)
11
+
12
+func TestConnWrapperSyscallConn(t *testing.T) {
13
+	listener, err := net.Listen("tcp4", "127.0.0.1:0")
14
+	require.NoError(t, err)
15
+	defer listener.Close() //nolint: errcheck
16
+
17
+	accepted := make(chan net.Conn, 1)
18
+	acceptErr := make(chan error, 1)
19
+	go func() {
20
+		conn, err := listener.Accept()
21
+		if err != nil {
22
+			acceptErr <- err
23
+
24
+			return
25
+		}
26
+		accepted <- conn
27
+	}()
28
+
29
+	clientConn, err := net.Dial("tcp4", listener.Addr().String())
30
+	require.NoError(t, err)
31
+	defer clientConn.Close() //nolint: errcheck
32
+
33
+	var conn net.Conn
34
+	select {
35
+	case err := <-acceptErr:
36
+		require.NoError(t, err)
37
+	case conn = <-accepted:
38
+	}
39
+	defer conn.Close() //nolint: errcheck
40
+
41
+	wrapped := connWrapper{proxyproto.NewConn(conn)}
42
+	_, ok := any(wrapped).(syscall.Conn)
43
+	require.True(t, ok)
44
+
45
+	rawConn, err := wrapped.SyscallConn()
46
+	require.NoError(t, err)
47
+	require.NotNil(t, rawConn)
48
+}

+ 17
- 5
internal/utils/net_listener.go Visa fil

@@ -1,9 +1,12 @@
1 1
 package utils
2 2
 
3 3
 import (
4
+	"context"
4 5
 	"fmt"
5 6
 	"net"
7
+	"syscall"
6 8
 
9
+	"github.com/9seconds/mtg/v2/essentials"
7 10
 	"github.com/9seconds/mtg/v2/network"
8 11
 )
9 12
 
@@ -26,13 +29,22 @@ func (l Listener) Accept() (net.Conn, error) {
26 29
 	return conn, nil
27 30
 }
28 31
 
29
-func NewListener(bindTo string, bufferSize int) (net.Listener, error) {
30
-	base, err := net.Listen("tcp", bindTo)
32
+func NewListener(bindTo string, windowClamp int) (net.Listener, error) {
33
+	var control func(string, string, syscall.RawConn) error
34
+	if windowClamp > 0 {
35
+		control = func(_, _ string, conn syscall.RawConn) error {
36
+			return essentials.SetRawTCPWindowClamp(conn, windowClamp)
37
+		}
38
+	}
39
+
40
+	listenConfig := net.ListenConfig{
41
+		Control: control,
42
+	}
43
+
44
+	base, err := listenConfig.Listen(context.Background(), "tcp", bindTo)
31 45
 	if err != nil {
32 46
 		return nil, fmt.Errorf("cannot build a base listener: %w", err)
33 47
 	}
34 48
 
35
-	return Listener{
36
-		Listener: base,
37
-	}, nil
49
+	return Listener{Listener: base}, nil
38 50
 }

+ 14
- 2
mtglib/proxy.go Visa fil

@@ -12,13 +12,16 @@ import (
12 12
 	"github.com/9seconds/mtg/v2/essentials"
13 13
 	"github.com/9seconds/mtg/v2/mtglib/internal/dc"
14 14
 	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
15
-	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
16 15
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
17 16
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
18 17
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
18
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
19 19
 	"github.com/panjf2000/ants/v2"
20 20
 )
21 21
 
22
+// Restore a large receive window after the tiny pre-handshake DPI desync clamp.
23
+const dpiDesyncPostHandshakeWindowClamp = 1 << 30
24
+
22 25
 // Proxy is an MTPROTO proxy structure.
23 26
 type Proxy struct {
24 27
 	ctx             context.Context
@@ -32,6 +35,7 @@ type Proxy struct {
32 35
 	domainFrontingPort          int
33 36
 	domainFrontingHost          string
34 37
 	domainFrontingProxyProtocol bool
38
+	dpiDesync                   bool
35 39
 	workerPool                  *ants.PoolWithFunc
36 40
 	telegram                    *dc.Telegram
37 41
 	configUpdater               *dc.PublicConfigUpdater
@@ -216,6 +220,12 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
216 220
 		return false
217 221
 	}
218 222
 
223
+	if p.dpiDesync {
224
+		if err := essentials.SetTCPWindowClamp(ctx.clientConn, dpiDesyncPostHandshakeWindowClamp); err != nil {
225
+			ctx.logger.DebugError("cannot restore TCP window clamp after handshake", err)
226
+		}
227
+	}
228
+
219 229
 	ctx.clientConn = tls.New(ctx.clientConn, true, false)
220 230
 
221 231
 	return true
@@ -285,7 +295,8 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error {
285 295
 		return fmt.Errorf("cannot parse telegram address %s: %w", foundAddr.Address, err)
286 296
 	}
287 297
 
288
-	p.eventStream.Send(ctx,
298
+	p.eventStream.Send(
299
+		ctx,
289 300
 		NewEventConnectedToDC(ctx.streamID,
290 301
 			net.ParseIP(telegramHost),
291 302
 			ctx.dc),
@@ -360,6 +371,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
360 371
 		logger:                   logger,
361 372
 		domainFrontingPort:       opts.getDomainFrontingPort(),
362 373
 		domainFrontingHost:       opts.DomainFrontingHost,
374
+		dpiDesync:                opts.DPIDesync,
363 375
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
364 376
 		idleTimeout:              opts.getIdleTimeout(),
365 377
 		handshakeTimeout:         opts.getHandshakeTimeout(),

+ 4
- 0
mtglib/proxy_opts.go Visa fil

@@ -177,6 +177,10 @@ type ProxyOpts struct {
177 177
 
178 178
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
179 179
 	DoppelGangerDRS bool
180
+
181
+	// DPIDesync enables post-handshake TCP window restore. Callers that set it
182
+	// must also apply the pre-handshake window clamp on accepted client sockets.
183
+	DPIDesync bool
180 184
 }
181 185
 
182 186
 func (p ProxyOpts) valid() error {

Laddar…
Avbryt
Spara