mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 06:40:59 +00:00
feat(settings): panel network proxy for the panel's own outbound requests
Add a panelProxy setting that routes the panel's self-initiated HTTP requests (geo updates, Xray version/core download, panel update check) through an admin-configured socks5/http(s) proxy, to bypass server-side filtering of GitHub/Telegram. The Telegram bot falls back to it when tgBotProxy is empty (socks5 only). New util/netproxy.NewHTTPClient builds the proxied client. Also fix the Mixed-inbound SOCKS/HTTP share URLs that had host:port and user:pass in the wrong order, and consolidate the Telegram settings tab (move API server into the general tab, drop the empty Proxy & Server tab).
This commit is contained in:
@@ -29,6 +29,7 @@ type AllSetting struct {
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge" validate:"gte=0,lte=525600"` // Session maximum age in minutes (cap at one year)
|
||||
TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"` // Trusted reverse proxy IPs/CIDRs for forwarded headers
|
||||
PanelProxy string `json:"panelProxy" form:"panelProxy"` // Proxy URL for the panel's own outbound requests (GitHub/Telegram)
|
||||
|
||||
// UI settings
|
||||
PageSize int `json:"pageSize" form:"pageSize" validate:"gte=1,lte=1000"` // Number of items per page in lists
|
||||
|
||||
@@ -131,7 +131,7 @@ func (s *PanelService) StartUpdate() error {
|
||||
}
|
||||
|
||||
func downloadPanelUpdater() (string, error) {
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
client := (&SettingService{}).NewProxiedHTTPClient(15 * time.Second)
|
||||
resp, err := client.Get(panelUpdaterURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download panel updater: %w", err)
|
||||
@@ -169,7 +169,7 @@ func downloadPanelUpdater() (string, error) {
|
||||
}
|
||||
|
||||
func fetchLatestPanelVersion() (string, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := (&SettingService{}).NewProxiedHTTPClient(10 * time.Second)
|
||||
resp, err := client.Get("https://api.github.com/repos/MHSanaei/3x-ui/releases/latest")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -617,8 +617,6 @@ func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
||||
return s.emaCPU, nil
|
||||
}
|
||||
|
||||
var xrayVersionsClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
const (
|
||||
maxXrayArchiveBytes = 200 << 20
|
||||
maxXrayBinaryBytes = 200 << 20
|
||||
@@ -630,7 +628,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
bufferSize = 8192
|
||||
)
|
||||
|
||||
resp, err := xrayVersionsClient.Get(XrayURL)
|
||||
resp, err := s.settingService.NewProxiedHTTPClient(10 * time.Second).Get(XrayURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -729,7 +727,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
|
||||
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
|
||||
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1273,6 +1271,8 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
client := s.settingService.NewProxiedHTTPClient(0)
|
||||
|
||||
downloadFile := func(url, destPath string) error {
|
||||
var req *http.Request
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
@@ -1288,7 +1288,6 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/netproxy"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/reflect_util"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/entity"
|
||||
@@ -88,6 +90,7 @@ var defaultValueMap = map[string]string{
|
||||
"externalTrafficInformURI": "",
|
||||
"restartXrayOnClientDisable": "true",
|
||||
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
||||
"panelProxy": "",
|
||||
|
||||
// LDAP defaults
|
||||
"ldapEnable": "false",
|
||||
@@ -351,6 +354,31 @@ func (s *SettingService) SetTgBotProxy(token string) error {
|
||||
return s.setString("tgBotProxy", token)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetPanelProxy() (string, error) {
|
||||
return s.getString("panelProxy")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetPanelProxy(proxyUrl string) error {
|
||||
return s.setString("panelProxy", proxyUrl)
|
||||
}
|
||||
|
||||
// NewProxiedHTTPClient returns an HTTP client that routes the panel's own
|
||||
// outbound requests through the configured panelProxy setting. An invalid or
|
||||
// missing proxy falls back to a direct client so existing behavior is preserved.
|
||||
func (s *SettingService) NewProxiedHTTPClient(timeout time.Duration) *http.Client {
|
||||
proxyUrl, err := s.GetPanelProxy()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to read panel proxy setting:", err)
|
||||
proxyUrl = ""
|
||||
}
|
||||
client, err := netproxy.NewHTTPClient(proxyUrl, timeout)
|
||||
if err != nil {
|
||||
logger.Warningf("Invalid panel proxy %q, using direct connection: %v", proxyUrl, err)
|
||||
return &http.Client{Timeout: timeout}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgBotAPIServer() (string, error) {
|
||||
return s.getString("tgBotAPIServer")
|
||||
}
|
||||
|
||||
@@ -246,6 +246,17 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||
logger.Warning("Failed to get Telegram bot proxy URL:", err)
|
||||
}
|
||||
|
||||
// Fall back to the panel-wide proxy when no dedicated bot proxy is set.
|
||||
// The bot's fasthttp dialer only supports SOCKS5, so other schemes are ignored.
|
||||
if tgBotProxy == "" {
|
||||
panelProxy, perr := t.settingService.GetPanelProxy()
|
||||
if perr != nil {
|
||||
logger.Warning("Failed to get panel proxy URL:", perr)
|
||||
} else if strings.HasPrefix(panelProxy, "socks5://") {
|
||||
tgBotProxy = panelProxy
|
||||
}
|
||||
}
|
||||
|
||||
// Get Telegram bot API server URL
|
||||
tgBotAPIServer, err := t.settingService.GetTgBotAPIServer()
|
||||
if err != nil {
|
||||
|
||||
@@ -686,6 +686,8 @@
|
||||
"panelUrlPathDesc": "The URI path for the web panel. (begins with ‘/‘ and concludes with ‘/‘)",
|
||||
"pageSize": "Pagination Size",
|
||||
"pageSizeDesc": "Define page size for inbounds table. (0 = disable)",
|
||||
"panelProxy": "Panel Network Proxy",
|
||||
"panelProxyDesc": "Routes the panel's own outbound requests (geo updates, Xray/panel version checks, Telegram) through this proxy to bypass server-side filtering of GitHub/Telegram. Accepts socks5:// or http(s)://, e.g. a local Xray SOCKS inbound. Leave empty for a direct connection.",
|
||||
"remarkModel": "Remark Model & Separation Character",
|
||||
"datepicker": "Calendar Type",
|
||||
"datepickerPlaceholder": "Select date",
|
||||
|
||||
@@ -621,6 +621,8 @@
|
||||
"panelUrlPathDesc": "برای وب پنل. با '/' شروع و با '/' خاتمه مییابد URI مسیر",
|
||||
"pageSize": "اندازه صفحه بندی جدول",
|
||||
"pageSizeDesc": "(اندازه صفحه برای جدول ورودیها.(0 = غیرفعال",
|
||||
"panelProxy": "پراکسی شبکهی پنل",
|
||||
"panelProxyDesc": "درخواستهای خروجیِ خودِ پنل (آپدیت geo، چک نسخهی Xray و پنل، تلگرام) را از این پراکسی عبور میدهد تا فیلترینگ سروری گیتهاب/تلگرام دور زده شود. پشتیبانی از socks5:// و http(s)://، برای نمونه یک اینباند SOCKS لوکالِ Xray. برای اتصال مستقیم خالی بگذارید.",
|
||||
"remarkModel": "نامکانفیگ و جداکننده",
|
||||
"datepicker": "نوع تقویم",
|
||||
"datepickerPlaceholder": "انتخاب تاریخ",
|
||||
|
||||
Reference in New Issue
Block a user