fix(tgbot): resolve the panel egress bridge per connection

The bot read the panel-egress bridge once at start, so when Xray came up
after the bot (or Panel Outbound was set later) it kept dialing Telegram
directly until restarted - on a filtered host it never connected.

With no dedicated bot proxy, the fasthttp client now resolves the bridge on
every new connection and falls back to a direct dial when it is absent.
Raised in #6682.
This commit is contained in:
MHSanaei
2026-10-03 01:35:13 +02:00
parent 4aa9a382b8
commit bb18734c77
2 changed files with 53 additions and 17 deletions
+16 -11
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"embed"
"math/big"
"net"
"net/url"
"os"
"regexp"
@@ -342,15 +343,6 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
logger.Warning("Failed to get Telegram bot proxy URL:", err)
}
// Fall back to the panel-wide egress bridge when no dedicated bot proxy is
// set. Resolved once at bot start: if Xray comes up later, the bot keeps
// its direct connection until it is restarted.
if tgBotProxy == "" {
if egress := t.settingService.PanelEgressProxyURL(); egress != "" && isSupportedBotProxyScheme(egress) {
tgBotProxy = egress
}
}
// Get Telegram bot API server URL
tgBotAPIServer, err := t.settingService.GetTgBotAPIServer()
if err != nil {
@@ -410,7 +402,7 @@ func isSupportedBotProxyScheme(proxyUrl string) bool {
}
// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string, panelEgress func() string) *fasthttp.Client {
client := &fasthttp.Client{
// Connection timeouts
ReadTimeout: 30 * time.Second,
@@ -437,11 +429,24 @@ func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
} else {
client.Dial = fasthttpproxy.FasthttpHTTPDialer(proxyUrl)
}
} else if panelEgress != nil {
client.Dial = panelEgressDial(panelEgress)
}
return client
}
// panelEgressDial resolves the panel egress bridge per connection, so a bridge
// that comes up after bot start (Xray started later) is used without a restart.
func panelEgressDial(resolve func() string) fasthttp.DialFunc {
return func(addr string) (net.Conn, error) {
if bridge := resolve(); bridge != "" {
return fasthttpproxy.FasthttpSocksDialer(bridge)(addr)
}
return fasthttp.Dial(addr)
}
}
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
// Validate proxy URL if provided
@@ -467,7 +472,7 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel
}
// Create robust fasthttp client
client := t.createRobustFastHTTPClient(proxyUrl)
client := t.createRobustFastHTTPClient(proxyUrl, t.settingService.PanelEgressProxyURL)
// Build bot options
var options []telego.BotOption
+37 -6
View File
@@ -59,7 +59,7 @@ func recordingDialTarget(t *testing.T, n int) (addr string, got chan []byte) {
func TestTgbotProxyDialerSelectsHTTPForHTTPScheme(t *testing.T) {
addr, got := recordingDialTarget(t, len("CONNECT "))
tg := &Tgbot{}
client := tg.createRobustFastHTTPClient("http://" + addr)
client := tg.createRobustFastHTTPClient("http://"+addr, nil)
if client.Dial == nil {
t.Fatal("Dial must be set for an http:// proxy")
}
@@ -77,7 +77,7 @@ func TestTgbotProxyDialerSelectsHTTPForHTTPScheme(t *testing.T) {
func TestTgbotProxyDialerSelectsSOCKSForSocks5Scheme(t *testing.T) {
addr, got := recordingDialTarget(t, 1)
tg := &Tgbot{}
client := tg.createRobustFastHTTPClient("socks5://" + addr)
client := tg.createRobustFastHTTPClient("socks5://"+addr, nil)
if client.Dial == nil {
t.Fatal("Dial must be set for a socks5:// proxy")
}
@@ -92,11 +92,42 @@ func TestTgbotProxyDialerSelectsSOCKSForSocks5Scheme(t *testing.T) {
}
}
func TestTgbotProxyDialerNoneWhenEmpty(t *testing.T) {
func TestTgbotPanelEgressBridgeAppearingAfterStartIsUsed(t *testing.T) {
addr, got := recordingDialTarget(t, 1)
bridge := ""
tg := &Tgbot{}
client := tg.createRobustFastHTTPClient("")
if client.Dial != nil {
t.Fatal("Dial must be nil when no proxy is configured")
client := tg.createRobustFastHTTPClient("", func() string { return bridge })
bridge = "socks5://" + addr
go func() { _, _ = client.Dial("example.com:443") }()
select {
case b := <-got:
if len(b) != 1 || b[0] != 0x05 {
t.Fatalf("expected SOCKS5 greeting (0x05) on the late bridge, got %v", b)
}
case <-time.After(3 * time.Second):
t.Fatal("bridge that came up after bot start never received a connection")
}
}
func TestTgbotPanelEgressDialsDirectWithoutBridge(t *testing.T) {
addr, got := recordingDialTarget(t, 1)
tg := &Tgbot{}
client := tg.createRobustFastHTTPClient("", func() string { return "" })
conn, err := client.Dial(addr)
if err != nil {
t.Fatalf("direct dial: %v", err)
}
defer conn.Close()
if _, err := conn.Write([]byte{0x42}); err != nil {
t.Fatalf("write: %v", err)
}
select {
case b := <-got:
if len(b) != 1 || b[0] != 0x42 {
t.Fatalf("expected the payload byte on a direct connection, got %v", b)
}
case <-time.After(3 * time.Second):
t.Fatal("target never received the direct connection")
}
}