move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
+181
View File
@@ -0,0 +1,181 @@
package dcs
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"math/big"
"net"
"sort"
"sync"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
var dnsKey struct {
rsa.PublicKey
once sync.Once
eBig *big.Int
}
//nolint:gochecknoinits
func init() {
dnsKey.once.Do(func() {
k, err := crypto.ParseRSAPublicKeys([]byte(`-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyr+18Rex2ohtVy8sroGPBwXD3DOoKCSpjDqYoXgCqB7ioln4eDCF
fOBUlfXUEvM/fnKCpF46VkAftlb4VuPDeQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd
192xRGreuXIaUKmkwlM9JID9WS2jUsTpzQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c
9G5IzzBm+otMS/YKwmR1olzRCyEkyAEjXWqBI9Ftv5eG8m0VkBzOG655WIYdyV0H
fDK/NWcvGqa0w/nriMD6mDjKOryamw0OP9QuYgMN0C9xMW9y8SmP4h92OAWodTYg
Y1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWYxwIDAQAB
-----END RSA PUBLIC KEY-----`))
if err != nil {
panic(err)
}
dnsKey.PublicKey = *k[0]
dnsKey.eBig = big.NewInt(int64(dnsKey.E))
})
}
// parseDNSList decodes raw encrypted simple config.
//
// Notice that parseDNSList does not decode base64, user should do it manually.
func parseDNSList(input [256]byte) (tg.HelpConfigSimple, error) {
// See https://github.com/tdlib/td/blob/master/td/telegram/ConfigManager.cpp#L148.
x := new(big.Int).SetBytes(input[:])
y := new(big.Int).Exp(x, dnsKey.eBig, dnsKey.N)
dataRSA := make([]byte, 256)
if !crypto.FillBytes(y, dataRSA) {
return tg.HelpConfigSimple{}, errors.New("dataRSA has invalid size")
}
block, err := aes.NewCipher(dataRSA[:32])
if err != nil {
return tg.HelpConfigSimple{}, err
}
d := cipher.NewCBCDecrypter(block, dataRSA[16:32])
dataCBC := dataRSA[32:]
d.CryptBlocks(dataCBC, dataCBC)
decrypted := dataCBC[:len(dataCBC)-16]
decryptedHash := sha256.Sum256(decrypted)
hash := dataCBC[len(dataCBC)-16:]
if !bytes.Equal(decryptedHash[:16], hash) {
return tg.HelpConfigSimple{}, errors.New("hash mismatch")
}
var cfg tg.HelpConfigSimple
if err := cfg.Decode(&bin.Buffer{Buf: decrypted[4:]}); err != nil {
return tg.HelpConfigSimple{}, err
}
return cfg, nil
}
type sortByLen []string
func (s sortByLen) Len() int {
return len(s)
}
func (s sortByLen) Less(i, j int) bool {
return len(s[i]) > len(s[j])
}
func (s sortByLen) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// DNSConfig is DC connection config obtained from DNS.
type DNSConfig struct {
// Date field of HelpConfigSimple.
Date int
// Expires field of HelpConfigSimple.
Expires int
// Rules field of HelpConfigSimple.
Rules []tg.AccessPointRule
}
// Options returns DC options from this config.
func (d DNSConfig) Options() (r []tg.DCOption) {
convertIP := func(ip int) string {
return net.IPv4(
byte(ip),
byte(ip>>8),
byte(ip>>16),
byte(ip>>24),
).String()
}
for _, rule := range d.Rules {
for _, ip := range rule.IPs {
switch ip := ip.(type) {
case *tg.IPPort:
r = append(r, tg.DCOption{
ID: rule.DCID,
IPAddress: convertIP(ip.Ipv4),
Port: ip.Port,
})
case *tg.IPPortSecret:
r = append(r, tg.DCOption{
TCPObfuscatedOnly: true,
ID: rule.DCID,
IPAddress: convertIP(ip.Ipv4),
Port: ip.Port,
Secret: ip.Secret,
})
}
}
}
return r
}
// ParseDNSConfig parses tg.HelpConfigSimple from TXT response.
func ParseDNSConfig(txt []string) (DNSConfig, error) {
encoding := base64.StdEncoding
const (
decodedLen = 256
encodedLen = 344
)
sort.Sort(sortByLen(txt))
var totalLength int
for i := range txt {
totalLength += len(txt[i])
}
if totalLength != encodedLen {
return DNSConfig{}, errors.Errorf("invalid input length %d", totalLength)
}
var (
encoded [encodedLen]byte
decoded [decodedLen]byte
)
n := 0
for i := range txt {
n += copy(encoded[n:], txt[i])
}
if _, err := encoding.Decode(decoded[:], encoded[:]); err != nil {
return DNSConfig{}, errors.Wrap(err, "decode")
}
cfg, err := parseDNSList(decoded)
if err != nil {
return DNSConfig{}, errors.Wrap(err, "decrypt config")
}
return DNSConfig{
Date: cfg.Date,
Expires: cfg.Expires,
Rules: cfg.Rules,
}, nil
}
+92
View File
@@ -0,0 +1,92 @@
package dcs
import (
"testing"
"github.com/stretchr/testify/require"
)
func testTXTResponse() []string {
return []string{
"LcmEoukF2bVjKwz3E+J9BsDdL+rv9lGqLQWIGXrWACT2ESk5xuOpA6Cz6klKRbhbwSiHOd2zC5PiR57j/OJHPpj4i+tw==",
"umjjLFLpOKtPeW9zHLq2ypbMzg/zkqvPhvhr0bxrLZlgPQ04l2GpO/4qZgAx3tk3BDHbY6/gmG1e8eaFBq3YSqR5SZ5hQ1Cm5f4/" +
"o67GYcPJClaf1TiHq3wVfsQ5OLnyJRw9A2ZfUfzIXxoSklPJrVdF/4hM1ZdUE0eWDAbmYf7JCeao8ecVVwKndd4CZHZS9wyf1T7DIUh95VpQ" +
"sn2klLPA6gA/2YNXOh9gITvjZrKuXLwwh9hBHhPvxv",
}
}
func Test_ParseDNSConfig(t *testing.T) {
t.Run("Good", func(t *testing.T) {
a := require.New(t)
cfg, err := ParseDNSConfig(testTXTResponse())
a.NoError(err)
a.Equal(1565541126, cfg.Expires)
a.Equal(1562949126, cfg.Date)
a.Len(cfg.Rules, 1)
rule := cfg.Rules[0]
a.Equal(2, rule.DCID)
})
t.Run("Bad", func(t *testing.T) {
tests := []struct {
name string
input []string
}{
{"Empty", nil},
{"InvalidHash", func() []string {
r := testTXTResponse()
first := r[0]
r[0] = string(first[0]+1) + first[1:]
return r
}()},
{"InvalidBase64", func() []string {
r := testTXTResponse()
first := r[0]
r[0] = string('#') + first[1:]
return r
}()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseDNSConfig(tt.input)
require.Error(t, err)
})
}
})
}
func BenchmarkDNSConfig(b *testing.B) {
message := testTXTResponse()
b.ResetTimer()
b.ReportAllocs()
var (
err error
cfgSink DNSConfig
)
for i := 0; i < b.N; i++ {
cfgSink, err = ParseDNSConfig(message)
if cfgSink.Date == 0 || err != nil {
b.Fatal(err)
}
}
}
func TestDNSConfig_Options(t *testing.T) {
a := require.New(t)
cfg, err := ParseDNSConfig(testTXTResponse())
a.NoError(err)
options := cfg.Options()
a.Len(options, 1)
option := options[0]
a.Equal(2, option.ID)
a.Equal(14544, option.Port)
a.Equal("98.210.59.139", option.IPAddress)
a.True(option.TCPObfuscatedOnly)
}
+2
View File
@@ -0,0 +1,2 @@
// Package dcs contains Telegram DCs list and some helpers.
package dcs
+52
View File
@@ -0,0 +1,52 @@
package dcs_test
import (
"context"
"fmt"
"time"
"golang.org/x/net/proxy"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs"
)
func ExampleDialFunc() {
// Dial using proxy from environment.
// Creating connection.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
client := telegram.NewClient(1, "appHash", telegram.Options{
Resolver: dcs.Plain(dcs.PlainOptions{Dial: proxy.Dial}),
})
_ = client.Run(ctx, func(ctx context.Context) error {
fmt.Println("Started")
return nil
})
}
func ExampleDialFunc_dialer() {
// Dial using SOCKS5 proxy.
sock5, _ := proxy.SOCKS5("tcp", "IP:PORT", &proxy.Auth{
User: "YOURUSERNAME",
Password: "YOURPASSWORD",
}, proxy.Direct)
dc := sock5.(proxy.ContextDialer)
// Creating connection.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
client := telegram.NewClient(1, "appHash", telegram.Options{
Resolver: dcs.Plain(dcs.PlainOptions{
Dial: dc.DialContext,
}),
})
_ = client.Run(ctx, func(ctx context.Context) error {
fmt.Println("Started")
return nil
})
}
+60
View File
@@ -0,0 +1,60 @@
package dcs
import (
"sort"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// FindDCs searches DCs candidates from given config.
func FindDCs(opts []tg.DCOption, dcID int, preferIPv6 bool) []tg.DCOption {
// Preallocate slice.
candidates := make([]tg.DCOption, 0, 32)
for _, candidateDC := range opts {
if candidateDC.ID != dcID {
continue
}
candidates = append(candidates, candidateDC)
}
if len(candidates) < 1 {
return nil
}
sort.Slice(candidates, func(i, j int) bool {
l, r := candidates[i], candidates[j]
// If we prefer IPv6 and left is IPv6 and right is not, so then
// left is smaller (would be before right).
if preferIPv6 {
if l.Ipv6 && !r.Ipv6 {
return true
}
if !l.Ipv6 && r.Ipv6 {
return false
}
}
// Also we prefer static addresses.
return l.Static && !r.Static
})
return candidates
}
// FindPrimaryDCs searches new primary DC from given config.
// Unlike FindDC, it filters CDNs and MediaOnly servers, returns error
// if not found.
func FindPrimaryDCs(opts []tg.DCOption, dcID int, preferIPv6 bool) []tg.DCOption {
candidates := FindDCs(opts, dcID, preferIPv6)
// Filter (in place) from SliceTricks.
n := 0
for _, opt := range candidates {
if !opt.MediaOnly && !opt.CDN {
candidates[n] = opt
n++
}
}
return candidates[:n]
}
+76
View File
@@ -0,0 +1,76 @@
package dcs
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestFindDCs(t *testing.T) {
dcOptions := []tg.DCOption{
{ID: 1, Ipv6: false},
{ID: 1, Ipv6: true},
{ID: 1, Ipv6: false, Static: true},
{ID: 2, Ipv6: true, Static: true},
{ID: 2, Ipv6: true},
{ID: 2, Ipv6: false},
}
for i := range dcOptions {
dcOptions[i].IPAddress = fmt.Sprintf("DC: %d, Index: %d", dcOptions[i].ID, i)
}
a := require.New(t)
dc := FindDCs(dcOptions, -2, false)
a.Empty(dc)
dc = FindDCs(dcOptions, -2, true)
a.Empty(dc)
// Prefer IPv6.
dc = FindDCs(dcOptions, 1, true)
a.True(dc[0].Ipv6)
// Prefer static.
dc = FindDCs(dcOptions, 1, false)
a.True(dc[0].Static)
// Prefer static and IPv6.
dc = FindDCs(dcOptions, 2, true)
a.True(dc[0].Static)
a.True(dc[0].Ipv6)
}
func TestFindPrimaryDCs(t *testing.T) {
dcOptions := []tg.DCOption{
{ID: 1, Ipv6: false},
{ID: 1, Ipv6: true},
{ID: 1, Ipv6: false, Static: true},
{ID: 2, Ipv6: true, Static: true, MediaOnly: true},
{ID: 2, Ipv6: true, CDN: true},
{ID: 2, Ipv6: false, CDN: true},
}
for i := range dcOptions {
dcOptions[i].IPAddress = fmt.Sprintf("DC: %d, Index: %d", dcOptions[i].ID, i)
}
a := require.New(t)
dc := FindPrimaryDCs(dcOptions, -2, false)
a.Empty(dc)
dc = FindPrimaryDCs(dcOptions, -2, true)
a.Empty(dc)
// Prefer IPv6.
dc = FindPrimaryDCs(dcOptions, 1, true)
a.True(dc[0].Ipv6)
// Prefer static.
dc = FindPrimaryDCs(dcOptions, 1, false)
a.True(dc[0].Static)
// Filter CDN/MediaOnly/TCPo.
dc = FindPrimaryDCs(dcOptions, 2, false)
a.Empty(dc)
}
+15
View File
@@ -0,0 +1,15 @@
package dcs
import "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
// List is a list of Telegram DC addresses and domains.
type List struct {
Options []tg.DCOption
Domains map[int]string
Test bool
}
// Zero returns true if this List is zero value.
func (d List) Zero() bool {
return d.Options == nil && d.Domains == nil && !d.Test
}
+140
View File
@@ -0,0 +1,140 @@
package dcs
import (
"context"
"io"
"net"
"github.com/go-faster/errors"
"go.uber.org/multierr"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy"
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy/obfuscator"
"go.mau.fi/mautrix-telegram/pkg/gotd/proto/codec"
"go.mau.fi/mautrix-telegram/pkg/gotd/transport"
)
var _ Resolver = mtProxy{}
type mtProxy struct {
dial DialFunc
protocol protocol
addr, network string
secret mtproxy.Secret
tag [4]byte
rand io.Reader
}
func (m mtProxy) Primary(ctx context.Context, dc int, _ List) (transport.Conn, error) {
return m.resolve(ctx, dc)
}
func (m mtProxy) MediaOnly(ctx context.Context, dc int, _ List) (transport.Conn, error) {
if dc > 0 {
dc *= -1
}
return m.resolve(ctx, dc)
}
func (m mtProxy) CDN(ctx context.Context, dc int, _ List) (transport.Conn, error) {
return m.resolve(ctx, dc)
}
func (m mtProxy) resolve(ctx context.Context, dc int) (transport.Conn, error) {
c, err := m.dial(ctx, m.network, m.addr)
if err != nil {
return nil, errors.Wrapf(err, "connect to the MTProxy %q", m.addr)
}
conn, err := m.handshakeConn(c, dc)
if err != nil {
err = errors.Wrap(err, "handshake")
return nil, multierr.Combine(err, c.Close())
}
return conn, nil
}
// handshakeConn inits given net.Conn as MTProto connection.
func (m mtProxy) handshakeConn(c net.Conn, dc int) (transport.Conn, error) {
var obsConn *obfuscator.Conn
switch m.secret.Type {
case mtproxy.Simple, mtproxy.Secured:
obsConn = obfuscator.Obfuscated2(m.rand, c)
case mtproxy.TLS:
obsConn = obfuscator.FakeTLS(m.rand, c)
default:
return nil, errors.Errorf("unknown MTProxy secret type: %d", m.secret.Type)
}
secret := m.secret
if err := obsConn.Handshake(m.tag, dc, secret); err != nil {
return nil, errors.Wrap(err, "MTProxy handshake")
}
transportConn, err := m.protocol.Handshake(obsConn)
if err != nil {
return nil, errors.Wrap(err, "transport handshake")
}
return transportConn, nil
}
// MTProxyOptions is MTProxy resolver creation options.
type MTProxyOptions struct {
// Dial specifies the dial function for creating unencrypted TCP connections.
// If Dial is nil, then the resolver dials using package net.
Dial DialFunc
// Network to use. Defaults to "tcp"
Network string
// Random source for MTProxy obfuscator.
Rand io.Reader
}
func (m *MTProxyOptions) setDefaults() {
if m.Dial == nil {
var d net.Dialer
m.Dial = d.DialContext
}
if m.Network == "" {
m.Network = "tcp"
}
if m.Rand == nil {
m.Rand = crypto.DefaultRand()
}
}
// MTProxy creates MTProxy obfuscated DC resolver.
//
// See https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation.
func MTProxy(addr string, secret []byte, opts MTProxyOptions) (Resolver, error) {
s, err := mtproxy.ParseSecret(secret)
if err != nil {
return nil, err
}
var cdc codec.Codec = codec.PaddedIntermediate{}
tag := codec.PaddedIntermediateClientStart
// FIXME(tdakkota): some proxies forces to use Padded (Secure) Intermediate
// even if secret denotes to use another transport type.
if s.Type != mtproxy.TLS {
if c, ok := s.ExpectedCodec(); ok {
cdc = c
tag = [4]byte{s.Tag, s.Tag, s.Tag, s.Tag}
}
}
opts.setDefaults()
return mtProxy{
dial: opts.Dial,
addr: addr,
network: opts.Network,
protocol: transport.NewProtocol(func() transport.Codec { return codec.NoHeader{Codec: cdc} }),
secret: s,
tag: tag,
rand: opts.Rand,
}, nil
}
+226
View File
@@ -0,0 +1,226 @@
package dcs
import (
"context"
"io"
"net"
"strconv"
"github.com/go-faster/errors"
"go.uber.org/multierr"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy"
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy/obfuscator"
"go.mau.fi/mautrix-telegram/pkg/gotd/proto/codec"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/transport"
)
var _ Resolver = plain{}
type plain struct {
dial DialFunc
protocol Protocol
rand io.Reader
network string
noObfuscated bool
preferIPv6 bool
}
func (p plain) Primary(ctx context.Context, dc int, list List) (transport.Conn, error) {
candidates := FindPrimaryDCs(list.Options, dc, p.preferIPv6)
if p.noObfuscated {
n := 0
for _, x := range candidates {
if !x.TCPObfuscatedOnly {
candidates[n] = x
n++
}
}
candidates = candidates[:n]
}
return p.connect(ctx, dc, list.Test, candidates)
}
func (p plain) MediaOnly(ctx context.Context, dc int, list List) (transport.Conn, error) {
candidates := FindDCs(list.Options, dc, p.preferIPv6)
// Filter (in place) from SliceTricks.
n := 0
for _, x := range candidates {
if x.MediaOnly {
candidates[n] = x
n++
}
}
return p.connect(ctx, dc, list.Test, candidates[:n])
}
func (p plain) CDN(ctx context.Context, dc int, list List) (transport.Conn, error) {
return nil, errors.Errorf("can't resolve %d: CDN is unsupported", dc)
}
func (p plain) dialTransport(ctx context.Context, test bool, dc tg.DCOption) (_ transport.Conn, rerr error) {
addr := net.JoinHostPort(dc.IPAddress, strconv.Itoa(dc.Port))
conn, err := p.dial(ctx, p.network, addr)
if err != nil {
return nil, err
}
defer func() {
if rerr != nil {
multierr.AppendInto(&rerr, conn.Close())
}
}()
proto := p.protocol
if dc.TCPObfuscatedOnly {
dcID := dc.ID
if test {
if dcID < 0 {
dcID -= 10000
} else {
dcID += 10000
}
}
var (
cdc codec.Codec = codec.Intermediate{}
tag = codec.IntermediateClientStart
obfs = obfuscator.Obfuscated2
)
secret, err := mtproxy.ParseSecret(dc.Secret)
if err != nil {
return nil, errors.Wrap(err, "check DC secret")
}
if secret.Type == mtproxy.TLS {
obfs = obfuscator.FakeTLS
} else if c, ok := secret.ExpectedCodec(); ok {
tag = [4]byte{secret.Tag, secret.Tag, secret.Tag, secret.Tag}
cdc = c
}
obfsConn := obfs(p.rand, conn)
if err := obfsConn.Handshake(tag, dcID, secret); err != nil {
return nil, err
}
conn = obfsConn
proto = transport.NewProtocol(func() transport.Codec {
return codec.NoHeader{Codec: cdc}
})
}
transportConn, err := proto.Handshake(conn)
if err != nil {
return nil, errors.Wrap(err, "transport handshake")
}
return transportConn, nil
}
func (p plain) connect(ctx context.Context, dc int, test bool, dcOptions []tg.DCOption) (transport.Conn, error) {
switch len(dcOptions) {
case 0:
return nil, errors.Errorf("no addresses for DC %d", dc)
case 1:
return p.dialTransport(ctx, test, dcOptions[0])
}
type dialResult struct {
conn transport.Conn
err error
}
// We use unbuffered channel to ensure that only one connection will be returned
// and all other will be closed.
results := make(chan dialResult)
tryDial := func(ctx context.Context, option tg.DCOption) {
conn, err := p.dialTransport(ctx, test, option)
select {
case results <- dialResult{
conn: conn,
err: err,
}:
case <-ctx.Done():
if conn != nil {
_ = conn.Close()
}
}
}
dialCtx, dialCancel := context.WithCancel(ctx)
defer dialCancel()
for _, dcOption := range dcOptions {
go tryDial(dialCtx, dcOption)
}
remain := len(dcOptions)
var rErr error
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-results:
remain--
if result.err != nil {
rErr = multierr.Append(rErr, result.err)
if remain == 0 {
return nil, rErr
}
continue
}
return result.conn, nil
}
}
}
// PlainOptions is plain resolver creation options.
type PlainOptions struct {
// Protocol is the transport protocol to use. Defaults to intermediate.
Protocol Protocol
// Dial specifies the dial function for creating unencrypted TCP connections.
// If Dial is nil, then the resolver dials using package net.
Dial DialFunc
// Random source for TCPObfuscated DCs.
Rand io.Reader
// Network to use. Defaults to "tcp".
Network string
// NoObfuscated denotes to filter out TCP Obfuscated Only DCs.
NoObfuscated bool
// PreferIPv6 gives IPv6 DCs higher precedence.
// Default is to prefer IPv4 DCs over IPv6.
PreferIPv6 bool
}
func (m *PlainOptions) setDefaults() {
if m.Protocol == nil {
m.Protocol = transport.Intermediate
}
if m.Dial == nil {
var d net.Dialer
m.Dial = d.DialContext
}
if m.Rand == nil {
m.Rand = crypto.DefaultRand()
}
if m.Network == "" {
m.Network = "tcp"
}
}
// Plain creates plain DC resolver.
func Plain(opts PlainOptions) Resolver {
opts.setDefaults()
return plain{
dial: opts.Dial,
protocol: opts.Protocol,
rand: opts.Rand,
network: opts.Network,
noObfuscated: opts.NoObfuscated,
preferIPv6: opts.PreferIPv6,
}
}
+134
View File
@@ -0,0 +1,134 @@
package dcs
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// Prod returns production DC list.
func Prod() List {
// https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp
// Also available with client.API().HelpGetConfig(ctx) [tg.DCOption].
// TODO(ernado): automate update from HelpGetConfig.
return List{
Options: []tg.DCOption{
{
ID: 1,
IPAddress: "149.154.175.52",
Port: 443,
},
{
Static: true,
ID: 1,
IPAddress: "149.154.175.53",
Port: 443,
},
{
Ipv6: true,
ID: 1,
IPAddress: "2001:0b28:f23d:f001:0000:0000:0000:000a",
Port: 443,
},
{
ID: 2,
IPAddress: "149.154.167.41",
Port: 443,
},
{
Static: true,
ID: 2,
IPAddress: "149.154.167.41",
Port: 443,
},
{
MediaOnly: true,
ID: 2,
IPAddress: "149.154.167.222",
Port: 443,
},
{
Ipv6: true,
ID: 2,
IPAddress: "2001:067c:04e8:f002:0000:0000:0000:000a",
Port: 443,
},
{
Ipv6: true,
MediaOnly: true,
ID: 2,
IPAddress: "2001:067c:04e8:f002:0000:0000:0000:000b",
Port: 443,
},
{
ID: 3,
IPAddress: "149.154.175.100",
Port: 443,
},
{
Static: true,
ID: 3,
IPAddress: "149.154.175.100",
Port: 443,
},
{
Ipv6: true,
ID: 3,
IPAddress: "2001:0b28:f23d:f003:0000:0000:0000:000a",
Port: 443,
},
{
ID: 4,
IPAddress: "149.154.167.91",
Port: 443,
},
{
Static: true,
ID: 4,
IPAddress: "149.154.167.91",
Port: 443,
},
{
Ipv6: true,
ID: 4,
IPAddress: "2001:067c:04e8:f004:0000:0000:0000:000a",
Port: 443,
},
{
MediaOnly: true,
ID: 4,
IPAddress: "149.154.166.120",
Port: 443,
},
{
Ipv6: true,
MediaOnly: true,
ID: 4,
IPAddress: "2001:067c:04e8:f004:0000:0000:0000:000b",
Port: 443,
},
{
Ipv6: true,
ID: 5,
IPAddress: "2001:0b28:f23f:f005:0000:0000:0000:000a",
Port: 443,
},
{
ID: 5,
IPAddress: "91.108.56.191",
Port: 443,
},
{
Static: true,
ID: 5,
IPAddress: "91.108.56.191",
Port: 443,
},
},
Domains: map[int]string{
1: "wss://pluto.web.telegram.org/apiws",
2: "wss://venus.web.telegram.org/apiws",
3: "wss://aurora.web.telegram.org/apiws",
4: "wss://vesta.web.telegram.org/apiws",
5: "wss://flora.web.telegram.org/apiws",
},
}
}
+17
View File
@@ -0,0 +1,17 @@
package dcs
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestProd(t *testing.T) {
require.NotEmpty(t, Prod())
// Check copying.
a := Prod().Options
a[0].IPAddress = "10"
b := Prod().Options
require.NotEqual(t, "10", b[0].IPAddress)
}
+11
View File
@@ -0,0 +1,11 @@
package dcs
import (
"net"
"go.mau.fi/mautrix-telegram/pkg/gotd/transport"
)
type protocol interface {
Handshake(conn net.Conn) (transport.Conn, error)
}
+28
View File
@@ -0,0 +1,28 @@
package dcs
import (
"context"
"net"
"go.mau.fi/mautrix-telegram/pkg/gotd/transport"
)
var _ Resolver = DefaultResolver()
// Resolver resolves DC and creates transport MTProto connection.
type Resolver interface {
Primary(ctx context.Context, dc int, list List) (transport.Conn, error)
MediaOnly(ctx context.Context, dc int, list List) (transport.Conn, error)
CDN(ctx context.Context, dc int, list List) (transport.Conn, error)
}
// DialFunc connects to the address on the named network.
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
// Protocol is MTProto transport protocol.
//
// See https://core.telegram.org/mtproto/mtproto-transports
type Protocol interface {
Codec() transport.Codec
Handshake(conn net.Conn) (transport.Conn, error)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build js && wasm
// +build js,wasm
package dcs
// DefaultResolver returns default DC resolver for current platform.
func DefaultResolver() Resolver {
return Websocket(WebsocketOptions{})
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !js || !wasm
// +build !js !wasm
package dcs
// DefaultResolver returns default DC resolver for current platform.
func DefaultResolver() Resolver {
return Plain(PlainOptions{})
}
+59
View File
@@ -0,0 +1,59 @@
package dcs
import "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
// Staging returns staging DC list.
//
// Deprecated: Use Test().
func Staging() List {
return Test()
}
// Test returns test DC list.
func Test() List {
return List{
Options: []tg.DCOption{
{
ID: 1,
IPAddress: "149.154.175.10",
Port: 443,
},
{
ID: 1,
Ipv6: true,
IPAddress: "2001:0b28:f23d:f001:0000:0000:0000:000e",
Port: 443,
},
{
ID: 2,
IPAddress: "149.154.167.40",
Port: 443,
},
{
ID: 2,
Ipv6: true,
IPAddress: "2001:067c:04e8:f002:0000:0000:0000:000e",
Port: 443,
},
{
ID: 3,
IPAddress: "149.154.175.117",
Port: 443,
},
{
ID: 3,
Ipv6: true,
IPAddress: "2001:0b28:f23d:f003:0000:0000:0000:000e",
Port: 443,
},
},
Domains: map[int]string{
1: "wss://pluto.web.telegram.org/apiws_test",
2: "wss://venus.web.telegram.org/apiws_test",
3: "wss://aurora.web.telegram.org/apiws_test",
4: "wss://vesta.web.telegram.org/apiws_test",
5: "wss://flora.web.telegram.org/apiws_test",
},
Test: true,
}
}
+17
View File
@@ -0,0 +1,17 @@
package dcs
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTestDCs(t *testing.T) {
require.NotEmpty(t, Prod())
// Check copying.
a := Test().Options
a[0].IPAddress = "10"
b := Test().Options
require.NotEqual(t, "10", b[0].IPAddress)
}
+103
View File
@@ -0,0 +1,103 @@
package dcs
import (
"context"
"io"
"github.com/coder/websocket"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy"
"go.mau.fi/mautrix-telegram/pkg/gotd/mtproxy/obfuscator"
"go.mau.fi/mautrix-telegram/pkg/gotd/proto/codec"
"go.mau.fi/mautrix-telegram/pkg/gotd/transport"
"go.mau.fi/mautrix-telegram/pkg/gotd/wsutil"
)
var _ Resolver = ws{}
type ws struct {
dialOptions *websocket.DialOptions
protocol protocol
tag [4]byte
rand io.Reader
}
func (w ws) connect(ctx context.Context, dc int, domains map[int]string) (transport.Conn, error) {
addr, ok := domains[dc]
if !ok {
return nil, errors.Errorf("domain for %d not found", dc)
}
conn, resp, err := websocket.Dial(ctx, addr, w.dialOptions)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if err != nil {
return nil, errors.Wrap(err, "dial ws")
}
obsConn := obfuscator.Obfuscated2(w.rand, wsutil.NetConn(conn))
if err := obsConn.Handshake(w.tag, dc, mtproxy.Secret{
Secret: nil,
Type: mtproxy.Simple,
}); err != nil {
return nil, errors.Wrap(err, "handshake")
}
transportConn, err := w.protocol.Handshake(obsConn)
if err != nil {
return nil, errors.Wrap(err, "transport handshake")
}
return transportConn, nil
}
func (w ws) Primary(ctx context.Context, dc int, list List) (transport.Conn, error) {
return w.connect(ctx, dc, list.Domains)
}
func (w ws) MediaOnly(ctx context.Context, dc int, list List) (transport.Conn, error) {
return nil, errors.Errorf("can't resolve %d: MediaOnly is unsupported", dc)
}
func (w ws) CDN(ctx context.Context, dc int, list List) (transport.Conn, error) {
return nil, errors.Errorf("can't resolve %d: CDN is unsupported", dc)
}
// WebsocketOptions is Websocket resolver creation options.
type WebsocketOptions struct {
// Dialer specifies the websocket dialer.
// If Dialer is nil, then the resolver dials using websocket.DefaultDialer.
DialOptions *websocket.DialOptions
// Random source for MTProxy obfuscator.
Rand io.Reader
}
func (m *WebsocketOptions) setDefaults() {
if m.DialOptions == nil {
m.DialOptions = &websocket.DialOptions{Subprotocols: []string{
"binary",
}}
}
if m.Rand == nil {
m.Rand = crypto.DefaultRand()
}
}
// Websocket creates Websocket DC resolver.
//
// See https://core.telegram.org/mtproto/transports#websocket.
func Websocket(opts WebsocketOptions) Resolver {
cdc := codec.Intermediate{}
opts.setDefaults()
return ws{
dialOptions: opts.DialOptions,
protocol: transport.NewProtocol(func() transport.Codec { return codec.NoHeader{Codec: cdc} }),
tag: cdc.ObfuscatedTag(),
rand: opts.Rand,
}
}