mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-25 22:06:09 +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:
@@ -0,0 +1,72 @@
|
||||
// Package netproxy builds HTTP clients that route the panel's own outbound
|
||||
// requests through an admin-configured proxy, used to reach GitHub and Telegram
|
||||
// from servers where those services are filtered.
|
||||
package netproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
// NewHTTPClient returns an *http.Client whose transport honors proxyURL.
|
||||
//
|
||||
// An empty proxyURL yields a plain client (unchanged behavior). socks5/socks5h
|
||||
// URLs are dialed through golang.org/x/net/proxy; http/https URLs use the
|
||||
// standard library proxy support. Any other scheme returns an error so callers
|
||||
// can log it and fall back to a direct connection.
|
||||
//
|
||||
// The proxy address is intentionally not subjected to SSRF filtering: it is
|
||||
// admin-configured and is commonly a loopback/private address (for example a
|
||||
// local Xray SOCKS inbound).
|
||||
func NewHTTPClient(proxyURL string, timeout time.Duration) (*http.Client, error) {
|
||||
if proxyURL == "" {
|
||||
return &http.Client{Timeout: timeout}, nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse proxy url: %w", err)
|
||||
}
|
||||
|
||||
transport := baseTransport()
|
||||
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "socks5", "socks5h":
|
||||
var auth *proxy.Auth
|
||||
if parsed.User != nil {
|
||||
password, _ := parsed.User.Password()
|
||||
auth = &proxy.Auth{User: parsed.User.Username(), Password: password}
|
||||
}
|
||||
dialer, err := proxy.SOCKS5("tcp", parsed.Host, auth, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create socks5 dialer: %w", err)
|
||||
}
|
||||
if contextDialer, ok := dialer.(proxy.ContextDialer); ok {
|
||||
transport.DialContext = contextDialer.DialContext
|
||||
} else {
|
||||
transport.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.Dial(network, addr)
|
||||
}
|
||||
}
|
||||
case "http", "https":
|
||||
transport.Proxy = http.ProxyURL(parsed)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy scheme %q", parsed.Scheme)
|
||||
}
|
||||
|
||||
return &http.Client{Timeout: timeout, Transport: transport}, nil
|
||||
}
|
||||
|
||||
func baseTransport() *http.Transport {
|
||||
if base, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
return base.Clone()
|
||||
}
|
||||
return &http.Transport{}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package netproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewHTTPClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
proxyURL string
|
||||
wantErr bool
|
||||
wantProxy bool
|
||||
wantDial bool
|
||||
}{
|
||||
{name: "empty returns direct client", proxyURL: ""},
|
||||
{name: "socks5 sets custom dialer", proxyURL: "socks5://127.0.0.1:1080", wantDial: true},
|
||||
{name: "socks5 with auth", proxyURL: "socks5://user:pass@127.0.0.1:1080", wantDial: true},
|
||||
{name: "http sets transport proxy", proxyURL: "http://127.0.0.1:8080", wantProxy: true},
|
||||
{name: "https sets transport proxy", proxyURL: "https://127.0.0.1:8080", wantProxy: true},
|
||||
{name: "unsupported scheme errors", proxyURL: "ftp://127.0.0.1:21", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, err := NewHTTPClient(tc.proxyURL, 5*time.Second)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for %q, got nil", tc.proxyURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for %q: %v", tc.proxyURL, err)
|
||||
}
|
||||
if client.Timeout != 5*time.Second {
|
||||
t.Errorf("timeout = %v, want 5s", client.Timeout)
|
||||
}
|
||||
if tc.wantProxy {
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok || transport.Proxy == nil {
|
||||
t.Errorf("expected transport with Proxy set for %q", tc.proxyURL)
|
||||
}
|
||||
}
|
||||
if tc.wantDial {
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok || transport.DialContext == nil {
|
||||
t.Errorf("expected transport with DialContext set for %q", tc.proxyURL)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user