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..f95845b 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" @@ -19,13 +20,17 @@ run = "go build" description = "Update dependencies" run = [ "go get -u", - "go mod tidy -go=1.25" + "go mod tidy -go=1.26" ] [tasks.lint] description = "Run linter" run = "golangci-lint run" +[tasks.vuln] +description = "Test for vulnerabilities" +run = "govulncheck ./..." + [tasks.test] description = "Run tests" run = "go test -v ./..." @@ -47,19 +52,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/example.config.toml b/example.config.toml index 68f7926..01ab257 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: @@ -57,7 +50,28 @@ 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, +# 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). +# +# 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. +# +# 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 @@ -80,14 +94,26 @@ 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"] +# 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] 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 diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 23580eb..307d154 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, @@ -258,13 +250,14 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen IPAllowlist: allowlist, EventStream: eventStream, - Secret: conf.Secret, - DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort), - PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), + Secret: conf.Secret, + DomainFrontingPort: conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort), + DomainFrontingIP: conf.GetDomainFrontingIP(nil), + DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false), + PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false), TolerateTimeSkewness: conf.TolerateTimeSkewness.Value, - DCOverrides: dcOverrides, } proxy, err := mtglib.NewProxy(opts) diff --git a/internal/cli/simple_run.go b/internal/cli/simple_run.go index 53deb61..192cf3c 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..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" ) @@ -21,16 +22,23 @@ 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"` - 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"` + DomainFronting struct { + IP TypeIP `json:"ip"` + Port TypePort `json:"port"` + ProxyProtocol TypeBool `json:"proxyProtocol"` + } `json:"domainFronting"` + Defense struct { AntiReplay struct { Optional @@ -65,10 +73,27 @@ 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) 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 { diff --git a/internal/config/parse.go b/internal/config/parse.go index 7cdf6da..e58bc14 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -9,16 +9,23 @@ 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"` - 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"` + 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"` @@ -60,10 +67,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/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" diff --git a/mtglib/conns.go b/mtglib/conns.go index 9c3fa67..8219b75 100644 --- a/mtglib/conns.go +++ b/mtglib/conns.go @@ -3,10 +3,12 @@ package mtglib import ( "bytes" "context" + "fmt" "io" - "sync" + "net" "github.com/9seconds/mtg/v2/essentials" + "github.com/pires/go-proxyproto" ) type connTraffic struct { @@ -40,22 +42,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) } @@ -67,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/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/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..fd8befa 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,56 +34,45 @@ type Logger interface { WarningError(msg string, err error) } -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"}, - }, - }, - 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"}, - }, - }, - } +type Updater interface { + Run(ctx context.Context) +} - defaultDCOverridesAddrSet = dcAddrSet{ - v4: map[int][]Addr{ - 203: { - {Network: "tcp4", Address: "91.105.192.100: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{ - 203: { - {Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]: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/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..a02b87c --- /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) //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, + } +} 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..bc1d148 --- /dev/null +++ b/mtglib/internal/dc/public_config_updater_test.go @@ -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{}) +} diff --git a/mtglib/internal/dc/telegram.go b/mtglib/internal/dc/telegram.go index e9f261f..fcd3fd7 100644 --- a/mtglib/internal/dc/telegram.go +++ b/mtglib/internal/dc/telegram.go @@ -2,16 +2,20 @@ package dc import ( "fmt" - "net" "strings" + "sync" ) type Telegram struct { + 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 +28,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 +44,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..72adf38 --- /dev/null +++ b/mtglib/internal/dc/updater.go @@ -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: + } + } + }) +} 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..e4980a7 100644 --- a/mtglib/internal/dc/view.go +++ b/mtglib/internal/dc/view.go @@ -1,20 +1,18 @@ package dc type dcView struct { - overrides dcAddrSet + publicConfigs 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, 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, 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/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{} 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_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/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) -} 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/obfuscated2/server_handshake_fuzz_internal_test.go b/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go similarity index 56% rename from mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go rename to mtglib/internal/obfuscation/handshake_frame_fuzz_test.go index 125fd9c..3806e15 100644 --- a/mtglib/internal/obfuscated2/server_handshake_fuzz_internal_test.go +++ b/mtglib/internal/obfuscation/handshake_frame_fuzz_test.go @@ -1,4 +1,4 @@ -package obfuscated2 +package obfuscation import ( "encoding/binary" @@ -7,9 +7,9 @@ import ( "github.com/stretchr/testify/assert" ) -func FuzzServerGenerateHandshakeFrame(f *testing.F) { - f.Fuzz(func(t *testing.T, arg int) { - frame := generateServerHanshakeFrame() +func FuzzGenerateHandshakeFrame(f *testing.F) { + f.Fuzz(func(t *testing.T, arg int16) { + frame := generateHandshake(int(arg)) assert.NotEqualValues(t, 0xef, frame.data[0]) @@ -18,13 +18,23 @@ func FuzzServerGenerateHandshakeFrame(f *testing.F) { 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, 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()) }) } 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..fbc018e --- /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.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/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json b/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json similarity index 100% rename from mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-4529d55776e2d427.json rename to mtglib/internal/obfuscation/testdata/client-handshake-snapshot-4529d55776e2d427.json diff --git a/mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json b/mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json similarity index 100% rename from mtglib/internal/obfuscated2/testdata/client-handshake-snapshot-585c944d672f60a2.json rename to mtglib/internal/obfuscation/testdata/client-handshake-snapshot-585c944d672f60a2.json 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: diff --git a/mtglib/proxy.go b/mtglib/proxy.go index 104c723..a0e4965 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" ) @@ -24,11 +24,15 @@ type Proxy struct { ctxCancel context.CancelFunc streamWaitGroup sync.WaitGroup - allowFallbackOnUnknownDC bool - tolerateTimeSkewness time.Duration - domainFrontingPort int - 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 secret Secret network Network @@ -40,8 +44,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 @@ -70,8 +80,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 } @@ -144,6 +154,7 @@ func (p *Proxy) Shutdown() { p.ctxCancel() p.streamWaitGroup.Wait() p.workerPool.Release() + p.configUpdater.Wait() p.allowlist.Shutdown() p.blocklist.Shutdown() @@ -201,19 +212,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 +230,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 +253,17 @@ func (p *Proxy) doTelegramCall(ctx *streamContext) error { 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 { - conn.Close() //nolint: errcheck - - return fmt.Errorf("cannot perform obfuscated2 handshake: %w", err) + conn.Close() // nolint: errcheck + 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: tgConn, + streamID: ctx.streamID, + stream: p.eventStream, + ctx: ctx, } p.eventStream.Send(ctx, @@ -279,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, @@ -300,12 +311,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, @@ -315,13 +329,26 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) { blocklist: opts.IPBlocklist, allowlist: opts.IPAllowlist, eventStream: opts.EventStream, - logger: opts.getLogger("proxy"), + logger: logger, domainFrontingPort: opts.getDomainFrontingPort(), + domainFrontingIP: opts.DomainFrontingIP, 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[:], + }, + domainFrontingProxyProtocol: opts.DomainFrontingProxyProtocol, } + proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv4, "tcp4") + proxy.configUpdater.Run(ctx, dc.PublicConfigUpdateURLv6, "tcp6") + pool, err := ants.NewPoolWithFunc(opts.getConcurrency(), func(arg any) { proxy.ServeConn(arg.(essentials.Conn)) //nolint: forcetypeassert diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index 53f3434..09f47f2 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -93,6 +93,23 @@ 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 + + // 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. @@ -117,7 +134,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 }