From bf38f9f8af16b5accff371ba27399dea7c262691 Mon Sep 17 00:00:00 2001 From: ivulit Date: Fri, 20 Feb 2026 12:34:17 +0300 Subject: [PATCH 01/18] Add domain-fronting-ip option Allow specifying an explicit IP address for the domain fronting host instead of relying on DNS resolution. Useful when DNS resolution of the fronting hostname is blocked. The hostname from the secret is still used for SNI in TLS handshake. --- internal/cli/run_proxy.go | 1 + internal/cli/simple_run.go | 7 +++++++ internal/config/config.go | 1 + internal/config/parse.go | 1 + mtglib/proxy.go | 10 +++++++++- mtglib/proxy_opts.go | 9 +++++++++ 6 files changed, 28 insertions(+), 1 deletion(-) diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 23580eb..e8f708d 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -260,6 +260,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen Secret: conf.Secret, DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort), + DomainFrontingIP: conf.DomainFrontingIP.String(), PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), diff --git a/internal/cli/simple_run.go b/internal/cli/simple_run.go index 53deb61..b0eebde 100644 --- a/internal/cli/simple_run.go +++ b/internal/cli/simple_run.go @@ -18,6 +18,7 @@ type SimpleRun struct { TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` //nolint: lll 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 DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` //nolint: lll + DomainFrontingIP string `kong:"name='domain-fronting-ip',help='An IP address to use for domain fronting instead of resolving the hostname via DNS.'"` //nolint: lll 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 Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` //nolint: lll Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //nolint: lll @@ -47,6 +48,12 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funle return fmt.Errorf("incorrect domain-fronting-port: %w", err) } + if s.DomainFrontingIP != "" { + if err := conf.DomainFrontingIP.Set(s.DomainFrontingIP); err != nil { + return fmt.Errorf("incorrect domain-fronting-ip: %w", err) + } + } + if err := conf.Network.DOHIP.Set(s.DOHIP.String()); err != nil { return fmt.Errorf("incorrect doh-ip: %w", err) } diff --git a/internal/config/config.go b/internal/config/config.go index 74abdf0..4f21244 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,7 @@ type Config struct { ProxyProtocolListener TypeBool `json:"proxyProtocolListener"` PreferIP TypePreferIP `json:"preferIp"` DomainFrontingPort TypePort `json:"domainFrontingPort"` + DomainFrontingIP TypeIP `json:"domainFrontingIp"` TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` Concurrency TypeConcurrency `json:"concurrency"` Defense struct { diff --git a/internal/config/parse.go b/internal/config/parse.go index 7cdf6da..3911528 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -16,6 +16,7 @@ type tomlConfig struct { ProxyProtocolListener bool `toml:"proxy-protocol-listener" json:"proxyProtocolListener"` PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"` DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"` + DomainFrontingIP string `toml:"domain-fronting-ip" json:"domainFrontingIp,omitempty"` TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` Defense struct { diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 104c723..872e97f 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -27,6 +27,7 @@ type Proxy struct { allowFallbackOnUnknownDC bool tolerateTimeSkewness time.Duration domainFrontingPort int + domainFrontingIP string workerPool *ants.PoolWithFunc telegram *dc.Telegram @@ -40,8 +41,14 @@ type Proxy struct { } // DomainFrontingAddress returns a host:port pair for a fronting domain. +// If DomainFrontingIP is set, it is used instead of resolving the hostname. func (p *Proxy) DomainFrontingAddress() string { - return net.JoinHostPort(p.secret.Host, strconv.Itoa(p.domainFrontingPort)) + host := p.secret.Host + if p.domainFrontingIP != "" { + host = p.domainFrontingIP + } + + return net.JoinHostPort(host, strconv.Itoa(p.domainFrontingPort)) } // ServeConn serves a connection. We do not check IP blocklist and concurrency @@ -317,6 +324,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { eventStream: opts.EventStream, logger: opts.getLogger("proxy"), domainFrontingPort: opts.getDomainFrontingPort(), + domainFrontingIP: opts.DomainFrontingIP, tolerateTimeSkewness: opts.getTolerateTimeSkewness(), allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, telegram: tg, diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 53f3434..2cf5532 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -93,6 +93,15 @@ type ProxyOpts struct { // This is an optional setting. DomainFrontingPort uint + // DomainFrontingIP is an IP address to use when connecting to the fronting + // domain instead of resolving the hostname from the secret via DNS. + // + // This is useful when DNS resolution of the fronting host is blocked. + // The hostname from the secret is still used for SNI in the TLS handshake. + // + // This is an optional setting. + DomainFrontingIP string + // AllowFallbackOnUnknownDC defines how proxy behaves if unknown DC was // requested. If this setting is set to false, then such connection will be // rejected. Otherwise, proxy will chose any DC. From 21129b6e008b45c81088a9fa1ec5b2566c60bf93 Mon Sep 17 00:00:00 2001 From: ivulit Date: Fri, 20 Feb 2026 12:34:22 +0300 Subject: [PATCH 02/18] Add domain-fronting-ip to example config --- example.config.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example.config.toml b/example.config.toml index 68f7926..638fb33 100644 --- a/example.config.toml +++ b/example.config.toml @@ -59,6 +59,14 @@ prefer-ip = "prefer-ipv6" # access. domain-fronting-port = 443 +# By default, mtg resolves the fronting hostname (from the secret) via DNS +# to establish a TCP connection. If DNS resolution of that hostname is blocked, +# you can specify an IP address to connect to directly. The hostname is still +# used for SNI in the TLS handshake. +# +# default value is not set (DNS resolution is used). +# domain-fronting-ip = "142.250.185.112" + # FakeTLS can compare timestamps to prevent probes. Each message has # encrypted timestamp. So, mtg can compare this timestamp and decide if # we need to proceed with connection or not. From d0065d35c25228ff05cfccbedab06dd0b6e933a0 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 20 Feb 2026 16:32:31 +0100 Subject: [PATCH 03/18] Add new obfuscation package --- mtglib/internal/obfuscation/conn.go | 34 ++++++ mtglib/internal/obfuscation/conn_test.go | 102 ++++++++++++++++ .../internal/obfuscation/handshake_frame.go | 111 ++++++++++++++++++ .../obfuscation/handshake_frame_fuzz_test.go | 33 ++++++ .../obfuscation/handshake_frame_test.go | 66 +++++++++++ mtglib/internal/obfuscation/init_test.go | 79 +++++++++++++ mtglib/internal/obfuscation/obfuscator.go | 87 ++++++++++++++ .../obfuscation/obfuscator_fuzz_test.go | 63 ++++++++++ .../internal/obfuscation/obfuscator_test.go | 94 +++++++++++++++ ...t-handshake-snapshot-4529d55776e2d427.json | 13 ++ ...t-handshake-snapshot-585c944d672f60a2.json | 13 ++ 11 files changed, 695 insertions(+) create mode 100644 mtglib/internal/obfuscation/conn.go create mode 100644 mtglib/internal/obfuscation/conn_test.go create mode 100644 mtglib/internal/obfuscation/handshake_frame.go create mode 100644 mtglib/internal/obfuscation/handshake_frame_fuzz_test.go create mode 100644 mtglib/internal/obfuscation/handshake_frame_test.go create mode 100644 mtglib/internal/obfuscation/init_test.go create mode 100644 mtglib/internal/obfuscation/obfuscator.go create mode 100644 mtglib/internal/obfuscation/obfuscator_fuzz_test.go create mode 100644 mtglib/internal/obfuscation/obfuscator_test.go create mode 100644 mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json create mode 100644 mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json diff --git a/mtglib/internal/obfuscation/conn.go b/mtglib/internal/obfuscation/conn.go new file mode 100644 index 0000000..0c07207 --- /dev/null +++ b/mtglib/internal/obfuscation/conn.go @@ -0,0 +1,34 @@ +package obfuscation + +import ( + "crypto/cipher" + + "github.com/9seconds/mtg/v2/essentials" +) + +type conn struct { + essentials.Conn + + sendCipher cipher.Stream + recvCipher cipher.Stream +} + +func (c conn) Read(p []byte) (int, error) { + n, err := c.Conn.Read(p) + if err != nil { + return n, err + } + + c.recvCipher.XORKeyStream(p, p[:n]) + + return n, nil +} + +func (c conn) Write(p []byte) (int, error) { + // yes, this is a bit violent and goes against a contract in io.Writer + // but we do it to avoid creating a new buffer just to perform this + // encryption. + c.sendCipher.XORKeyStream(p, p) + + return c.Conn.Write(p) +} diff --git a/mtglib/internal/obfuscation/conn_test.go b/mtglib/internal/obfuscation/conn_test.go new file mode 100644 index 0000000..0dfc1cd --- /dev/null +++ b/mtglib/internal/obfuscation/conn_test.go @@ -0,0 +1,102 @@ +package obfuscation + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "testing" + + "github.com/9seconds/mtg/v2/essentials" + "github.com/9seconds/mtg/v2/internal/testlib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type ConnTestSuite struct { + suite.Suite + + secret []byte +} + +func (s *ConnTestSuite) SetupSuite() { + secret := [32]byte{} + s.secret = secret[:] +} + +func (s *ConnTestSuite) TestRead() { + testData := map[string]string{ + "data1": "b8f4b41993", + "": "", + "___": "83ca9f", + } + + for incoming, outgoing := range testData { + s.T().Run(incoming, func(t *testing.T) { + connMock := &testlib.EssentialsConnMock{} + testConn := s.makeConn(connMock) + data := make([]byte, len(incoming)) + + connMock.On("Read", make([]byte, len(incoming))).Return(len(incoming), nil).Run(func(args mock.Arguments) { + arg := args.Get(0).([]byte) + copy(arg, []byte(incoming)) + }) + + n, err := testConn.Read(data) + + assert.Equal(t, len(data), n) + assert.NoError(t, err) + assert.Equal(t, outgoing, hex.EncodeToString(data)) + + connMock.AssertExpectations(t) + }) + } +} + +func (s *ConnTestSuite) TestWrite() { + testData := map[string]string{ + "b8f4b41993": "data1", + "": "", + "83ca9f": "___", + } + + for incoming, outgoing := range testData { + s.T().Run(incoming, func(t *testing.T) { + connMock := &testlib.EssentialsConnMock{} + testConn := s.makeConn(connMock) + toWrite, _ := hex.DecodeString(incoming) + data := make([]byte, len(toWrite)) + + connMock.On("Write", []byte(outgoing)).Return(len(toWrite), nil) + + n, err := testConn.Write(toWrite) + assert.Equal(t, len(data), n) + assert.NoError(t, err) + + connMock.AssertExpectations(t) + }) + } +} + +func (s *ConnTestSuite) makeConn(rawConn *testlib.EssentialsConnMock) essentials.Conn { + rblock, err := aes.NewCipher(s.secret) + if err != nil { + panic(err) + } + + wblock, err := aes.NewCipher(s.secret) + if err != nil { + panic(err) + } + + return conn{ + Conn: rawConn, + sendCipher: cipher.NewCTR(wblock, s.secret[:aes.BlockSize]), + recvCipher: cipher.NewCTR(rblock, s.secret[:aes.BlockSize]), + } +} + +func TestConn(t *testing.T) { + t.Parallel() + suite.Run(t, &ConnTestSuite{}) +} diff --git a/mtglib/internal/obfuscation/handshake_frame.go b/mtglib/internal/obfuscation/handshake_frame.go new file mode 100644 index 0000000..6e1f0a8 --- /dev/null +++ b/mtglib/internal/obfuscation/handshake_frame.go @@ -0,0 +1,111 @@ +package obfuscation + +import ( + "crypto/rand" + "encoding/binary" + "slices" +) + +// https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation +const ( + // default DC is nothing is selected + defaultDC = 2 + + // the length of the handshake frame. Always 64 bytes + hfLen = 64 + + hfLenKey = 32 + hfLenIV = 16 + hfLenConnectionType = 4 + + // A structure of obfuscated handshake frame is following: + // + // [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd]. + // + // - 8 bytes of noise + // - 32 bytes of AES Key + // - 16 bytes of AES IV + // - 4 bytes of 'connection type' - this has some setting like a connection type + // - 2 bytes of 'DC'. DC is little endian int16 + // - 2 bytes of noise + hfOffsetKey = 8 + hfOffsetIV = hfOffsetKey + hfLenKey + hfOffsetConnectionType = hfOffsetIV + hfLenIV + hfOffsetDC = hfOffsetConnectionType + hfLenConnectionType +) + +// Connection-Type: Secure. We support only fake tls. +var hfConnectionType = [hfLenConnectionType]byte{0xdd, 0xdd, 0xdd, 0xdd} + +type handshakeFrame struct { + data [hfLen]byte +} + +func (h *handshakeFrame) key() []byte { + return h.data[hfOffsetKey : hfOffsetKey+hfLenKey] +} + +func (h *handshakeFrame) iv() []byte { + return h.data[hfOffsetIV : hfOffsetIV+hfLenIV] +} + +func (h *handshakeFrame) connectionType() []byte { + return h.data[hfOffsetConnectionType : hfOffsetConnectionType+hfLenConnectionType] +} + +func (h *handshakeFrame) dcSlice() []byte { + return h.data[hfOffsetDC : hfOffsetDC+2] +} + +func (h *handshakeFrame) dc() int { + idx := int16(binary.LittleEndian.Uint16(h.dcSlice())) + + switch { + case idx > 0: + return int(idx) + case idx < 0: + return -int(idx) + } + + return defaultDC +} + +func (h *handshakeFrame) revert() { + slices.Reverse(h.data[hfOffsetKey:hfOffsetConnectionType]) +} + +func generateHandshake(dc int) handshakeFrame { + frame := handshakeFrame{} + + for { + if _, err := rand.Read(frame.data[:]); err != nil { + panic(err) + } + + // https://github.com/tdlib/td/blob/master/td/mtproto/TcpTransport.cpp#L157-L158. + if frame.data[0] == 0xef { // abridged header + // https://core.telegram.org/mtproto/mtproto-transports#abridged + continue + } + + switch binary.LittleEndian.Uint32(frame.data[:4]) { + case 0x44414548, // HEAD + 0x54534f50, // POST + 0x20544547, // GET + 0x4954504f, // OPTI + 0x02010316, // ???? + 0xdddddddd, // PaddedIntermediate header + 0xeeeeeeee: // Intermediate header + continue + } + + if frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7] == 0 { + continue + } + + copy(frame.connectionType(), hfConnectionType[:]) + binary.LittleEndian.PutUint16(frame.dcSlice(), uint16(dc)) + + return frame + } +} diff --git a/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go b/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go new file mode 100644 index 0000000..71965ec --- /dev/null +++ b/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go @@ -0,0 +1,33 @@ +package obfuscation + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" +) + +func FuzzGenerateHandshakeFrame(f *testing.F) { + f.Fuzz(func(t *testing.T, arg int16) { + frame := generateHandshake(int(arg)) + + assert.NotEqualValues(t, 0xef, frame.data[0]) + + firstBytes := binary.LittleEndian.Uint32(frame.data[:4]) + assert.NotEqualValues(t, 0x44414548, firstBytes) + assert.NotEqualValues(t, 0x54534f50, firstBytes) + assert.NotEqualValues(t, 0x20544547, firstBytes) + assert.NotEqualValues(t, 0x4954504f, firstBytes) + assert.NotEqualValues(t, 0x02010316, firstBytes) + assert.NotEqualValues(t, 0xeeeeeeee, firstBytes) + assert.NotEqualValues(t, 0xdddddddd, firstBytes) + + assert.NotEqualValues( + t, + 0, + frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7]) + + assert.Equal(t, hfConnectionType[:], frame.connectionType()) + assert.EqualValues(t, arg, frame.dc()) + }) +} diff --git a/mtglib/internal/obfuscation/handshake_frame_test.go b/mtglib/internal/obfuscation/handshake_frame_test.go new file mode 100644 index 0000000..3b25492 --- /dev/null +++ b/mtglib/internal/obfuscation/handshake_frame_test.go @@ -0,0 +1,66 @@ +package obfuscation + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type HandshakeFrameTestSuite struct { + suite.Suite + + frame handshakeFrame + reverted handshakeFrame +} + +func (h *HandshakeFrameTestSuite) SetupSuite() { + for i := range hfLen { + h.frame.data[i] = byte(i + 1) + h.reverted.data[i] = byte(hfLen - i) + } +} + +func (h *HandshakeFrameTestSuite) TestKey() { + key := h.frame.key() + h.EqualValues(8+1, key[0]) + h.EqualValues(8+hfLenKey, key[len(key)-1]) + h.Len(key, hfLenKey) +} + +func (h *HandshakeFrameTestSuite) TestIV() { + iv := h.frame.iv() + h.EqualValues(40+1, iv[0]) + h.EqualValues(40+hfLenIV, iv[len(iv)-1]) + h.Len(iv, hfLenIV) +} + +func (h *HandshakeFrameTestSuite) TestConnectionType() { + connectionType := h.frame.connectionType() + h.EqualValues(56+1, connectionType[0]) + h.EqualValues(56+hfLenConnectionType, connectionType[len(connectionType)-1]) + h.Len(connectionType, hfLenConnectionType) +} + +func (h *HandshakeFrameTestSuite) TestDCSlice() { + dcSlice := h.frame.dcSlice() + h.EqualValues(61, dcSlice[0]) + h.EqualValues(61+1, dcSlice[1]) + h.Len(dcSlice, 2) +} + +func (h *HandshakeFrameTestSuite) TestDC() { + h.Equal(15933, h.frame.dc()) +} + +func (h *HandshakeFrameTestSuite) TestRevert() { + fr := h.frame + fr.revert() + + h.Equal(h.reverted.key(), fr.key()) + h.Equal(h.reverted.iv(), fr.iv()) +} + +func TestHandshakeFrame(t *testing.T) { + t.Parallel() + suite.Run(t, &HandshakeFrameTestSuite{}) +} diff --git a/mtglib/internal/obfuscation/init_test.go b/mtglib/internal/obfuscation/init_test.go new file mode 100644 index 0000000..ff2ef75 --- /dev/null +++ b/mtglib/internal/obfuscation/init_test.go @@ -0,0 +1,79 @@ +package obfuscation_test + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type snapshotBytes struct { + data []byte +} + +func (s snapshotBytes) MarshalText() ([]byte, error) { + if len(s.data) == 0 { + return nil, nil + } + + return []byte(base64.RawStdEncoding.EncodeToString(s.data)), nil +} + +func (s *snapshotBytes) UnmarshalText(data []byte) error { + val, err := base64.RawStdEncoding.DecodeString(string(data)) + if err != nil { + return fmt.Errorf("cannot unmarshal %v: %w", len(val), err) + } + + s.data = val + + return nil +} + +type ObfuscatedSnapshot struct { + Secret snapshotBytes `json:"secret"` + Frame snapshotBytes `json:"frame"` + DC int16 `json:"dc"` + Encrypted struct { + Text snapshotBytes `json:"text"` + Cipher snapshotBytes `json:"cipher"` + } `json:"encrypted"` + Decrypted struct { + Text snapshotBytes `json:"text"` + Cipher snapshotBytes `json:"cipher"` + } `json:"decrypted"` +} + +type SnapshotTestSuite struct { + suite.Suite + + snapshots map[string]*ObfuscatedSnapshot +} + +func (s *SnapshotTestSuite) Setup(dirname, namePrefix string) { + s.snapshots = make(map[string]*ObfuscatedSnapshot) + + files, err := os.ReadDir("testdata") + require.NoError(s.T(), err) + + for _, v := range files { + if !strings.HasPrefix(v.Name(), namePrefix) { + continue + } + + filename := filepath.Join("testdata", v.Name()) + + contents, err := os.ReadFile(filename) + require.NoError(s.T(), err) + + value := &ObfuscatedSnapshot{} + require.NoError(s.T(), json.Unmarshal(contents, value)) + + s.snapshots[v.Name()] = value + } +} diff --git a/mtglib/internal/obfuscation/obfuscator.go b/mtglib/internal/obfuscation/obfuscator.go new file mode 100644 index 0000000..09e63f1 --- /dev/null +++ b/mtglib/internal/obfuscation/obfuscator.go @@ -0,0 +1,87 @@ +package obfuscation + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "fmt" + "hash" + "io" + + "github.com/9seconds/mtg/v2/essentials" +) + +type Obfuscator struct { + Secret []byte +} + +func (o Obfuscator) ReadHandshake(r essentials.Conn) (int, essentials.Conn, error) { + frame := handshakeFrame{} + + if _, err := io.ReadFull(r, frame.data[:]); err != nil { + return 0, nil, fmt.Errorf("cannot read frame: %w", err) + } + + hasher := sha256.New() + recvCipher := o.getCipher(&frame, hasher) + + frame.revert() + hasher.Reset() + sendCipher := o.getCipher(&frame, hasher) + + recvCipher.XORKeyStream(frame.data[:], frame.data[:]) + + if val := frame.connectionType(); subtle.ConstantTimeCompare(val, hfConnectionType[:]) != 1 { + return 0, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(val)) + } + + cn := conn{ + Conn: r, + recvCipher: recvCipher, + sendCipher: sendCipher, + } + + return frame.dc(), cn, nil +} + +func (o Obfuscator) SendHandshake(w essentials.Conn, dc int) (essentials.Conn, error) { + frame := generateHandshake(dc) + copyFrame := frame + hasher := sha256.New() + + sendCipher := o.getCipher(&frame, hasher) + + frame.revert() + hasher.Reset() + recvCipher := o.getCipher(&frame, hasher) + + sendCipher.XORKeyStream(frame.data[:], frame.data[:]) + copy(frame.key(), copyFrame.key()) + copy(frame.iv(), copyFrame.iv()) + + if _, err := w.Write(frame.data[:]); err != nil { + return nil, fmt.Errorf("cannot send a handshake: %w", err) + } + + return conn{ + Conn: w, + recvCipher: recvCipher, + sendCipher: sendCipher, + }, nil +} + +func (o Obfuscator) getCipher(f *handshakeFrame, hasher hash.Hash) cipher.Stream { + blockKey := f.key() + + if o.Secret != nil { + hasher.Write(blockKey) + hasher.Write(o.Secret) + blockKey = hasher.Sum(nil) + } + + block, _ := aes.NewCipher(blockKey) + + return cipher.NewCTR(block, f.iv()) +} diff --git a/mtglib/internal/obfuscation/obfuscator_fuzz_test.go b/mtglib/internal/obfuscation/obfuscator_fuzz_test.go new file mode 100644 index 0000000..1697c32 --- /dev/null +++ b/mtglib/internal/obfuscation/obfuscator_fuzz_test.go @@ -0,0 +1,63 @@ +package obfuscation_test + +import ( + "bytes" + "testing" + + "github.com/9seconds/mtg/v2/internal/testlib" + "github.com/9seconds/mtg/v2/mtglib" + "github.com/9seconds/mtg/v2/mtglib/internal/obfuscation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func FuzzClientServerHandshakes(f *testing.F) { + f.Add(int16(1), make([]byte, mtglib.SecretKeyLength)) + + f.Fuzz(func(t *testing.T, dc int16, data []byte) { + if dc <= 0 { + dc = 1 + } + + client := obfuscation.Obfuscator{ + Secret: data, + } + server := client + + clientToServerBuf := &bytes.Buffer{} + + writeConnMock := &testlib.EssentialsConnMock{} + writeConnMock. + On("Write", mock.AnythingOfType("[]uint8")). + Once(). + Return(64, nil). + Run(func(args mock.Arguments) { + arg := args.Get(0).([]byte) + n, err := clientToServerBuf.Write(arg) + assert.Equal(t, 64, n) + assert.NoError(t, err) + }) + + readConnMock := &testlib.EssentialsConnMock{} + readConnMock. + On("Read", mock.AnythingOfType("[]uint8")). + Once(). + Return(64, nil). + Run(func(args mock.Arguments) { + arg := args.Get(0).([]byte) + n, err := clientToServerBuf.Read(arg) + assert.Equal(t, 64, n) + assert.NoError(t, err) + }) + + _, err := client.SendHandshake(writeConnMock, int(dc)) + assert.NoError(t, err) + + readDc, _, err := server.ReadHandshake(readConnMock) + assert.NoError(t, err) + assert.EqualValues(t, dc, readDc) + + writeConnMock.AssertExpectations(t) + readConnMock.AssertExpectations(t) + }) +} diff --git a/mtglib/internal/obfuscation/obfuscator_test.go b/mtglib/internal/obfuscation/obfuscator_test.go new file mode 100644 index 0000000..0280b26 --- /dev/null +++ b/mtglib/internal/obfuscation/obfuscator_test.go @@ -0,0 +1,94 @@ +package obfuscation_test + +import ( + "bytes" + "testing" + + "github.com/9seconds/mtg/v2/internal/testlib" + "github.com/9seconds/mtg/v2/mtglib" + "github.com/9seconds/mtg/v2/mtglib/internal/obfuscation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type ObfuscatorTestSuite struct { + SnapshotTestSuite + + secret *mtglib.Secret +} + +func (s *ObfuscatorTestSuite) SetupSuite() { + s.SnapshotTestSuite.Setup("", "client-handshake") + + secret := mtglib.GenerateSecret("hostname.com") + s.secret = &secret +} + +func (s *ObfuscatorTestSuite) TestSnapshot() { + for name, snapshot := range s.snapshots { + s.T().Run(name, func(t *testing.T) { + obfs := obfuscation.Obfuscator{ + Secret: snapshot.Secret.data, + } + + connMock := &testlib.EssentialsConnMock{} + + connMockReadBuffer := &bytes.Buffer{} + connMockReadBuffer.Write(snapshot.Frame.data) + connMockReadBuffer.Write(snapshot.Decrypted.Cipher.data) + + connMockWriteBuffer := &bytes.Buffer{} + + connMock. + On("Read", mock.AnythingOfType("[]uint8")). + Return(64, nil). + Run(func(args mock.Arguments) { + arr := args.Get(0).([]byte) + _, err := connMockReadBuffer.Read(arr) + require.NoError(t, err) + }) + + dc, cn, err := obfs.ReadHandshake(connMock) + assert.EqualValues(t, 2, dc) + assert.NoError(t, err) + + connMock.Calls = []mock.Call{} + connMock.ExpectedCalls = []*mock.Call{} + + connMock. + On("Read", mock.AnythingOfType("[]uint8")). + Return(len(snapshot.Decrypted.Cipher.data), nil). + Run(func(args mock.Arguments) { + arr := args.Get(0).([]byte) + _, err := connMockReadBuffer.Read(arr) + require.NoError(t, err) + }) + connMock. + On("Write", mock.AnythingOfType("[]uint8")). + Return(len(snapshot.Encrypted.Cipher.data), nil). + Run(func(args mock.Arguments) { + arr := args.Get(0).([]byte) + _, err := connMockWriteBuffer.Write(arr) + require.NoError(t, err) + }) + + readBuf := make([]byte, len(snapshot.Decrypted.Text.data)) + _, err = cn.Read(readBuf) + assert.NoError(t, err) + assert.Equal(t, readBuf, snapshot.Decrypted.Text.data) + + _, err = cn.Write(snapshot.Encrypted.Text.data) + assert.NoError(t, err) + assert.Equal(t, connMockWriteBuffer.Bytes(), snapshot.Encrypted.Cipher.data) + + connMock.AssertExpectations(t) + }) + } +} + +func TestObfuscator(t *testing.T) { + t.Parallel() + suite.Run(t, &ObfuscatorTestSuite{}) +} diff --git a/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json b/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json new file mode 100644 index 0000000..f34dc15 --- /dev/null +++ b/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json @@ -0,0 +1,13 @@ +{ + "secret": "NnoYmu4Y+jHBkAVO/UqOlQ", + "frame": "gDcXwaMY4RwlR+nJw+ILDr123UJHHjjE/U5pF4m/Y04AmH7lEpEL6UYRnIYDbDlOHSDxc1ToziPvNlJJh8RMow", + "dc": 2, + "encrypted": { + "text": "AQIDBAUGBwgJCg", + "cipher": "wZV3TR39l9nRoQ" + }, + "decrypted": { + "text": "4wZj6mUUew", + "cipher": "YWJjZGVmZw" + } +} diff --git a/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json b/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json new file mode 100644 index 0000000..a59adb4 --- /dev/null +++ b/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json @@ -0,0 +1,13 @@ +{ + "secret": "NnoYmu4Y+jHBkAVO/UqOlQ", + "frame": "M2WyxeiwIQB+ZOFxNzSNHtu9OdESkfxv3JkKFimCxUoYA3BD/Ql9nXB/OIonCKLUKCcS0VzZ2P6/+5oQ9GI8YA", + "dc": 2, + "encrypted": { + "text": "AQIDBAUGBwgJCg", + "cipher": "tzAwrCz00odERg" + }, + "decrypted": { + "text": "QkIvwGQDgA", + "cipher": "YWJjZGVmZw" + } +} From 140e9dfc2e670ee7967b680a1333350125abd258 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 23 Feb 2026 01:03:05 +0100 Subject: [PATCH 04/18] Integrate obfuscation package --- mtglib/internal/dc/addr.go | 13 +++++++--- mtglib/proxy.go | 52 +++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/mtglib/internal/dc/addr.go b/mtglib/internal/dc/addr.go index 8e433e3..d12fa3d 100644 --- a/mtglib/internal/dc/addr.go +++ b/mtglib/internal/dc/addr.go @@ -1,10 +1,17 @@ package dc +import ( + "fmt" + + "github.com/9seconds/mtg/v2/mtglib/internal/obfuscation" +) + type Addr struct { - Network string - Address string + Network string + Address string + Obfuscator obfuscation.Obfuscator } func (d Addr) String() string { - return d.Address + return fmt.Sprintf("addr=%s, secret=%v", d.Address, d.Obfuscator.Secret) } diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 104c723..9dc6bba 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -13,7 +13,7 @@ import ( "github.com/9seconds/mtg/v2/mtglib/internal/dc" "github.com/9seconds/mtg/v2/mtglib/internal/faketls" "github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" - "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" + "github.com/9seconds/mtg/v2/mtglib/internal/obfuscation" "github.com/9seconds/mtg/v2/mtglib/internal/relay" "github.com/panjf2000/ants/v2" ) @@ -29,6 +29,7 @@ type Proxy struct { domainFrontingPort int workerPool *ants.PoolWithFunc telegram *dc.Telegram + clientObfuscatror obfuscation.Obfuscator secret Secret network Network @@ -70,8 +71,8 @@ func (p *Proxy) ServeConn(conn essentials.Conn) { return } - if err := p.doObfuscated2Handshake(ctx); err != nil { - p.logger.InfoError("obfuscated2 handshake is failed", err) + if err := p.doObfuscatedHandshake(ctx); err != nil { + p.logger.InfoError("obfuscated handshake is failed", err) return } @@ -201,19 +202,15 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool { return true } -func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error { - dc, encryptor, decryptor, err := obfuscated2.ClientHandshake(p.secret.Key[:], ctx.clientConn) +func (p *Proxy) doObfuscatedHandshake(ctx *streamContext) error { + dc, conn, err := p.clientObfuscatror.ReadHandshake(ctx.clientConn) if err != nil { return fmt.Errorf("cannot process client handshake: %w", err) } ctx.dc = dc + ctx.clientConn = conn ctx.logger = ctx.logger.BindInt("dc", dc) - ctx.clientConn = obfuscated2.Conn{ - Conn: ctx.clientConn, - Encryptor: encryptor, - Decryptor: decryptor, - } return nil } @@ -223,17 +220,22 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error { addresses := p.telegram.GetAddresses(dcid) if len(addresses) == 0 && p.allowFallbackOnUnknownDC { - ctx.logger = ctx.logger.BindInt("fallback_dc", dc.DefaultDC) + ctx.logger = ctx.logger.BindInt("original_dc", dcid) ctx.logger.Warning("unknown DC, fallbacks") + ctx.dc = dc.DefaultDC addresses = p.telegram.GetAddresses(dc.DefaultDC) } - var conn essentials.Conn - var err error + var ( + conn essentials.Conn + err error + foundAddr dc.Addr + ) for _, addr := range addresses { conn, err = p.network.Dial(addr.Network, addr.Address) if err == nil { + foundAddr = addr break } } @@ -241,22 +243,17 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error { return fmt.Errorf("no addresses to call: %w", err) } - encryptor, decryptor, err := obfuscated2.ServerHandshake(conn) + conn, err = foundAddr.Obfuscator.SendHandshake(conn, ctx.dc) if err != nil { - conn.Close() //nolint: errcheck - - return fmt.Errorf("cannot perform obfuscated2 handshake: %w", err) + conn.Close() + return fmt.Errorf("cannot perform server handshake: %w", err) } - ctx.telegramConn = obfuscated2.Conn{ - Conn: connTraffic{ - Conn: conn, - streamID: ctx.streamID, - stream: p.eventStream, - ctx: ctx, - }, - Encryptor: encryptor, - Decryptor: decryptor, + ctx.telegramConn = connTraffic{ + Conn: conn, + streamID: ctx.streamID, + stream: p.eventStream, + ctx: ctx, } p.eventStream.Send(ctx, @@ -320,6 +317,9 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { tolerateTimeSkewness: opts.getTolerateTimeSkewness(), allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, telegram: tg, + clientObfuscatror: obfuscation.Obfuscator{ + Secret: opts.Secret.Key[:], + }, } pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), From 3e75e4fa63b0f8bd47d71f341544ecf7e8458118 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 23 Feb 2026 01:03:17 +0100 Subject: [PATCH 05/18] Delete old obfuscated2 package --- .../internal/obfuscated2/client_handshake.go | 54 ------- .../client_handshake_fuzz_internal_test.go | 32 ---- .../obfuscated2/client_handshake_test.go | 89 ------------ mtglib/internal/obfuscated2/conn.go | 37 ----- .../internal/obfuscated2/handshake_frame.go | 71 --------- .../handshake_frame_internal_test.go | 73 ---------- mtglib/internal/obfuscated2/init_test.go | 137 ------------------ mtglib/internal/obfuscated2/pools.go | 39 ----- .../internal/obfuscated2/server_handshake.go | 67 --------- .../server_handshake_fuzz_internal_test.go | 30 ---- .../obfuscated2/server_handshake_fuzz_test.go | 58 -------- .../obfuscated2/server_handshake_test.go | 65 --------- ...t-handshake-snapshot-4529d55776e2d427.json | 13 -- ...t-handshake-snapshot-585c944d672f60a2.json | 13 -- mtglib/internal/obfuscated2/utils.go | 15 -- 15 files changed, 793 deletions(-) delete mode 100644 mtglib/internal/obfuscated2/client_handshake.go delete mode 100644 mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go delete mode 100644 mtglib/internal/obfuscated2/client_handshake_test.go delete mode 100644 mtglib/internal/obfuscated2/conn.go delete mode 100644 mtglib/internal/obfuscated2/handshake_frame.go delete mode 100644 mtglib/internal/obfuscated2/handshake_frame_internal_test.go delete mode 100644 mtglib/internal/obfuscated2/init_test.go delete mode 100644 mtglib/internal/obfuscated2/pools.go delete mode 100644 mtglib/internal/obfuscated2/server_handshake.go delete mode 100644 mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go delete mode 100644 mtglib/internal/obfuscated2/server_handshake_fuzz_test.go delete mode 100644 mtglib/internal/obfuscated2/server_handshake_test.go delete mode 100644 mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json delete mode 100644 mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json delete mode 100644 mtglib/internal/obfuscated2/utils.go diff --git a/mtglib/internal/obfuscated2/client_handshake.go b/mtglib/internal/obfuscated2/client_handshake.go deleted file mode 100644 index 229cf83..0000000 --- a/mtglib/internal/obfuscated2/client_handshake.go +++ /dev/null @@ -1,54 +0,0 @@ -package obfuscated2 - -import ( - "crypto/cipher" - "crypto/subtle" - "encoding/hex" - "fmt" - "io" -) - -type clientHandhakeFrame struct { - handshakeFrame -} - -func (c *clientHandhakeFrame) decryptor(secret []byte) cipher.Stream { - hasher := acquireSha256Hasher() - defer releaseSha256Hasher(hasher) - - hasher.Write(c.key()) - hasher.Write(secret) - - return makeAesCtr(hasher.Sum(nil), c.iv()) -} - -func (c *clientHandhakeFrame) encryptor(secret []byte) cipher.Stream { - invertedHandshake := c.invert() - - hasher := acquireSha256Hasher() - defer releaseSha256Hasher(hasher) - - hasher.Write(invertedHandshake.key()) - hasher.Write(secret) - - return makeAesCtr(hasher.Sum(nil), invertedHandshake.iv()) -} - -func ClientHandshake(secret []byte, reader io.Reader) (int, cipher.Stream, cipher.Stream, error) { - handshake := clientHandhakeFrame{} - - if _, err := io.ReadFull(reader, handshake.data[:]); err != nil { - return 0, nil, nil, fmt.Errorf("cannot read frame: %w", err) - } - - decryptor := handshake.decryptor(secret) - encryptor := handshake.encryptor(secret) - - decryptor.XORKeyStream(handshake.data[:], handshake.data[:]) - - if val := handshake.connectionType(); subtle.ConstantTimeCompare(handshakeConnectionType, val) != 1 { - return 0, nil, nil, fmt.Errorf("unsupported connection type: %s", hex.EncodeToString(val)) - } - - return handshake.dc(), encryptor, decryptor, nil -} diff --git a/mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go b/mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go deleted file mode 100644 index 01a8a1e..0000000 --- a/mtglib/internal/obfuscated2/client_handshake_fuzz_internal_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package obfuscated2 - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/require" -) - -var FuzzClientHandshakeSecret = []byte{1, 2, 3} - -func FuzzClientHandshake(f *testing.F) { - f.Add([]byte{1, 2, 3}) - - f.Fuzz(func(t *testing.T, frame []byte) { - data := bytes.NewReader(frame) - - if _, _, _, err := ClientHandshake(FuzzClientHandshakeSecret, data); err != nil { - return - } - - handshake := clientHandhakeFrame{} - require.Len(t, frame, handshakeFrameLen) - - copy(handshake.data[:], frame) - - decryptor := handshake.decryptor(FuzzClientHandshakeSecret) - decryptor.XORKeyStream(handshake.data[:], handshake.data[:]) - - require.Equal(t, handshakeConnectionType, handshake.connectionType()) - }) -} diff --git a/mtglib/internal/obfuscated2/client_handshake_test.go b/mtglib/internal/obfuscated2/client_handshake_test.go deleted file mode 100644 index 59996fc..0000000 --- a/mtglib/internal/obfuscated2/client_handshake_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package obfuscated2_test - -import ( - "bytes" - "testing" - - "github.com/9seconds/mtg/v2/internal/testlib" - "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" -) - -type ClientHandshakeTestSuite struct { - suite.Suite - SnapshotTestSuite -} - -func (suite *ClientHandshakeTestSuite) SetupSuite() { - suite.NoError(suite.IngestSnapshots(".", "client-handshake-snapshot-")) -} - -func (suite *ClientHandshakeTestSuite) TestCannotRead() { - buf := bytes.NewBuffer([]byte{1, 2, 3}) - _, _, _, err := obfuscated2.ClientHandshake([]byte{1, 2, 3}, buf) //nolint: dogsled - - suite.Error(err) -} - -func (suite *ClientHandshakeTestSuite) TestOk() { - for nameV, snapshotV := range suite.snapshots { - snapshot := snapshotV - - suite.T().Run(nameV, func(t *testing.T) { - buf := bytes.NewBuffer(snapshot.Frame.data) - - dc, encryptor, decryptor, err := obfuscated2.ClientHandshake( - snapshot.Secret.data, buf) - assert.NoError(t, err) - assert.EqualValues(t, snapshot.DC, dc) - - writeData := make([]byte, len(snapshot.Encrypted.Text.data)) - readData := make([]byte, len(snapshot.Decrypted.Text.data)) - - connMock := &testlib.EssentialsConnMock{} - connMock.On("Read", mock.Anything). - Once(). - Return(len(snapshot.Decrypted.Text.data), nil). - Run(func(args mock.Arguments) { - arr, ok := args.Get(0).([]byte) - - suite.True(ok) - copy(arr, snapshot.Decrypted.Cipher.data) - }) - connMock.On("Write", mock.Anything). - Once(). - Return(len(snapshot.Encrypted.Text.data), nil). - Run(func(args mock.Arguments) { - arr, ok := args.Get(0).([]byte) - - suite.True(ok) - copy(writeData, arr) - }) - - conn := obfuscated2.Conn{ - Conn: connMock, - Encryptor: encryptor, - Decryptor: decryptor, - } - - n, err := conn.Read(readData) - assert.Equal(t, len(readData), n) - assert.NoError(t, err) - assert.Equal(t, snapshot.Decrypted.Text.data, readData) - - n, err = conn.Write(snapshot.Encrypted.Text.data) - assert.Equal(t, len(writeData), n) - assert.NoError(t, err) - assert.Equal(t, snapshot.Encrypted.Cipher.data, writeData) - - connMock.AssertExpectations(t) - }) - } -} - -func TestClientHandshake(t *testing.T) { - t.Parallel() - suite.Run(t, &ClientHandshakeTestSuite{}) -} diff --git a/mtglib/internal/obfuscated2/conn.go b/mtglib/internal/obfuscated2/conn.go deleted file mode 100644 index 29e323e..0000000 --- a/mtglib/internal/obfuscated2/conn.go +++ /dev/null @@ -1,37 +0,0 @@ -package obfuscated2 - -import ( - "crypto/cipher" - - "github.com/9seconds/mtg/v2/essentials" -) - -type Conn struct { - essentials.Conn - - Encryptor cipher.Stream - Decryptor cipher.Stream -} - -func (c Conn) Read(p []byte) (int, error) { - n, err := c.Conn.Read(p) - if err != nil { - return n, err //nolint: wrapcheck - } - - c.Decryptor.XORKeyStream(p, p[:n]) - - return n, nil -} - -func (c Conn) Write(p []byte) (int, error) { - buf := acquireBytesBuffer() - defer releaseBytesBuffer(buf) - - buf.Write(p) - - payload := buf.Bytes() - c.Encryptor.XORKeyStream(payload, payload) - - return c.Conn.Write(payload) //nolint: wrapcheck -} diff --git a/mtglib/internal/obfuscated2/handshake_frame.go b/mtglib/internal/obfuscated2/handshake_frame.go deleted file mode 100644 index 0dd3c6c..0000000 --- a/mtglib/internal/obfuscated2/handshake_frame.go +++ /dev/null @@ -1,71 +0,0 @@ -package obfuscated2 - -const ( - // DefaultDC defines a number of the default DC to use. This value used - // only if a value from obfuscated2 handshake frame is 0 (default). - DefaultDC = 2 - - handshakeFrameLen = 64 - - handshakeFrameLenKey = 32 - handshakeFrameLenIV = 16 - handshakeFrameLenConnectionType = 4 - - handshakeFrameOffsetStart = 8 - handshakeFrameOffsetKey = handshakeFrameOffsetStart - handshakeFrameOffsetIV = handshakeFrameOffsetKey + handshakeFrameLenKey - handshakeFrameOffsetConnectionType = handshakeFrameOffsetIV + handshakeFrameLenIV - handshakeFrameOffsetDC = handshakeFrameOffsetConnectionType + handshakeFrameLenConnectionType -) - -// Connection-Type: Secure. We support only fake tls. -var handshakeConnectionType = []byte{0xdd, 0xdd, 0xdd, 0xdd} - -// A structure of obfuscated2 handshake frame is following: -// -// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd]. -// -// - 8 bytes of noise -// - 32 bytes of AES Key -// - 16 bytes of AES IV -// - 4 bytes of 'connection type' - this has some setting like a connection type -// - 2 bytes of 'DC'. DC is little endian int16 -// - 2 bytes of noise -type handshakeFrame struct { - data [handshakeFrameLen]byte -} - -func (h *handshakeFrame) dc() int { - idx := int16(h.data[handshakeFrameOffsetDC]) | int16(h.data[handshakeFrameOffsetDC+1])<<8 //nolint: lll // little endian for int16 is here - - switch { - case idx > 0: - return int(idx) - case idx < 0: - return -int(idx) - default: - return DefaultDC - } -} - -func (h *handshakeFrame) key() []byte { - return h.data[handshakeFrameOffsetKey:handshakeFrameOffsetIV] -} - -func (h *handshakeFrame) iv() []byte { - return h.data[handshakeFrameOffsetIV:handshakeFrameOffsetConnectionType] -} - -func (h *handshakeFrame) connectionType() []byte { - return h.data[handshakeFrameOffsetConnectionType:handshakeFrameOffsetDC] -} - -func (h *handshakeFrame) invert() handshakeFrame { - copyFrame := *h - - for i := range handshakeFrameLenKey + handshakeFrameLenIV { - copyFrame.data[handshakeFrameOffsetKey+i] = h.data[handshakeFrameOffsetConnectionType-1-i] - } - - return copyFrame -} diff --git a/mtglib/internal/obfuscated2/handshake_frame_internal_test.go b/mtglib/internal/obfuscated2/handshake_frame_internal_test.go deleted file mode 100644 index 011c601..0000000 --- a/mtglib/internal/obfuscated2/handshake_frame_internal_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package obfuscated2 - -import ( - "crypto/rand" - "encoding/base64" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type HandshakeFrameTestSuite struct { - suite.Suite -} - -func (suite *HandshakeFrameTestSuite) Decode(value string) []byte { - v, err := base64.RawStdEncoding.DecodeString(value) - suite.NoError(err) - - return v -} - -func (suite *HandshakeFrameTestSuite) Encode(value []byte) string { - return base64.RawStdEncoding.EncodeToString(value) -} - -func (suite *HandshakeFrameTestSuite) TestOk() { - hf := handshakeFrame{} - testFrame := suite.Decode( - "L9TmCzzxl9bPKODBpZeVM/qqNUxQ/axxBup1S2ymbIfUd6f7YSyzzM9EmTFv2/XzGqJGEHuj2zofmUGBLghu5g") - copy(hf.data[:], testFrame) - - suite.Equal("zyjgwaWXlTP6qjVMUP2scQbqdUtspmyH1Hen+2Ess8w", suite.Encode(hf.key())) - suite.Equal("z0SZMW/b9fMaokYQe6PbOg", suite.Encode(hf.iv())) - suite.Equal("H5lBgQ", suite.Encode(hf.connectionType())) - suite.EqualValues(2094, hf.dc()) - - inverted := hf.invert() - suite.Equal("OtujexBGohrz9dtvMZlEz8yzLGH7p3fUh2ymbEt16gY", suite.Encode(inverted.key())) - suite.Equal("caz9UEw1qvozlZelweAozw", suite.Encode(inverted.iv())) - suite.Equal("H5lBgQ", suite.Encode(inverted.connectionType())) - suite.EqualValues(2094, inverted.dc()) -} - -func (suite *HandshakeFrameTestSuite) TestDC() { - testData := map[int16]int{ - 1: 1, - -1: 1, - 0: DefaultDC, - } - - for k, v := range testData { - incoming := k - expected := v - - suite.T().Run(strconv.Itoa(int(incoming)), func(t *testing.T) { - frame := handshakeFrame{} - - rand.Read(frame.data[:]) //nolint: errcheck - - frame.data[handshakeFrameOffsetDC] = byte(incoming) - frame.data[handshakeFrameOffsetDC+1] = byte(incoming >> 8) - - assert.Equal(t, expected, frame.dc()) - }) - } -} - -func TestHandshakeFrame(t *testing.T) { - t.Parallel() - suite.Run(t, &HandshakeFrameTestSuite{}) -} diff --git a/mtglib/internal/obfuscated2/init_test.go b/mtglib/internal/obfuscated2/init_test.go deleted file mode 100644 index 5aa85a9..0000000 --- a/mtglib/internal/obfuscated2/init_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package obfuscated2_test - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "encoding/base64" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/9seconds/mtg/v2/internal/testlib" - "github.com/9seconds/mtg/v2/mtglib/internal/obfuscated2" - "github.com/stretchr/testify/require" -) - -type snapshotBytes struct { - data []byte -} - -func (s snapshotBytes) MarshalText() ([]byte, error) { - if len(s.data) == 0 { - return nil, nil - } - - return []byte(base64.RawStdEncoding.EncodeToString(s.data)), nil -} - -func (s *snapshotBytes) UnmarshalText(data []byte) error { - val, err := base64.RawStdEncoding.DecodeString(string(data)) - if err != nil { - return fmt.Errorf("cannot unmarshal %v: %w", len(val), err) - } - - s.data = val - - return nil -} - -type Obfuscated2Snapshot struct { - Secret snapshotBytes `json:"secret"` - Frame snapshotBytes `json:"frame"` - DC int16 `json:"dc"` - Encrypted struct { - Text snapshotBytes `json:"text"` - Cipher snapshotBytes `json:"cipher"` - } `json:"encrypted"` - Decrypted struct { - Text snapshotBytes `json:"text"` - Cipher snapshotBytes `json:"cipher"` - } `json:"decrypted"` -} - -type SnapshotTestSuite struct { - snapshots map[string]*Obfuscated2Snapshot -} - -type ServerHandshakeTestData struct { - connMock *testlib.EssentialsConnMock - - proxyConn obfuscated2.Conn - encryptor cipher.Stream - decryptor cipher.Stream -} - -func (suite *SnapshotTestSuite) IngestSnapshots(dirname, namePrefix string) error { - suite.snapshots = map[string]*Obfuscated2Snapshot{} - - files, err := os.ReadDir(filepath.Join("testdata", dirname)) - if err != nil { - return fmt.Errorf("cannot ingest snapshots: %w", err) - } - - for _, v := range files { - if !strings.HasPrefix(v.Name(), namePrefix) { - continue - } - - filename := filepath.Join("testdata", dirname, v.Name()) - - contents, err := os.ReadFile(filename) - if err != nil { - return fmt.Errorf("cannot read %s: %w", filename, err) - } - - value := &Obfuscated2Snapshot{} - - if err := json.Unmarshal(contents, value); err != nil { - return fmt.Errorf("cannot unmarshal %s: %w", filename, err) - } - - suite.snapshots[v.Name()] = value - } - - return nil -} - -func NewServerHandshakeTestData(t *testing.T) ServerHandshakeTestData { - buf := &bytes.Buffer{} - connMock := &testlib.EssentialsConnMock{} - - handshakeEnc, handshakeDec, err := obfuscated2.ServerHandshake(buf) - require.NoError(t, err) - - serverEncrypted := buf.Bytes() - decBlock, _ := aes.NewCipher(serverEncrypted[8 : 8+32]) - decryptor := cipher.NewCTR(decBlock, serverEncrypted[8+32:8+32+16]) - - serverDecrypted := make([]byte, len(serverEncrypted)) - decryptor.XORKeyStream(serverDecrypted, serverEncrypted) - - require.Equal(t, "3d3d3Q", - base64.RawStdEncoding.EncodeToString(serverDecrypted[8+32+16:8+32+16+4])) - - serverEncryptedReverted := make([]byte, len(serverEncrypted)) - - for i := range 32 + 16 { - serverEncryptedReverted[8+i] = serverEncrypted[8+32+16-1-i] - } - - encBlock, _ := aes.NewCipher(serverEncryptedReverted[8 : 8+32]) - encryptor := cipher.NewCTR(encBlock, serverEncryptedReverted[8+32:8+32+16]) - - return ServerHandshakeTestData{ - connMock: connMock, - proxyConn: obfuscated2.Conn{ - Conn: connMock, - Encryptor: handshakeEnc, - Decryptor: handshakeDec, - }, - encryptor: encryptor, - decryptor: decryptor, - } -} diff --git a/mtglib/internal/obfuscated2/pools.go b/mtglib/internal/obfuscated2/pools.go deleted file mode 100644 index 5f10fd2..0000000 --- a/mtglib/internal/obfuscated2/pools.go +++ /dev/null @@ -1,39 +0,0 @@ -package obfuscated2 - -import ( - "bytes" - "crypto/sha256" - "hash" - "sync" -) - -var ( - sha256HasherPool = sync.Pool{ - New: func() any { - return sha256.New() - }, - } - bytesBufferPool = sync.Pool{ - New: func() any { - return &bytes.Buffer{} - }, - } -) - -func acquireSha256Hasher() hash.Hash { - return sha256HasherPool.Get().(hash.Hash) //nolint: forcetypeassert -} - -func releaseSha256Hasher(h hash.Hash) { - h.Reset() - sha256HasherPool.Put(h) -} - -func acquireBytesBuffer() *bytes.Buffer { - return bytesBufferPool.Get().(*bytes.Buffer) //nolint: forcetypeassert -} - -func releaseBytesBuffer(buf *bytes.Buffer) { - buf.Reset() - bytesBufferPool.Put(buf) -} diff --git a/mtglib/internal/obfuscated2/server_handshake.go b/mtglib/internal/obfuscated2/server_handshake.go deleted file mode 100644 index 7e52e02..0000000 --- a/mtglib/internal/obfuscated2/server_handshake.go +++ /dev/null @@ -1,67 +0,0 @@ -package obfuscated2 - -import ( - "crypto/cipher" - "crypto/rand" - "encoding/binary" - "fmt" - "io" -) - -type serverHandshakeFrame struct { - handshakeFrame -} - -func (s *serverHandshakeFrame) decryptor() cipher.Stream { - invertedHandshake := s.invert() - - return makeAesCtr(invertedHandshake.key(), invertedHandshake.iv()) -} - -func (s *serverHandshakeFrame) encryptor() cipher.Stream { - return makeAesCtr(s.key(), s.iv()) -} - -func ServerHandshake(writer io.Writer) (cipher.Stream, cipher.Stream, error) { - handshake := generateServerHanshakeFrame() - copyHandshake := handshake - encryptor := handshake.encryptor() - decryptor := handshake.decryptor() - - encryptor.XORKeyStream(handshake.data[:], handshake.data[:]) - copy(handshake.key(), copyHandshake.key()) - copy(handshake.iv(), copyHandshake.iv()) - - if _, err := writer.Write(handshake.data[:]); err != nil { - return nil, nil, fmt.Errorf("cannot send a handshake frame to telegram: %w", err) - } - - return encryptor, decryptor, nil -} - -func generateServerHanshakeFrame() serverHandshakeFrame { - frame := serverHandshakeFrame{} - - for { - if _, err := rand.Read(frame.data[:]); err != nil { - panic(err) - } - - if frame.data[0] == 0xef { // taken from tg sources - continue - } - - switch binary.LittleEndian.Uint32(frame.data[:4]) { - case 0x44414548, 0x54534f50, 0x20544547, 0x4954504f, 0xeeeeeeee: // taken from tg sources - continue - } - - if frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7] == 0 { - continue - } - - copy(frame.connectionType(), handshakeConnectionType) - - return frame - } -} diff --git a/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go b/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go deleted file mode 100644 index 125fd9c..0000000 --- a/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package obfuscated2 - -import ( - "encoding/binary" - "testing" - - "github.com/stretchr/testify/assert" -) - -func FuzzServerGenerateHandshakeFrame(f *testing.F) { - f.Fuzz(func(t *testing.T, arg int) { - frame := generateServerHanshakeFrame() - - assert.NotEqualValues(t, 0xef, frame.data[0]) - - firstBytes := binary.LittleEndian.Uint32(frame.data[:4]) - assert.NotEqualValues(t, 0x44414548, firstBytes) - assert.NotEqualValues(t, 0x54534f50, firstBytes) - assert.NotEqualValues(t, 0x20544547, firstBytes) - assert.NotEqualValues(t, 0x4954504f, firstBytes) - assert.NotEqualValues(t, 0xeeeeeeee, firstBytes) - - assert.NotEqualValues( - t, - 0, - frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7]) - - assert.Equal(t, handshakeConnectionType, frame.connectionType()) - }) -} diff --git a/mtglib/internal/obfuscated2/server_handshake_fuzz_test.go b/mtglib/internal/obfuscated2/server_handshake_fuzz_test.go deleted file mode 100644 index 33e0e0d..0000000 --- a/mtglib/internal/obfuscated2/server_handshake_fuzz_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package obfuscated2_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func FuzzServerSend(f *testing.F) { - f.Add([]byte{1, 2, 3, 4, 5}) - - f.Fuzz(func(t *testing.T, data []byte) { - handshakeData := NewServerHandshakeTestData(t) - - handshakeData.connMock. - On("Write", mock.Anything). - Return(len(data), nil). - Once(). - Run(func(args mock.Arguments) { - message := make([]byte, len(data)) - handshakeData.decryptor.XORKeyStream(message, args.Get(0).([]byte)) //nolint: forcetypeassert - assert.Equal(t, message, data) - }) - - n, err := handshakeData.proxyConn.Write(data) - - assert.EqualValues(t, len(data), n) - assert.NoError(t, err) - handshakeData.connMock.AssertExpectations(t) - }) -} - -func FuzzServerReceive(f *testing.F) { - f.Add([]byte{1, 2, 3, 4, 5}) - - f.Fuzz(func(t *testing.T, data []byte) { - handshakeData := NewServerHandshakeTestData(t) - buffer := make([]byte, len(data)) - - handshakeData.connMock. - On("Read", mock.Anything). - Return(len(data), nil). - Once(). - Run(func(args mock.Arguments) { - message := make([]byte, len(data)) - handshakeData.encryptor.XORKeyStream(message, data) - copy(args.Get(0).([]byte), message) //nolint: forcetypeassert - }) - - n, err := handshakeData.proxyConn.Read(buffer) - - assert.EqualValues(t, len(data), n) - assert.NoError(t, err) - assert.Equal(t, data, buffer) - handshakeData.connMock.AssertExpectations(t) - }) -} diff --git a/mtglib/internal/obfuscated2/server_handshake_test.go b/mtglib/internal/obfuscated2/server_handshake_test.go deleted file mode 100644 index b46f06c..0000000 --- a/mtglib/internal/obfuscated2/server_handshake_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package obfuscated2_test - -import ( - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" -) - -type ServerHandshakeTestSuite struct { - suite.Suite - - data ServerHandshakeTestData -} - -func (suite *ServerHandshakeTestSuite) SetupTest() { - suite.data = NewServerHandshakeTestData(suite.T()) -} - -func (suite *ServerHandshakeTestSuite) TearDownTest() { - suite.data.connMock.AssertExpectations(suite.T()) -} - -func (suite *ServerHandshakeTestSuite) TestSendToTelegram() { - messageToTelegram := []byte{10, 11, 12, 13, 14, 'a'} - - suite.data.connMock. - On("Write", mock.Anything). - Return(len(messageToTelegram), nil). - Once(). - Run(func(args mock.Arguments) { - message := make([]byte, len(messageToTelegram)) - suite.data.decryptor.XORKeyStream(message, args.Get(0).([]byte)) //nolint: forcetypeassert - suite.Equal(messageToTelegram, message) - }) - - n, err := suite.data.proxyConn.Write(messageToTelegram) - suite.EqualValues(len(messageToTelegram), n) - suite.NoError(err) -} - -func (suite *ServerHandshakeTestSuite) TestRecieveFromTelegram() { - messageFromTelegram := []byte{10, 11, 12, 13, 14, 'a'} - buffer := make([]byte, len(messageFromTelegram)) - - suite.data.connMock. - On("Read", mock.Anything). - Return(len(messageFromTelegram), nil). - Once(). - Run(func(args mock.Arguments) { - message := make([]byte, len(messageFromTelegram)) - suite.data.encryptor.XORKeyStream(message, messageFromTelegram) - copy(args.Get(0).([]byte), message) //nolint: forcetypeassert - }) - - n, err := suite.data.proxyConn.Read(buffer) - suite.EqualValues(len(messageFromTelegram), n) - suite.NoError(err) - suite.Equal(messageFromTelegram, buffer) -} - -func TestServerHandshake(t *testing.T) { - t.Parallel() - suite.Run(t, &ServerHandshakeTestSuite{}) -} diff --git a/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json b/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json deleted file mode 100644 index f34dc15..0000000 --- a/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "secret": "NnoYmu4Y+jHBkAVO/UqOlQ", - "frame": "gDcXwaMY4RwlR+nJw+ILDr123UJHHjjE/U5pF4m/Y04AmH7lEpEL6UYRnIYDbDlOHSDxc1ToziPvNlJJh8RMow", - "dc": 2, - "encrypted": { - "text": "AQIDBAUGBwgJCg", - "cipher": "wZV3TR39l9nRoQ" - }, - "decrypted": { - "text": "4wZj6mUUew", - "cipher": "YWJjZGVmZw" - } -} diff --git a/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json b/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json deleted file mode 100644 index a59adb4..0000000 --- a/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "secret": "NnoYmu4Y+jHBkAVO/UqOlQ", - "frame": "M2WyxeiwIQB+ZOFxNzSNHtu9OdESkfxv3JkKFimCxUoYA3BD/Ql9nXB/OIonCKLUKCcS0VzZ2P6/+5oQ9GI8YA", - "dc": 2, - "encrypted": { - "text": "AQIDBAUGBwgJCg", - "cipher": "tzAwrCz00odERg" - }, - "decrypted": { - "text": "QkIvwGQDgA", - "cipher": "YWJjZGVmZw" - } -} diff --git a/mtglib/internal/obfuscated2/utils.go b/mtglib/internal/obfuscated2/utils.go deleted file mode 100644 index a3fdcf9..0000000 --- a/mtglib/internal/obfuscated2/utils.go +++ /dev/null @@ -1,15 +0,0 @@ -package obfuscated2 - -import ( - "crypto/aes" - "crypto/cipher" -) - -func makeAesCtr(key, iv []byte) cipher.Stream { - block, err := aes.NewCipher(key) - if err != nil { - panic(err) - } - - return cipher.NewCTR(block, iv) -} From ee524abdb5686300300b5fd9baf664d189977f3c Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 23 Feb 2026 01:03:34 +0100 Subject: [PATCH 06/18] Remove redundant copyBufferPool from relay --- mtglib/internal/relay/pools.go | 19 ------------------- mtglib/internal/relay/relay.go | 7 +++---- 2 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 mtglib/internal/relay/pools.go diff --git a/mtglib/internal/relay/pools.go b/mtglib/internal/relay/pools.go deleted file mode 100644 index db42933..0000000 --- a/mtglib/internal/relay/pools.go +++ /dev/null @@ -1,19 +0,0 @@ -package relay - -import "sync" - -var copyBufferPool = sync.Pool{ - New: func() any { - rv := make([]byte, copyBufferSize) - - return &rv - }, -} - -func acquireCopyBuffer() *[]byte { - return copyBufferPool.Get().(*[]byte) //nolint: forcetypeassert -} - -func releaseCopyBuffer(buf *[]byte) { - copyBufferPool.Put(buf) -} diff --git a/mtglib/internal/relay/relay.go b/mtglib/internal/relay/relay.go index 060a05c..756a52d 100644 --- a/mtglib/internal/relay/relay.go +++ b/mtglib/internal/relay/relay.go @@ -35,13 +35,12 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials. } func pump(log Logger, src, dst essentials.Conn, direction string) { + var buf [copyBufferSize]byte + defer src.CloseRead() //nolint: errcheck defer dst.CloseWrite() //nolint: errcheck - copyBuffer := acquireCopyBuffer() - defer releaseCopyBuffer(copyBuffer) - - n, err := io.CopyBuffer(src, dst, *copyBuffer) + n, err := io.CopyBuffer(src, dst, buf[:]) switch { case err == nil: From e50cee57488ab7b1c6922283d2432fb099014781 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 23 Feb 2026 01:08:11 +0100 Subject: [PATCH 07/18] Do not use unnecessary lock in connRewind --- mtglib/conns.go | 12 ++---------- mtglib/proxy.go | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/mtglib/conns.go b/mtglib/conns.go index 9c3fa67..1759ca1 100644 --- a/mtglib/conns.go +++ b/mtglib/conns.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "sync" "github.com/9seconds/mtg/v2/essentials" ) @@ -40,22 +39,15 @@ func (c connTraffic) Write(b []byte) (int, error) { type connRewind struct { essentials.Conn - active io.Reader buf bytes.Buffer - mutex sync.RWMutex + active io.Reader } func (c *connRewind) Read(p []byte) (int, error) { - c.mutex.RLock() - defer c.mutex.RUnlock() - - return c.active.Read(p) //nolint: wrapcheck + return c.active.Read(p) } func (c *connRewind) Rewind() { - c.mutex.Lock() - defer c.mutex.Unlock() - c.active = io.MultiReader(&c.buf, c.Conn) } diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 9dc6bba..fe69862 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -243,14 +243,14 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error { return fmt.Errorf("no addresses to call: %w", err) } - conn, err = foundAddr.Obfuscator.SendHandshake(conn, ctx.dc) + tgConn, err := foundAddr.Obfuscator.SendHandshake(conn, ctx.dc) if err != nil { conn.Close() return fmt.Errorf("cannot perform server handshake: %w", err) } ctx.telegramConn = connTraffic{ - Conn: conn, + Conn: tgConn, streamID: ctx.streamID, stream: p.eventStream, ctx: ctx, From 908842063a7d666b717dc06fca95c61aef5b70f2 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 23 Feb 2026 10:27:01 +0100 Subject: [PATCH 08/18] Do not use additional bytes buffer for faketls --- mtglib/internal/faketls/conn.go | 19 +++++++++---------- mtglib/internal/faketls/pools.go | 21 --------------------- mtglib/internal/faketls/welcome.go | 7 +++---- 3 files changed, 12 insertions(+), 35 deletions(-) delete mode 100644 mtglib/internal/faketls/pools.go diff --git a/mtglib/internal/faketls/conn.go b/mtglib/internal/faketls/conn.go index 85acee2..2d04978 100644 --- a/mtglib/internal/faketls/conn.go +++ b/mtglib/internal/faketls/conn.go @@ -47,10 +47,7 @@ func (c *Conn) Write(p []byte) (int, error) { rec.Type = record.TypeApplicationData rec.Version = record.Version12 - sendBuffer := acquireBytesBuffer() - defer releaseBytesBuffer(sendBuffer) - - lenP := len(p) + written := 0 for len(p) > 0 { chunkSize := rand.IntN(record.TLSMaxRecordSize) @@ -60,14 +57,16 @@ func (c *Conn) Write(p []byte) (int, error) { rec.Payload.Reset() rec.Payload.Write(p[:chunkSize]) - rec.Dump(sendBuffer) //nolint: errcheck + + err := rec.Dump(c.Conn) + written += chunkSize + + if err != nil { + return written, err + } p = p[chunkSize:] } - if _, err := c.Conn.Write(sendBuffer.Bytes()); err != nil { - return 0, err //nolint: wrapcheck - } - - return lenP, nil + return written, nil } diff --git a/mtglib/internal/faketls/pools.go b/mtglib/internal/faketls/pools.go deleted file mode 100644 index b1f4275..0000000 --- a/mtglib/internal/faketls/pools.go +++ /dev/null @@ -1,21 +0,0 @@ -package faketls - -import ( - "bytes" - "sync" -) - -var bytesBufferPool = sync.Pool{ - New: func() any { - return &bytes.Buffer{} - }, -} - -func acquireBytesBuffer() *bytes.Buffer { - return bytesBufferPool.Get().(*bytes.Buffer) //nolint: forcetypeassert -} - -func releaseBytesBuffer(b *bytes.Buffer) { - b.Reset() - bytesBufferPool.Put(b) -} diff --git a/mtglib/internal/faketls/welcome.go b/mtglib/internal/faketls/welcome.go index 0d91dac..e9fa85b 100644 --- a/mtglib/internal/faketls/welcome.go +++ b/mtglib/internal/faketls/welcome.go @@ -1,6 +1,7 @@ package faketls import ( + "bytes" "crypto/hmac" "crypto/rand" "crypto/sha256" @@ -13,8 +14,7 @@ import ( ) func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) error { - buf := acquireBytesBuffer() - defer releaseBytesBuffer(buf) + buf := &bytes.Buffer{} rec := record.AcquireRecord() defer record.ReleaseRecord(rec) @@ -58,8 +58,7 @@ func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) } func generateServerHello(writer io.Writer, clientHello ClientHello) { - bodyBuf := acquireBytesBuffer() - defer releaseBytesBuffer(bodyBuf) + bodyBuf := &bytes.Buffer{} sliceBuf := [2]byte{} digest := [RandomLen]byte{} From 94d46d2c6588701f4eac49dcf5558c49ffd3e757 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Mon, 23 Feb 2026 22:36:49 +0100 Subject: [PATCH 09/18] Add fetching of addresses from proxyGetConfig endpoint --- mtglib/internal/dc/addr_test.go | 16 --- mtglib/internal/dc/init.go | 33 +++-- mtglib/internal/dc/init_test.go | 43 ++++++ mtglib/internal/dc/public_config_updater.go | 93 ++++++++++++ .../internal/dc/public_config_updater_test.go | 135 ++++++++++++++++++ mtglib/internal/dc/telegram.go | 43 ++---- mtglib/internal/dc/updater.go | 39 +++++ mtglib/internal/dc/updater_test.go | 55 +++++++ mtglib/internal/dc/view.go | 11 +- mtglib/internal/dc/view_test.go | 16 +-- mtglib/proxy.go | 15 +- 11 files changed, 419 insertions(+), 80 deletions(-) delete mode 100644 mtglib/internal/dc/addr_test.go create mode 100644 mtglib/internal/dc/init_test.go create mode 100644 mtglib/internal/dc/public_config_updater.go create mode 100644 mtglib/internal/dc/public_config_updater_test.go create mode 100644 mtglib/internal/dc/updater.go create mode 100644 mtglib/internal/dc/updater_test.go diff --git a/mtglib/internal/dc/addr_test.go b/mtglib/internal/dc/addr_test.go deleted file mode 100644 index c288c0d..0000000 --- a/mtglib/internal/dc/addr_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package dc_test - -import ( - "testing" - - "github.com/9seconds/mtg/v2/mtglib/internal/dc" - "github.com/stretchr/testify/assert" -) - -func TestAddr(t *testing.T) { - t.Parallel() - - addr := dc.Addr{Network: "tcp4", Address: "127.0.0.1:443"} - - assert.Equal(t, "127.0.0.1:443", addr.String()) -} diff --git a/mtglib/internal/dc/init.go b/mtglib/internal/dc/init.go index c253f6e..2c0f42c 100644 --- a/mtglib/internal/dc/init.go +++ b/mtglib/internal/dc/init.go @@ -1,5 +1,10 @@ package dc +import ( + "context" + "time" +) + type preferIP uint8 const ( @@ -10,7 +15,18 @@ const ( ) const ( + // Default DC to connect to if not sure. DefaultDC = 2 + + // How often should we request updates from + // https://core.telegram.org/getProxyConfig + PublicConfigUpdateEach = time.Hour + PublicConfigUpdateURLv4 = "https://core.telegram.org/getProxyConfig" + PublicConfigUpdateURLv6 = "https://core.telegram.org/getProxyConfigV6" + + // How often should we extract hosts from Telegram using help.getConfig + // method. + OwnConfigUpdateEach = time.Hour ) type Logger interface { @@ -18,6 +34,10 @@ type Logger interface { WarningError(msg string, err error) } +type Updater interface { + Run(ctx context.Context) +} + var ( // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 defaultDCAddrSet = dcAddrSet{ @@ -57,17 +77,4 @@ var ( }, }, } - - defaultDCOverridesAddrSet = dcAddrSet{ - v4: map[int][]Addr{ - 203: { - {Network: "tcp4", Address: "91.105.192.100:443"}, - }, - }, - v6: map[int][]Addr{ - 203: { - {Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"}, - }, - }, - } ) diff --git a/mtglib/internal/dc/init_test.go b/mtglib/internal/dc/init_test.go new file mode 100644 index 0000000..426a547 --- /dev/null +++ b/mtglib/internal/dc/init_test.go @@ -0,0 +1,43 @@ +package dc + +import ( + "context" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type LoggerMock struct { + mock.Mock +} + +func (m *LoggerMock) Info(msg string) { + m.Called(msg) +} + +func (m *LoggerMock) WarningError(msg string, err error) { + m.Called(msg, err) +} + +type UpdaterTestSuiteBase struct { + suite.Suite + + ctx context.Context + ctxCancel context.CancelFunc + loggerMock *LoggerMock +} + +func (s *UpdaterTestSuiteBase) SetupTest() { + ctx, cancel := context.WithCancel(context.Background()) + + s.loggerMock = &LoggerMock{} + s.loggerMock.On("Info", mock.AnythingOfType("string")) + s.loggerMock.On("WarningError", mock.AnythingOfType("string"), mock.Anything) + + s.ctx = ctx + s.ctxCancel = cancel +} + +func (s *UpdaterTestSuiteBase) TearDownTest() { + s.ctxCancel() +} diff --git a/mtglib/internal/dc/public_config_updater.go b/mtglib/internal/dc/public_config_updater.go new file mode 100644 index 0000000..40ecbe7 --- /dev/null +++ b/mtglib/internal/dc/public_config_updater.go @@ -0,0 +1,93 @@ +package dc + +import ( + "bufio" + "context" + "fmt" + "io" + "net/http" + "regexp" + "strconv" +) + +var publicConfigRe = regexp.MustCompile(`^\s*proxy_for\s+(\d+)\s+(\S+?)?;\s*$`) + +type PublicConfigUpdater struct { + updater + + http *http.Client + tg *Telegram +} + +func (p PublicConfigUpdater) Run(ctx context.Context, url, network string) { + p.run(ctx, func() error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + panic(err) + } + + resp, err := p.http.Do(req) + if err != nil { + if resp != nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + return fmt.Errorf("cannot fetch url %s: %w", url, err) + } + + if resp.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("unexpected status code from %s: %d", url, resp.StatusCode) + } + + scanner := bufio.NewScanner(resp.Body) + addrs := map[int][]Addr{} + + for scanner.Scan() { + matches := publicConfigRe.FindStringSubmatch(scanner.Text()) + if len(matches) != 3 { + continue + } + + dc, err := strconv.Atoi(matches[1]) + if err != nil { + continue + } + + switch dc { + // this is a list of DC we currently support. Other are ignored. + case 203: // CDN DC + p.logger.Info(fmt.Sprintf("found %s address for DC %d", matches[2], dc)) + addrs[dc] = append(addrs[dc], Addr{ + Network: network, + Address: matches[2], + }) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("cannot read response body from %s: %w", url, err) + } + + p.tg.lock.Lock() + defer p.tg.lock.Unlock() + + if network == "tcp4" { + p.tg.view.publicConfigs.v4 = addrs + } else { + p.tg.view.publicConfigs.v6 = addrs + } + + return nil + }) +} + +func NewPublicConfigUpdater(tg *Telegram, logger Logger, client *http.Client) PublicConfigUpdater { + return PublicConfigUpdater{ + updater: updater{ + logger: logger, + period: PublicConfigUpdateEach, + }, + http: client, + tg: tg, + } +} diff --git a/mtglib/internal/dc/public_config_updater_test.go b/mtglib/internal/dc/public_config_updater_test.go new file mode 100644 index 0000000..4da1773 --- /dev/null +++ b/mtglib/internal/dc/public_config_updater_test.go @@ -0,0 +1,135 @@ +package dc + +import ( + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type PublicConfigUpdaterTestSuite struct { + UpdaterTestSuiteBase + + u PublicConfigUpdater + lock sync.Mutex + srv *httptest.Server + responseHandler func(w http.ResponseWriter) +} + +func (s *PublicConfigUpdaterTestSuite) SetupSuite() { + s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.lock.Lock() + s.responseHandler(w) + s.lock.Unlock() + })) +} + +func (s *PublicConfigUpdaterTestSuite) TearDownSuite() { + s.srv.Close() +} + +func (s *PublicConfigUpdaterTestSuite) SetupTest() { + s.UpdaterTestSuiteBase.SetupTest() + + tg, err := New("prefer-ipv4") + require.NoError(s.T(), err) + + s.u = NewPublicConfigUpdater(tg, s.loggerMock, s.srv.Client()) +} + +func (s *PublicConfigUpdaterTestSuite) Test502StatusCode() { + done := false + + s.responseHandler = func(w http.ResponseWriter) { + w.WriteHeader(http.StatusBadGateway) + done = true + } + go s.u.Run(s.ctx, s.srv.URL, "tcp4") + + s.Eventually(func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + return done + }, time.Second, 10*time.Millisecond) + + s.Len(s.u.tg.view.publicConfigs.v4, 0) +} + +func (s *PublicConfigUpdaterTestSuite) TestEmptyFile() { + done := false + + s.responseHandler = func(w http.ResponseWriter) { + done = true + w.WriteHeader(http.StatusOK) + } + go s.u.Run(s.ctx, s.srv.URL, "tcp4") + + s.Eventually(func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + return done + }, time.Second, 10*time.Millisecond) + + s.Len(s.u.tg.view.publicConfigs.v4, 0) +} + +func (s *PublicConfigUpdaterTestSuite) TestGarbage() { + result := ` +proxy_for -1 -1; +proxy_for 100 100.10.0.0:3333; +lala 0 0 +` + done := false + + s.responseHandler = func(w http.ResponseWriter) { + done = true + w.WriteHeader(http.StatusOK) + w.Write([]byte(result)) + } + go s.u.Run(s.ctx, s.srv.URL, "tcp4") + + s.Eventually(func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + return done + }, time.Second, 10*time.Millisecond) + + s.Len(s.u.tg.view.publicConfigs.v4, 0) +} + +func (s *PublicConfigUpdaterTestSuite) TestOk() { + result := ` +proxy_for 203 100.10.0.0:3333; +proxy_for -100 101.10.0.0:3333; +` + done := false + + s.responseHandler = func(w http.ResponseWriter) { + done = true + w.WriteHeader(http.StatusOK) + w.Write([]byte(result)) + } + go s.u.Run(s.ctx, s.srv.URL, "tcp4") + + s.Eventually(func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + return done + }, time.Second, 10*time.Millisecond) + + s.Len(s.u.tg.view.publicConfigs.v4, 1) + s.Len(s.u.tg.view.publicConfigs.v4[203], 1) + s.Equal("100.10.0.0:3333", s.u.tg.view.publicConfigs.v4[203][0].Address) +} + +func TestPublicConfigUpdater(t *testing.T) { + suite.Run(t, &PublicConfigUpdaterTestSuite{}) +} diff --git a/mtglib/internal/dc/telegram.go b/mtglib/internal/dc/telegram.go index e9f261f..c9a85d4 100644 --- a/mtglib/internal/dc/telegram.go +++ b/mtglib/internal/dc/telegram.go @@ -1,17 +1,23 @@ package dc import ( + "context" "fmt" - "net" "strings" + "sync" ) type Telegram struct { + ctx context.Context + lock sync.RWMutex view dcView preferIP preferIP } func (t *Telegram) GetAddresses(dc int) []Addr { + t.lock.RLock() + defer t.lock.RUnlock() + switch t.preferIP { case preferIPOnlyIPv4: return t.view.getV4(dc) @@ -24,7 +30,7 @@ func (t *Telegram) GetAddresses(dc int) []Addr { return append(t.view.getV6(dc), t.view.getV4(dc)...) } -func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error) { +func New(ipPreference string) (*Telegram, error) { var pref preferIP switch strings.ToLower(ipPreference) { @@ -40,40 +46,7 @@ func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error) return nil, fmt.Errorf("unknown ip preference %s", ipPreference) } - overrides := dcAddrSet{ - v4: map[int][]Addr{}, - v6: map[int][]Addr{}, - } - for dc, addrs := range userOverrides { - for _, addr := range addrs { - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, fmt.Errorf("incorrect host %s: %w", addr, err) - } - - parsed := net.ParseIP(host) - if parsed == nil { - return nil, fmt.Errorf("incorrect host %s", addr) - } - - if parsed.To4() != nil { - overrides.v4[dc] = append(overrides.v4[dc], Addr{ - Network: "tcp4", - Address: addr, - }) - } else { - overrides.v6[dc] = append(overrides.v6[dc], Addr{ - Network: "tcp6", - Address: addr, - }) - } - } - } - return &Telegram{ - view: dcView{ - overrides: overrides, - }, preferIP: pref, }, nil } diff --git a/mtglib/internal/dc/updater.go b/mtglib/internal/dc/updater.go new file mode 100644 index 0000000..5d57af4 --- /dev/null +++ b/mtglib/internal/dc/updater.go @@ -0,0 +1,39 @@ +package dc + +import ( + "context" + "time" +) + +type updater struct { + logger Logger + period time.Duration +} + +func (u updater) run(ctx context.Context, callback func() error) { + ticker := time.NewTicker(u.period) + + defer func() { + ticker.Stop() + + select { + case <-ticker.C: + default: + } + }() + + for { + u.logger.Info("start update") + if err := callback(); err != nil { + u.logger.WarningError("cannot update: %w", err) + } + u.logger.Info("updated") + + select { + case <-ctx.Done(): + u.logger.Info("stop updating") + return + case <-ticker.C: + } + } +} diff --git a/mtglib/internal/dc/updater_test.go b/mtglib/internal/dc/updater_test.go new file mode 100644 index 0000000..f6916a4 --- /dev/null +++ b/mtglib/internal/dc/updater_test.go @@ -0,0 +1,55 @@ +package dc + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type UpdaterTestSuite struct { + UpdaterTestSuiteBase + + u updater +} + +func (s *UpdaterTestSuite) SetupTest() { + s.UpdaterTestSuiteBase.SetupTest() + s.u = updater{ + logger: s.loggerMock, + period: 100 * time.Millisecond, + } +} + +func (s *UpdaterTestSuite) TestPeriodicUpdates() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + lock := &sync.Mutex{} + collected := []time.Time{} + + go s.u.run(s.ctx, func() error { + select { + case <-s.ctx.Done(): + case value := <-ticker.C: + lock.Lock() + collected = append(collected, value) + lock.Unlock() + } + + return nil + }) + + s.Eventually(func() bool { + lock.Lock() + defer lock.Unlock() + + return len(collected) == 3 + }, time.Second, 10*time.Millisecond) +} + +func TestUpdater(t *testing.T) { + t.Parallel() + suite.Run(t, &UpdaterTestSuite{}) +} diff --git a/mtglib/internal/dc/view.go b/mtglib/internal/dc/view.go index efee1c0..de06499 100644 --- a/mtglib/internal/dc/view.go +++ b/mtglib/internal/dc/view.go @@ -1,20 +1,21 @@ package dc type dcView struct { - overrides dcAddrSet + publicConfigs dcAddrSet + ownConfigs dcAddrSet } func (d dcView) getV4(dc int) []Addr { - addrs := d.overrides.getV4(dc) - addrs = append(addrs, defaultDCOverridesAddrSet.getV4(dc)...) + addrs := d.publicConfigs.getV4(dc) + addrs = append(addrs, d.ownConfigs.getV4(dc)...) addrs = append(addrs, defaultDCAddrSet.getV4(dc)...) return addrs } func (d dcView) getV6(dc int) []Addr { - addrs := d.overrides.getV6(dc) - addrs = append(addrs, defaultDCOverridesAddrSet.getV6(dc)...) + addrs := d.publicConfigs.getV6(dc) + addrs = append(addrs, d.ownConfigs.getV6(dc)...) addrs = append(addrs, defaultDCAddrSet.getV6(dc)...) return addrs diff --git a/mtglib/internal/dc/view_test.go b/mtglib/internal/dc/view_test.go index 74d46cb..90742c0 100644 --- a/mtglib/internal/dc/view_test.go +++ b/mtglib/internal/dc/view_test.go @@ -16,7 +16,7 @@ type ViewTestSuite struct { func (suite *ViewTestSuite) SetupSuite() { suite.view = dcView{ - overrides: dcAddrSet{ + publicConfigs: dcAddrSet{ v4: map[int][]Addr{ 111: { {Network: "tcp4", Address: "127.0.0.1:443"}, @@ -37,15 +37,14 @@ func (suite *ViewTestSuite) SetupSuite() { func (suite *ViewTestSuite) TestGetV4() { testData := map[int][]Addr{ 111: { - {"tcp4", "127.0.0.1:443"}, + {Network: "tcp4", Address: "127.0.0.1:443"}, }, 203: { - {"tcp4", "127.0.0.2:443"}, - {"tcp4", "91.105.192.100:443"}, + {Network: "tcp4", Address: "127.0.0.2:443"}, }, 2: { - {"tcp4", "149.154.167.51:443"}, - {"tcp4", "95.161.76.100:443"}, + {Network: "tcp4", Address: "149.154.167.51:443"}, + {Network: "tcp4", Address: "95.161.76.100:443"}, }, } @@ -60,11 +59,10 @@ func (suite *ViewTestSuite) TestGetV6() { testData := map[int][]Addr{ 111: {}, 203: { - {"tcp6", "xxx"}, - {"tcp6", "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"}, + {Network: "tcp6", Address: "xxx"}, }, 1: { - {"tcp6", "[2001:b28:f23d:f001::a]:443"}, + {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"}, }, } diff --git a/mtglib/proxy.go b/mtglib/proxy.go index fe69862..78f239d 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -297,12 +297,15 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { return nil, fmt.Errorf("invalid settings: %w", err) } - tg, err := dc.New(opts.getPreferIP(), opts.DCOverrides) + tg, err := dc.New(opts.getPreferIP()) if err != nil { return nil, fmt.Errorf("cannot build telegram dc fetcher: %w", err) } ctx, cancel := context.WithCancel(context.Background()) + logger := opts.getLogger("proxy") + updatersLogger := logger.Named("telegram-updaters") + proxy := &Proxy{ ctx: ctx, ctxCancel: cancel, @@ -312,7 +315,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { blocklist: opts.IPBlocklist, allowlist: opts.IPAllowlist, eventStream: opts.EventStream, - logger: opts.getLogger("proxy"), + logger: logger, domainFrontingPort: opts.getDomainFrontingPort(), tolerateTimeSkewness: opts.getTolerateTimeSkewness(), allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, @@ -322,6 +325,14 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { }, } + publicConfigUpdater := dc.NewPublicConfigUpdater( + tg, + updatersLogger.Named("public-config"), + opts.Network.MakeHTTPClient(nil), + ) + go publicConfigUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4") + go publicConfigUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6") + pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), func(arg any) { proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert From e4a9a9630963dfdc4aa93a0e166948bc87b8b3ad Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 13:31:36 +0100 Subject: [PATCH 10/18] Remove mentioning of DC overrides --- example.config.toml | 9 --------- internal/cli/run_proxy.go | 9 --------- internal/config/config.go | 4 ---- internal/config/parse.go | 4 ---- mtglib/internal/dc/view.go | 3 --- mtglib/proxy_opts.go | 2 +- 6 files changed, 1 insertion(+), 30 deletions(-) diff --git a/example.config.toml b/example.config.toml index 68f7926..eaff5de 100644 --- a/example.config.toml +++ b/example.config.toml @@ -80,15 +80,6 @@ tolerate-time-skewness = "5s" # Otherwise, chose a new DC. allow-fallback-on-unknown-dc = false -# Telegram uses different DCs for different purposes. Unfortunately, most of -# DCs are not public, and dependent on a location of the current user, so -# mtg cannot know upfront about all of them, and how to access them. It has -# a default list of DCs, including some CDN IPs, but it is possible that some -# of them are not working for you. In this case, you can override them here. -[[dc-overrides]] -dc = 101 -ips = ["127.0.0.1:443"] - # network defines different network-related settings [network] # please be aware that mtg needs to do some external requests. For diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 23580eb..d9f3d1f 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -242,14 +242,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen return fmt.Errorf("cannot build ip allowlist: %w", err) } - dcOverrides := map[int][]string{} - for _, override := range conf.DCOverrides { - dcid := override.DC.Get() - for _, addr := range override.IPs { - dcOverrides[dcid] = append(dcOverrides[dcid], addr.Get("")) - } - } - opts := mtglib.ProxyOpts{ Logger: logger, Network: ntw, @@ -264,7 +256,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, - DCOverrides: dcOverrides, } proxy, err := mtglib.NewProxy(opts) diff --git a/internal/config/config.go b/internal/config/config.go index 74abdf0..0a5a942 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -65,10 +65,6 @@ type Config struct { MetricPrefix TypeMetricPrefix `json:"metricPrefix"` } `json:"prometheus"` } `json:"stats"` - DCOverrides []struct { - DC TypeDC `json:"dc"` - IPs []TypeHostPort `json:"ips"` - } `json:"dcOverrides"` } func (c *Config) Validate() error { diff --git a/internal/config/parse.go b/internal/config/parse.go index 7cdf6da..80fb07c 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -60,10 +60,6 @@ type tomlConfig struct { MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` } `toml:"prometheus" json:"prometheus,omitempty"` } `toml:"stats" json:"stats,omitempty"` - DCOverrides []struct { - DC uint `toml:"dc" json:"dc"` - IPs []string `toml:"ips" json:"ips"` - } `toml:"dc-overrides" json:"dcOverrides,omitempty"` } func Parse(rawData []byte) (*Config, error) { diff --git a/mtglib/internal/dc/view.go b/mtglib/internal/dc/view.go index de06499..e4980a7 100644 --- a/mtglib/internal/dc/view.go +++ b/mtglib/internal/dc/view.go @@ -2,12 +2,10 @@ package dc type dcView struct { publicConfigs dcAddrSet - ownConfigs dcAddrSet } func (d dcView) getV4(dc int) []Addr { addrs := d.publicConfigs.getV4(dc) - addrs = append(addrs, d.ownConfigs.getV4(dc)...) addrs = append(addrs, defaultDCAddrSet.getV4(dc)...) return addrs @@ -15,7 +13,6 @@ func (d dcView) getV4(dc int) []Addr { func (d dcView) getV6(dc int) []Addr { addrs := d.publicConfigs.getV6(dc) - addrs = append(addrs, d.ownConfigs.getV6(dc)...) addrs = append(addrs, defaultDCAddrSet.getV6(dc)...) return addrs diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 53f3434..0399d80 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -117,7 +117,7 @@ type ProxyOpts struct { // DCOverrides defines a set of IP addresses that should be used // with a higher priority to those that are calculated somehow by mtg. // - // This is an optional setting + // OBSOLETE and DEPRECATED. Ignored. DCOverrides map[int][]string } From 36c766b3317b21b2c5d565e4929a2c3aa9116855 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 13:35:06 +0100 Subject: [PATCH 11/18] Fix lint issues --- mtglib/internal/dc/public_config_updater.go | 4 ++-- mtglib/internal/dc/public_config_updater_test.go | 4 ++-- mtglib/internal/dc/telegram.go | 2 -- mtglib/internal/obfuscation/obfuscator_test.go | 2 +- mtglib/proxy.go | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mtglib/internal/dc/public_config_updater.go b/mtglib/internal/dc/public_config_updater.go index 40ecbe7..8f22832 100644 --- a/mtglib/internal/dc/public_config_updater.go +++ b/mtglib/internal/dc/public_config_updater.go @@ -29,8 +29,8 @@ func (p PublicConfigUpdater) Run(ctx context.Context, url, network string) { resp, err := p.http.Do(req) if err != nil { if resp != nil { - io.Copy(io.Discard, resp.Body) - resp.Body.Close() + io.Copy(io.Discard, resp.Body) //nolint: errcheck + resp.Body.Close() //nolint: errcheck } return fmt.Errorf("cannot fetch url %s: %w", url, err) } diff --git a/mtglib/internal/dc/public_config_updater_test.go b/mtglib/internal/dc/public_config_updater_test.go index 4da1773..f51f3a2 100644 --- a/mtglib/internal/dc/public_config_updater_test.go +++ b/mtglib/internal/dc/public_config_updater_test.go @@ -90,7 +90,7 @@ lala 0 0 s.responseHandler = func(w http.ResponseWriter) { done = true w.WriteHeader(http.StatusOK) - w.Write([]byte(result)) + w.Write([]byte(result)) //nolint: errcheck } go s.u.Run(s.ctx, s.srv.URL, "tcp4") @@ -114,7 +114,7 @@ proxy_for -100 101.10.0.0:3333; s.responseHandler = func(w http.ResponseWriter) { done = true w.WriteHeader(http.StatusOK) - w.Write([]byte(result)) + w.Write([]byte(result)) //nolint: errcheck } go s.u.Run(s.ctx, s.srv.URL, "tcp4") diff --git a/mtglib/internal/dc/telegram.go b/mtglib/internal/dc/telegram.go index c9a85d4..fcd3fd7 100644 --- a/mtglib/internal/dc/telegram.go +++ b/mtglib/internal/dc/telegram.go @@ -1,14 +1,12 @@ package dc import ( - "context" "fmt" "strings" "sync" ) type Telegram struct { - ctx context.Context lock sync.RWMutex view dcView preferIP preferIP diff --git a/mtglib/internal/obfuscation/obfuscator_test.go b/mtglib/internal/obfuscation/obfuscator_test.go index 0280b26..fbc018e 100644 --- a/mtglib/internal/obfuscation/obfuscator_test.go +++ b/mtglib/internal/obfuscation/obfuscator_test.go @@ -20,7 +20,7 @@ type ObfuscatorTestSuite struct { } func (s *ObfuscatorTestSuite) SetupSuite() { - s.SnapshotTestSuite.Setup("", "client-handshake") + s.Setup("", "client-handshake") secret := mtglib.GenerateSecret("hostname.com") s.secret = &secret diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 78f239d..1f12345 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -245,7 +245,7 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error { tgConn, err := foundAddr.Obfuscator.SendHandshake(conn, ctx.dc) if err != nil { - conn.Close() + conn.Close() // nolint: errcheck return fmt.Errorf("cannot perform server handshake: %w", err) } From 5b91edf5c4270dc6bcaa1ee951c95fc5f5a762c6 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 13:58:16 +0100 Subject: [PATCH 12/18] Fix tests --- internal/cli/simple_run.go | 2 +- mtglib/internal/dc/init.go | 74 +++++++++---------- mtglib/internal/dc/public_config_updater.go | 6 +- .../internal/dc/public_config_updater_test.go | 56 +++++--------- mtglib/internal/dc/updater.go | 50 +++++++------ mtglib/proxy.go | 16 ++-- 6 files changed, 95 insertions(+), 109 deletions(-) diff --git a/internal/cli/simple_run.go b/internal/cli/simple_run.go index b0eebde..192cf3c 100644 --- a/internal/cli/simple_run.go +++ b/internal/cli/simple_run.go @@ -18,7 +18,7 @@ type SimpleRun struct { TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"` //nolint: lll 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 DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` //nolint: lll - DomainFrontingIP string `kong:"name='domain-fronting-ip',help='An IP address to use for domain fronting instead of resolving the hostname via DNS.'"` //nolint: lll + DomainFrontingIP string `kong:"name='domain-fronting-ip',help='An IP address to use for domain fronting instead of resolving the hostname via DNS.'"` //nolint: lll 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 Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` //nolint: lll Socks5Proxies []string `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"` //nolint: lll diff --git a/mtglib/internal/dc/init.go b/mtglib/internal/dc/init.go index 2c0f42c..fd8befa 100644 --- a/mtglib/internal/dc/init.go +++ b/mtglib/internal/dc/init.go @@ -38,43 +38,41 @@ type Updater interface { Run(ctx context.Context) } -var ( - // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 - defaultDCAddrSet = dcAddrSet{ - v4: map[int][]Addr{ - 1: { - {Network: "tcp4", Address: "149.154.175.50:443"}, - }, - 2: { - {Network: "tcp4", Address: "149.154.167.51:443"}, - {Network: "tcp4", Address: "95.161.76.100:443"}, - }, - 3: { - {Network: "tcp4", Address: "149.154.175.100:443"}, - }, - 4: { - {Network: "tcp4", Address: "149.154.167.91:443"}, - }, - 5: { - {Network: "tcp4", Address: "149.154.171.5:443"}, - }, +// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 +var defaultDCAddrSet = dcAddrSet{ + v4: map[int][]Addr{ + 1: { + {Network: "tcp4", Address: "149.154.175.50:443"}, }, - v6: map[int][]Addr{ - 1: { - {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"}, - }, - 2: { - {Network: "tcp6", Address: "[2001:67c:04e8:f002::a]:443"}, - }, - 3: { - {Network: "tcp6", Address: "[2001:b28:f23d:f003::a]:443"}, - }, - 4: { - {Network: "tcp6", Address: "[2001:67c:04e8:f004::a]:443"}, - }, - 5: { - {Network: "tcp6", Address: "[2001:b28:f23f:f005::a]:443"}, - }, + 2: { + {Network: "tcp4", Address: "149.154.167.51:443"}, + {Network: "tcp4", Address: "95.161.76.100:443"}, }, - } -) + 3: { + {Network: "tcp4", Address: "149.154.175.100:443"}, + }, + 4: { + {Network: "tcp4", Address: "149.154.167.91:443"}, + }, + 5: { + {Network: "tcp4", Address: "149.154.171.5:443"}, + }, + }, + v6: map[int][]Addr{ + 1: { + {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"}, + }, + 2: { + {Network: "tcp6", Address: "[2001:67c:04e8:f002::a]:443"}, + }, + 3: { + {Network: "tcp6", Address: "[2001:b28:f23d:f003::a]:443"}, + }, + 4: { + {Network: "tcp6", Address: "[2001:67c:04e8:f004::a]:443"}, + }, + 5: { + {Network: "tcp6", Address: "[2001:b28:f23f:f005::a]:443"}, + }, + }, +} diff --git a/mtglib/internal/dc/public_config_updater.go b/mtglib/internal/dc/public_config_updater.go index 8f22832..a02b87c 100644 --- a/mtglib/internal/dc/public_config_updater.go +++ b/mtglib/internal/dc/public_config_updater.go @@ -19,7 +19,7 @@ type PublicConfigUpdater struct { tg *Telegram } -func (p PublicConfigUpdater) Run(ctx context.Context, url, network string) { +func (p *PublicConfigUpdater) Run(ctx context.Context, url, network string) { p.run(ctx, func() error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -81,8 +81,8 @@ func (p PublicConfigUpdater) Run(ctx context.Context, url, network string) { }) } -func NewPublicConfigUpdater(tg *Telegram, logger Logger, client *http.Client) PublicConfigUpdater { - return PublicConfigUpdater{ +func NewPublicConfigUpdater(tg *Telegram, logger Logger, client *http.Client) *PublicConfigUpdater { + return &PublicConfigUpdater{ updater: updater{ logger: logger, period: PublicConfigUpdateEach, diff --git a/mtglib/internal/dc/public_config_updater_test.go b/mtglib/internal/dc/public_config_updater_test.go index f51f3a2..bc1d148 100644 --- a/mtglib/internal/dc/public_config_updater_test.go +++ b/mtglib/internal/dc/public_config_updater_test.go @@ -14,7 +14,7 @@ import ( type PublicConfigUpdaterTestSuite struct { UpdaterTestSuiteBase - u PublicConfigUpdater + u *PublicConfigUpdater lock sync.Mutex srv *httptest.Server responseHandler func(w http.ResponseWriter) @@ -42,39 +42,27 @@ func (s *PublicConfigUpdaterTestSuite) SetupTest() { } func (s *PublicConfigUpdaterTestSuite) Test502StatusCode() { - done := false - s.responseHandler = func(w http.ResponseWriter) { w.WriteHeader(http.StatusBadGateway) - done = true } - go s.u.Run(s.ctx, s.srv.URL, "tcp4") + s.u.Run(s.ctx, s.srv.URL, "tcp4") - s.Eventually(func() bool { - s.lock.Lock() - defer s.lock.Unlock() - - return done - }, time.Second, 10*time.Millisecond) + time.Sleep(100 * time.Millisecond) + s.ctxCancel() + s.u.Wait() s.Len(s.u.tg.view.publicConfigs.v4, 0) } func (s *PublicConfigUpdaterTestSuite) TestEmptyFile() { - done := false - s.responseHandler = func(w http.ResponseWriter) { - done = true w.WriteHeader(http.StatusOK) } - go s.u.Run(s.ctx, s.srv.URL, "tcp4") + s.u.Run(s.ctx, s.srv.URL, "tcp4") - s.Eventually(func() bool { - s.lock.Lock() - defer s.lock.Unlock() - - return done - }, time.Second, 10*time.Millisecond) + time.Sleep(100 * time.Millisecond) + s.ctxCancel() + s.u.Wait() s.Len(s.u.tg.view.publicConfigs.v4, 0) } @@ -85,21 +73,16 @@ proxy_for -1 -1; proxy_for 100 100.10.0.0:3333; lala 0 0 ` - done := false s.responseHandler = func(w http.ResponseWriter) { - done = true w.WriteHeader(http.StatusOK) w.Write([]byte(result)) //nolint: errcheck } - go s.u.Run(s.ctx, s.srv.URL, "tcp4") + s.u.Run(s.ctx, s.srv.URL, "tcp4") - s.Eventually(func() bool { - s.lock.Lock() - defer s.lock.Unlock() - - return done - }, time.Second, 10*time.Millisecond) + time.Sleep(100 * time.Millisecond) + s.ctxCancel() + s.u.Wait() s.Len(s.u.tg.view.publicConfigs.v4, 0) } @@ -109,21 +92,16 @@ func (s *PublicConfigUpdaterTestSuite) TestOk() { proxy_for 203 100.10.0.0:3333; proxy_for -100 101.10.0.0:3333; ` - done := false s.responseHandler = func(w http.ResponseWriter) { - done = true w.WriteHeader(http.StatusOK) w.Write([]byte(result)) //nolint: errcheck } - go s.u.Run(s.ctx, s.srv.URL, "tcp4") + s.u.Run(s.ctx, s.srv.URL, "tcp4") - s.Eventually(func() bool { - s.lock.Lock() - defer s.lock.Unlock() - - return done - }, time.Second, 10*time.Millisecond) + time.Sleep(100 * time.Millisecond) + s.ctxCancel() + s.u.Wait() s.Len(s.u.tg.view.publicConfigs.v4, 1) s.Len(s.u.tg.view.publicConfigs.v4[203], 1) diff --git a/mtglib/internal/dc/updater.go b/mtglib/internal/dc/updater.go index 5d57af4..72adf38 100644 --- a/mtglib/internal/dc/updater.go +++ b/mtglib/internal/dc/updater.go @@ -2,38 +2,46 @@ package dc import ( "context" + "sync" "time" ) type updater struct { + wg sync.WaitGroup logger Logger period time.Duration } -func (u updater) run(ctx context.Context, callback func() error) { - ticker := time.NewTicker(u.period) +func (u *updater) Wait() { + u.wg.Wait() +} - defer func() { - ticker.Stop() +func (u *updater) run(ctx context.Context, callback func() error) { + u.wg.Go(func() { + ticker := time.NewTicker(u.period) - select { - case <-ticker.C: - default: - } - }() + defer func() { + ticker.Stop() + + select { + case <-ticker.C: + default: + } + }() - for { - u.logger.Info("start update") - if err := callback(); err != nil { - u.logger.WarningError("cannot update: %w", err) - } - u.logger.Info("updated") + for { + u.logger.Info("start update") + if err := callback(); err != nil { + u.logger.WarningError("cannot update: %w", err) + } + u.logger.Info("updated") - select { - case <-ctx.Done(): - u.logger.Info("stop updating") - return - case <-ticker.C: + select { + case <-ctx.Done(): + u.logger.Info("stop updating") + return + case <-ticker.C: + } } - } + }) } diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 567b88e..468fa10 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -30,6 +30,7 @@ type Proxy struct { domainFrontingIP string workerPool *ants.PoolWithFunc telegram *dc.Telegram + configUpdater *dc.PublicConfigUpdater clientObfuscatror obfuscation.Obfuscator secret Secret @@ -152,6 +153,7 @@ func (p *Proxy) Shutdown() { p.ctxCancel() p.streamWaitGroup.Wait() p.workerPool.Release() + p.configUpdater.Wait() p.allowlist.Shutdown() p.blocklist.Shutdown() @@ -328,18 +330,18 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { tolerateTimeSkewness: opts.getTolerateTimeSkewness(), allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, telegram: tg, + configUpdater: dc.NewPublicConfigUpdater( + tg, + updatersLogger.Named("public-config"), + opts.Network.MakeHTTPClient(nil), + ), clientObfuscatror: obfuscation.Obfuscator{ Secret: opts.Secret.Key[:], }, } - publicConfigUpdater := dc.NewPublicConfigUpdater( - tg, - updatersLogger.Named("public-config"), - opts.Network.MakeHTTPClient(nil), - ) - go publicConfigUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4") - go publicConfigUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6") + proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4") + proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6") pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), func(arg any) { From 5fe3fdd73cc5133cb25a1b3371c5f1ddebf9453b Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 14:21:16 +0100 Subject: [PATCH 13/18] Check for vulnerabilities --- .github/workflows/govulncheck.yml | 38 +++++++++++++++++++++++++++++++ .mise.toml | 5 ++++ mise.lock | 4 ++++ 3 files changed, 47 insertions(+) create mode 100644 .github/workflows/govulncheck.yml diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml new file mode 100644 index 0000000..a0f91cd --- /dev/null +++ b/.github/workflows/govulncheck.yml @@ -0,0 +1,38 @@ +--- + +name: Vulnerability checks + +permissions: + actions: read + checks: read + contents: read + deployments: read + issues: read + discussions: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read + +on: + push: + pull_request: + schedule: # daily at 10:22 UTC + - cron: '22 10 * * *' + workflow_dispatch: + +jobs: + vuln: + name: Test vulnerabilities + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: jdx/mise-action@v3 + name: Install mise + + - name: Run tests + run: mise tasks run vuln diff --git a/.mise.toml b/.mise.toml index 06b9ea5..7d6978f 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,6 +1,7 @@ [tools] "go:golang.org/x/pkgsite/cmd/pkgsite" = "latest" "go:golang.org/x/tools/gopls" = "latest" +"go:golang.org/x/vuln/cmd/govulncheck" = "latest" "go:mvdan.cc/gofumpt" = "latest" go = "latest" golangci-lint = "latest" @@ -26,6 +27,10 @@ run = [ description = "Run linter" run = "golangci-lint run" +[tasks.vuln] +description = "Test for vulnerabilities" +run = "govulncheck ./..." + [tasks.test] description = "Run tests" run = "go test -v ./..." diff --git a/mise.lock b/mise.lock index d6b2d74..ac22b61 100644 --- a/mise.lock +++ b/mise.lock @@ -15,6 +15,10 @@ backend = "go:golang.org/x/pkgsite/cmd/pkgsite" version = "0.21.1" backend = "go:golang.org/x/tools/gopls" +[[tools."go:golang.org/x/vuln/cmd/govulncheck"]] +version = "1.1.4" +backend = "go:golang.org/x/vuln/cmd/govulncheck" + [[tools."go:mvdan.cc/gofumpt"]] version = "0.9.2" backend = "go:mvdan.cc/gofumpt" From bb320e9d897a5f038fc022aac7ea139560f50502 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 14:02:51 +0100 Subject: [PATCH 14/18] Update fuzz tests --- .mise.toml | 16 ++++------------ .../obfuscation/handshake_frame_fuzz_test.go | 7 +++++++ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.mise.toml b/.mise.toml index 06b9ea5..91c3d4f 100644 --- a/.mise.toml +++ b/.mise.toml @@ -47,19 +47,11 @@ run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/f [tasks."test:fuzz:client-handshake"] description = "Run fuzzy test for ClientHandshake" -run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHandshake ./mtglib/internal/obfuscated2" +run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientServerHandshake ./mtglib/internal/obfuscation" -[tasks."test:fuzz:server-generate-handshake-frame"] -description = "Run fuzzy test for ServerGenerateHandshakeFrame" -run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerGenerateHandshakeFrame ./mtglib/internal/obfuscated2" - -[tasks."test:fuzz:server-receive"] -description = "Run fuzzy test for ServerReceive" -run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerReceive ./mtglib/internal/obfuscated2" - -[tasks."test:fuzz:server-send"] -description = "Run fuzzy test for ServerSend" -run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerSend ./mtglib/internal/obfuscated2" +[tasks."test:fuzz:server-handshake-frame"] +description = "Run fuzzy test for GenerateHandshakeFrame" +run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzGenerateHandshakeFrame ./mtglib/internal/obfuscation" [tasks.static] description = "Build static binary" diff --git a/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go b/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go index 71965ec..3806e15 100644 --- a/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go +++ b/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go @@ -28,6 +28,13 @@ func FuzzGenerateHandshakeFrame(f *testing.F) { frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7]) assert.Equal(t, hfConnectionType[:], frame.connectionType()) + + if arg < 0 { + arg = -arg + } else if arg == 0 { + arg = defaultDC + } + assert.EqualValues(t, arg, frame.dc()) }) } From cde313b359a9063883c181c51f61705f0c96e7de Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 16:36:27 +0100 Subject: [PATCH 15/18] Add support for domain fronting proxy protocol --- example.config.toml | 4 ++ internal/cli/run_proxy.go | 9 ++-- internal/config/config.go | 23 +++++---- internal/config/parse.go | 23 +++++---- mtglib/conns.go | 36 +++++++++++++ mtglib/conns_internal_test.go | 96 +++++++++++++++++++++++++++++++++++ mtglib/proxy.go | 44 +++++++++------- mtglib/proxy_opts.go | 8 +++ 8 files changed, 198 insertions(+), 45 deletions(-) diff --git a/example.config.toml b/example.config.toml index ce95f95..5d3c5c2 100644 --- a/example.config.toml +++ b/example.config.toml @@ -67,6 +67,10 @@ domain-fronting-port = 443 # default value is not set (DNS resolution is used). # domain-fronting-ip = "142.250.185.112" +# This makes a communication between both fronting website and mtg to use +# proxy protocol. +domain-fronting-proxy-protocol = false + # FakeTLS can compare timestamps to prevent probes. Each message has # encrypted timestamp. So, mtg can compare this timestamp and decide if # we need to proceed with connection or not. diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 056b1b6..f7e9c6e 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -250,10 +250,11 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen IPAllowlist: allowlist, EventStream: eventStream, - Secret: conf.Secret, - DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort), - DomainFrontingIP: conf.DomainFrontingIP.String(), - PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), + Secret: conf.Secret, + DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort), + DomainFrontingIP: conf.DomainFrontingIP.String(), + DomainFrontingProxyProtocol: conf.DomainFrontingProxyProtocol.Get(false), + PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, diff --git a/internal/config/config.go b/internal/config/config.go index ea4cbb6..5c7890e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,17 +21,18 @@ type ListConfig struct { } type Config struct { - Debug TypeBool `json:"debug"` - AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"` - Secret mtglib.Secret `json:"secret"` - BindTo TypeHostPort `json:"bindTo"` - ProxyProtocolListener TypeBool `json:"proxyProtocolListener"` - PreferIP TypePreferIP `json:"preferIp"` - DomainFrontingPort TypePort `json:"domainFrontingPort"` - DomainFrontingIP TypeIP `json:"domainFrontingIp"` - TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` - Concurrency TypeConcurrency `json:"concurrency"` - Defense struct { + Debug TypeBool `json:"debug"` + AllowFallbackOnUnknownDC TypeBool `json:"allowFallbackOnUnknownDc"` + Secret mtglib.Secret `json:"secret"` + BindTo TypeHostPort `json:"bindTo"` + ProxyProtocolListener TypeBool `json:"proxyProtocolListener"` + PreferIP TypePreferIP `json:"preferIp"` + DomainFrontingPort TypePort `json:"domainFrontingPort"` + DomainFrontingIP TypeIP `json:"domainFrontingIp"` + DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"` + TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` + Concurrency TypeConcurrency `json:"concurrency"` + Defense struct { AntiReplay struct { Optional diff --git a/internal/config/parse.go b/internal/config/parse.go index 9461c07..c72730a 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -9,17 +9,18 @@ import ( ) type tomlConfig struct { - Debug bool `toml:"debug" json:"debug,omitempty"` - AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"` - Secret string `toml:"secret" json:"secret"` - BindTo string `toml:"bind-to" json:"bindTo"` - ProxyProtocolListener bool `toml:"proxy-protocol-listener" json:"proxyProtocolListener"` - PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"` - DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"` - DomainFrontingIP string `toml:"domain-fronting-ip" json:"domainFrontingIp,omitempty"` - TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` - Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` - Defense struct { + Debug bool `toml:"debug" json:"debug,omitempty"` + AllowFallbackOnUnknownDC bool `toml:"allow-fallback-on-unknown-dc" json:"allowFallbackOnUnknownDc,omitempty"` + Secret string `toml:"secret" json:"secret"` + BindTo string `toml:"bind-to" json:"bindTo"` + ProxyProtocolListener bool `toml:"proxy-protocol-listener" json:"proxyProtocolListener"` + PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"` + DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"` + DomainFrontingIP string `toml:"domain-fronting-ip" json:"domainFrontingIp,omitempty"` + DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"` + TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` + Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` + Defense struct { AntiReplay struct { Enabled bool `toml:"enabled" json:"enabled,omitempty"` MaxSize string `toml:"max-size" json:"maxSize,omitempty"` diff --git a/mtglib/conns.go b/mtglib/conns.go index 1759ca1..8219b75 100644 --- a/mtglib/conns.go +++ b/mtglib/conns.go @@ -3,9 +3,12 @@ package mtglib import ( "bytes" "context" + "fmt" "io" + "net" "github.com/9seconds/mtg/v2/essentials" + "github.com/pires/go-proxyproto" ) type connTraffic struct { @@ -59,3 +62,36 @@ func newConnRewind(conn essentials.Conn) *connRewind { return rv } + +type connProxyProtocol struct { + essentials.Conn + + sourceAddr net.Addr + headersWritten bool +} + +func (c *connProxyProtocol) Write(p []byte) (int, error) { + if !c.headersWritten { + headers := proxyproto.HeaderProxyFromAddrs(2, c.sourceAddr, c.RemoteAddr()) + + toSend, err := headers.Format() + if err != nil { + panic(err) + } + + if _, err := c.Conn.Write(toSend); err != nil { + return 0, fmt.Errorf("cannot send proxy protocol header: %w", err) + } + + c.headersWritten = true + } + + return c.Conn.Write(p) +} + +func newConnProxyProtocol(source, target essentials.Conn) *connProxyProtocol { + return &connProxyProtocol{ + Conn: target, + sourceAddr: source.RemoteAddr(), + } +} diff --git a/mtglib/conns_internal_test.go b/mtglib/conns_internal_test.go index 2797d05..123258f 100644 --- a/mtglib/conns_internal_test.go +++ b/mtglib/conns_internal_test.go @@ -1,14 +1,17 @@ package mtglib import ( + "bufio" "bytes" "context" "errors" "io" + "net" "testing" "time" "github.com/9seconds/mtg/v2/internal/testlib" + "github.com/pires/go-proxyproto" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -200,6 +203,94 @@ func (suite *ConnRewindTestSuite) TestRead() { suite.Equal([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, data) } +type ConnProxyProtocolTestSuite struct { + suite.Suite + + sourceConnMock *testlib.EssentialsConnMock + targetConnMock *testlib.EssentialsConnMock + conn *connProxyProtocol +} + +func (suite *ConnProxyProtocolTestSuite) SetupTest() { + suite.sourceConnMock = &testlib.EssentialsConnMock{} + suite.targetConnMock = &testlib.EssentialsConnMock{} + + localAddr := &net.TCPAddr{ + IP: net.ParseIP("127.0.0.1").To4(), + } + remoteAddr := &net.TCPAddr{ + IP: net.ParseIP("127.0.0.2").To4(), + } + + suite.sourceConnMock. + On("RemoteAddr"). + Return(localAddr) + suite.targetConnMock. + On("RemoteAddr"). + Maybe(). + Return(remoteAddr) + + suite.conn = newConnProxyProtocol(suite.sourceConnMock, suite.targetConnMock) +} + +func (suite *ConnProxyProtocolTestSuite) TestRead() { + value := []byte{1, 2, 3, 4, 5} + toRead := make([]byte, len(value)) + + suite.targetConnMock. + On("Read", mock.AnythingOfType("[]uint8")). + Once(). + Return(len(toRead), nil). + Run(func(args mock.Arguments) { + arr := args.Get(0).([]byte) + copy(arr, value) + }) + + n, err := suite.conn.Read(toRead) + suite.Equal(len(value), n) + suite.NoError(err) + suite.Equal(value, toRead) +} + +func (suite *ConnProxyProtocolTestSuite) TestWrite() { + value := []byte{1, 2, 3, 4, 5} + buf := &bytes.Buffer{} + bufReader := bufio.NewReader(buf) + + suite.targetConnMock. + On("Write", mock.AnythingOfType("[]uint8")). + Return(28, nil). + Run(func(args mock.Arguments) { + arr := args.Get(0).([]byte) + buf.Write(arr) + }) + + _, err := suite.conn.Write(value) + suite.NoError(err) + + header, err := proxyproto.Read(bufReader) + suite.NoError(err) + + sourceAddr, destAddr, ok := header.TCPAddrs() + suite.True(ok) + suite.Equal(suite.sourceConnMock.RemoteAddr(), sourceAddr) + suite.Equal(suite.targetConnMock.RemoteAddr(), destAddr) + + read, _ := io.ReadAll(bufReader) + suite.Equal(value, read) + + _, err = suite.conn.Write(value) + suite.NoError(err) + + read, _ = io.ReadAll(bufReader) + suite.Equal(value, read) +} + +func (suite *ConnProxyProtocolTestSuite) TearDownTest() { + suite.sourceConnMock.AssertExpectations(suite.T()) + suite.targetConnMock.AssertExpectations(suite.T()) +} + func TestConnTraffic(t *testing.T) { t.Parallel() suite.Run(t, &ConnTrafficTestSuite{}) @@ -209,3 +300,8 @@ func TestConnRewind(t *testing.T) { t.Parallel() suite.Run(t, &ConnRewindTestSuite{}) } + +func TestConnProxyProtocol(t *testing.T) { + t.Parallel() + suite.Run(t, &ConnProxyProtocolTestSuite{}) +} diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 468fa10..750658d 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -24,12 +24,13 @@ type Proxy struct { ctxCancel context.CancelFunc streamWaitGroup sync.WaitGroup - allowFallbackOnUnknownDC bool - tolerateTimeSkewness time.Duration - domainFrontingPort int - domainFrontingIP string - workerPool *ants.PoolWithFunc - telegram *dc.Telegram + allowFallbackOnUnknownDC bool + tolerateTimeSkewness time.Duration + domainFrontingPort int + domainFrontingIP string + domainFrontingProxyProtocol bool + workerPool *ants.PoolWithFunc + telegram *dc.Telegram configUpdater *dc.PublicConfigUpdater clientObfuscatror obfuscation.Obfuscator @@ -285,6 +286,10 @@ func (p *Proxy) doDomainFronting(ctx *streamContext, conn *connRewind) { return } + if p.domainFrontingProxyProtocol { + frontConn = newConnProxyProtocol(ctx.clientConn, frontConn) + } + frontConn = connTraffic{ Conn: frontConn, ctx: ctx, @@ -316,20 +321,20 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { updatersLogger := logger.Named("telegram-updaters") proxy := &Proxy{ - ctx: ctx, - ctxCancel: cancel, - secret: opts.Secret, - network: opts.Network, - antiReplayCache: opts.AntiReplayCache, - blocklist: opts.IPBlocklist, - allowlist: opts.IPAllowlist, - eventStream: opts.EventStream, + ctx: ctx, + ctxCancel: cancel, + secret: opts.Secret, + network: opts.Network, + antiReplayCache: opts.AntiReplayCache, + blocklist: opts.IPBlocklist, + allowlist: opts.IPAllowlist, + eventStream: opts.EventStream, logger: logger, - domainFrontingPort: opts.getDomainFrontingPort(), - domainFrontingIP: opts.DomainFrontingIP, - tolerateTimeSkewness: opts.getTolerateTimeSkewness(), - allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, - telegram: tg, + domainFrontingPort: opts.getDomainFrontingPort(), + domainFrontingIP: opts.DomainFrontingIP, + tolerateTimeSkewness: opts.getTolerateTimeSkewness(), + allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, + telegram: tg, configUpdater: dc.NewPublicConfigUpdater( tg, updatersLogger.Named("public-config"), @@ -338,6 +343,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { clientObfuscatror: obfuscation.Obfuscator{ Secret: opts.Secret.Key[:], }, + domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol, } proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4") diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 3d84003..09f47f2 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -102,6 +102,14 @@ type ProxyOpts struct { // This is an optional setting. DomainFrontingIP string + // DomainFrontingProxyProtocol is used if communication between upstream + // endpoint and mtg supports proxy protocol. This is useful in case + // if mtg is also placed behind load balancer, and this will make + // fronting webserver to know about real IP addresses + // + // This is an optional setting. + DomainFrontingProxyProtocol bool + // AllowFallbackOnUnknownDC defines how proxy behaves if unknown DC was // requested. If this setting is set to false, then such connection will be // rejected. Otherwise, proxy will chose any DC. From af72b2a574aa91affff77c8fa18b79c025e0b6f7 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 16:56:42 +0100 Subject: [PATCH 16/18] Delete obsoleted setting --- example.config.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/example.config.toml b/example.config.toml index 5d3c5c2..4676021 100644 --- a/example.config.toml +++ b/example.config.toml @@ -36,13 +36,6 @@ bind-to = "0.0.0.0:3128" # All other incoming connections are going to be dropped. concurrency = 8192 -# A size of user-space buffer for TCP to use. Since we do 2 connections, -# then we have tcp-buffer * (4 + 2) per each connection: read/write for -# each connection + 2 copy buffers to pump the data between sockets. -# -# Deprecated: this setting is no longer makes any effect. -# tcp-buffer = "4kb" - # Sometimes you want to enforce mtg to use some types of # IP connectivity to Telegram. We have 4 modes: # - prefer-ipv6: From 1cb225f52c14104a835558ea33f1a8ee8c25ae2f Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 17:40:27 +0100 Subject: [PATCH 17/18] Introduce [domain-fronting] config --- example.config.toml | 36 +++++++++++++++++++++++++++++++++--- internal/cli/run_proxy.go | 6 +++--- internal/config/config.go | 29 ++++++++++++++++++++++++++++- internal/config/parse.go | 7 ++++++- mtglib/proxy.go | 30 +++++++++++++++--------------- 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/example.config.toml b/example.config.toml index 4676021..01ab257 100644 --- a/example.config.toml +++ b/example.config.toml @@ -50,7 +50,10 @@ prefer-ip = "prefer-ipv6" # FakeTLS uses domain fronting protection. So it needs to know a port to # access. -domain-fronting-port = 443 +# +# Deprecated: use [domain-fronting] configuration block. If relevant option +# is defined there, this one would be ignored. +# domain-fronting-port = 443 # By default, mtg resolves the fronting hostname (from the secret) via DNS # to establish a TCP connection. If DNS resolution of that hostname is blocked, @@ -58,11 +61,17 @@ domain-fronting-port = 443 # used for SNI in the TLS handshake. # # default value is not set (DNS resolution is used). -# domain-fronting-ip = "142.250.185.112" +# +# Deprecated: use [domain-fronting] configuration block. If relevant option +# is defined there, this one would be ignored. +# domain-fronting-ip = "10.0.0.10" # This makes a communication between both fronting website and mtg to use # proxy protocol. -domain-fronting-proxy-protocol = false +# +# Deprecated: use [domain-fronting] configuration block. If relevant option +# is defined there, this one would be ignored. +# domain-fronting-proxy-protocol = false # FakeTLS can compare timestamps to prevent probes. Each message has # encrypted timestamp. So, mtg can compare this timestamp and decide if @@ -85,6 +94,27 @@ tolerate-time-skewness = "5s" # Otherwise, chose a new DC. allow-fallback-on-unknown-dc = false +# This section is relevant to communication with fronting domain. Usually +# you do not need to setup anything here but there are plenty of cases, especially +# if you put mtg behind load balancer, when some specific configuration is +# required. +[domain-fronting] +# By default, mtg resolves the fronting hostname (from the secret) via DNS +# to establish a TCP connection. If DNS resolution of that hostname is blocked, +# you can specify an IP address to connect to directly. The hostname is still +# used for SNI in the TLS handshake. +# +# default value is not set (DNS resolution is used). +# ip = "10.10.10.11" + +# FakeTLS uses domain fronting protection. So it needs to know a port to +# access. Default value is 443 +# port = 443 + +# This makes a communication between both fronting website and mtg to use +# proxy protocol. +# proxy-protocol = false + # network defines different network-related settings [network] # please be aware that mtg needs to do some external requests. For diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index f7e9c6e..307d154 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -251,9 +251,9 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen EventStream: eventStream, Secret: conf.Secret, - DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort), - DomainFrontingIP: conf.DomainFrontingIP.String(), - DomainFrontingProxyProtocol: conf.DomainFrontingProxyProtocol.Get(false), + DomainFrontingPort: conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort), + DomainFrontingIP: conf.GetDomainFrontingIP(nil), + DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false), PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), diff --git a/internal/config/config.go b/internal/config/config.go index 5c7890e..30c0c20 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "net" "github.com/9seconds/mtg/v2/mtglib" ) @@ -32,7 +33,12 @@ type Config struct { DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"` TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` Concurrency TypeConcurrency `json:"concurrency"` - Defense struct { + DomainFronting struct { + IP TypeIP `json:"ip"` + Port TypePort `json:"port"` + ProxyProtocol TypeBool `json:"proxyProtocol"` + } `json:"domainFronting"` + Defense struct { AntiReplay struct { Optional @@ -69,6 +75,27 @@ type Config struct { } `json:"stats"` } +func (c *Config) GetDomainFrontingPort(defaultValue uint) uint { + if port := c.DomainFronting.Port.Get(0); port != 0 { + return port + } + return c.DomainFrontingPort.Get(defaultValue) +} + +func (c *Config) GetDomainFrontingIP(defaultValue net.IP) string { + if ip := c.DomainFronting.IP.Get(nil); ip != nil { + return ip.String() + } + if ip := c.DomainFrontingIP.Get(defaultValue); ip != nil { + return ip.String() + } + return "" +} + +func (c *Config) GetDomainFrontingProxyProtocol(defaultValue bool) bool { + return c.DomainFronting.ProxyProtocol.Get(false) || c.DomainFrontingProxyProtocol.Get(defaultValue) +} + func (c *Config) Validate() error { if !c.Secret.Valid() { return fmt.Errorf("invalid secret %s", c.Secret.String()) diff --git a/internal/config/parse.go b/internal/config/parse.go index c72730a..e58bc14 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -20,7 +20,12 @@ type tomlConfig struct { DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"` TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` - Defense struct { + DomainFronting struct { + IP string `toml:"ip" json:"ip,omitempty"` + Port uint `toml:"port" json:"port,omitempty"` + ProxyProtocol bool `toml:"proxy-protocol" json:"proxyProtocol,omitempty"` + } `toml:"domain-fronting" json:"domainFronting,omitempty"` + Defense struct { AntiReplay struct { Enabled bool `toml:"enabled" json:"enabled,omitempty"` MaxSize string `toml:"max-size" json:"maxSize,omitempty"` diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 750658d..a0e4965 100644 --- a/mtglib/proxy.go +++ b/mtglib/proxy.go @@ -31,8 +31,8 @@ type Proxy struct { domainFrontingProxyProtocol bool workerPool *ants.PoolWithFunc telegram *dc.Telegram - configUpdater *dc.PublicConfigUpdater - clientObfuscatror obfuscation.Obfuscator + configUpdater *dc.PublicConfigUpdater + clientObfuscatror obfuscation.Obfuscator secret Secret network Network @@ -321,20 +321,20 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { updatersLogger := logger.Named("telegram-updaters") proxy := &Proxy{ - ctx: ctx, - ctxCancel: cancel, - secret: opts.Secret, - network: opts.Network, - antiReplayCache: opts.AntiReplayCache, - blocklist: opts.IPBlocklist, - allowlist: opts.IPAllowlist, - eventStream: opts.EventStream, + ctx: ctx, + ctxCancel: cancel, + secret: opts.Secret, + network: opts.Network, + antiReplayCache: opts.AntiReplayCache, + blocklist: opts.IPBlocklist, + allowlist: opts.IPAllowlist, + eventStream: opts.EventStream, logger: logger, - domainFrontingPort: opts.getDomainFrontingPort(), - domainFrontingIP: opts.DomainFrontingIP, - tolerateTimeSkewness: opts.getTolerateTimeSkewness(), - allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, - telegram: tg, + domainFrontingPort: opts.getDomainFrontingPort(), + domainFrontingIP: opts.DomainFrontingIP, + tolerateTimeSkewness: opts.getTolerateTimeSkewness(), + allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, + telegram: tg, configUpdater: dc.NewPublicConfigUpdater( tg, updatersLogger.Named("public-config"), From 42f612f49ece5a6943f4a1f122de393fd1045594 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Tue, 24 Feb 2026 18:08:45 +0100 Subject: [PATCH 18/18] Use go tag for 1.26 --- .mise.toml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mise.toml b/.mise.toml index 23fc392..f95845b 100644 --- a/.mise.toml +++ b/.mise.toml @@ -20,7 +20,7 @@ run = "go build" description = "Update dependencies" run = [ "go get -u", - "go mod tidy -go=1.25" + "go mod tidy -go=1.26" ] [tasks.lint] diff --git a/go.mod b/go.mod index 5520d48..e75cb4c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/9seconds/mtg/v2 -go 1.25 +go 1.26 require ( github.com/OneOfOne/xxhash v1.2.8