From be9a229d688b19340792f686be3bedbd3adabe4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:46:22 +0000 Subject: [PATCH] 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> --- infra/conf/transport_internet.go | 11 +-- infra/conf/xray.go | 14 +-- transport/internet/browser_dialer/dialer.go | 90 +++++++++++-------- .../internet/browser_dialer/dialer_test.go | 22 ----- transport/internet/dialer.go | 4 +- transport/internet/splithttp/dialer.go | 2 +- transport/internet/websocket/dialer.go | 2 +- 7 files changed, 66 insertions(+), 79 deletions(-) delete mode 100644 transport/internet/browser_dialer/dialer_test.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 5057cdc0..3c3e10a2 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1972,12 +1972,12 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { config.ProtocolName = protocol } 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" { - 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") { - 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" { splitHTTPSettings := c.SplitHTTPSettings @@ -1988,13 +1988,14 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { splitHTTPSettingsCopy := *splitHTTPSettings hs, err := splitHTTPSettingsCopy.Build() 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" { - 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) } } diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 70936a0a..cb9367de 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -363,7 +363,6 @@ type Config struct { BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"` Version *VersionConfig `json:"version"` Geodata *GeodataConfig `json:"geodata"` - BrowserDialers []string `json:"browserDialers"` } func (c *Config) findInboundTag(tag string) int { @@ -439,10 +438,6 @@ func (c *Config) Override(o *Config, fn string) { if o.Geodata != nil { 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 if len(o.InboundConfigs) > 0 { for i := range o.InboundConfigs { @@ -609,14 +604,10 @@ func (c *Config) Build() (*core.Config, error) { if len(c.Transport) > 0 { 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 } - if err := browser_dialer.ConfigureDialers(c.BrowserDialers); err != nil { - return nil, errors.New("failed to configure browserDialers").Base(err) - } - for _, rawInboundConfig := range inbounds { ic, err := rawInboundConfig.Build() if err != nil { @@ -638,6 +629,9 @@ func (c *Config) Build() (*core.Config, error) { } 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 } diff --git a/transport/internet/browser_dialer/dialer.go b/transport/internet/browser_dialer/dialer.go index 1fba0975..27d5f86d 100644 --- a/transport/internet/browser_dialer/dialer.go +++ b/transport/internet/browser_dialer/dialer.go @@ -13,7 +13,6 @@ import ( pathlib "path" "strings" "sync" - "sync/atomic" "time" "github.com/gorilla/websocket" @@ -34,9 +33,9 @@ type task struct { var dialersByAddress = map[string]*dialerInstance{} var serversByListenAddr = map[string]*dialerServer{} -var configuredURLs atomic.Value var initMu sync.Mutex var initialized bool +var pendingURLs map[string]struct{} 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 { envAddress := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) if envAddress == "" { 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() defer initMu.Unlock() 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 { return err } - next := make(map[string]struct{}, len(urls)) - listenAddrByPort := make(map[string]string, len(urls)) - for _, browserDialerURL := range urls { - if browserDialerURL == "" { - return errors.New("browserDialers url cannot be empty") - } + pendingURLs = map[string]struct{}{} + return nil +} + +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) if !ok { - return errors.New("invalid browserDialers url: ", browserDialerURL) + return errors.New("invalid browser dialer url: ", browserDialerURL) } _, port, err := net.SplitHostPort(listenAddr) 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 { - 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 - next[browserDialerURL] = struct{}{} } for existingAddr := range serversByListenAddr { _, existingPort, splitErr := net.SplitHostPort(existingAddr) @@ -103,20 +118,19 @@ func ConfigureDialers(urls []string) error { continue } 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 { - 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 { 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 return nil } @@ -141,8 +155,8 @@ func parseBrowserDialerAddress(addr string) (string, string, bool) { return "", "", false } - parsedAddr, err := url.Parse("http://" + addr) - if err != nil || parsedAddr.Host == "" || parsedAddr.Path == "" || parsedAddr.RawQuery != "" || parsedAddr.Fragment != "" { + parsedAddr, err := url.Parse(addr) + if err != nil || !strings.EqualFold(parsedAddr.Scheme, "browser") || parsedAddr.Host == "" || parsedAddr.Path == "" || parsedAddr.RawQuery != "" || parsedAddr.Fragment != "" { return "", "", false } listenAddr := parsedAddr.Host @@ -247,23 +261,23 @@ func closeConnection(w http.ResponseWriter) { func getDialerByAddress(addr string) (*dialerInstance, error) { listenAddr, path, ok := parseBrowserDialerAddress(addr) if !ok { - return nil, errors.New("invalid browserDialers url: ", addr) + return nil, errors.New("invalid browser dialer url: ", addr) } key := listenAddr + path if dialer, found := dialersByAddress[key]; found { 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) { listenAddr, path, ok := parseBrowserDialerAddress(addr) if !ok { - return nil, errors.New("invalid browserDialers url: ", addr) + return nil, errors.New("invalid browser dialer url: ", addr) } _, port, err := net.SplitHostPort(listenAddr) 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 @@ -276,7 +290,7 @@ func ensureDialerWithAddress(addr string) (*dialerInstance, error) { for existingAddr := range serversByListenAddr { _, existingPort, splitErr := net.SplitHostPort(existingAddr) 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) @@ -399,7 +413,7 @@ func dialTaskWithAddress(addr string, task task) (*websocket.Conn, error) { } 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) if err != nil { diff --git a/transport/internet/browser_dialer/dialer_test.go b/transport/internet/browser_dialer/dialer_test.go deleted file mode 100644 index 9384306f..00000000 --- a/transport/internet/browser_dialer/dialer_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/transport/internet/dialer.go b/transport/internet/dialer.go index b0cf50d2..1a4fe468 100644 --- a/transport/internet/dialer.go +++ b/transport/internet/dialer.go @@ -270,8 +270,8 @@ func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig } if len(sockopt.DialerProxy) > 0 { - if browser_dialer.HasConfiguredURL(sockopt.DialerProxy) { - return nil, errors.New("dialerProxy url ", sockopt.DialerProxy, " is in browserDialers and only supports WebSocket or splithttp").AtError() + if browser_dialer.IsBrowserDialerProxy(sockopt.DialerProxy) { + return nil, errors.New("dialerProxy ", sockopt.DialerProxy, " only supports WebSocket or splithttp").AtError() } if obm == nil { return nil, errors.New("there is no outbound manager for dialerProxy").AtError() diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 052ac20f..9b556298 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -63,7 +63,7 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in realityConfig := reality.ConfigFromStreamSettings(streamSettings) browserDialer := "" if streamSettings.SocketSettings != nil { - if browser_dialer.HasConfiguredURL(streamSettings.SocketSettings.DialerProxy) { + if browser_dialer.IsBrowserDialerProxy(streamSettings.SocketSettings.DialerProxy) { browserDialer = streamSettings.SocketSettings.DialerProxy } } diff --git a/transport/internet/websocket/dialer.go b/transport/internet/websocket/dialer.go index 48ad45df..6641ed50 100644 --- a/transport/internet/websocket/dialer.go +++ b/transport/internet/websocket/dialer.go @@ -119,7 +119,7 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in browserDialer := "" if streamSettings.SocketSettings != nil { - if browser_dialer.HasConfiguredURL(streamSettings.SocketSettings.DialerProxy) { + if browser_dialer.IsBrowserDialerProxy(streamSettings.SocketSettings.DialerProxy) { browserDialer = streamSettings.SocketSettings.DialerProxy } }