mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-04 13:12:07 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user