refactor: switch browser dialer to browser:// dialerProxy collection

Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/84d72770-6ad0-447a-8d86-94d692972a05

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-26 20:46:22 +00:00
committed by GitHub
parent 7416fd225e
commit be9a229d68
7 changed files with 66 additions and 79 deletions

View File

@@ -1972,12 +1972,12 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config.ProtocolName = protocol config.ProtocolName = protocol
} }
if c.SocketSettings != nil && c.SocketSettings.DialerProxy != "" { if c.SocketSettings != nil && c.SocketSettings.DialerProxy != "" {
if browser_dialer.HasConfiguredURL(c.SocketSettings.DialerProxy) { if browser_dialer.IsBrowserDialerProxy(c.SocketSettings.DialerProxy) {
if config.ProtocolName != "websocket" && config.ProtocolName != "splithttp" { if config.ProtocolName != "websocket" && config.ProtocolName != "splithttp" {
return nil, errors.New("dialerProxy url ", c.SocketSettings.DialerProxy, " is in browserDialers and only supports websocket or splithttp") return nil, errors.New("dialerProxy ", c.SocketSettings.DialerProxy, " only supports websocket or splithttp")
} }
if strings.EqualFold(c.Security, "reality") { if strings.EqualFold(c.Security, "reality") {
return nil, errors.New("dialerProxy url ", c.SocketSettings.DialerProxy, " is in browserDialers and does not support REALITY") return nil, errors.New("dialerProxy ", c.SocketSettings.DialerProxy, " does not support REALITY")
} }
if config.ProtocolName == "splithttp" { if config.ProtocolName == "splithttp" {
splitHTTPSettings := c.SplitHTTPSettings splitHTTPSettings := c.SplitHTTPSettings
@@ -1988,13 +1988,14 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
splitHTTPSettingsCopy := *splitHTTPSettings splitHTTPSettingsCopy := *splitHTTPSettings
hs, err := splitHTTPSettingsCopy.Build() hs, err := splitHTTPSettingsCopy.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build XHTTP config for browserDialers validation.").Base(err) return nil, errors.New("failed to build XHTTP config for browser dialer validation").Base(err)
} }
if splitHTTPConfig, ok := hs.(*splithttp.Config); ok && splitHTTPConfig.Mode != "auto" && splitHTTPConfig.Mode != "packet-up" { if splitHTTPConfig, ok := hs.(*splithttp.Config); ok && splitHTTPConfig.Mode != "auto" && splitHTTPConfig.Mode != "packet-up" {
return nil, errors.New("dialerProxy url ", c.SocketSettings.DialerProxy, " is in browserDialers and only supports XHTTP modes \"auto\" or \"packet-up\", got: \"", splitHTTPConfig.Mode, "\"") return nil, errors.New("dialerProxy ", c.SocketSettings.DialerProxy, " only supports XHTTP modes \"auto\" or \"packet-up\", got: \"", splitHTTPConfig.Mode, "\"")
} }
} }
} }
browser_dialer.RegisterDialerProxyURL(c.SocketSettings.DialerProxy)
} }
} }

View File

@@ -363,7 +363,6 @@ type Config struct {
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"` BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
Version *VersionConfig `json:"version"` Version *VersionConfig `json:"version"`
Geodata *GeodataConfig `json:"geodata"` Geodata *GeodataConfig `json:"geodata"`
BrowserDialers []string `json:"browserDialers"`
} }
func (c *Config) findInboundTag(tag string) int { func (c *Config) findInboundTag(tag string) int {
@@ -439,10 +438,6 @@ func (c *Config) Override(o *Config, fn string) {
if o.Geodata != nil { if o.Geodata != nil {
c.Geodata = o.Geodata c.Geodata = o.Geodata
} }
if o.BrowserDialers != nil {
c.BrowserDialers = o.BrowserDialers
}
// update the Inbound in slice if the only one in override config has same tag // update the Inbound in slice if the only one in override config has same tag
if len(o.InboundConfigs) > 0 { if len(o.InboundConfigs) > 0 {
for i := range o.InboundConfigs { for i := range o.InboundConfigs {
@@ -609,14 +604,10 @@ func (c *Config) Build() (*core.Config, error) {
if len(c.Transport) > 0 { if len(c.Transport) > 0 {
return nil, errors.PrintRemovedFeatureError("Global transport config", "streamSettings in inbounds and outbounds") return nil, errors.PrintRemovedFeatureError("Global transport config", "streamSettings in inbounds and outbounds")
} }
if err := browser_dialer.CheckLegacyEnv(); err != nil { if err := browser_dialer.BeginCollectingDialerProxyURLs(); err != nil {
return nil, err return nil, err
} }
if err := browser_dialer.ConfigureDialers(c.BrowserDialers); err != nil {
return nil, errors.New("failed to configure browserDialers").Base(err)
}
for _, rawInboundConfig := range inbounds { for _, rawInboundConfig := range inbounds {
ic, err := rawInboundConfig.Build() ic, err := rawInboundConfig.Build()
if err != nil { if err != nil {
@@ -638,6 +629,9 @@ func (c *Config) Build() (*core.Config, error) {
} }
config.Outbound = append(config.Outbound, oc) config.Outbound = append(config.Outbound, oc)
} }
if err := browser_dialer.ConfigureCollectedDialerProxyURLs(); err != nil {
return nil, errors.New("failed to configure browser dialer").Base(err)
}
return config, nil return config, nil
} }

View File

@@ -13,7 +13,6 @@ import (
pathlib "path" pathlib "path"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -34,9 +33,9 @@ type task struct {
var dialersByAddress = map[string]*dialerInstance{} var dialersByAddress = map[string]*dialerInstance{}
var serversByListenAddr = map[string]*dialerServer{} var serversByListenAddr = map[string]*dialerServer{}
var configuredURLs atomic.Value
var initMu sync.Mutex var initMu sync.Mutex
var initialized bool var initialized bool
var pendingURLs map[string]struct{}
const browserDialerSubprotocol = "browser-dialer" const browserDialerSubprotocol = "browser-dialer"
@@ -49,53 +48,69 @@ var upgrader = &websocket.Upgrader{
}, },
} }
func HasConfiguredURL(url string) bool {
if url == "" {
return false
}
urls, _ := configuredURLs.Load().(map[string]struct{})
_, ok := urls[url]
return ok
}
func CheckLegacyEnv() error { func CheckLegacyEnv() error {
envAddress := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) envAddress := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
if envAddress == "" { if envAddress == "" {
return nil return nil
} }
return errors.PrintRemovedFeatureError("env "+platform.BrowserDialerAddress, "root browserDialers + sockopt.dialerProxy") return errors.PrintRemovedFeatureError("env "+platform.BrowserDialerAddress, "sockopt.dialerProxy with browser://host:port/uuid")
} }
func ConfigureDialers(urls []string) error { func IsBrowserDialerProxy(raw string) bool {
parsed, err := url.Parse(raw)
return err == nil && strings.EqualFold(parsed.Scheme, "browser")
}
func BeginCollectingDialerProxyURLs() error {
initMu.Lock() initMu.Lock()
defer initMu.Unlock() defer initMu.Unlock()
if initialized { if initialized {
return errors.New("browserDialers does not support dynamic add/remove; restart is required after changing configuration") return errors.New("browser dialer does not support dynamic add/remove; restart is required after changing configuration")
} }
if err := CheckLegacyEnv(); err != nil { if err := CheckLegacyEnv(); err != nil {
return err return err
} }
next := make(map[string]struct{}, len(urls)) pendingURLs = map[string]struct{}{}
listenAddrByPort := make(map[string]string, len(urls)) return nil
for _, browserDialerURL := range urls {
if browserDialerURL == "" {
return errors.New("browserDialers url cannot be empty")
} }
func RegisterDialerProxyURL(raw string) {
if !IsBrowserDialerProxy(raw) {
return
}
initMu.Lock()
defer initMu.Unlock()
if pendingURLs == nil {
pendingURLs = map[string]struct{}{}
}
pendingURLs[raw] = struct{}{}
}
func ConfigureCollectedDialerProxyURLs() error {
initMu.Lock()
defer initMu.Unlock()
if initialized {
return errors.New("browser dialer does not support dynamic add/remove; restart is required after changing configuration")
}
if err := CheckLegacyEnv(); err != nil {
return err
}
listenAddrByPort := make(map[string]string, len(pendingURLs))
for browserDialerURL := range pendingURLs {
listenAddr, _, ok := parseBrowserDialerAddress(browserDialerURL) listenAddr, _, ok := parseBrowserDialerAddress(browserDialerURL)
if !ok { if !ok {
return errors.New("invalid browserDialers url: ", browserDialerURL) return errors.New("invalid browser dialer url: ", browserDialerURL)
} }
_, port, err := net.SplitHostPort(listenAddr) _, port, err := net.SplitHostPort(listenAddr)
if err != nil { if err != nil {
return errors.New("invalid browserDialers listen address: ", listenAddr) return errors.New("invalid browser dialer listen address: ", listenAddr)
} }
if existingAddr, found := listenAddrByPort[port]; found && existingAddr != listenAddr { if existingAddr, found := listenAddrByPort[port]; found && existingAddr != listenAddr {
return errors.New("browserDialers cannot use the same port with a different listen address: ", existingAddr, " and ", listenAddr) return errors.New("browser dialer cannot use the same port with a different listen address: ", existingAddr, " and ", listenAddr)
} }
listenAddrByPort[port] = listenAddr listenAddrByPort[port] = listenAddr
next[browserDialerURL] = struct{}{}
} }
for existingAddr := range serversByListenAddr { for existingAddr := range serversByListenAddr {
_, existingPort, splitErr := net.SplitHostPort(existingAddr) _, existingPort, splitErr := net.SplitHostPort(existingAddr)
@@ -103,20 +118,19 @@ func ConfigureDialers(urls []string) error {
continue continue
} }
if newAddr, found := listenAddrByPort[existingPort]; found && newAddr != existingAddr { if newAddr, found := listenAddrByPort[existingPort]; found && newAddr != existingAddr {
return errors.New("browserDialers cannot use the same port with a different listen address: ", existingAddr, " and ", newAddr) return errors.New("browser dialer cannot use the same port with a different listen address: ", existingAddr, " and ", newAddr)
} }
} }
for browserDialerURL := range next { for browserDialerURL := range pendingURLs {
if err := EnsureDialerWithAddress(browserDialerURL); err != nil { if err := EnsureDialerWithAddress(browserDialerURL); err != nil {
return errors.New("failed to initialize browserDialers listener for url ", browserDialerURL).Base(err) return errors.New("failed to initialize browser dialer listener for url ", browserDialerURL).Base(err)
} }
} }
for listenAddr, server := range serversByListenAddr { for listenAddr, server := range serversByListenAddr {
if err := server.start(); err != nil { if err := server.start(); err != nil {
return errors.New("failed to start browserDialers listener on ", listenAddr).Base(err) return errors.New("failed to start browser dialer listener on ", listenAddr).Base(err)
} }
} }
configuredURLs.Store(next)
initialized = true initialized = true
return nil return nil
} }
@@ -141,8 +155,8 @@ func parseBrowserDialerAddress(addr string) (string, string, bool) {
return "", "", false return "", "", false
} }
parsedAddr, err := url.Parse("http://" + addr) parsedAddr, err := url.Parse(addr)
if err != nil || parsedAddr.Host == "" || parsedAddr.Path == "" || parsedAddr.RawQuery != "" || parsedAddr.Fragment != "" { if err != nil || !strings.EqualFold(parsedAddr.Scheme, "browser") || parsedAddr.Host == "" || parsedAddr.Path == "" || parsedAddr.RawQuery != "" || parsedAddr.Fragment != "" {
return "", "", false return "", "", false
} }
listenAddr := parsedAddr.Host listenAddr := parsedAddr.Host
@@ -247,23 +261,23 @@ func closeConnection(w http.ResponseWriter) {
func getDialerByAddress(addr string) (*dialerInstance, error) { func getDialerByAddress(addr string) (*dialerInstance, error) {
listenAddr, path, ok := parseBrowserDialerAddress(addr) listenAddr, path, ok := parseBrowserDialerAddress(addr)
if !ok { if !ok {
return nil, errors.New("invalid browserDialers url: ", addr) return nil, errors.New("invalid browser dialer url: ", addr)
} }
key := listenAddr + path key := listenAddr + path
if dialer, found := dialersByAddress[key]; found { if dialer, found := dialersByAddress[key]; found {
return dialer, nil return dialer, nil
} }
return nil, errors.New("browser dialer is not configured for browserDialers url: ", addr) return nil, errors.New("browser dialer is not configured for url: ", addr)
} }
func ensureDialerWithAddress(addr string) (*dialerInstance, error) { func ensureDialerWithAddress(addr string) (*dialerInstance, error) {
listenAddr, path, ok := parseBrowserDialerAddress(addr) listenAddr, path, ok := parseBrowserDialerAddress(addr)
if !ok { if !ok {
return nil, errors.New("invalid browserDialers url: ", addr) return nil, errors.New("invalid browser dialer url: ", addr)
} }
_, port, err := net.SplitHostPort(listenAddr) _, port, err := net.SplitHostPort(listenAddr)
if err != nil { if err != nil {
return nil, errors.New("invalid browserDialers listen address: ", listenAddr) return nil, errors.New("invalid browser dialer listen address: ", listenAddr)
} }
key := listenAddr + path key := listenAddr + path
@@ -276,7 +290,7 @@ func ensureDialerWithAddress(addr string) (*dialerInstance, error) {
for existingAddr := range serversByListenAddr { for existingAddr := range serversByListenAddr {
_, existingPort, splitErr := net.SplitHostPort(existingAddr) _, existingPort, splitErr := net.SplitHostPort(existingAddr)
if splitErr == nil && existingPort == port { if splitErr == nil && existingPort == port {
return nil, errors.New("browserDialers cannot use the same port with a different listen address: ", existingAddr, " and ", listenAddr) return nil, errors.New("browser dialer cannot use the same port with a different listen address: ", existingAddr, " and ", listenAddr)
} }
} }
newServer, serverErr := newDialerServer(listenAddr) newServer, serverErr := newDialerServer(listenAddr)
@@ -399,7 +413,7 @@ func dialTaskWithAddress(addr string, task task) (*websocket.Conn, error) {
} }
if addr == "" { if addr == "" {
return nil, errors.New("browser dialer is not configured; set root browserDialers and use sockopt.dialerProxy url") return nil, errors.New("browser dialer is not configured; set sockopt.dialerProxy to browser://host:port/uuid")
} }
dialer, err := getDialerByAddress(addr) dialer, err := getDialerByAddress(addr)
if err != nil { if err != nil {

View File

@@ -1,22 +0,0 @@
package browser_dialer
import "testing"
func TestParseBrowserDialerAddressRequireUUIDPath(t *testing.T) {
valid := "127.0.0.1:8080/123e4567-e89b-12d3-a456-426614174000"
if _, _, ok := parseBrowserDialerAddress(valid); !ok {
t.Fatalf("expected valid browser dialer address: %s", valid)
}
invalid := []string{
"127.0.0.1:8080/example",
"127.0.0.1:8080/short",
"127.0.0.1:8080/123e4567e89b12d3a456426614174000",
"127.0.0.1:8080/123e4567-e89b-12d3-a456-426614174000/extra",
}
for _, addr := range invalid {
if _, _, ok := parseBrowserDialerAddress(addr); ok {
t.Fatalf("expected invalid browser dialer address: %s", addr)
}
}
}

View File

@@ -270,8 +270,8 @@ func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig
} }
if len(sockopt.DialerProxy) > 0 { if len(sockopt.DialerProxy) > 0 {
if browser_dialer.HasConfiguredURL(sockopt.DialerProxy) { if browser_dialer.IsBrowserDialerProxy(sockopt.DialerProxy) {
return nil, errors.New("dialerProxy url ", sockopt.DialerProxy, " is in browserDialers and only supports WebSocket or splithttp").AtError() return nil, errors.New("dialerProxy ", sockopt.DialerProxy, " only supports WebSocket or splithttp").AtError()
} }
if obm == nil { if obm == nil {
return nil, errors.New("there is no outbound manager for dialerProxy").AtError() return nil, errors.New("there is no outbound manager for dialerProxy").AtError()

View File

@@ -63,7 +63,7 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in
realityConfig := reality.ConfigFromStreamSettings(streamSettings) realityConfig := reality.ConfigFromStreamSettings(streamSettings)
browserDialer := "" browserDialer := ""
if streamSettings.SocketSettings != nil { if streamSettings.SocketSettings != nil {
if browser_dialer.HasConfiguredURL(streamSettings.SocketSettings.DialerProxy) { if browser_dialer.IsBrowserDialerProxy(streamSettings.SocketSettings.DialerProxy) {
browserDialer = streamSettings.SocketSettings.DialerProxy browserDialer = streamSettings.SocketSettings.DialerProxy
} }
} }

View File

@@ -119,7 +119,7 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in
browserDialer := "" browserDialer := ""
if streamSettings.SocketSettings != nil { if streamSettings.SocketSettings != nil {
if browser_dialer.HasConfiguredURL(streamSettings.SocketSettings.DialerProxy) { if browser_dialer.IsBrowserDialerProxy(streamSettings.SocketSettings.DialerProxy) {
browserDialer = streamSettings.SocketSettings.DialerProxy browserDialer = streamSettings.SocketSettings.DialerProxy
} }
} }