diff --git a/internal/web/service/tgbot/tgbot.go b/internal/web/service/tgbot/tgbot.go index 4304c60cd..7e091f1a2 100644 --- a/internal/web/service/tgbot/tgbot.go +++ b/internal/web/service/tgbot/tgbot.go @@ -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 diff --git a/internal/web/service/tgbot/tgbot_test.go b/internal/web/service/tgbot/tgbot_test.go index 77f8202a4..f770cb321 100644 --- a/internal/web/service/tgbot/tgbot_test.go +++ b/internal/web/service/tgbot/tgbot_test.go @@ -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") } }