Pārlūkot izejas kodu

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.
MHSanaei 12 stundas atpakaļ
vecāks
revīzija
bb18734c77

+ 16 - 11
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

+ 37 - 6
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")
 	}
 }