Compare commits

...
Author SHA1 Message Date
e088cc4a25 Preserve startup error when browser dialer cleanup fails
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b8c640ab-f93c-4609-8e34-a8e14e2be9e7

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-27 13:38:42 +00:00
41bc47b1ec Handle browser dialer cleanup errors on instance lifecycle
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b8c640ab-f93c-4609-8e34-a8e14e2be9e7

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-27 13:36:11 +00:00
8043924af5 Move browser dialer start stop to instance lifecycle
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b8c640ab-f93c-4609-8e34-a8e14e2be9e7

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-27 13:32:47 +00:00
3a826b7ece docs: clarify idempotent browser dialer listener startup
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/bd037532-6fba-48d4-bf72-77fbb3bf8fc4

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-27 11:55:25 +00:00
ccac265911 chore: make browser dialer listener startup idempotent
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/bd037532-6fba-48d4-bf72-77fbb3bf8fc4

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-27 11:52:48 +00:00
c423317305 fix: split browser dialer configure and listener startup phases
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/bd037532-6fba-48d4-bf72-77fbb3bf8fc4

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-27 11:50:15 +00:00
286a702bfa refactor: split browser dialer manager logic to keep dialer.go minimal
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/2f611863-296d-48df-b3c4-e02384132848

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 21:28:35 +00:00
9d4dd2c32f refactor: switch browser dialer scheme back to browser and trim wrappers
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/4df6450c-a247-4e14-951f-2859a7875fe3

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 21:18:00 +00:00
8939657016 refactor: drop socket proto browser_dialer and remove dialer init locks
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b6b773ed-3d59-4a25-b688-8032a496a8de

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 21:07:28 +00:00
f1f0d5970a fix: require full valid browser dialer URL for http scheme
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/de6da8b1-0d9b-4d56-a5a2-d0b06edc2965

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 20:56:05 +00:00
3004e1e399 refactor: use http:// dialerProxy scheme for browser dialer
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/de6da8b1-0d9b-4d56-a5a2-d0b06edc2965

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 20:54:10 +00:00
2ecfbcf1b6 chore: align browser dialer collection error style
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>
2026-04-26 20:50:38 +00:00
9f8f5c2591 chore: polish browser dialer URL collection error message
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>
2026-04-26 20:49:28 +00:00
bb79f554dd fix: tighten browser URL collection order and error handling
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>
2026-04-26 20:48:03 +00:00
be9a229d68 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>
2026-04-26 20:46:22 +00:00
7416fd225e refactor: simplify browser dialer parsed path normalization
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/56a85a3a-5e08-44ce-97d9-d441379c11cc

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 19:02:41 +00:00
e79f3a4361 fix: normalize parsed browser dialer path with explicit leading slash
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/56a85a3a-5e08-44ce-97d9-d441379c11cc

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 19:00:58 +00:00
9421ac0b13 refactor: further simplify browser dialer URL parsing path
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/56a85a3a-5e08-44ce-97d9-d441379c11cc

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 18:59:10 +00:00
7a9c592221 refactor: use browserDialers URL array and dialerProxy URL matching
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/6ed9ad08-e0b0-43d4-878b-7fe355be5325

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 18:49:21 +00:00
266ae17654 refactor: simplify browser dialer static state and remove sockopt browserDialer parsing
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/4875f50c-9a90-4d34-afbe-2e629296faa0

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 18:38:07 +00:00
1cc7349529 refactor: enforce browserDialers-only usage via dialerProxy tags
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/92209153-115a-4303-8c4e-5825c971881b

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 18:23:29 +00:00
61c39a2834 chore: align browserDialers runtime error wording
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/808be5b1-7ed2-4309-87f1-18a808d6aba4

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 17:57:52 +00:00
1d13700481 chore: polish browserDialers xhttp runtime error client behavior
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/808be5b1-7ed2-4309-87f1-18a808d6aba4

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 17:56:03 +00:00
ca3cd5fb88 feat: add root browserDialers tags for dialerProxy integration
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/808be5b1-7ed2-4309-87f1-18a808d6aba4

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 17:54:10 +00:00
97ad6cef43 chore: polish xhttp browser dialer mode validation messages
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/071df77b-69fb-4b1f-a14d-9dab447e1efa

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 17:20:23 +00:00
181193581b chore: improve xhttp browser dialer validation errors
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/071df77b-69fb-4b1f-a14d-9dab447e1efa

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 17:18:37 +00:00
5906445c68 fix: restrict xhttp browser dialer mode and remove added tests
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/071df77b-69fb-4b1f-a14d-9dab447e1efa

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 17:16:42 +00:00
a54c54a559 fix: enforce same-port different-address browser dialer rejection
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b21c3fc4-8476-4107-975a-9d921d55ffea

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:42:56 +00:00
64f783f2b0 fix: allow same-address browser dialer port reuse across outbounds
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b21c3fc4-8476-4107-975a-9d921d55ffea

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:39:27 +00:00
12ecf47fcc fix: clean browser dialer path parsing guard
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/3aee4c73-7847-433c-905a-2eafe5b1bfe8

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:27:13 +00:00
4636ca2675 fix: clean up dialer refactor review issues
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/3aee4c73-7847-433c-905a-2eafe5b1bfe8

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:25:25 +00:00
5afd664c8b refactor: simplify browser dialer and remove added conf tests
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/3aee4c73-7847-433c-905a-2eafe5b1bfe8

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:23:37 +00:00
1d4250e6f0 fix: normalize browser dialer UUID before parsing
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/d0035ff5-3633-402f-890e-e68c267a65c1

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:07:23 +00:00
e06c536ce6 refactor: tighten browser dialer UUID and config error handling
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/d0035ff5-3633-402f-890e-e68c267a65c1

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:05:57 +00:00
aeb689284c feat: validate and initialize browser dialer at config build
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/d0035ff5-3633-402f-890e-e68c267a65c1

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 16:03:45 +00:00
9ad099774a refactor: simplify browser dialer UUID path validation
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/9288cc3c-d788-49d9-8bdc-f03281c238bf

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 15:54:00 +00:00
8fca7745fb feat: require UUID path for sockopt browser dialer
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/f1db6008-e292-4b43-a2e0-0bc80567ebba

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 15:49:47 +00:00
57253b736d refactor: use path plus header for browser dialer upgrade
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/33541da0-fdc4-4415-b7c4-7183f01b762e

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 15:43:35 +00:00
c48c475256 feat: require browserDialer path and multiplex by path
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/d3d25e7b-e62f-49e0-90be-0ca7f974e115

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 15:20:37 +00:00
2691a1aa0e feat: use path-based browser dialer csrf endpoint
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/b6b47cc0-5a64-49d5-9447-22c9c202c95d

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 15:01:48 +00:00
84d04d0eab chore: warn legacy browser dialer env has been removed
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/d30a26b2-60aa-4f86-b2ed-3c6997652bfb

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 07:55:48 +00:00
27b8b29ef8 refactor: remove browser dialer env reload path and refresh proto metadata
Agent-Logs-Url: https://github.com/XTLS/Xray-core/sessions/e3502f01-e171-4e6f-9ae7-dc29b6682951

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-04-26 07:44:58 +00:00
2fdfa727b4 refactor: improve browser dialer instance handling and diagnostics
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>
2026-04-26 07:26:34 +00:00
c2116bb869 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>
2026-04-26 07:22:38 +00:00
10 changed files with 442 additions and 105 deletions
+11
View File
@@ -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.
@@ -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)
@@ -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
}
}
+30
View File
@@ -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"
@@ -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":
+7 -1
View File
@@ -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 (
@@ -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 {
@@ -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()
@@ -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
}
+28 -96
View File
@@ -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,
}
@@ -105,7 +37,7 @@ func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
}
}
return dialTask(task)
return dialTaskWithAddress(addr, task)
}
type httpExtra struct {
@@ -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
}
@@ -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
@@ -218,8 +155,3 @@ func CheckOK(conn *websocket.Conn) error {
return nil
}
func init() {
Reload()
}
@@ -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;
@@ -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";
@@ -0,0 +1,318 @@
package browser_dialer
import (
"bytes"
"context"
stderrors "errors"
"net"
"net/http"
"net/url"
pathlib "path"
"strings"
"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/serial"
"github.com/xtls/xray-core/common/uuid"
)
var dialersByAddress = map[string]*dialerInstance{}
var serversByListenAddr = map[string]*dialerServer{}
var initialized bool
var pendingURLs map[string]struct{}
const browserDialerSubprotocol = "browser-dialer"
var upgrader = &websocket.Upgrader{
ReadBufferSize: 0,
WriteBufferSize: 0,
HandshakeTimeout: time.Second * 4,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func CheckLegacyEnv() error {
envAddress := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" })
if envAddress == "" {
return nil
}
return errors.PrintRemovedFeatureError("env "+platform.BrowserDialerAddress, "sockopt.dialerProxy with browser://host:port/uuid")
}
func IsBrowserDialerProxy(raw string) bool {
_, _, ok := parseBrowserDialerAddress(raw)
return ok
}
func BeginCollectingDialerProxyURLs() error {
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
}
pendingURLs = map[string]struct{}{}
return nil
}
func RegisterDialerProxyURL(raw string) error {
if !IsBrowserDialerProxy(raw) {
return nil
}
if pendingURLs == nil {
return errors.New("browser dialer url collection is not initialized")
}
pendingURLs[raw] = struct{}{}
return nil
}
func ConfigureCollectedDialerProxyURLs() error {
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 browser dialer url: ", browserDialerURL)
}
_, port, err := net.SplitHostPort(listenAddr)
if err != nil {
return errors.New("invalid browser dialer listen address: ", listenAddr)
}
if existingAddr, found := listenAddrByPort[port]; found && existingAddr != listenAddr {
return errors.New("browser dialer cannot use the same port with a different listen address: ", existingAddr, " and ", listenAddr)
}
listenAddrByPort[port] = listenAddr
}
for existingAddr := range serversByListenAddr {
_, existingPort, splitErr := net.SplitHostPort(existingAddr)
if splitErr != nil {
continue
}
if newAddr, found := listenAddrByPort[existingPort]; found && newAddr != existingAddr {
return errors.New("browser dialer cannot use the same port with a different listen address: ", existingAddr, " and ", newAddr)
}
}
for browserDialerURL := range pendingURLs {
if _, err := ensureDialerWithAddress(browserDialerURL); err != nil {
return errors.New("failed to initialize browser dialer listener for url ", browserDialerURL).Base(err)
}
}
return nil
}
// StartCollectedDialerProxyURLs starts listeners for browser dialer URLs
// prepared by ConfigureCollectedDialerProxyURLs.
// Subsequent calls after successful initialization return nil.
func StartCollectedDialerProxyURLs() error {
if initialized {
return nil
}
for listenAddr, server := range serversByListenAddr {
if err := server.start(); err != nil {
return errors.New("failed to start browser dialer listener on ", listenAddr).Base(err)
}
}
initialized = true
return nil
}
func StopCollectedDialerProxyURLs() error {
var stopErrs []string
for listenAddr, server := range serversByListenAddr {
if err := server.stop(); err != nil {
stopErrs = append(stopErrs, serial.Concat("failed to stop browser dialer listener on ", listenAddr, ": ", err))
}
}
dialersByAddress = map[string]*dialerInstance{}
serversByListenAddr = map[string]*dialerServer{}
pendingURLs = nil
initialized = false
if len(stopErrs) > 0 {
return errors.New(strings.Join(stopErrs, "; "))
}
return nil
}
type dialerInstance struct {
conns chan *websocket.Conn
page []byte
}
type dialerServer struct {
server *http.Server
pageRoutes map[string]*dialerInstance
started bool
}
func parseBrowserDialerAddress(addr string) (string, string, bool) {
if addr == "" {
return "", "", false
}
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
if _, _, err := net.SplitHostPort(listenAddr); err != nil {
return "", "", false
}
path := strings.TrimSuffix(parsedAddr.Path, "/")
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
cleanPath := pathlib.Clean(path)
if cleanPath == "." || cleanPath == "/" || cleanPath != path {
return "", "", false
}
if strings.Count(cleanPath, "/") != 1 {
return "", "", false
}
id := strings.TrimPrefix(cleanPath, "/")
id = strings.ToLower(id)
parsedUUID, err := uuid.ParseString(id)
if err != nil || parsedUUID.String() != id {
return "", "", false
}
return listenAddr, "/" + id, true
}
func newDialerServer(listenAddr string) (*dialerServer, error) {
dialer := &dialerServer{
pageRoutes: make(map[string]*dialerInstance),
}
dialer.server = &http.Server{
Addr: listenAddr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pageDialer := dialer.pageRoutes[r.URL.Path]
if pageDialer != nil && websocket.IsWebSocketUpgrade(r) {
ok := false
for _, protocol := range websocket.Subprotocols(r) {
if protocol == browserDialerSubprotocol {
ok = true
break
}
}
if !ok {
closeConnection(w)
return
}
if conn, err := upgrader.Upgrade(w, r, http.Header{"Sec-WebSocket-Protocol": []string{browserDialerSubprotocol}}); err == nil {
pageDialer.conns <- conn
} else {
errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error: ", err)
}
return
}
if pageDialer != nil {
w.Header().Set("Access-Control-Allow-Origin", "*")
if _, err := w.Write(pageDialer.page); err != nil {
errors.LogError(context.Background(), "Browser dialer http page write unexpected error: ", err)
}
return
}
closeConnection(w)
}),
}
return dialer, nil
}
func (d *dialerServer) start() error {
if d.started {
return nil
}
listener, err := net.Listen("tcp", d.server.Addr)
if err != nil {
return err
}
d.started = true
go func() {
if err := d.server.Serve(listener); err != nil && !stderrors.Is(err, http.ErrServerClosed) {
errors.LogError(context.Background(), "Browser dialer http server unexpected error on ", d.server.Addr, ": ", err)
}
}()
return nil
}
func (d *dialerServer) stop() error {
if !d.started {
return nil
}
d.started = false
return d.server.Close()
}
func closeConnection(w http.ResponseWriter) {
hijacker, ok := w.(http.Hijacker)
if !ok {
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
return
}
conn.Close()
}
func getDialerByAddress(addr string) (*dialerInstance, error) {
listenAddr, path, ok := parseBrowserDialerAddress(addr)
if !ok {
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 url: ", addr)
}
func ensureDialerWithAddress(addr string) (*dialerInstance, error) {
listenAddr, path, ok := parseBrowserDialerAddress(addr)
if !ok {
return nil, errors.New("invalid browser dialer url: ", addr)
}
_, port, err := net.SplitHostPort(listenAddr)
if err != nil {
return nil, errors.New("invalid browser dialer listen address: ", listenAddr)
}
key := listenAddr + path
if dialer, found := dialersByAddress[key]; found {
return dialer, nil
}
server, found := serversByListenAddr[listenAddr]
if !found {
for existingAddr := range serversByListenAddr {
_, existingPort, splitErr := net.SplitHostPort(existingAddr)
if splitErr == nil && existingPort == port {
return nil, errors.New("browser dialer cannot use the same port with a different listen address: ", existingAddr, " and ", listenAddr)
}
}
newServer, serverErr := newDialerServer(listenAddr)
if serverErr != nil {
return nil, serverErr
}
server = newServer
serversByListenAddr[listenAddr] = server
}
dialer := &dialerInstance{
conns: make(chan *websocket.Conn, 256),
page: bytes.ReplaceAll(webpage, []byte("dialerPath"), []byte(strings.TrimPrefix(path, "/"))),
}
dialersByAddress[key] = dialer
server.pageRoutes[path] = dialer
return dialer, nil
}
+4
View File
@@ -14,6 +14,7 @@ import (
"github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet/browser_dialer"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/pipe"
)
@@ -269,6 +270,9 @@ func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig
}
if len(sockopt.DialerProxy) > 0 {
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()
}
@@ -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
}
+31 -2
View File
@@ -40,6 +40,20 @@ type dialerConf struct {
*internet.MemoryStreamConfig
}
type errorDialerClient struct {
err error
}
func (c *errorDialerClient) IsClosed() bool { return true }
func (c *errorDialerClient) OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error) {
return nil, nil, nil, c.err
}
func (c *errorDialerClient) PostPacket(context.Context, string, string, string, buf.MultiBuffer) error {
return c.err
}
var (
globalDialerMap map[dialerConf]*XmuxManager
globalDialerAccess sync.Mutex
@@ -47,9 +61,24 @@ var (
func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (DialerClient, *XmuxClient) {
realityConfig := reality.ConfigFromStreamSettings(streamSettings)
browserDialer := ""
if streamSettings.SocketSettings != nil {
if browser_dialer.IsBrowserDialerProxy(streamSettings.SocketSettings.DialerProxy) {
browserDialer = streamSettings.SocketSettings.DialerProxy
}
}
if browser_dialer.HasBrowserDialer() && realityConfig == nil {
return &BrowserDialerClient{transportConfig: streamSettings.ProtocolSettings.(*Config)}, nil
if browserDialer != "" && realityConfig == nil {
transportConfig := streamSettings.ProtocolSettings.(*Config)
if transportConfig.Mode != "auto" && transportConfig.Mode != "packet-up" {
return &errorDialerClient{
err: errors.New("browserDialer with splithttp only supports modes \"auto\" or \"packet-up\", got: \"", transportConfig.Mode, "\""),
}, nil
}
return &BrowserDialerClient{
transportConfig: transportConfig,
browserDialer: browserDialer,
}, nil
}
globalDialerAccess.Lock()
+8 -2
View File
@@ -117,8 +117,14 @@ 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 {
if browser_dialer.IsBrowserDialerProxy(streamSettings.SocketSettings.DialerProxy) {
browserDialer = streamSettings.SocketSettings.DialerProxy
}
}
if browserDialer != "" {
conn, err := browser_dialer.DialWSWithAddress(browserDialer, uri, ed)
if err != nil {
return nil, err
}