From d7864fcd3a5f31115c3b4e527991e33339d10ab3 Mon Sep 17 00:00:00 2001 From: lavacat Date: Thu, 16 Apr 2026 13:14:29 +0300 Subject: [PATCH] client: add initial proxy support (#1062) --- cmd/mautrix-telegram/legacymigrate.go | 6 +++ pkg/connector/client.go | 6 ++- pkg/connector/config.go | 14 ++++++ pkg/connector/example-config.yaml | 10 +++++ pkg/connector/login.go | 5 +++ pkg/connector/loginbot.go | 15 +++++-- pkg/connector/proxy.go | 61 +++++++++++++++++++++++++++ 7 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 pkg/connector/proxy.go diff --git a/cmd/mautrix-telegram/legacymigrate.go b/cmd/mautrix-telegram/legacymigrate.go index c525218e..2d014e10 100644 --- a/cmd/mautrix-telegram/legacymigrate.go +++ b/cmd/mautrix-telegram/legacymigrate.go @@ -88,6 +88,12 @@ func migrateLegacyConfig(helper up.Helper) { bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "max_initial_member_sync"}, []string{"network", "member_list", "max_initial_sync"}) bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "sync_channel_members"}, []string{"network", "member_list", "sync_broadcast_channels"}) bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "skip_deleted_members"}, []string{"network", "member_list", "skip_deleted"}) + bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "proxy", "type"}, []string{"network", "proxy", "type"}) + proxyAddress, _ := helper.Get(up.Str, "telegram", "proxy", "address") + proxyPort, _ := helper.Get(up.Int, "telegram", "proxy", "port") + helper.Set(up.Str, fmt.Sprintf("%s:%s", proxyAddress, proxyPort), "network", "proxy", "address") + bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "proxy", "username"}, []string{"network", "proxy", "username"}) + bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "proxy", "password"}, []string{"network", "proxy", "password"}) bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "max_member_count"}, []string{"network", "max_member_count"}) bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "sync_update_limit"}, []string{"network", "sync", "update_limit"}) bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "sync_create_limit"}, []string{"network", "sync", "create_limit"}) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index ea4293bf..aa757276 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -210,11 +210,15 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge Storage: client.ScopedStore, AccessHasher: client.ScopedStore, }) - + resolver, err := GetProxyResolver(tc.Config.ProxyConfig) + if err != nil { + return nil, err + } client.client = telegram.NewClient(tc.Config.APIID, tc.Config.APIHash, telegram.Options{ CustomSessionStorage: &login.Metadata.(*UserLoginMetadata).Session, Logger: zaplog, UpdateHandler: client.updatesManager, + Resolver: resolver, OnDead: client.onDead, OnSession: client.onSession, OnConnected: client.onConnected, diff --git a/pkg/connector/config.go b/pkg/connector/config.go index f290888e..7308846b 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -55,6 +55,13 @@ type DeviceInfo struct { LangCode string `yaml:"lang_code"` } +type ProxyConfig struct { + Type string `yaml:"type"` + Address string `yaml:"address"` + Username string `yaml:"username"` + Password string `yaml:"password"` +} + type TelegramConfig struct { APIID int `yaml:"api_id"` APIHash string `yaml:"api_hash"` @@ -68,6 +75,8 @@ type TelegramConfig struct { TimeoutSeconds int `yaml:"timeout_seconds"` } `yaml:"ping"` + ProxyConfig ProxyConfig `yaml:"proxy"` + Sync struct { UpdateLimit int `yaml:"update_limit"` CreateLimit int `yaml:"create_limit"` @@ -161,6 +170,10 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "member_list", "skip_deleted") helper.Copy(up.Int, "ping", "interval_seconds") helper.Copy(up.Int, "ping", "timeout_seconds") + helper.Copy(up.Str, "proxy", "type") + helper.Copy(up.Str|up.Null, "proxy", "address") + helper.Copy(up.Str|up.Null, "proxy", "username") + helper.Copy(up.Str|up.Null, "proxy", "password") helper.Copy(up.Int, "sync", "update_limit") helper.Copy(up.Int, "sync", "create_limit") helper.Copy(up.Int, "sync", "login_sync_limit") @@ -187,6 +200,7 @@ func (tc *TelegramConnector) GetConfig() (example string, data any, upgrader up. {"animated_sticker"}, {"member_list"}, {"ping"}, + {"proxy"}, {"sync"}, {"takeout"}, {"max_member_count"}, diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index 8b43ab0e..d7f23a29 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -54,6 +54,16 @@ ping: # The timeout (in seconds) for a single ping. timeout_seconds: 10 +# Proxy settings +proxy: + # Allowed types: disabled, socks5, mtproxy + type: disabled + # Proxy IP address/domain name and port. + address: "127.0.0.1:1080" + # Proxy authentication (optional). Put MTProxy secret in password field. + username: + password: + sync: # Number of most recently active dialogs to check when syncing chats. # Set to -1 to remove limit. diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 7acba94d..9b5f599f 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -125,7 +125,12 @@ func (bl *baseLogin) makeClient(ctx context.Context, dispatcher *tg.UpdateDispat if dispatcher == nil { dispatcher = ptr.Ptr(tg.NewUpdateDispatcher()) } + resolver, err := GetProxyResolver(bl.main.Config.ProxyConfig) + if err != nil { + return err + } bl.client = telegram.NewClient(bl.main.Config.APIID, bl.main.Config.APIHash, telegram.Options{ + Resolver: resolver, CustomSessionStorage: &bl.session, Logger: zaplog, Device: bl.main.deviceConfig(), diff --git a/pkg/connector/loginbot.go b/pkg/connector/loginbot.go index 89852785..c23bd88a 100644 --- a/pkg/connector/loginbot.go +++ b/pkg/connector/loginbot.go @@ -63,7 +63,16 @@ func (bl *BotLogin) SubmitUserInput(ctx context.Context, input map[string]string ctx = log.WithContext(ctx) botToken := input[LoginStepIDBotToken] - err := logoutBotAPI(ctx, botToken) + dialFunc, err := GetProxyDialFunc(bl.main.Config.ProxyConfig) + if err != nil { + return nil, err + } + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: dialFunc, + }, + } + err = logoutBotAPI(ctx, botToken, httpClient) if err != nil { return nil, fmt.Errorf("failed to logout from bot API: %w", err) } @@ -86,12 +95,12 @@ type botAPIResponse struct { Description string `json:"description"` } -func logoutBotAPI(ctx context.Context, token string) error { +func logoutBotAPI(ctx context.Context, token string, client *http.Client) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.telegram.org/bot"+token+"/logOut", nil) if err != nil { return fmt.Errorf("failed to prepare request: %w", err) } - resp, err := http.DefaultClient.Do(req) + resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to send request: %w", err) } diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go new file mode 100644 index 00000000..2ee361c5 --- /dev/null +++ b/pkg/connector/proxy.go @@ -0,0 +1,61 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2026 Vladislav Agarkov +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "fmt" + + "golang.org/x/net/proxy" + + "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs" +) + +func GetProxyDialFunc(cfg ProxyConfig) (dcs.DialFunc, error) { + switch cfg.Type { + // we can't proxy HTTP through mtproxy + case "disabled", "mtproxy": + return nil, nil + case "socks5": + var auth *proxy.Auth + if cfg.Username != "" && cfg.Password != "" { + auth = &proxy.Auth{User: cfg.Username, Password: cfg.Password} + } + sock5, err := proxy.SOCKS5("tcp", cfg.Address, auth, proxy.Direct) + if err != nil { + return nil, err + } + return sock5.(proxy.ContextDialer).DialContext, nil + default: + return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type) + } +} + +func GetProxyResolver(cfg ProxyConfig) (dcs.Resolver, error) { + switch cfg.Type { + case "disabled", "socks5": + dialer, err := GetProxyDialFunc(cfg) + if err != nil { + return nil, err + } + resolver := dcs.Plain(dcs.PlainOptions{Dial: dialer}) + return resolver, nil + case "mtproxy": + return dcs.MTProxy(cfg.Address, []byte(cfg.Password), dcs.MTProxyOptions{}) + default: + return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type) + } +}