From c2116bb8698ee461c5e9386ac2d990eb625a4b4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:22:38 +0000 Subject: [PATCH] feat: add browserDialer under sockopt and wire transports Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/56665ec5-84ea-4bc3-a812-2e699e0e880d Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- infra/conf/transport_internet.go | 2 + transport/internet/browser_dialer/dialer.go | 189 +++++++++++++----- transport/internet/config.pb.go | 15 +- transport/internet/config.proto | 2 + .../internet/splithttp/browser_client.go | 5 +- transport/internet/splithttp/dialer.go | 11 +- transport/internet/websocket/dialer.go | 8 +- 7 files changed, 169 insertions(+), 63 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 15d5cfbd..b06a8894 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1074,6 +1074,7 @@ type SocketConfig struct { AddressPortStrategy string `json:"addressPortStrategy"` HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"` TrustedXForwardedFor []string `json:"trustedXForwardedFor"` + BrowserDialer string `json:"browserDialer"` } // Build implements Buildable. @@ -1194,6 +1195,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) { AddressPortStrategy: addressPortStrategy, HappyEyeballs: happyEyeballs, TrustedXForwardedFor: c.TrustedXForwardedFor, + BrowserDialer: c.BrowserDialer, }, nil } diff --git a/transport/internet/browser_dialer/dialer.go b/transport/internet/browser_dialer/dialer.go index 53955bc4..d7cfb810 100644 --- a/transport/internet/browser_dialer/dialer.go +++ b/transport/internet/browser_dialer/dialer.go @@ -20,14 +20,15 @@ import ( var webpage []byte type task struct { - Method string `json:"method"` - URL string `json:"url"` - Extra any `json:"extra,omitempty"` - StreamResponse bool `json:"streamResponse"` + Method string `json:"method"` + URL string `json:"url"` + Extra any `json:"extra,omitempty"` + StreamResponse bool `json:"streamResponse"` } var conns chan *websocket.Conn var server *http.Server +var sockoptDialers map[string]*dialerInstance var mu sync.Mutex var upgrader = &websocket.Upgrader{ @@ -41,46 +42,18 @@ var upgrader = &websocket.Upgrader{ // Used by external projects when using xray as a go module func Reload() { - addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) + addr := getEnvAddress() mu.Lock() defer mu.Unlock() - if server != nil { - server.Close() - } - if HasBrowserDialer() { - for len(conns) > 0 { - select { - case c := <-conns: - c.Close() - default: - } - } - conns = nil - } + closeDialerInstance(&dialerInstance{conns: conns, server: server}) + conns = nil + server = nil + if addr != "" { - token := uuid.New() - csrfToken := token.String() - webpage := bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken)) - conns = make(chan *websocket.Conn, 256) - server = &http.Server{ - Addr: addr, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/websocket" { - if r.URL.Query().Get("token") == csrfToken { - if conn, err := upgrader.Upgrade(w, r, nil); err == nil { - conns <- conn - } else { - errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error") - } - } - } else { - w.Header().Set("Access-Control-Allow-Origin", "*"); - w.Write(webpage) - } - }), - } - go server.ListenAndServe() + dialer := newDialerInstance(addr) + conns = dialer.conns + server = dialer.server } } @@ -88,14 +61,92 @@ func HasBrowserDialer() bool { return conns != nil } +func HasBrowserDialerWithAddress(addr string) bool { + return connsByAddress(addr) != nil +} + type webSocketExtra struct { Protocol string `json:"protocol,omitempty"` } +type dialerInstance struct { + conns chan *websocket.Conn + server *http.Server +} + +func getEnvAddress() string { + return platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) +} + +func newDialerInstance(addr string) *dialerInstance { + token := uuid.New() + csrfToken := token.String() + page := bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken)) + dialer := &dialerInstance{ + conns: make(chan *websocket.Conn, 256), + } + dialer.server = &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/websocket" { + if r.URL.Query().Get("token") == csrfToken { + if conn, err := upgrader.Upgrade(w, r, nil); err == nil { + dialer.conns <- conn + } else { + errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error") + } + } + } else { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write(page) + } + }), + } + go dialer.server.ListenAndServe() + return dialer +} + +func closeDialerInstance(d *dialerInstance) { + if d == nil { + return + } + if d.server != nil { + d.server.Close() + } + for len(d.conns) > 0 { + select { + case c := <-d.conns: + c.Close() + default: + } + } +} + +func getDialerByAddress(addr string) *dialerInstance { + if addr == "" { + return nil + } + mu.Lock() + defer mu.Unlock() + if sockoptDialers == nil { + sockoptDialers = make(map[string]*dialerInstance) + } + if dialer, found := sockoptDialers[addr]; found { + return dialer + } + dialer := newDialerInstance(addr) + sockoptDialers[addr] = dialer + return dialer +} + func DialWS(uri string, ed []byte) (*websocket.Conn, error) { + return DialWSWithAddress("", uri, ed) +} + +func DialWSWithAddress(addr string, uri string, ed []byte) (*websocket.Conn, error) { task := task{ - Method: "WS", - URL: uri, + Method: "WS", + URL: uri, StreamResponse: true, } @@ -105,7 +156,7 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) { } } - return dialTask(task) + return dialTaskWithAddress(addr, task) } type httpExtra struct { @@ -143,29 +194,37 @@ func httpExtraFromHeadersAndCookies(headers http.Header, cookies []*http.Cookie) } func DialGet(uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) { + return DialGetWithAddress("", uri, headers, cookies) +} + +func DialGetWithAddress(addr string, uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) { task := task{ - Method: "GET", - URL: uri, - Extra: httpExtraFromHeadersAndCookies(headers, cookies), + Method: "GET", + URL: uri, + Extra: httpExtraFromHeadersAndCookies(headers, cookies), StreamResponse: true, } - return dialTask(task) + return dialTaskWithAddress(addr, task) } func DialPacket(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { - return dialWithBody(method, uri, headers, cookies, payload) + return DialPacketWithAddress("", method, uri, headers, cookies, payload) } -func dialWithBody(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { +func DialPacketWithAddress(addr string, method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { + return dialWithBody(addr, method, uri, headers, cookies, payload) +} + +func dialWithBody(addr string, method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { task := task{ - Method: method, - URL: uri, - Extra: httpExtraFromHeadersAndCookies(headers, cookies), + Method: method, + URL: uri, + Extra: httpExtraFromHeadersAndCookies(headers, cookies), StreamResponse: false, } - conn, err := dialTask(task) + conn, err := dialTaskWithAddress(addr, task) if err != nil { return err } @@ -185,11 +244,20 @@ func dialWithBody(method string, uri string, headers http.Header, cookies []*htt } func dialTask(task task) (*websocket.Conn, error) { + return dialTaskWithAddress("", task) +} + +func dialTaskWithAddress(addr string, task task) (*websocket.Conn, error) { data, err := json.Marshal(task) if err != nil { return nil, err } + conns := connsByAddress(addr) + if conns == nil { + return nil, errors.New("browser dialer is not configured") + } + var conn *websocket.Conn for { conn = <-conns @@ -219,7 +287,20 @@ func CheckOK(conn *websocket.Conn) error { return nil } +func connsByAddress(addr string) chan *websocket.Conn { + if addr != "" { + dialer := getDialerByAddress(addr) + if dialer == nil { + return nil + } + return dialer.conns + } + if HasBrowserDialer() { + return conns + } + return nil +} + func init() { Reload() } - diff --git a/transport/internet/config.pb.go b/transport/internet/config.pb.go index e2339fe8..2fea7a48 100644 --- a/transport/internet/config.pb.go +++ b/transport/internet/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.5 +// protoc v3.21.12 // source: transport/internet/config.proto package internet @@ -749,6 +749,7 @@ type SocketConfig struct { AddressPortStrategy AddressPortStrategy `protobuf:"varint,21,opt,name=address_port_strategy,json=addressPortStrategy,proto3,enum=xray.transport.internet.AddressPortStrategy" json:"address_port_strategy,omitempty"` HappyEyeballs *HappyEyeballsConfig `protobuf:"bytes,22,opt,name=happy_eyeballs,json=happyEyeballs,proto3" json:"happy_eyeballs,omitempty"` TrustedXForwardedFor []string `protobuf:"bytes,23,rep,name=trusted_x_forwarded_for,json=trustedXForwardedFor,proto3" json:"trusted_x_forwarded_for,omitempty"` + BrowserDialer string `protobuf:"bytes,24,opt,name=browser_dialer,json=browserDialer,proto3" json:"browser_dialer,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -944,6 +945,13 @@ func (x *SocketConfig) GetTrustedXForwardedFor() []string { return nil } +func (x *SocketConfig) GetBrowserDialer() string { + if x != nil { + return x.BrowserDialer + } + return "" +} + type HappyEyeballsConfig struct { state protoimpl.MessageState `protogen:"open.v1"` PrioritizeIpv6 bool `protobuf:"varint,1,opt,name=prioritize_ipv6,json=prioritizeIpv6,proto3" json:"prioritize_ipv6,omitempty"` @@ -1066,7 +1074,7 @@ const file_transport_internet_config_proto_rawDesc = "" + "\x05level\x18\x03 \x01(\tR\x05level\x12\x10\n" + "\x03opt\x18\x04 \x01(\tR\x03opt\x12\x14\n" + "\x05value\x18\x05 \x01(\tR\x05value\x12\x12\n" + - "\x04type\x18\x06 \x01(\tR\x04type\"\x89\t\n" + + "\x04type\x18\x06 \x01(\tR\x04type\"\xb0\t\n" + "\fSocketConfig\x12\x12\n" + "\x04mark\x18\x01 \x01(\x05R\x04mark\x12\x10\n" + "\x03tfo\x18\x02 \x01(\x05R\x03tfo\x12H\n" + @@ -1091,7 +1099,8 @@ const file_transport_internet_config_proto_rawDesc = "" + "\rcustomSockopt\x18\x14 \x03(\v2&.xray.transport.internet.CustomSockoptR\rcustomSockopt\x12`\n" + "\x15address_port_strategy\x18\x15 \x01(\x0e2,.xray.transport.internet.AddressPortStrategyR\x13addressPortStrategy\x12S\n" + "\x0ehappy_eyeballs\x18\x16 \x01(\v2,.xray.transport.internet.HappyEyeballsConfigR\rhappyEyeballs\x125\n" + - "\x17trusted_x_forwarded_for\x18\x17 \x03(\tR\x14trustedXForwardedFor\"/\n" + + "\x17trusted_x_forwarded_for\x18\x17 \x03(\tR\x14trustedXForwardedFor\x12%\n" + + "\x0ebrowser_dialer\x18\x18 \x01(\tR\rbrowserDialer\"/\n" + "\n" + "TProxyMode\x12\a\n" + "\x03Off\x10\x00\x12\n" + diff --git a/transport/internet/config.proto b/transport/internet/config.proto index ad23f047..79c22bf6 100644 --- a/transport/internet/config.proto +++ b/transport/internet/config.proto @@ -161,6 +161,8 @@ message SocketConfig { HappyEyeballsConfig happy_eyeballs = 22; repeated string trusted_x_forwarded_for = 23; + + string browser_dialer = 24; } message HappyEyeballsConfig { diff --git a/transport/internet/splithttp/browser_client.go b/transport/internet/splithttp/browser_client.go index a70447f2..7334d875 100644 --- a/transport/internet/splithttp/browser_client.go +++ b/transport/internet/splithttp/browser_client.go @@ -15,6 +15,7 @@ import ( // BrowserDialerClient implements splithttp.DialerClient in terms of browser dialer type BrowserDialerClient struct { transportConfig *Config + browserDialer string } func (c *BrowserDialerClient) IsClosed() bool { @@ -33,7 +34,7 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, sessio c.transportConfig.FillStreamRequest(request, sessionId, "") - conn, err := browser_dialer.DialGet(request.URL.String(), request.Header, request.Cookies()) + conn, err := browser_dialer.DialGetWithAddress(c.browserDialer, request.URL.String(), request.Header, request.Cookies()) dummyAddr := &net.IPAddr{} if err != nil { return nil, dummyAddr, dummyAddr, err @@ -62,7 +63,7 @@ func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, sessio } } - err = browser_dialer.DialPacket(method, request.URL.String(), request.Header, request.Cookies(), bytes) + err = browser_dialer.DialPacketWithAddress(c.browserDialer, method, request.URL.String(), request.Header, request.Cookies(), bytes) if err != nil { return err } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 6f4ec1d8..cda9cadb 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -47,9 +47,16 @@ var ( func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (DialerClient, *XmuxClient) { realityConfig := reality.ConfigFromStreamSettings(streamSettings) + browserDialer := "" + if streamSettings.SocketSettings != nil { + browserDialer = streamSettings.SocketSettings.BrowserDialer + } - if browser_dialer.HasBrowserDialer() && realityConfig == nil { - return &BrowserDialerClient{transportConfig: streamSettings.ProtocolSettings.(*Config)}, nil + if browser_dialer.HasBrowserDialerWithAddress(browserDialer) && realityConfig == nil { + return &BrowserDialerClient{ + transportConfig: streamSettings.ProtocolSettings.(*Config), + browserDialer: browserDialer, + }, nil } globalDialerAccess.Lock() diff --git a/transport/internet/websocket/dialer.go b/transport/internet/websocket/dialer.go index e5354908..99fbc877 100644 --- a/transport/internet/websocket/dialer.go +++ b/transport/internet/websocket/dialer.go @@ -117,8 +117,12 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in } uri := protocol + "://" + host + wsSettings.GetNormalizedPath() - if browser_dialer.HasBrowserDialer() { - conn, err := browser_dialer.DialWS(uri, ed) + browserDialer := "" + if streamSettings.SocketSettings != nil { + browserDialer = streamSettings.SocketSettings.BrowserDialer + } + if browser_dialer.HasBrowserDialerWithAddress(browserDialer) { + conn, err := browser_dialer.DialWSWithAddress(browserDialer, uri, ed) if err != nil { return nil, err }