ソースを参照

Add Linux DPI desync mode for FakeTLS

pull/572/head
Gzzle 1週間前
コミット
9c49759382

+ 7
- 0
README.md ファイルの表示

369
   -n, --doh-ip=1.1.1.1                 IP address of DNS-over-HTTP to use.
369
   -n, --doh-ip=1.1.1.1                 IP address of DNS-over-HTTP to use.
370
   -t, --timeout=10s                    Network timeout to use
370
   -t, --timeout=10s                    Network timeout to use
371
   -a, --antireplay-cache-size="1MB"    A size of anti-replay cache to use.
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
 So, if you want to startup a proxy with CLI only, you can do something like
375
 So, if you want to startup a proxy with CLI only, you can do something like
401
 This is enough to run the whole application. All other
402
 This is enough to run the whole application. All other
402
 options already have sensible defaults for the app at almost any scale.
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
 ### Run a proxy
409
 ### Run a proxy
405
 
410
 
406
 Put a binary and a config into your webserver. Just for example,
411
 Put a binary and a config into your webserver. Just for example,
422
 DynamicUser=true
427
 DynamicUser=true
423
 LimitNOFILE=65536
428
 LimitNOFILE=65536
424
 AmbientCapabilities=CAP_NET_BIND_SERVICE
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
 [Install]
433
 [Install]
427
 WantedBy=multi-user.target
434
 WantedBy=multi-user.target

+ 16
- 0
essentials/conns.go ファイルの表示

1
 package essentials
1
 package essentials
2
 
2
 
3
 import (
3
 import (
4
+	"fmt"
4
 	"io"
5
 	"io"
5
 	"net"
6
 	"net"
7
+	"syscall"
6
 )
8
 )
7
 
9
 
8
 // CloseableReader is an [io.Reader] interface that can close its reading end.
10
 // CloseableReader is an [io.Reader] interface that can close its reading end.
49
 func WrapNetConn(conn net.Conn) Conn {
51
 func WrapNetConn(conn net.Conn) Conn {
50
 	return netConnWrapper{conn}
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 ファイルの表示

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 ファイルの表示

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 ファイルの表示

37
 # default value is false.
37
 # default value is false.
38
 # proxy-protocol-listener = false
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
 # Defines how many concurrent connections are allowed to this proxy.
46
 # Defines how many concurrent connections are allowed to this proxy.
41
 # All other incoming connections are going to be dropped.
47
 # All other incoming connections are going to be dropped.
42
 concurrency = 8192
48
 concurrency = 8192

+ 25
- 3
internal/cli/run_proxy.go ファイルの表示

11
 	"github.com/9seconds/mtg/v2/antireplay"
11
 	"github.com/9seconds/mtg/v2/antireplay"
12
 	"github.com/9seconds/mtg/v2/events"
12
 	"github.com/9seconds/mtg/v2/events"
13
 	"github.com/9seconds/mtg/v2/internal/config"
13
 	"github.com/9seconds/mtg/v2/internal/config"
14
+	"github.com/9seconds/mtg/v2/internal/desync"
14
 	"github.com/9seconds/mtg/v2/internal/proxyprotocol"
15
 	"github.com/9seconds/mtg/v2/internal/proxyprotocol"
15
 	"github.com/9seconds/mtg/v2/internal/utils"
16
 	"github.com/9seconds/mtg/v2/internal/utils"
16
 	"github.com/9seconds/mtg/v2/ipblocklist"
17
 	"github.com/9seconds/mtg/v2/ipblocklist"
179
 			conf.Stats.StatsD.Address.Get(""),
180
 			conf.Stats.StatsD.Address.Get(""),
180
 			logger.Named("statsd"),
181
 			logger.Named("statsd"),
181
 			conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix),
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
 		if err != nil {
185
 		if err != nil {
184
 			return nil, fmt.Errorf("cannot build statsd observer: %w", err)
186
 			return nil, fmt.Errorf("cannot build statsd observer: %w", err)
185
 		}
187
 		}
254
 	}
256
 	}
255
 }
257
 }
256
 
258
 
259
+const dpiDesyncHandshakeWindowClamp = 256
260
+
257
 func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop
261
 func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop
258
 	logger := makeLogger(conf)
262
 	logger := makeLogger(conf)
259
 
263
 
279
 		ntw,
283
 		ntw,
280
 		func(ctx context.Context, size int) {
284
 		func(ctx context.Context, size int) {
281
 			eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
285
 			eventStream.Send(ctx, mtglib.NewEventIPListSize(size, true))
282
-		})
286
+		},
287
+	)
283
 	if err != nil {
288
 	if err != nil {
284
 		return fmt.Errorf("cannot build ip blocklist: %w", err)
289
 		return fmt.Errorf("cannot build ip blocklist: %w", err)
285
 	}
290
 	}
296
 		return fmt.Errorf("cannot build ip allowlist: %w", err)
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
 	doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
311
 	doppelGangerURLs := make([]string, len(conf.Defense.Doppelganger.URLs))
300
 	for i, v := range conf.Defense.Doppelganger.URLs {
312
 	for i, v := range conf.Defense.Doppelganger.URLs {
301
 		doppelGangerURLs[i] = v.String()
313
 		doppelGangerURLs[i] = v.String()
326
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
338
 		DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
327
 		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
339
 		DoppelGangerEach:    conf.Defense.Doppelganger.UpdateEach.Get(mtglib.DoppelGangerEach),
328
 		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
340
 		DoppelGangerDRS:     conf.Defense.Doppelganger.DRS.Get(false),
341
+
342
+		DPIDesync: windowClamp > 0,
329
 	}
343
 	}
330
 
344
 
331
 	proxy, err := mtglib.NewProxy(opts)
345
 	proxy, err := mtglib.NewProxy(opts)
333
 		return fmt.Errorf("cannot create a proxy: %w", err)
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
 	if err != nil {
351
 	if err != nil {
338
 		return fmt.Errorf("cannot start proxy: %w", err)
352
 		return fmt.Errorf("cannot start proxy: %w", err)
339
 	}
353
 	}
348
 
362
 
349
 	ctx := utils.RootContext()
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
 	go proxy.Serve(listener) //nolint: errcheck
373
 	go proxy.Serve(listener) //nolint: errcheck
352
 
374
 
353
 	<-ctx.Done()
375
 	<-ctx.Done()

+ 14
- 11
internal/cli/simple_run.go ファイルの表示

13
 	BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
13
 	BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"`
14
 	Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"`
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
 	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
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
 	conf.Debug.Value = s.Debug
101
 	conf.Debug.Value = s.Debug
100
 	conf.AllowFallbackOnUnknownDC.Value = true
102
 	conf.AllowFallbackOnUnknownDC.Value = true
101
 	conf.Defense.AntiReplay.Enabled.Value = true
103
 	conf.Defense.AntiReplay.Enabled.Value = true
104
+	conf.DPIDesync.Value = s.DPIDesync
102
 	conf.ProxyProtocolListener.Value = s.ProxyProtocolListener
105
 	conf.ProxyProtocolListener.Value = s.ProxyProtocolListener
103
 
106
 
104
 	if err := conf.Validate(); err != nil {
107
 	if err := conf.Validate(); err != nil {

+ 5
- 4
internal/config/config.go ファイルの表示

27
 	Secret                      mtglib.Secret   `json:"secret"`
27
 	Secret                      mtglib.Secret   `json:"secret"`
28
 	BindTo                      TypeHostPort    `json:"bindTo"`
28
 	BindTo                      TypeHostPort    `json:"bindTo"`
29
 	ProxyProtocolListener       TypeBool        `json:"proxyProtocolListener"`
29
 	ProxyProtocolListener       TypeBool        `json:"proxyProtocolListener"`
30
+	DPIDesync                   TypeBool        `json:"dpiDesync"`
30
 	PreferIP                    TypePreferIP    `json:"preferIp"`
31
 	PreferIP                    TypePreferIP    `json:"preferIp"`
31
 	AutoUpdate                  TypeBool        `json:"autoUpdate"`
32
 	AutoUpdate                  TypeBool        `json:"autoUpdate"`
32
 	DomainFrontingPort          TypePort        `json:"domainFrontingPort"`
33
 	DomainFrontingPort          TypePort        `json:"domainFrontingPort"`
71
 			Interval TypeDuration    `json:"interval"`
72
 			Interval TypeDuration    `json:"interval"`
72
 			Count    TypeConcurrency `json:"count"`
73
 			Count    TypeConcurrency `json:"count"`
73
 		} `json:"keepAlive"`
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
 	} `json:"network"`
79
 	} `json:"network"`
79
 	Stats struct {
80
 	Stats struct {
80
 		StatsD struct {
81
 		StatsD struct {

+ 6
- 0
internal/config/config_test.go ファイルの表示

68
 	suite.Nil(conf.PublicIPv6.Get(nil))
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
 func (suite *ConfigTestSuite) TestString() {
77
 func (suite *ConfigTestSuite) TestString() {
72
 	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
78
 	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
73
 	suite.NoError(err)
79
 	suite.NoError(err)

+ 1
- 0
internal/config/parse.go ファイルの表示

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

+ 3
- 0
internal/config/testdata/dpi_desync.toml ファイルの表示

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

+ 372
- 0
internal/desync/service_linux.go ファイルの表示

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 ファイルの表示

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 ファイルの表示

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 ファイルの表示

1
 package proxyprotocol
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
 type connWrapper struct {
10
 type connWrapper struct {
6
 	*proxyproto.Conn
11
 	*proxyproto.Conn
23
 
28
 
24
 	return tcpConn.CloseWrite()
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 ファイルの表示

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 ファイルの表示

1
 package utils
1
 package utils
2
 
2
 
3
 import (
3
 import (
4
+	"context"
4
 	"fmt"
5
 	"fmt"
5
 	"net"
6
 	"net"
7
+	"syscall"
6
 
8
 
9
+	"github.com/9seconds/mtg/v2/essentials"
7
 	"github.com/9seconds/mtg/v2/network"
10
 	"github.com/9seconds/mtg/v2/network"
8
 )
11
 )
9
 
12
 
26
 	return conn, nil
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
 	if err != nil {
45
 	if err != nil {
32
 		return nil, fmt.Errorf("cannot build a base listener: %w", err)
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 ファイルの表示

12
 	"github.com/9seconds/mtg/v2/essentials"
12
 	"github.com/9seconds/mtg/v2/essentials"
13
 	"github.com/9seconds/mtg/v2/mtglib/internal/dc"
13
 	"github.com/9seconds/mtg/v2/mtglib/internal/dc"
14
 	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
14
 	"github.com/9seconds/mtg/v2/mtglib/internal/doppel"
15
-	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
16
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
15
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
17
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
16
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
18
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
17
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
18
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
19
 	"github.com/panjf2000/ants/v2"
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
 // Proxy is an MTPROTO proxy structure.
25
 // Proxy is an MTPROTO proxy structure.
23
 type Proxy struct {
26
 type Proxy struct {
24
 	ctx             context.Context
27
 	ctx             context.Context
32
 	domainFrontingPort          int
35
 	domainFrontingPort          int
33
 	domainFrontingHost          string
36
 	domainFrontingHost          string
34
 	domainFrontingProxyProtocol bool
37
 	domainFrontingProxyProtocol bool
38
+	dpiDesync                   bool
35
 	workerPool                  *ants.PoolWithFunc
39
 	workerPool                  *ants.PoolWithFunc
36
 	telegram                    *dc.Telegram
40
 	telegram                    *dc.Telegram
37
 	configUpdater               *dc.PublicConfigUpdater
41
 	configUpdater               *dc.PublicConfigUpdater
216
 		return false
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
 	ctx.clientConn = tls.New(ctx.clientConn, true, false)
229
 	ctx.clientConn = tls.New(ctx.clientConn, true, false)
220
 
230
 
221
 	return true
231
 	return true
285
 		return fmt.Errorf("cannot parse telegram address %s: %w", foundAddr.Address, err)
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
 		NewEventConnectedToDC(ctx.streamID,
300
 		NewEventConnectedToDC(ctx.streamID,
290
 			net.ParseIP(telegramHost),
301
 			net.ParseIP(telegramHost),
291
 			ctx.dc),
302
 			ctx.dc),
360
 		logger:                   logger,
371
 		logger:                   logger,
361
 		domainFrontingPort:       opts.getDomainFrontingPort(),
372
 		domainFrontingPort:       opts.getDomainFrontingPort(),
362
 		domainFrontingHost:       opts.DomainFrontingHost,
373
 		domainFrontingHost:       opts.DomainFrontingHost,
374
+		dpiDesync:                opts.DPIDesync,
363
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
375
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
364
 		idleTimeout:              opts.getIdleTimeout(),
376
 		idleTimeout:              opts.getIdleTimeout(),
365
 		handshakeTimeout:         opts.getHandshakeTimeout(),
377
 		handshakeTimeout:         opts.getHandshakeTimeout(),

+ 4
- 0
mtglib/proxy_opts.go ファイルの表示

177
 
177
 
178
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
178
 	// DoppelGangerDRS defines if TLS Dynamic Record Sizing is active.
179
 	DoppelGangerDRS bool
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
 func (p ProxyOpts) valid() error {
186
 func (p ProxyOpts) valid() error {

読み込み中…
キャンセル
保存