Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c2116bb
feat: add browserDialer under sockopt and wire transports
Copilot Apr 26, 2026
2fdfa72
refactor: improve browser dialer instance handling and diagnostics
Copilot Apr 26, 2026
27b8b29
refactor: remove browser dialer env reload path and refresh proto met…
Copilot Apr 26, 2026
84d04d0
chore: warn legacy browser dialer env has been removed
Copilot Apr 26, 2026
2691a1a
feat: use path-based browser dialer csrf endpoint
Copilot Apr 26, 2026
c48c475
feat: require browserDialer path and multiplex by path
Copilot Apr 26, 2026
57253b7
refactor: use path plus header for browser dialer upgrade
Copilot Apr 26, 2026
8fca774
feat: require UUID path for sockopt browser dialer
Copilot Apr 26, 2026
9ad0997
refactor: simplify browser dialer UUID path validation
Copilot Apr 26, 2026
aeb6892
feat: validate and initialize browser dialer at config build
Copilot Apr 26, 2026
e06c536
refactor: tighten browser dialer UUID and config error handling
Copilot Apr 26, 2026
1d4250e
fix: normalize browser dialer UUID before parsing
Copilot Apr 26, 2026
5afd664
refactor: simplify browser dialer and remove added conf tests
Copilot Apr 26, 2026
4636ca2
fix: clean up dialer refactor review issues
Copilot Apr 26, 2026
12ecf47
fix: clean browser dialer path parsing guard
Copilot Apr 26, 2026
64f783f
fix: allow same-address browser dialer port reuse across outbounds
Copilot Apr 26, 2026
a54c54a
fix: enforce same-port different-address browser dialer rejection
Copilot Apr 26, 2026
5906445
fix: restrict xhttp browser dialer mode and remove added tests
Copilot Apr 26, 2026
1811935
chore: improve xhttp browser dialer validation errors
Copilot Apr 26, 2026
97ad6ce
chore: polish xhttp browser dialer mode validation messages
Copilot Apr 26, 2026
ca3cd5f
feat: add root browserDialers tags for dialerProxy integration
Copilot Apr 26, 2026
1d13700
chore: polish browserDialers xhttp runtime error client behavior
Copilot Apr 26, 2026
61c39a2
chore: align browserDialers runtime error wording
Copilot Apr 26, 2026
1cc7349
refactor: enforce browserDialers-only usage via dialerProxy tags
Copilot Apr 26, 2026
266ae17
refactor: simplify browser dialer static state and remove sockopt bro…
Copilot Apr 26, 2026
7a9c592
refactor: use browserDialers URL array and dialerProxy URL matching
Copilot Apr 26, 2026
9421ac0
refactor: further simplify browser dialer URL parsing path
Copilot Apr 26, 2026
e79f3a4
fix: normalize parsed browser dialer path with explicit leading slash
Copilot Apr 26, 2026
7416fd2
refactor: simplify browser dialer parsed path normalization
Copilot Apr 26, 2026
be9a229
refactor: switch browser dialer to browser:// dialerProxy collection
Copilot Apr 26, 2026
bb79f55
fix: tighten browser URL collection order and error handling
Copilot Apr 26, 2026
9f8f5c2
chore: polish browser dialer URL collection error message
Copilot Apr 26, 2026
2ecfbcf
chore: align browser dialer collection error style
Copilot Apr 26, 2026
3004e1e
refactor: use http:// dialerProxy scheme for browser dialer
Copilot Apr 26, 2026
f1f0d59
fix: require full valid browser dialer URL for http scheme
Copilot Apr 26, 2026
8939657
refactor: drop socket proto browser_dialer and remove dialer init locks
Copilot Apr 26, 2026
9d4dd2c
refactor: switch browser dialer scheme back to browser and trim wrappers
Copilot Apr 26, 2026
286a702
refactor: split browser dialer manager logic to keep dialer.go minimal
Copilot Apr 26, 2026
c423317
fix: split browser dialer configure and listener startup phases
Copilot Apr 27, 2026
ccac265
chore: make browser dialer listener startup idempotent
Copilot Apr 27, 2026
3a826b7
docs: clarify idempotent browser dialer listener startup
Copilot Apr 27, 2026
8043924
Move browser dialer start stop to instance lifecycle
Copilot Apr 27, 2026
41bc47b
Handle browser dialer cleanup errors on instance lifecycle
Copilot Apr 27, 2026
e088cc4
Preserve startup error when browser dialer cleanup fails
Copilot Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/xray.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/browser_dialer"
)

// Server is an instance of Xray. At any time, there must be at most one Server instance running.
Expand Down Expand Up @@ -262,6 +263,9 @@ func (s *Instance) Close() error {
s.running = false

var errs []interface{}
if err := browser_dialer.StopCollectedDialerProxyURLs(); err != nil {
errs = append(errs, err)
}
for _, f := range s.features {
if err := f.Close(); err != nil {
errs = append(errs, err)
Expand Down Expand Up @@ -385,9 +389,16 @@ func (s *Instance) Start() error {
s.statusLock.Lock()
defer s.statusLock.Unlock()

if err := browser_dialer.StartCollectedDialerProxyURLs(); err != nil {
return err
}
s.running = true
for _, f := range s.features {
if err := f.Start(); err != nil {
s.running = false
if stopErr := browser_dialer.StopCollectedDialerProxyURLs(); stopErr != nil {
return errors.New("browser dialer cleanup after startup failure also failed: ", stopErr).Base(err)
}
return err
}
}
Expand Down
30 changes: 30 additions & 0 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/browser_dialer"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/header/dns"
Expand Down Expand Up @@ -1970,6 +1971,35 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}
config.ProtocolName = protocol
}
if c.SocketSettings != nil && c.SocketSettings.DialerProxy != "" {
if browser_dialer.IsBrowserDialerProxy(c.SocketSettings.DialerProxy) {
if config.ProtocolName != "websocket" && config.ProtocolName != "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 ", c.SocketSettings.DialerProxy, " does not support REALITY")
}
if config.ProtocolName == "splithttp" {
splitHTTPSettings := c.SplitHTTPSettings
if c.XHTTPSettings != nil {
splitHTTPSettings = c.XHTTPSettings
}
if splitHTTPSettings != nil {
splitHTTPSettingsCopy := *splitHTTPSettings
hs, err := splitHTTPSettingsCopy.Build()
if err != nil {
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 ", c.SocketSettings.DialerProxy, " only supports XHTTP modes \"auto\" or \"packet-up\", got: \"", splitHTTPConfig.Mode, "\"")
}
}
}
if err := browser_dialer.RegisterDialerProxyURL(c.SocketSettings.DialerProxy); err != nil {
return nil, errors.New("failed to collect browser dialer URL").Base(err)
}
}
}

switch strings.ToLower(c.Security) {
case "", "none":
Expand Down
8 changes: 7 additions & 1 deletion infra/conf/xray.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/browser_dialer"
)

var (
Expand Down Expand Up @@ -437,7 +438,6 @@ func (c *Config) Override(o *Config, fn string) {
if o.Geodata != nil {
c.Geodata = o.Geodata
}

// 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 {
Expand Down Expand Up @@ -604,6 +604,9 @@ 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.BeginCollectingDialerProxyURLs(); err != nil {
return nil, err
}

for _, rawInboundConfig := range inbounds {
ic, err := rawInboundConfig.Build()
Expand All @@ -626,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
}
Expand Down
124 changes: 28 additions & 96 deletions transport/internet/browser_dialer/dialer.go
Original file line number Diff line number Diff line change
@@ -1,101 +1,33 @@
package browser_dialer

import (
"bytes"
"context"
_ "embed"
"encoding/base64"
"encoding/json"
"net/http"
"sync"
"time"

"github.com/gorilla/websocket"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/uuid"
)

//go:embed dialer.html
var webpage []byte

type task struct {
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 mu sync.Mutex

var upgrader = &websocket.Upgrader{
ReadBufferSize: 0,
WriteBufferSize: 0,
HandshakeTimeout: time.Second * 4,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

// Used by external projects when using xray as a go module
func Reload() {
addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
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
}
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()
}
}

func HasBrowserDialer() bool {
return conns != nil
Method string `json:"method"`
URL string `json:"url"`
Extra any `json:"extra,omitempty"`
StreamResponse bool `json:"streamResponse"`
}

type webSocketExtra struct {
Protocol string `json:"protocol,omitempty"`
}

func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
func DialWSWithAddress(addr string, uri string, ed []byte) (*websocket.Conn, error) {
task := task{
Method: "WS",
URL: uri,
Method: "WS",
URL: uri,
StreamResponse: true,
}

Expand All @@ -105,7 +37,7 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
}
}

return dialTask(task)
return dialTaskWithAddress(addr, task)
}

type httpExtra struct {
Expand Down Expand Up @@ -142,30 +74,26 @@ func httpExtraFromHeadersAndCookies(headers http.Header, cookies []*http.Cookie)
return &extra
}

func DialGet(uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) {
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)
}

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 {
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
}
Expand All @@ -184,12 +112,21 @@ func dialWithBody(method string, uri string, headers http.Header, cookies []*htt
return nil
}

func dialTask(task task) (*websocket.Conn, error) {
func dialTaskWithAddress(addr string, task task) (*websocket.Conn, error) {
data, err := json.Marshal(task)
if err != nil {
return nil, err
}

if addr == "" {
return nil, errors.New("browser dialer is not configured; set sockopt.dialerProxy to browser://host:port/uuid")
}
dialer, err := getDialerByAddress(addr)
if err != nil {
return nil, err
}
conns := dialer.conns

var conn *websocket.Conn
for {
conn = <-conns
Expand Down Expand Up @@ -218,8 +155,3 @@ func CheckOK(conn *websocket.Conn) error {

return nil
}

func init() {
Reload()
}

4 changes: 2 additions & 2 deletions transport/internet/browser_dialer/dialer.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// Enable a much more aggressive JIT for performance gains

// Copyright (c) 2021 XRAY. Mozilla Public License 2.0.
let url = "ws://" + window.location.host + "/websocket?token=csrfToken";
let url = "ws://" + window.location.host + "/dialerPath";
let clientIdleCount = 0;
let upstreamGetCount = 0;
let upstreamWsCount = 0;
Expand Down Expand Up @@ -67,7 +67,7 @@
}
clientIdleCount += 1;
console.log("Prepare", url);
let ws = new WebSocket(url);
let ws = new WebSocket(url, "browser-dialer");
// arraybuffer is significantly faster in chrome than default
// blob, tested with chrome 123
ws.binaryType = "arraybuffer";
Expand Down
Loading