浏览代码

Merge 408e2c7150 into 8d143d7bde

pull/496/merge
Alexey Dolotov 19 小时前
父节点
当前提交
35b3b8802e
没有帐户链接到提交者的电子邮件

+ 2
- 2
.mise.toml 查看文件

@@ -58,11 +58,11 @@ run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/intern
58 58
 
59 59
 [tasks."test:fuzz:client-handshake"]
60 60
 description = "Run fuzzy test for ClientHandshake"
61
-run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientServerHandshake ./mtglib/internal/obfuscation"
61
+run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientServerHandshake ./mtglib/obfuscation"
62 62
 
63 63
 [tasks."test:fuzz:server-handshake-frame"]
64 64
 description = "Run fuzzy test for GenerateHandshakeFrame"
65
-run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzGenerateHandshakeFrame ./mtglib/internal/obfuscation"
65
+run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzGenerateHandshakeFrame ./mtglib/obfuscation"
66 66
 
67 67
 [tasks.static]
68 68
 description = "Build static binary"

+ 26
- 16
internal/cli/doctor.go 查看文件

@@ -18,6 +18,7 @@ import (
18 18
 	"github.com/9seconds/mtg/v2/internal/config"
19 19
 	"github.com/9seconds/mtg/v2/internal/utils"
20 20
 	"github.com/9seconds/mtg/v2/mtglib"
21
+	"github.com/9seconds/mtg/v2/mtglib/dcprobe"
21 22
 	"github.com/9seconds/mtg/v2/network/v2"
22 23
 	"github.com/beevik/ntp"
23 24
 )
@@ -46,7 +47,7 @@ var (
46 47
 	)
47 48
 
48 49
 	tplODCConnect = template.Must(
49
-		template.New("").Parse("  ✅ DC {{ .dc }}\n"),
50
+		template.New("").Parse("  ✅ DC {{ .dc }} (rpc {{ .rtt }})\n"),
50 51
 	)
51 52
 	tplEDCConnect = template.Must(
52 53
 		template.New("").Parse("  ❌ DC {{ .dc }}: {{ .error }}\n"),
@@ -237,17 +238,21 @@ func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
237 238
 	dcs := slices.Collect(maps.Keys(essentials.TelegramCoreAddresses))
238 239
 	slices.Sort(dcs)
239 240
 
240
-	errs := make([]error, len(dcs))
241
+	type dcResult struct {
242
+		rtt time.Duration
243
+		err error
244
+	}
245
+	results := make([]dcResult, len(dcs))
241 246
 
242 247
 	var wg sync.WaitGroup
243 248
 	for i, dc := range dcs {
244 249
 		wg.Go(func() {
245 250
 			defer func() {
246 251
 				if r := recover(); r != nil {
247
-					errs[i] = fmt.Errorf("panic: %v", r)
252
+					results[i].err = fmt.Errorf("panic: %v", r)
248 253
 				}
249 254
 			}()
250
-			errs[i] = d.checkNetworkAddresses(ntw, essentials.TelegramCoreAddresses[dc])
255
+			results[i].rtt, results[i].err = d.checkNetworkAddresses(ntw, dc, essentials.TelegramCoreAddresses[dc])
251 256
 		})
252 257
 	}
253 258
 	wg.Wait()
@@ -255,14 +260,15 @@ func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
255 260
 	ok := true
256 261
 
257 262
 	for i, dc := range dcs {
258
-		if errs[i] == nil {
263
+		if results[i].err == nil {
259 264
 			tplODCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
260
-				"dc": dc,
265
+				"dc":  dc,
266
+				"rtt": results[i].rtt.Round(time.Microsecond),
261 267
 			})
262 268
 		} else {
263 269
 			tplEDCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
264 270
 				"dc":    dc,
265
-				"error": errs[i],
271
+				"error": results[i].err,
266 272
 			})
267 273
 			ok = false
268 274
 		}
@@ -271,7 +277,7 @@ func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
271 277
 	return ok
272 278
 }
273 279
 
274
-func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) error {
280
+func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, dc int, addresses []string) (time.Duration, error) {
275 281
 	checkAddresses := []string{}
276 282
 
277 283
 	switch d.conf.PreferIP.Get("prefer-ip4") {
@@ -302,29 +308,33 @@ func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) e
302 308
 	}
303 309
 
304 310
 	if len(checkAddresses) == 0 {
305
-		return fmt.Errorf("no suitable addresses after IP version filtering")
311
+		return 0, fmt.Errorf("no suitable addresses after IP version filtering")
306 312
 	}
307 313
 
308 314
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
309 315
 	defer cancel()
310 316
 
311
-	var (
312
-		conn net.Conn
313
-		err  error
314
-	)
317
+	var lastErr error
315 318
 
316 319
 	for _, addr := range checkAddresses {
317
-		conn, err = ntw.DialContext(ctx, "tcp", addr)
320
+		conn, err := ntw.DialContext(ctx, "tcp", addr)
318 321
 		if err != nil {
322
+			lastErr = fmt.Errorf("tcp connect to %s: %w", addr, err)
319 323
 			continue
320 324
 		}
321 325
 
326
+		rtt, err := dcprobe.Probe(ctx, conn, dc)
322 327
 		conn.Close() //nolint: errcheck
323 328
 
324
-		return nil
329
+		if err != nil {
330
+			lastErr = fmt.Errorf("rpc handshake to %s: %w", addr, err)
331
+			continue
332
+		}
333
+
334
+		return rtt, nil
325 335
 	}
326 336
 
327
-	return err
337
+	return 0, lastErr
328 338
 }
329 339
 
330 340
 func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {

+ 181
- 0
mtglib/dcprobe/probe.go 查看文件

@@ -0,0 +1,181 @@
1
+// Package dcprobe verifies that a TCP endpoint is a real Telegram DC by
2
+// performing the unauthenticated first step of the MTProto handshake
3
+// (req_pq_multi -> resPQ) on top of mtg's existing obfuscated2 transport.
4
+//
5
+// No auth_key is generated; no long-lived state is introduced. Two TL
6
+// messages, one round-trip. A generic listener cannot fake the reply
7
+// because it must echo back our random nonce in resPQ.
8
+//
9
+// References:
10
+//   - https://core.telegram.org/mtproto/auth_key      (handshake step 1)
11
+//   - https://core.telegram.org/schema/mtproto        (TL schema)
12
+//   - https://core.telegram.org/mtproto/mtproto-transports#padded-intermediate
13
+package dcprobe
14
+
15
+import (
16
+	"bytes"
17
+	"context"
18
+	"crypto/rand"
19
+	"encoding/binary"
20
+	"errors"
21
+	"fmt"
22
+	"io"
23
+	"net"
24
+	"time"
25
+
26
+	"github.com/9seconds/mtg/v2/essentials"
27
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
28
+)
29
+
30
+// MTProto wire constants (https://core.telegram.org/schema/mtproto).
31
+//
32
+//	req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
33
+//	resPQ#05162463 nonce:int128 server_nonce:int128 pq:string
34
+//	    server_public_key_fingerprints:Vector<long> = ResPQ;
35
+const (
36
+	ctorReqPQMulti uint32 = 0xbe7e8ef1
37
+	ctorResPQ      uint32 = 0x05162463
38
+
39
+	// Minimum legal resPQ frame: 20-byte unencrypted-message envelope +
40
+	// 4-byte ctor + 16-byte nonce echo. Anything below cannot be a resPQ.
41
+	minResPQFrame = 20 + 4 + 16
42
+	// Upper bound: real resPQ replies are ~84 bytes (envelope + ~64-byte
43
+	// payload). 256 is comfortable headroom; anything beyond is hostile or
44
+	// not Telegram.
45
+	maxResPQFrame = 256
46
+)
47
+
48
+// Probe sends req_pq_multi over an obfuscated2 + padded-intermediate transport
49
+// and verifies that the peer replies with a matching resPQ.
50
+//
51
+// conn must be a freshly opened reliable byte stream (typically TCP) to a
52
+// Telegram DC, but a SOCKS/proxy-wrapped net.Conn works just as well — Probe
53
+// adapts whatever it gets to the half-close interface mtg's obfuscator
54
+// requires. Probe does NOT close conn — the caller does. dc is the DC number
55
+// (1..5) that gets baked into the obfuscated2 handshake frame.
56
+//
57
+// The returned duration is the round-trip from "first byte sent after the
58
+// obfs handshake" to "resPQ frame fully read".
59
+func Probe(ctx context.Context, conn net.Conn, dc int) (time.Duration, error) {
60
+	if deadline, ok := ctx.Deadline(); ok {
61
+		_ = conn.SetDeadline(deadline)
62
+		defer func() { _ = conn.SetDeadline(time.Time{}) }()
63
+	}
64
+
65
+	// Honour ctx cancellation as well as its deadline: a parent ctx that is
66
+	// canceled (without an earlier deadline expiring) would otherwise let
67
+	// Probe block on an in-flight Read until the deadline. Forcing the
68
+	// deadline to "now" makes the next syscall return an i/o timeout error
69
+	// that Probe wraps and surfaces.
70
+	stop := context.AfterFunc(ctx, func() {
71
+		_ = conn.SetDeadline(time.Now())
72
+	})
73
+	defer stop()
74
+
75
+	// 1. obfuscated2 handshake. Empty Secret = no MTProxy secret mixing,
76
+	// which is how mtg itself talks to a DC (see mtglib/proxy.go).
77
+	obfsConn, err := obfuscation.Obfuscator{}.SendHandshake(adaptConn(conn), dc)
78
+	if err != nil {
79
+		return 0, fmt.Errorf("obfuscated2 handshake: %w", err)
80
+	}
81
+
82
+	// 2. build req_pq_multi TL payload: 4-byte LE constructor + 16-byte nonce.
83
+	var nonce [16]byte
84
+	if _, err := rand.Read(nonce[:]); err != nil {
85
+		return 0, fmt.Errorf("read nonce: %w", err)
86
+	}
87
+	tlBody := make([]byte, 4+16)
88
+	binary.LittleEndian.PutUint32(tlBody[:4], ctorReqPQMulti)
89
+	copy(tlBody[4:], nonce[:])
90
+
91
+	// 3. wrap in an MTProto unencrypted message envelope (per
92
+	// https://core.telegram.org/mtproto/description#unencrypted-message):
93
+	//   auth_key_id:long(=0) | message_id:long | message_data_length:int | message_data:bytes
94
+	// Without this envelope the DC silently drops the connection.
95
+	msg := make([]byte, 8+8+4+len(tlBody))
96
+	// auth_key_id = 0 (already zeroed by make)
97
+	binary.LittleEndian.PutUint64(msg[8:16], generateMessageID())
98
+	binary.LittleEndian.PutUint32(msg[16:20], uint32(len(tlBody)))
99
+	copy(msg[20:], tlBody)
100
+
101
+	// 4. wrap in a padded-intermediate frame: length(LE) + msg.
102
+	// Padding is allowed [0..15] but not required when len(msg) % 4 == 0.
103
+	frame := make([]byte, 4+len(msg))
104
+	binary.LittleEndian.PutUint32(frame[:4], uint32(len(msg)))
105
+	copy(frame[4:], msg)
106
+
107
+	start := time.Now()
108
+	if _, err := obfsConn.Write(frame); err != nil {
109
+		return 0, fmt.Errorf("write req_pq_multi: %w", err)
110
+	}
111
+
112
+	// 5. read padded-intermediate reply: length, then that many bytes.
113
+	// The reply is itself an MTProto unencrypted message (same envelope as
114
+	// what we sent), so we must skip 20 bytes to get to the resPQ TL.
115
+	var lenBuf [4]byte
116
+	if _, err := io.ReadFull(obfsConn, lenBuf[:]); err != nil {
117
+		return 0, fmt.Errorf("read frame length: %w", err)
118
+	}
119
+	respLen := binary.LittleEndian.Uint32(lenBuf[:])
120
+	if respLen < minResPQFrame {
121
+		return 0, fmt.Errorf("%w: resPQ frame too short (%d bytes)", ErrNotTelegram, respLen)
122
+	}
123
+	if respLen > maxResPQFrame {
124
+		return 0, fmt.Errorf("%w: resPQ frame too large (%d bytes, max %d)", ErrNotTelegram, respLen, maxResPQFrame)
125
+	}
126
+	resp := make([]byte, respLen)
127
+	if _, err := io.ReadFull(obfsConn, resp); err != nil {
128
+		return 0, fmt.Errorf("read resPQ frame: %w", err)
129
+	}
130
+	rtt := time.Since(start)
131
+
132
+	// 6. unwrap the MTProto envelope: skip auth_key_id(8) + message_id(8) +
133
+	// message_data_length(4) = 20 bytes.
134
+	tlResp := resp[20:]
135
+
136
+	// 7. verify constructor and nonce echo. We deliberately do not parse
137
+	// server_nonce, pq, or fingerprints — they are not needed to prove
138
+	// the peer can speak MTProto.
139
+	if got := binary.LittleEndian.Uint32(tlResp[:4]); got != ctorResPQ {
140
+		return rtt, fmt.Errorf("%w: got constructor 0x%08x, want resPQ 0x%08x", ErrNotTelegram, got, ctorResPQ)
141
+	}
142
+	if !bytes.Equal(tlResp[4:4+16], nonce[:]) {
143
+		return rtt, fmt.Errorf("%w: nonce echo mismatch", ErrNotTelegram)
144
+	}
145
+
146
+	return rtt, nil
147
+}
148
+
149
+// generateMessageID returns an MTProto message_id roughly synchronized with
150
+// server time, with the lower 2 bits cleared (client-to-server requests).
151
+// See https://core.telegram.org/mtproto/description#message-identifier-msg-id.
152
+func generateMessageID() uint64 {
153
+	nano := uint64(time.Now().UnixNano())
154
+	sec := nano / 1_000_000_000
155
+	nsInSec := nano % 1_000_000_000
156
+	subsec := (nsInSec << 32) / 1_000_000_000
157
+	id := (sec << 32) | subsec
158
+	return id &^ 3
159
+}
160
+
161
+// ErrNotTelegram is returned (wrapped) when the peer's reply is not a
162
+// well-formed resPQ matching our nonce. Use errors.Is to distinguish
163
+// "the TCP connection was OK but the peer is not a Telegram DC" from
164
+// transport errors.
165
+var ErrNotTelegram = errors.New("peer did not respond with a matching resPQ")
166
+
167
+// adaptConn returns conn as essentials.Conn if it already satisfies the
168
+// interface (typically *net.TCPConn), otherwise wraps it with no-op
169
+// CloseRead/CloseWrite. mtg's obfuscator only ever calls Read/Write/Close,
170
+// so the no-ops are safe.
171
+func adaptConn(conn net.Conn) essentials.Conn {
172
+	if ec, ok := conn.(essentials.Conn); ok {
173
+		return ec
174
+	}
175
+	return halfCloseShim{Conn: conn}
176
+}
177
+
178
+type halfCloseShim struct{ net.Conn }
179
+
180
+func (halfCloseShim) CloseRead() error  { return nil }
181
+func (halfCloseShim) CloseWrite() error { return nil }

+ 110
- 0
mtglib/dcprobe/probe_test.go 查看文件

@@ -0,0 +1,110 @@
1
+package dcprobe_test
2
+
3
+import (
4
+	"context"
5
+	"errors"
6
+	"io"
7
+	"net"
8
+	"os"
9
+	"testing"
10
+	"time"
11
+
12
+	"github.com/9seconds/mtg/v2/mtglib/dcprobe"
13
+)
14
+
15
+// TestProbeAgainstTelegramDCs makes outbound TCP connections to public
16
+// Telegram DCs. Skipped by default; opt-in with MTG_PROBE_NETWORK=1.
17
+func TestProbeAgainstTelegramDCs(t *testing.T) {
18
+	if os.Getenv("MTG_PROBE_NETWORK") != "1" {
19
+		t.Skip("skipping network probe (set MTG_PROBE_NETWORK=1 to enable)")
20
+	}
21
+
22
+	cases := []struct {
23
+		dc   int
24
+		addr string
25
+	}{
26
+		{1, "149.154.175.50:443"},
27
+		{2, "149.154.167.51:443"},
28
+		{2, "95.161.76.100:443"},
29
+		{3, "149.154.175.100:443"},
30
+		{4, "149.154.167.91:443"},
31
+		{5, "149.154.171.5:443"},
32
+		{1, "[2001:b28:f23d:f001::a]:443"},
33
+		{2, "[2001:67c:04e8:f002::a]:443"},
34
+	}
35
+
36
+	for _, tc := range cases {
37
+		t.Run(tc.addr, func(t *testing.T) {
38
+			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
39
+			defer cancel()
40
+
41
+			conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", tc.addr)
42
+			if err != nil {
43
+				t.Fatalf("dial: %v", err)
44
+			}
45
+			t.Cleanup(func() { _ = conn.Close() })
46
+
47
+			rtt, err := dcprobe.Probe(ctx, conn, tc.dc)
48
+			if err != nil {
49
+				t.Fatalf("probe DC %d: %v", tc.dc, err)
50
+			}
51
+			t.Logf("DC %d (%s): rtt=%s", tc.dc, tc.addr, rtt)
52
+		})
53
+	}
54
+}
55
+
56
+// TestProbeRejectsMisbehavingPeer connects to an in-process listener that
57
+// accepts the obfs2 handshake, then writes back arbitrary bytes. With
58
+// overwhelming probability the decrypted reply fails one of: frame-length
59
+// bounds, resPQ constructor, or nonce echo. All three paths wrap
60
+// ErrNotTelegram, so we assert errors.Is.
61
+func TestProbeRejectsMisbehavingPeer(t *testing.T) {
62
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
63
+	if err != nil {
64
+		t.Fatal(err)
65
+	}
66
+	t.Cleanup(func() { _ = ln.Close() })
67
+
68
+	go func() {
69
+		c, err := ln.Accept()
70
+		if err != nil {
71
+			return
72
+		}
73
+		defer c.Close() //nolint: errcheck
74
+
75
+		// Discard the 64-byte obfs2 handshake the client sends.
76
+		var hs [64]byte
77
+		if _, err := io.ReadFull(c, hs[:]); err != nil {
78
+			return
79
+		}
80
+		// Write enough garbage to satisfy any plausible respLen the client
81
+		// might decode (we cap at maxResPQFrame=256 in probe.go). Whatever
82
+		// the client decrypts will fail constructor or nonce verification.
83
+		junk := make([]byte, 512)
84
+		for i := range junk {
85
+			junk[i] = byte(i)
86
+		}
87
+		_, _ = c.Write(junk)
88
+		// Keep the conn open until the client closes it (avoids racing the
89
+		// client's read against our close).
90
+		_, _ = io.Copy(io.Discard, c)
91
+	}()
92
+
93
+	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
94
+	defer cancel()
95
+
96
+	conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", ln.Addr().String())
97
+	if err != nil {
98
+		t.Fatal(err)
99
+	}
100
+	t.Cleanup(func() { _ = conn.Close() })
101
+
102
+	_, err = dcprobe.Probe(ctx, conn, 2)
103
+	if err == nil {
104
+		t.Fatal("expected ErrNotTelegram, got nil")
105
+	}
106
+	if !errors.Is(err, dcprobe.ErrNotTelegram) {
107
+		t.Fatalf("expected errors.Is(err, ErrNotTelegram) to be true, got: %v", err)
108
+	}
109
+	t.Logf("rejected: %v", err)
110
+}

+ 1
- 1
mtglib/internal/dc/addr.go 查看文件

@@ -3,7 +3,7 @@ package dc
3 3
 import (
4 4
 	"fmt"
5 5
 
6
-	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
6
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
7 7
 )
8 8
 
9 9
 type Addr struct {

mtglib/internal/obfuscation/conn.go → mtglib/obfuscation/conn.go 查看文件


mtglib/internal/obfuscation/conn_test.go → mtglib/obfuscation/conn_test.go 查看文件


mtglib/internal/obfuscation/handshake_frame.go → mtglib/obfuscation/handshake_frame.go 查看文件


mtglib/internal/obfuscation/handshake_frame_fuzz_test.go → mtglib/obfuscation/handshake_frame_fuzz_test.go 查看文件


mtglib/internal/obfuscation/handshake_frame_test.go → mtglib/obfuscation/handshake_frame_test.go 查看文件


mtglib/internal/obfuscation/init_test.go → mtglib/obfuscation/init_test.go 查看文件


mtglib/internal/obfuscation/obfuscator.go → mtglib/obfuscation/obfuscator.go 查看文件

@@ -13,10 +13,17 @@ import (
13 13
 	"github.com/9seconds/mtg/v2/essentials"
14 14
 )
15 15
 
16
+// Obfuscator implements the obfuscated2 handshake
17
+// (https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation).
18
+// Set Secret to the MTProxy secret for key-mixed handshakes; leave nil for
19
+// direct DC connections.
16 20
 type Obfuscator struct {
17 21
 	Secret []byte
18 22
 }
19 23
 
24
+// ReadHandshake reads the 64-byte obfuscated2 client handshake from r,
25
+// validates it, and returns the DC the client requested along with a
26
+// transparent en/decrypting wrapper over r.
20 27
 func (o Obfuscator) ReadHandshake(r essentials.Conn) (int, essentials.Conn, error) {
21 28
 	frame := handshakeFrame{}
22 29
 
@@ -46,6 +53,8 @@ func (o Obfuscator) ReadHandshake(r essentials.Conn) (int, essentials.Conn, erro
46 53
 	return frame.dc(), cn, nil
47 54
 }
48 55
 
56
+// SendHandshake writes a fresh 64-byte obfuscated2 handshake for the given
57
+// DC to w and returns a transparent en/decrypting wrapper over w.
49 58
 func (o Obfuscator) SendHandshake(w essentials.Conn, dc int) (essentials.Conn, error) {
50 59
 	frame := generateHandshake(dc)
51 60
 	copyFrame := frame

mtglib/internal/obfuscation/obfuscator_fuzz_test.go → mtglib/obfuscation/obfuscator_fuzz_test.go 查看文件

@@ -6,7 +6,7 @@ import (
6 6
 
7 7
 	"github.com/9seconds/mtg/v2/internal/testlib"
8 8
 	"github.com/9seconds/mtg/v2/mtglib"
9
-	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
9
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
10 10
 	"github.com/stretchr/testify/assert"
11 11
 	"github.com/stretchr/testify/mock"
12 12
 )

mtglib/internal/obfuscation/obfuscator_test.go → mtglib/obfuscation/obfuscator_test.go 查看文件

@@ -6,7 +6,7 @@ import (
6 6
 
7 7
 	"github.com/9seconds/mtg/v2/internal/testlib"
8 8
 	"github.com/9seconds/mtg/v2/mtglib"
9
-	"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
9
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
10 10
 	"github.com/stretchr/testify/assert"
11 11
 	"github.com/stretchr/testify/mock"
12 12
 	"github.com/stretchr/testify/require"

mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json → mtglib/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json 查看文件


mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json → mtglib/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json 查看文件


+ 1
- 1
mtglib/proxy.go 查看文件

@@ -12,7 +12,7 @@ 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/internal/obfuscation"
15
+	"github.com/9seconds/mtg/v2/mtglib/obfuscation"
16 16
 	"github.com/9seconds/mtg/v2/mtglib/internal/relay"
17 17
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls"
18 18
 	"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"

正在加载...
取消
保存