Merge pull request #336 from 9seconds/obfuscated2

Fetch DC203 from Telegram
This commit is contained in:
Sergei Arkhipov
2026-02-24 16:41:06 +01:00
committed by GitHub
48 changed files with 1147 additions and 996 deletions
+4 -12
View File
@@ -52,19 +52,11 @@ run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/f
[tasks."test:fuzz:client-handshake"] [tasks."test:fuzz:client-handshake"]
description = "Run fuzzy test for ClientHandshake" 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"] [tasks."test:fuzz:server-handshake-frame"]
description = "Run fuzzy test for ServerGenerateHandshakeFrame" description = "Run fuzzy test for GenerateHandshakeFrame"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzServerGenerateHandshakeFrame ./mtglib/internal/obfuscated2" run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzGenerateHandshakeFrame ./mtglib/internal/obfuscation"
[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.static] [tasks.static]
description = "Build static binary" description = "Build static binary"
-9
View File
@@ -88,15 +88,6 @@ tolerate-time-skewness = "5s"
# Otherwise, chose a new DC. # Otherwise, chose a new DC.
allow-fallback-on-unknown-dc = false 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 defines different network-related settings
[network] [network]
# please be aware that mtg needs to do some external requests. For # please be aware that mtg needs to do some external requests. For
-9
View File
@@ -242,14 +242,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
return fmt.Errorf("cannot build ip allowlist: %w", err) 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{ opts := mtglib.ProxyOpts{
Logger: logger, Logger: logger,
Network: ntw, Network: ntw,
@@ -265,7 +257,6 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
DCOverrides: dcOverrides,
} }
proxy, err := mtglib.NewProxy(opts) proxy, err := mtglib.NewProxy(opts)
-4
View File
@@ -66,10 +66,6 @@ type Config struct {
MetricPrefix TypeMetricPrefix `json:"metricPrefix"` MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
} `json:"prometheus"` } `json:"prometheus"`
} `json:"stats"` } `json:"stats"`
DCOverrides []struct {
DC TypeDC `json:"dc"`
IPs []TypeHostPort `json:"ips"`
} `json:"dcOverrides"`
} }
func (c *Config) Validate() error { func (c *Config) Validate() error {
-4
View File
@@ -61,10 +61,6 @@ type tomlConfig struct {
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
} `toml:"prometheus" json:"prometheus,omitempty"` } `toml:"prometheus" json:"prometheus,omitempty"`
} `toml:"stats" json:"stats,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) { func Parse(rawData []byte) (*Config, error) {
+2 -10
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"io" "io"
"sync"
"github.com/9seconds/mtg/v2/essentials" "github.com/9seconds/mtg/v2/essentials"
) )
@@ -40,22 +39,15 @@ func (c connTraffic) Write(b []byte) (int, error) {
type connRewind struct { type connRewind struct {
essentials.Conn essentials.Conn
active io.Reader
buf bytes.Buffer buf bytes.Buffer
mutex sync.RWMutex active io.Reader
} }
func (c *connRewind) Read(p []byte) (int, error) { func (c *connRewind) Read(p []byte) (int, error) {
c.mutex.RLock() return c.active.Read(p)
defer c.mutex.RUnlock()
return c.active.Read(p) //nolint: wrapcheck
} }
func (c *connRewind) Rewind() { func (c *connRewind) Rewind() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.active = io.MultiReader(&c.buf, c.Conn) c.active = io.MultiReader(&c.buf, c.Conn)
} }
+8 -1
View File
@@ -1,10 +1,17 @@
package dc package dc
import (
"fmt"
"github.com/9seconds/mtg/v2/mtglib/internal/obfuscation"
)
type Addr struct { type Addr struct {
Network string Network string
Address string Address string
Obfuscator obfuscation.Obfuscator
} }
func (d Addr) String() string { func (d Addr) String() string {
return d.Address return fmt.Sprintf("addr=%s, secret=%v", d.Address, d.Obfuscator.Secret)
} }
-16
View File
@@ -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())
}
+21 -16
View File
@@ -1,5 +1,10 @@
package dc package dc
import (
"context"
"time"
)
type preferIP uint8 type preferIP uint8
const ( const (
@@ -10,7 +15,18 @@ const (
) )
const ( const (
// Default DC to connect to if not sure.
DefaultDC = 2 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 { type Logger interface {
@@ -18,9 +34,12 @@ type Logger interface {
WarningError(msg string, err error) WarningError(msg string, err error)
} }
var ( type Updater interface {
Run(ctx context.Context)
}
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
defaultDCAddrSet = dcAddrSet{ var defaultDCAddrSet = dcAddrSet{
v4: map[int][]Addr{ v4: map[int][]Addr{
1: { 1: {
{Network: "tcp4", Address: "149.154.175.50:443"}, {Network: "tcp4", Address: "149.154.175.50:443"},
@@ -57,17 +76,3 @@ 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"},
},
},
}
)
+43
View File
@@ -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()
}
@@ -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) //nolint: errcheck
resp.Body.Close() //nolint: errcheck
}
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,
}
}
@@ -0,0 +1,113 @@
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() {
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusBadGateway)
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
s.Len(s.u.tg.view.publicConfigs.v4, 0)
}
func (s *PublicConfigUpdaterTestSuite) TestEmptyFile() {
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
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
`
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(result)) //nolint: errcheck
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
time.Sleep(100 * time.Millisecond)
s.ctxCancel()
s.u.Wait()
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;
`
s.responseHandler = func(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(result)) //nolint: errcheck
}
s.u.Run(s.ctx, s.srv.URL, "tcp4")
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)
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{})
}
+6 -35
View File
@@ -2,16 +2,20 @@ package dc
import ( import (
"fmt" "fmt"
"net"
"strings" "strings"
"sync"
) )
type Telegram struct { type Telegram struct {
lock sync.RWMutex
view dcView view dcView
preferIP preferIP preferIP preferIP
} }
func (t *Telegram) GetAddresses(dc int) []Addr { func (t *Telegram) GetAddresses(dc int) []Addr {
t.lock.RLock()
defer t.lock.RUnlock()
switch t.preferIP { switch t.preferIP {
case preferIPOnlyIPv4: case preferIPOnlyIPv4:
return t.view.getV4(dc) return t.view.getV4(dc)
@@ -24,7 +28,7 @@ func (t *Telegram) GetAddresses(dc int) []Addr {
return append(t.view.getV6(dc), t.view.getV4(dc)...) 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 var pref preferIP
switch strings.ToLower(ipPreference) { switch strings.ToLower(ipPreference) {
@@ -40,40 +44,7 @@ func New(ipPreference string, userOverrides map[int][]string) (*Telegram, error)
return nil, fmt.Errorf("unknown ip preference %s", ipPreference) 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{ return &Telegram{
view: dcView{
overrides: overrides,
},
preferIP: pref, preferIP: pref,
}, nil }, nil
} }
+47
View File
@@ -0,0 +1,47 @@
package dc
import (
"context"
"sync"
"time"
)
type updater struct {
wg sync.WaitGroup
logger Logger
period time.Duration
}
func (u *updater) Wait() {
u.wg.Wait()
}
func (u *updater) run(ctx context.Context, callback func() error) {
u.wg.Go(func() {
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:
}
}
})
}
+55
View File
@@ -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{})
}
+3 -5
View File
@@ -1,20 +1,18 @@
package dc package dc
type dcView struct { type dcView struct {
overrides dcAddrSet publicConfigs dcAddrSet
} }
func (d dcView) getV4(dc int) []Addr { func (d dcView) getV4(dc int) []Addr {
addrs := d.overrides.getV4(dc) addrs := d.publicConfigs.getV4(dc)
addrs = append(addrs, defaultDCOverridesAddrSet.getV4(dc)...)
addrs = append(addrs, defaultDCAddrSet.getV4(dc)...) addrs = append(addrs, defaultDCAddrSet.getV4(dc)...)
return addrs return addrs
} }
func (d dcView) getV6(dc int) []Addr { func (d dcView) getV6(dc int) []Addr {
addrs := d.overrides.getV6(dc) addrs := d.publicConfigs.getV6(dc)
addrs = append(addrs, defaultDCOverridesAddrSet.getV6(dc)...)
addrs = append(addrs, defaultDCAddrSet.getV6(dc)...) addrs = append(addrs, defaultDCAddrSet.getV6(dc)...)
return addrs return addrs
+7 -9
View File
@@ -16,7 +16,7 @@ type ViewTestSuite struct {
func (suite *ViewTestSuite) SetupSuite() { func (suite *ViewTestSuite) SetupSuite() {
suite.view = dcView{ suite.view = dcView{
overrides: dcAddrSet{ publicConfigs: dcAddrSet{
v4: map[int][]Addr{ v4: map[int][]Addr{
111: { 111: {
{Network: "tcp4", Address: "127.0.0.1:443"}, {Network: "tcp4", Address: "127.0.0.1:443"},
@@ -37,15 +37,14 @@ func (suite *ViewTestSuite) SetupSuite() {
func (suite *ViewTestSuite) TestGetV4() { func (suite *ViewTestSuite) TestGetV4() {
testData := map[int][]Addr{ testData := map[int][]Addr{
111: { 111: {
{"tcp4", "127.0.0.1:443"}, {Network: "tcp4", Address: "127.0.0.1:443"},
}, },
203: { 203: {
{"tcp4", "127.0.0.2:443"}, {Network: "tcp4", Address: "127.0.0.2:443"},
{"tcp4", "91.105.192.100:443"},
}, },
2: { 2: {
{"tcp4", "149.154.167.51:443"}, {Network: "tcp4", Address: "149.154.167.51:443"},
{"tcp4", "95.161.76.100:443"}, {Network: "tcp4", Address: "95.161.76.100:443"},
}, },
} }
@@ -60,11 +59,10 @@ func (suite *ViewTestSuite) TestGetV6() {
testData := map[int][]Addr{ testData := map[int][]Addr{
111: {}, 111: {},
203: { 203: {
{"tcp6", "xxx"}, {Network: "tcp6", Address: "xxx"},
{"tcp6", "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"},
}, },
1: { 1: {
{"tcp6", "[2001:b28:f23d:f001::a]:443"}, {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"},
}, },
} }
+9 -10
View File
@@ -47,10 +47,7 @@ func (c *Conn) Write(p []byte) (int, error) {
rec.Type = record.TypeApplicationData rec.Type = record.TypeApplicationData
rec.Version = record.Version12 rec.Version = record.Version12
sendBuffer := acquireBytesBuffer() written := 0
defer releaseBytesBuffer(sendBuffer)
lenP := len(p)
for len(p) > 0 { for len(p) > 0 {
chunkSize := rand.IntN(record.TLSMaxRecordSize) chunkSize := rand.IntN(record.TLSMaxRecordSize)
@@ -60,14 +57,16 @@ func (c *Conn) Write(p []byte) (int, error) {
rec.Payload.Reset() rec.Payload.Reset()
rec.Payload.Write(p[:chunkSize]) 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:] p = p[chunkSize:]
} }
if _, err := c.Conn.Write(sendBuffer.Bytes()); err != nil { return written, nil
return 0, err //nolint: wrapcheck
}
return lenP, nil
} }
-21
View File
@@ -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)
}
+3 -4
View File
@@ -1,6 +1,7 @@
package faketls package faketls
import ( import (
"bytes"
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
@@ -13,8 +14,7 @@ import (
) )
func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) error { func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello) error {
buf := acquireBytesBuffer() buf := &bytes.Buffer{}
defer releaseBytesBuffer(buf)
rec := record.AcquireRecord() rec := record.AcquireRecord()
defer record.ReleaseRecord(rec) defer record.ReleaseRecord(rec)
@@ -58,8 +58,7 @@ func SendWelcomePacket(writer io.Writer, secret []byte, clientHello ClientHello)
} }
func generateServerHello(writer io.Writer, clientHello ClientHello) { func generateServerHello(writer io.Writer, clientHello ClientHello) {
bodyBuf := acquireBytesBuffer() bodyBuf := &bytes.Buffer{}
defer releaseBytesBuffer(bodyBuf)
sliceBuf := [2]byte{} sliceBuf := [2]byte{}
digest := [RandomLen]byte{} digest := [RandomLen]byte{}
@@ -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
}
@@ -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())
})
}
@@ -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{})
}
-37
View File
@@ -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
}
@@ -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
}
@@ -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{})
}
-137
View File
@@ -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,
}
}
-39
View File
@@ -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)
}
@@ -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
}
}
@@ -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)
})
}
@@ -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{})
}
-15
View File
@@ -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)
}
+34
View File
@@ -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)
}
+102
View File
@@ -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{})
}
@@ -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
}
}
@@ -1,4 +1,4 @@
package obfuscated2 package obfuscation
import ( import (
"encoding/binary" "encoding/binary"
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func FuzzServerGenerateHandshakeFrame(f *testing.F) { func FuzzGenerateHandshakeFrame(f *testing.F) {
f.Fuzz(func(t *testing.T, arg int) { f.Fuzz(func(t *testing.T, arg int16) {
frame := generateServerHanshakeFrame() frame := generateHandshake(int(arg))
assert.NotEqualValues(t, 0xef, frame.data[0]) assert.NotEqualValues(t, 0xef, frame.data[0])
@@ -18,13 +18,23 @@ func FuzzServerGenerateHandshakeFrame(f *testing.F) {
assert.NotEqualValues(t, 0x54534f50, firstBytes) assert.NotEqualValues(t, 0x54534f50, firstBytes)
assert.NotEqualValues(t, 0x20544547, firstBytes) assert.NotEqualValues(t, 0x20544547, firstBytes)
assert.NotEqualValues(t, 0x4954504f, firstBytes) assert.NotEqualValues(t, 0x4954504f, firstBytes)
assert.NotEqualValues(t, 0x02010316, firstBytes)
assert.NotEqualValues(t, 0xeeeeeeee, firstBytes) assert.NotEqualValues(t, 0xeeeeeeee, firstBytes)
assert.NotEqualValues(t, 0xdddddddd, firstBytes)
assert.NotEqualValues( assert.NotEqualValues(
t, t,
0, 0,
frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7]) frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7])
assert.Equal(t, handshakeConnectionType, frame.connectionType()) assert.Equal(t, hfConnectionType[:], frame.connectionType())
if arg < 0 {
arg = -arg
} else if arg == 0 {
arg = defaultDC
}
assert.EqualValues(t, arg, frame.dc())
}) })
} }
@@ -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{})
}
+79
View File
@@ -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
}
}
+87
View File
@@ -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())
}
@@ -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)
})
}
@@ -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.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{})
}
-19
View File
@@ -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)
}
+3 -4
View File
@@ -35,13 +35,12 @@ func Relay(ctx context.Context, log Logger, telegramConn, clientConn essentials.
} }
func pump(log Logger, src, dst essentials.Conn, direction string) { func pump(log Logger, src, dst essentials.Conn, direction string) {
var buf [copyBufferSize]byte
defer src.CloseRead() //nolint: errcheck defer src.CloseRead() //nolint: errcheck
defer dst.CloseWrite() //nolint: errcheck defer dst.CloseWrite() //nolint: errcheck
copyBuffer := acquireCopyBuffer() n, err := io.CopyBuffer(src, dst, buf[:])
defer releaseCopyBuffer(copyBuffer)
n, err := io.CopyBuffer(src, dst, *copyBuffer)
switch { switch {
case err == nil: case err == nil:
+37 -24
View File
@@ -13,7 +13,7 @@ import (
"github.com/9seconds/mtg/v2/mtglib/internal/dc" "github.com/9seconds/mtg/v2/mtglib/internal/dc"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls" "github.com/9seconds/mtg/v2/mtglib/internal/faketls"
"github.com/9seconds/mtg/v2/mtglib/internal/faketls/record" "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/9seconds/mtg/v2/mtglib/internal/relay"
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
) )
@@ -30,6 +30,8 @@ type Proxy struct {
domainFrontingIP string domainFrontingIP string
workerPool *ants.PoolWithFunc workerPool *ants.PoolWithFunc
telegram *dc.Telegram telegram *dc.Telegram
configUpdater *dc.PublicConfigUpdater
clientObfuscatror obfuscation.Obfuscator
secret Secret secret Secret
network Network network Network
@@ -77,8 +79,8 @@ func (p *Proxy) ServeConn(conn essentials.Conn) {
return return
} }
if err := p.doObfuscated2Handshake(ctx); err != nil { if err := p.doObfuscatedHandshake(ctx); err != nil {
p.logger.InfoError("obfuscated2 handshake is failed", err) p.logger.InfoError("obfuscated handshake is failed", err)
return return
} }
@@ -151,6 +153,7 @@ func (p *Proxy) Shutdown() {
p.ctxCancel() p.ctxCancel()
p.streamWaitGroup.Wait() p.streamWaitGroup.Wait()
p.workerPool.Release() p.workerPool.Release()
p.configUpdater.Wait()
p.allowlist.Shutdown() p.allowlist.Shutdown()
p.blocklist.Shutdown() p.blocklist.Shutdown()
@@ -208,19 +211,15 @@ func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
return true return true
} }
func (p *Proxy) doObfuscated2Handshake(ctx *streamContext) error { func (p *Proxy) doObfuscatedHandshake(ctx *streamContext) error {
dc, encryptor, decryptor, err := obfuscated2.ClientHandshake(p.secret.Key[:], ctx.clientConn) dc, conn, err := p.clientObfuscatror.ReadHandshake(ctx.clientConn)
if err != nil { if err != nil {
return fmt.Errorf("cannot process client handshake: %w", err) return fmt.Errorf("cannot process client handshake: %w", err)
} }
ctx.dc = dc ctx.dc = dc
ctx.clientConn = conn
ctx.logger = ctx.logger.BindInt("dc", dc) ctx.logger = ctx.logger.BindInt("dc", dc)
ctx.clientConn = obfuscated2.Conn{
Conn: ctx.clientConn,
Encryptor: encryptor,
Decryptor: decryptor,
}
return nil return nil
} }
@@ -230,17 +229,22 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error {
addresses := p.telegram.GetAddresses(dcid) addresses := p.telegram.GetAddresses(dcid)
if len(addresses) == 0 && p.allowFallbackOnUnknownDC { 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.logger.Warning("unknown DC, fallbacks")
ctx.dc = dc.DefaultDC
addresses = p.telegram.GetAddresses(dc.DefaultDC) addresses = p.telegram.GetAddresses(dc.DefaultDC)
} }
var conn essentials.Conn var (
var err error conn essentials.Conn
err error
foundAddr dc.Addr
)
for _, addr := range addresses { for _, addr := range addresses {
conn, err = p.network.Dial(addr.Network, addr.Address) conn, err = p.network.Dial(addr.Network, addr.Address)
if err == nil { if err == nil {
foundAddr = addr
break break
} }
} }
@@ -248,22 +252,17 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error {
return fmt.Errorf("no addresses to call: %w", err) return fmt.Errorf("no addresses to call: %w", err)
} }
encryptor, decryptor, err := obfuscated2.ServerHandshake(conn) tgConn, err := foundAddr.Obfuscator.SendHandshake(conn, ctx.dc)
if err != nil { if err != nil {
conn.Close() // nolint: errcheck conn.Close() // nolint: errcheck
return fmt.Errorf("cannot perform server handshake: %w", err)
return fmt.Errorf("cannot perform obfuscated2 handshake: %w", err)
} }
ctx.telegramConn = obfuscated2.Conn{ ctx.telegramConn = connTraffic{
Conn: connTraffic{ Conn: tgConn,
Conn: conn,
streamID: ctx.streamID, streamID: ctx.streamID,
stream: p.eventStream, stream: p.eventStream,
ctx: ctx, ctx: ctx,
},
Encryptor: encryptor,
Decryptor: decryptor,
} }
p.eventStream.Send(ctx, p.eventStream.Send(ctx,
@@ -307,12 +306,15 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
return nil, fmt.Errorf("invalid settings: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("cannot build telegram dc fetcher: %w", err) return nil, fmt.Errorf("cannot build telegram dc fetcher: %w", err)
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
logger := opts.getLogger("proxy")
updatersLogger := logger.Named("telegram-updaters")
proxy := &Proxy{ proxy := &Proxy{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
@@ -322,14 +324,25 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
blocklist: opts.IPBlocklist, blocklist: opts.IPBlocklist,
allowlist: opts.IPAllowlist, allowlist: opts.IPAllowlist,
eventStream: opts.EventStream, eventStream: opts.EventStream,
logger: opts.getLogger("proxy"), logger: logger,
domainFrontingPort: opts.getDomainFrontingPort(), domainFrontingPort: opts.getDomainFrontingPort(),
domainFrontingIP: opts.DomainFrontingIP, domainFrontingIP: opts.DomainFrontingIP,
tolerateTimeSkewness: opts.getTolerateTimeSkewness(), tolerateTimeSkewness: opts.getTolerateTimeSkewness(),
allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC, allowFallbackOnUnknownDC: opts.AllowFallbackOnUnknownDC,
telegram: tg, telegram: tg,
configUpdater: dc.NewPublicConfigUpdater(
tg,
updatersLogger.Named("public-config"),
opts.Network.MakeHTTPClient(nil),
),
clientObfuscatror: obfuscation.Obfuscator{
Secret: opts.Secret.Key[:],
},
} }
proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4")
proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6")
pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), pool, err := ants.NewPoolWithFunc(opts.getConcurrency(),
func(arg any) { func(arg any) {
proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert
+1 -1
View File
@@ -126,7 +126,7 @@ type ProxyOpts struct {
// DCOverrides defines a set of IP addresses that should be used // DCOverrides defines a set of IP addresses that should be used
// with a higher priority to those that are calculated somehow by mtg. // 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 DCOverrides map[int][]string
} }