From a3c054a54c21a49013e24880a6c8bc2138d75c28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 19:37:25 +0000 Subject: [PATCH 01/78] Bump golang.org/x/net from 0.53.0 to 0.54.0 (#6113) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0. - [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index cd9a22a1e..1177edd27 100644 --- a/go.mod +++ b/go.mod @@ -21,11 +21,11 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.50.0 + golang.org/x/crypto v0.51.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/net v0.53.0 + golang.org/x/net v0.54.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.43.0 + golang.org/x/sys v0.44.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v1.0.1 @@ -46,10 +46,10 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.43.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b10a8085e..8a332d6ad 100644 --- a/go.sum +++ b/go.sum @@ -88,18 +88,18 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= @@ -111,21 +111,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 5488b86c6723188982eb392891de98285bfeae27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 19:37:44 +0000 Subject: [PATCH 02/78] Bump google.golang.org/grpc from 1.81.0 to 1.81.1 (#6133) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.0 to 1.81.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.81.0...v1.81.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.81.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1177edd27..283c72531 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v1.0.1 - google.golang.org/grpc v1.81.0 + google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h12.io/socks v1.0.3 diff --git a/go.sum b/go.sum index 8a332d6ad..754a9524c 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From da9ba693cb0604d877aca6367ac9a1cd5ee43840 Mon Sep 17 00:00:00 2001 From: Yury Kastov <9641779+kastov@users.noreply.github.com> Date: Fri, 22 May 2026 22:49:19 +0300 Subject: [PATCH 03/78] Config: Support abstract unix sockets in remote loader (#6111) Replaces https://github.com/XTLS/Xray-core/pull/5200 --- app/router/webhook.go | 50 +----------- common/utils/unixsocket.go | 55 +++++++++++++ main/confloader/external/external.go | 117 ++++++++++++++------------- 3 files changed, 121 insertions(+), 101 deletions(-) create mode 100644 common/utils/unixsocket.go diff --git a/app/router/webhook.go b/app/router/webhook.go index 32ae28872..535ed294f 100644 --- a/app/router/webhook.go +++ b/app/router/webhook.go @@ -7,60 +7,16 @@ import ( "io" "net" "net/http" - "path/filepath" - "runtime" - "strings" "sync" - "syscall" "time" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/features/routing" routing_session "github.com/xtls/xray-core/features/routing/session" ) -// parseURL splits a webhook URL into an HTTP URL and an optional Unix socket -// path. For regular http/https URLs the input is returned unchanged with an -// empty socketPath. For Unix sockets the format is: -// -// /path/to/socket.sock:/http/path -// @abstract:/http/path -// @@padded:/http/path -// -// The :/ separator after the socket path delimits the HTTP request path. -// If omitted, "/" is used. -func parseURL(raw string) (httpURL, socketPath string) { - if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { - return raw, "" - } - if idx := strings.Index(raw, ":/"); idx >= 0 { - return "http://localhost" + raw[idx+1:], raw[:idx] - } - return "http://localhost/", raw -} -// resolveSocketPath applies platform-specific transformations to a Unix -// socket path, matching the behaviour of the listen side in -// transport/internet/system_listener.go. -// -// For abstract sockets (prefix @) on Linux/Android: -// - single @ — used as-is (lock-free abstract socket) -// - double @@ — stripped to single @ and padded to -// syscall.RawSockaddrUnix{}.Path length (HAProxy compat) -func resolveSocketPath(path string) string { - if len(path) == 0 || path[0] != '@' { - return path - } - if runtime.GOOS != "linux" && runtime.GOOS != "android" { - return path - } - if len(path) > 1 && path[1] == '@' { - fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) - copy(fullAddr, path[1:]) - return string(fullAddr) - } - return path -} func ptr[T any](v T) *T { return &v } @@ -96,7 +52,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) { return nil, nil } - httpURL, socketPath := parseURL(cfg.Url) + httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url) h := &WebhookNotifier{ url: httpURL, deduplication: cfg.Deduplication, @@ -107,7 +63,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) { } if socketPath != "" { - dialAddr := resolveSocketPath(socketPath) + dialAddr := utils.ResolveSocketPath(socketPath) h.client.Transport = &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { var d net.Dialer diff --git a/common/utils/unixsocket.go b/common/utils/unixsocket.go new file mode 100644 index 000000000..52d36256e --- /dev/null +++ b/common/utils/unixsocket.go @@ -0,0 +1,55 @@ +package utils + +import ( + "path/filepath" + "runtime" + "strings" + "syscall" +) + +// ResolveSocketPath applies platform-specific transformations to a Unix +// socket path, matching the listen-side behaviour in +// transport/internet/system_listener.go. +// +// For abstract sockets (prefix @) on Linux/Android: +// - single @ — used as-is (lock-free abstract socket) +// - double @@ — stripped to single @ and padded to +// syscall.RawSockaddrUnix{}.Path length (HAProxy compat) +// +// Filesystem paths and abstract sockets on other platforms are returned +// unchanged. +func ResolveSocketPath(path string) string { + if len(path) == 0 || path[0] != '@' { + return path + } + if runtime.GOOS != "linux" && runtime.GOOS != "android" { + return path + } + if len(path) > 1 && path[1] == '@' { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) + copy(fullAddr, path[1:]) + return string(fullAddr) + } + return path +} + +// SplitHTTPUnixURL splits a target into an HTTP URL and an optional Unix +// socket path. For regular http(s) URLs the input is returned unchanged +// with an empty socketPath. For Unix sockets the format is: +// +// /path/to/socket.sock[:/http/path] +// @abstract[:/http/path] +// @@padded[:/http/path] +// +// The :/ separator delimits the socket path from the HTTP request path. +// If omitted, "/" is used. +func SplitHTTPUnixURL(raw string) (httpURL, socketPath string) { + if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { + return raw, "" + } + if idx := strings.Index(raw, ":/"); idx >= 0 { + return "http://localhost" + raw[idx+1:], raw[:idx] + } + return "http://localhost/", raw +} + diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index 110b483da..f9ea874c8 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -3,8 +3,8 @@ package external import ( "bytes" "context" - "net" "io" + "net" "net/http" "net/url" "os" @@ -13,6 +13,7 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/main/confloader" ) @@ -20,9 +21,10 @@ func ConfigLoader(arg string) (out io.Reader, err error) { var data []byte switch { case strings.HasPrefix(arg, "http+unix://"): - data, err = FetchUnixSocketHTTPContent(arg) + errors.PrintDeprecatedFeatureWarning(`"http+unix://" prefix`, `direct Unix socket path (e.g. /path/socket.sock:/api or @abstract:/api)`) + data, err = FetchHTTPContent(httpUnixToCanonical(arg)) - case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): + case isRemoteSource(arg): data, err = FetchHTTPContent(arg) case arg == "stdin:": @@ -39,19 +41,38 @@ func ConfigLoader(arg string) (out io.Reader, err error) { return } +// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL +// or a Unix socket HTTP endpoint. +// +// http(s)://host/api regular HTTP(S) +// /path/to/socket.sock[:/api] filesystem socket +// @abstract[:/api] abstract socket (Linux/Android) +// @@padded[:/api] padded abstract socket (HAProxy compat) +// +// When the ":/" separator is omitted on a socket target, the request is +// made to "/". func FetchHTTPContent(target string) ([]byte, error) { - parsedTarget, err := url.Parse(target) + httpURL, socketPath := utils.SplitHTTPUnixURL(target) + + parsedTarget, err := url.Parse(httpURL) if err != nil { return nil, errors.New("invalid URL: ", target).Base(err) } - if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { - return nil, errors.New("invalid scheme: ", parsedTarget.Scheme) - } - client := &http.Client{ Timeout: 30 * time.Second, } + + if socketPath != "" { + dialAddr := utils.ResolveSocketPath(socketPath) + client.Transport = &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", dialAddr) + }, + } + } + resp, err := client.Do(&http.Request{ Method: "GET", URL: parsedTarget, @@ -74,58 +95,46 @@ func FetchHTTPContent(target string) ([]byte, error) { return content, nil } -// Format: http+unix:///path/to/socket.sock/api/endpoint -func FetchUnixSocketHTTPContent(target string) ([]byte, error) { - path := strings.TrimPrefix(target, "http+unix://") - - if !strings.HasPrefix(path, "/") { - return nil, errors.New("unix socket path must be absolute") + +// isRemoteSource reports whether arg should be fetched via HTTP (regular +// network or Unix socket) rather than read from the local filesystem. +// Recognized forms: +// +// - http(s)://... regular HTTP(S) +// - @abstract[:/api] abstract socket (Linux/Android) +// - /abs/path:/api filesystem socket, explicit HTTP path +// - /abs/path filesystem socket detected via os.ModeSocket +func isRemoteSource(arg string) bool { + if arg == "" { + return false } - - var socketPath, httpPath string - - sockIdx := strings.Index(path, ".sock") - if sockIdx != -1 { - socketPath = path[:sockIdx+5] - httpPath = path[sockIdx+5:] - if httpPath == "" { - httpPath = "/" - } - } else { - return nil, errors.New("cannot determine socket path, socket file should have .sock extension") + if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + return true } - - if _, err := os.Stat(socketPath); err != nil { - return nil, errors.New("socket file not found: ", socketPath).Base(err) + if arg[0] == '@' { + return true } - - client := &http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, "unix", socketPath) - }, - }, + if arg[0] != '/' { + return false } - defer client.CloseIdleConnections() - - resp, err := client.Get("http://localhost" + httpPath) - if err != nil { - return nil, errors.New("failed to fetch from unix socket: ", socketPath).Base(err) + if strings.Contains(arg, ":/") { + return true } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, errors.New("unexpected HTTP status code: ", resp.StatusCode) + info, err := os.Stat(arg) + return err == nil && info.Mode()&os.ModeSocket != 0 +} + + +// httpUnixToCanonical converts the deprecated http+unix:///path/to/socket.sock/api +// URL into the canonical /path/to/socket.sock:/api form by inserting ":" +// between the ".sock" extension and the HTTP path. Inputs without a path +// after ".sock" are returned with just the "http+unix://" prefix stripped. +func httpUnixToCanonical(target string) string { + raw := strings.TrimPrefix(target, "http+unix://") + if i := strings.Index(raw, ".sock/"); i >= 0 { + raw = raw[:i+5] + ":" + raw[i+5:] } - - content, err := buf.ReadAllToBytes(resp.Body) - if err != nil { - return nil, errors.New("failed to read response").Base(err) - } - - return content, nil + return raw } func init() { From 359a28f8765ad230422ae62e34667c9d9b1a58c0 Mon Sep 17 00:00:00 2001 From: bytecategory Date: Sat, 23 May 2026 17:24:37 +0800 Subject: [PATCH 04/78] XDNS finalmask: Support AAAA & A (#6123) https://github.com/XTLS/Xray-core/pull/6123#issuecomment-4439994266 https://github.com/XTLS/Xray-core/pull/6123#issuecomment-4441819696 https://github.com/XTLS/Xray-core/pull/6123#issuecomment-4522352460 Example: https://github.com/XTLS/Xray-core/pull/6123#issue-4436283689 Document: https://xtls.github.io/config/transports/finalmask.html#xdns --- transport/internet/finalmask/xdns/client.go | 55 ++--- transport/internet/finalmask/xdns/dns.go | 6 + transport/internet/finalmask/xdns/dns_test.go | 110 +++++++++ .../finalmask/xdns/record_transport.go | 226 ++++++++++++++++++ transport/internet/finalmask/xdns/server.go | 153 ++++-------- transport/internet/finalmask/xdns/spec.go | 80 +++++++ 6 files changed, 493 insertions(+), 137 deletions(-) create mode 100644 transport/internet/finalmask/xdns/record_transport.go create mode 100644 transport/internet/finalmask/xdns/spec.go diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 7ceb7c50d..7513a0a9d 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -10,7 +10,6 @@ import ( "io" "net" "strconv" - "strings" "sync" "sync/atomic" "time" @@ -40,6 +39,7 @@ type xdnsConnClient struct { net.PacketConn resolverAddrs []*net.UDPAddr + resolverTypes []uint16 resolverIdx uint32 resolverSend map[string]*atomic.Uint32 @@ -61,17 +61,15 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { var domains []Name var servers []string + var resolverTypes []uint16 for _, rs := range c.Resolvers { - parts := strings.Split(rs, "+udp://") - if len(parts) != 2 { - return nil, errors.New("invalid resolvers") - } - domain, err := ParseName(parts[0]) + domain, server, resolverType, err := parseResolver(rs) if err != nil { - return nil, err + return nil, errors.New("invalid resolvers").Base(err) } domains = append(domains, domain) - servers = append(servers, parts[1]) + servers = append(servers, server) + resolverTypes = append(resolverTypes, resolverType) } var resolverAddrs []*net.UDPAddr @@ -98,6 +96,7 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { PacketConn: raw, resolverAddrs: resolverAddrs, + resolverTypes: resolverTypes, resolverIdx: 0, resolverSend: resolverSend, @@ -216,7 +215,7 @@ func (c *xdnsConnClient) sendLoop() { default: } } else { - encoded, _ := encode(nil, c.clientID, c.domains[c.resolverIdx]) + encoded, _ := encode(nil, c.clientID, c.domains[c.resolverIdx], c.resolverTypes[c.resolverIdx]) p = &packet{ p: encoded, } @@ -276,7 +275,8 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { return 0, io.ErrClosedPipe } - encoded, err := encode(p, c.clientID, c.domains[c.resolverIdx%uint32(len(c.resolverAddrs))]) + idx := c.resolverIdx % uint32(len(c.resolverAddrs)) + encoded, err := encode(p, c.clientID, c.domains[idx], c.resolverTypes[idx]) if err != nil { errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p)) return 0, nil @@ -299,7 +299,7 @@ func (c *xdnsConnClient) Close() error { return c.PacketConn.Close() } -func encode(p []byte, clientID []byte, domain Name) ([]byte, error) { +func encode(p []byte, clientID []byte, domain Name, qtype uint16) ([]byte, error) { var decoded []byte { if len(p) >= 224 { @@ -338,7 +338,7 @@ func encode(p []byte, clientID []byte, domain Name) ([]byte, error) { Question: []Question{ { Name: name, - Type: RRTypeTXT, + Type: qtype, Class: ClassIN, }, }, @@ -396,29 +396,22 @@ func dnsResponsePayload(resp *Message, domains []Name) []byte { return nil } - if len(resp.Answer) != 1 { + if len(resp.Answer) == 0 { return nil } - answer := resp.Answer[0] - var ok bool - for _, domain := range domains { - _, ok = answer.Name.TrimSuffix(domain) - if ok { - break + for _, answer := range resp.Answer { + var ok bool + for _, domain := range domains { + _, ok = answer.Name.TrimSuffix(domain) + if ok { + break + } + } + if !ok { + return nil } } - if !ok { - return nil - } - if answer.Type != RRTypeTXT { - return nil - } - payload, err := DecodeRDataTXT(answer.Data) - if err != nil { - return nil - } - - return payload + return decodeResponsePayload(resp.Answer) } diff --git a/transport/internet/finalmask/xdns/dns.go b/transport/internet/finalmask/xdns/dns.go index 4cdac7cdb..774903857 100644 --- a/transport/internet/finalmask/xdns/dns.go +++ b/transport/internet/finalmask/xdns/dns.go @@ -45,8 +45,14 @@ var ( ) const ( + // https://tools.ietf.org/html/rfc1035#section-3.2.2 + RRTypeA = 1 + // https://tools.ietf.org/html/rfc1035#section-3.2.2 + RRTypeCNAME = 5 // https://tools.ietf.org/html/rfc1035#section-3.2.2 RRTypeTXT = 16 + // https://tools.ietf.org/html/rfc3596#section-2.1 + RRTypeAAAA = 28 // https://tools.ietf.org/html/rfc6891#section-6.1.1 RRTypeOPT = 41 diff --git a/transport/internet/finalmask/xdns/dns_test.go b/transport/internet/finalmask/xdns/dns_test.go index 2ddc9da50..45da317d4 100644 --- a/transport/internet/finalmask/xdns/dns_test.go +++ b/transport/internet/finalmask/xdns/dns_test.go @@ -593,3 +593,113 @@ func TestRDataTXTRoundTrip(t *testing.T) { } } } + +func TestIPAnswerPayloadRoundTrip(t *testing.T) { + for _, rrType := range []uint16{RRTypeA, RRTypeAAAA} { + for _, payload := range [][]byte{ + {}, + {0x01}, + []byte("hello world"), + bytes.Repeat([]byte{0xab}, payloadChunkSizeForType(rrType)*3+1), + } { + question := Question{ + Name: mustParseName("example.com"), + Type: rrType, + Class: ClassIN, + } + answers, err := answersForPayload(question, responseTTL, payload) + if err != nil { + t.Fatalf("answersForPayload(%d) err = %v", rrType, err) + } + + if len(answers) > 1 { + answers[0], answers[len(answers)-1] = answers[len(answers)-1], answers[0] + } + + decoded := decodeResponsePayload(answers) + if !bytes.Equal(decoded, payload) { + t.Fatalf("rrType=%d decoded %x want %x", rrType, decoded, payload) + } + } + } +} + +func TestParseResolver(t *testing.T) { + tests := []struct { + resolver string + rrType uint16 + }{ + {"example.com+udp://1.1.1.1:53", RRTypeTXT}, + {"example.com:txt+udp://1.1.1.1:53", RRTypeTXT}, + {"example.com:a+udp://1.1.1.1:53", RRTypeA}, + {"example.com:aaaa+udp://1.1.1.1:53", RRTypeAAAA}, + } + + for _, test := range tests { + domain, server, rrType, err := parseResolver(test.resolver) + if err != nil { + t.Fatalf("parseResolver(%q) err = %v", test.resolver, err) + } + if domain.String() != "example.com" || server != "1.1.1.1:53" || rrType != test.rrType { + t.Fatalf("parseResolver(%q) = (%q, %q, %d)", test.resolver, domain.String(), server, rrType) + } + } +} + +func TestParseDomainSpec(t *testing.T) { + tests := []struct { + spec string + def string + rrType uint16 + wantErr bool + }{ + {"example.com", "", 0, false}, + {"example.com", "txt", RRTypeTXT, false}, + {"example.com:a", "", RRTypeA, false}, + {"example.com:aaaa", "", RRTypeAAAA, false}, + {"example.com:doh", "", 0, true}, + } + + for _, test := range tests { + got, err := parseDomainSpec(test.spec, test.def) + if test.wantErr { + if err == nil { + t.Fatalf("parseDomainSpec(%q, %q) err = nil", test.spec, test.def) + } + continue + } + if err != nil { + t.Fatalf("parseDomainSpec(%q, %q) err = %v", test.spec, test.def, err) + } + if got.name.String() != "example.com" || got.rrType != test.rrType { + t.Fatalf("parseDomainSpec(%q, %q) = (%q, %d)", test.spec, test.def, got.name.String(), got.rrType) + } + } +} + +func TestResponseForMethodRestriction(t *testing.T) { + query := &Message{ + ID: 1, + Flags: 0x0100, + Question: []Question{{ + Name: mustParseName("abc.example.com"), + Type: RRTypeTXT, + Class: ClassIN, + }}, + Additional: []RR{{ + Name: Name{}, + Type: RRTypeOPT, + Class: 4096, + }}, + } + + resp, _ := responseFor(query, []domainSpec{{name: mustParseName("example.com"), rrType: RRTypeA}}) + if resp == nil || resp.Rcode() != RcodeNameError { + t.Fatalf("responseFor method restriction rcode = %v", resp) + } + + resp, _ = responseFor(query, []domainSpec{{name: mustParseName("example.com")}}) + if resp == nil || resp.Rcode() != RcodeNoError { + t.Fatalf("responseFor unrestricted rcode = %v", resp) + } +} diff --git a/transport/internet/finalmask/xdns/record_transport.go b/transport/internet/finalmask/xdns/record_transport.go new file mode 100644 index 000000000..8428baa40 --- /dev/null +++ b/transport/internet/finalmask/xdns/record_transport.go @@ -0,0 +1,226 @@ +package xdns + +import "bytes" + +const ipRecordHeaderSize = 2 + +func maxEncodedPayloadForType(rrType uint16) int { + switch rrType { + case RRTypeA: + return maxEncodedPayloadA + case RRTypeAAAA: + return maxEncodedPayloadAAAA + default: + return maxEncodedPayloadTXT + } +} + +func rrDataSizeForType(rrType uint16) int { + switch rrType { + case RRTypeA: + return 4 + case RRTypeAAAA: + return 16 + default: + return 0 + } +} + +func payloadChunkSizeForType(rrType uint16) int { + size := rrDataSizeForType(rrType) + if size <= ipRecordHeaderSize { + return 0 + } + return size - ipRecordHeaderSize +} + +func answersForPayload(question Question, ttl uint32, payload []byte) ([]RR, error) { + switch question.Type { + case RRTypeTXT: + return []RR{ + { + Name: question.Name, + Type: question.Type, + Class: question.Class, + TTL: ttl, + Data: EncodeRDataTXT(payload), + }, + }, nil + case RRTypeA, RRTypeAAAA: + return ipAnswersForPayload(question, ttl, payload) + default: + return nil, ErrIntegerOverflow + } +} + +func ipAnswersForPayload(question Question, ttl uint32, payload []byte) ([]RR, error) { + chunkSize := payloadChunkSizeForType(question.Type) + rrDataSize := rrDataSizeForType(question.Type) + if chunkSize == 0 || rrDataSize == 0 { + return nil, ErrIntegerOverflow + } + + numRecords := 1 + if len(payload) > 0 { + numRecords = (len(payload) + chunkSize - 1) / chunkSize + } + if numRecords > 256 { + return nil, ErrIntegerOverflow + } + + answers := make([]RR, 0, numRecords) + for i := 0; i < numRecords; i++ { + offset := i * chunkSize + n := len(payload) - offset + if n < 0 { + n = 0 + } + if n > chunkSize { + n = chunkSize + } + + data := make([]byte, rrDataSize) + data[0] = byte(i) + data[1] = byte(n) + copy(data[ipRecordHeaderSize:], payload[offset:offset+n]) + + answers = append(answers, RR{ + Name: question.Name, + Type: question.Type, + Class: question.Class, + TTL: ttl, + Data: data, + }) + } + + return answers, nil +} + +func decodeResponsePayload(answers []RR) []byte { + if len(answers) == 0 { + return nil + } + + switch answers[0].Type { + case RRTypeTXT: + if len(answers) != 1 { + return nil + } + payload, err := DecodeRDataTXT(answers[0].Data) + if err != nil { + return nil + } + return payload + case RRTypeA, RRTypeAAAA: + return decodeIPAnswerPayload(answers, answers[0].Type) + default: + return nil + } +} + +func decodeIPAnswerPayload(answers []RR, rrType uint16) []byte { + chunkSize := payloadChunkSizeForType(rrType) + rrDataSize := rrDataSizeForType(rrType) + if chunkSize == 0 || rrDataSize == 0 || len(answers) > 256 { + return nil + } + + parts := make([][]byte, len(answers)) + for _, answer := range answers { + if answer.Type != rrType || len(answer.Data) != rrDataSize { + return nil + } + idx := int(answer.Data[0]) + n := int(answer.Data[1]) + if idx >= len(answers) || n > chunkSize || parts[idx] != nil { + return nil + } + + part := make([]byte, n) + copy(part, answer.Data[ipRecordHeaderSize:ipRecordHeaderSize+n]) + parts[idx] = part + } + + var payload bytes.Buffer + for _, part := range parts { + if part == nil { + return nil + } + payload.Write(part) + } + return payload.Bytes() +} + +func computeMaxEncodedPayload(limit int) int { + return computeMaxEncodedPayloadForType(limit, RRTypeTXT) +} + +func computeMaxEncodedPayloadForType(limit int, rrType uint16) int { + maxLengthName, err := NewName([][]byte{ + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + }) + if err != nil { + panic(err) + } + { + n := 0 + for _, label := range maxLengthName { + n += len(label) + 1 + } + n += 1 + if n != 255 { + panic("computeMaxEncodedPayload n != 255") + } + } + + queryLimit := uint16(limit) + if int(queryLimit) != limit { + queryLimit = 0xffff + } + query := &Message{ + Question: []Question{ + { + Name: maxLengthName, + Type: rrType, + Class: ClassIN, + }, + }, + Additional: []RR{ + { + Name: Name{}, + Type: RRTypeOPT, + Class: queryLimit, + TTL: 0, + Data: []byte{}, + }, + }, + } + resp, _ := responseFor(query, []domainSpec{{name: Name{[]byte{}}}}) + + low := 0 + high := 32768 + if chunkSize := payloadChunkSizeForType(rrType); chunkSize > 0 { + high = 256*chunkSize + 1 + } + for low+1 < high { + mid := (low + high) / 2 + resp.Answer, err = answersForPayload(query.Question[0], responseTTL, make([]byte, mid)) + if err != nil { + panic(err) + } + buf, err := resp.WireFormat() + if err != nil { + panic(err) + } + if len(buf) <= limit { + low = mid + } else { + high = mid + } + } + + return low +} diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index c96149ad5..654f7fdba 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -21,8 +21,10 @@ const ( ) var ( - maxUDPPayload = 1280 - 40 - 8 - maxEncodedPayload = computeMaxEncodedPayload(maxUDPPayload) + maxUDPPayload = 1280 - 40 - 8 + maxEncodedPayloadTXT = computeMaxEncodedPayloadForType(maxUDPPayload, RRTypeTXT) + maxEncodedPayloadA = computeMaxEncodedPayloadForType(maxUDPPayload, RRTypeA) + maxEncodedPayloadAAAA = computeMaxEncodedPayloadForType(maxUDPPayload, RRTypeAAAA) ) func clientIDToAddr(clientID [8]byte) *net.UDPAddr { @@ -44,15 +46,16 @@ type record struct { } type queue struct { - last time.Time - queue chan []byte - stash chan []byte + last time.Time + rrType uint16 + queue chan []byte + stash chan []byte } type xdnsConnServer struct { net.PacketConn - domains []Name + domains []domainSpec ch chan *record readQueue chan *packet @@ -66,9 +69,9 @@ func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { if len(c.Domains) == 0 { return nil, errors.New("empty domains") } - domains := make([]Name, 0, len(c.Domains)) + domains := make([]domainSpec, 0, len(c.Domains)) for _, domain := range c.Domains { - domain, err := ParseName(domain) + domain, err := parseDomainSpec(domain, "") if err != nil { return nil, err } @@ -234,6 +237,7 @@ func (c *xdnsConnServer) recvLoop() { func (c *xdnsConnServer) sendLoop() { var nextRec *record for { + var err error rec := nextRec nextRec = nil @@ -246,18 +250,8 @@ func (c *xdnsConnServer) sendLoop() { } if rec.Resp.Rcode() == RcodeNoError && len(rec.Resp.Question) == 1 { - rec.Resp.Answer = []RR{ - { - Name: rec.Resp.Question[0].Name, - Type: rec.Resp.Question[0].Type, - Class: rec.Resp.Question[0].Class, - TTL: responseTTL, - Data: nil, - }, - } - var payload bytes.Buffer - limit := maxEncodedPayload + limit := maxEncodedPayloadForType(rec.Resp.Question[0].Type) timer := time.NewTimer(maxResponseDelay) for { @@ -267,6 +261,7 @@ func (c *xdnsConnServer) sendLoop() { c.mutex.Unlock() return } + q.rrType = rec.Resp.Question[0].Type c.mutex.Unlock() var p []byte @@ -294,7 +289,11 @@ func (c *xdnsConnServer) sendLoop() { } limit -= 2 + len(p) - if payload.Len() > 0 && limit < 0 { + if limit < 0 { + if payload.Len() == 0 { + errors.LogDebug(context.Background(), rec.Addr, " ", rec.ClientAddr, " xdns payload too large for rrtype ", rec.Resp.Question[0].Type, " ", len(p)) + continue + } c.stash(q, p) break } @@ -308,7 +307,11 @@ func (c *xdnsConnServer) sendLoop() { } timer.Stop() - rec.Resp.Answer[0].Data = EncodeRDataTXT(payload.Bytes()) + rec.Resp.Answer, err = answersForPayload(rec.Resp.Question[0], responseTTL, payload.Bytes()) + if err != nil { + errors.LogDebug(context.Background(), rec.Addr, " ", rec.ClientAddr, " xdns encode err ", err) + continue + } } buf, err := rec.Resp.WireFormat() @@ -349,11 +352,6 @@ func (c *xdnsConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { } func (c *xdnsConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if len(p)+2 > maxEncodedPayload { - errors.LogDebug(context.Background(), addr, " mask write err short write ", len(p), "+2 > ", maxEncodedPayload) - return 0, nil - } - c.mutex.Lock() defer c.mutex.Unlock() @@ -361,6 +359,14 @@ func (c *xdnsConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { if q == nil { return 0, io.ErrClosedPipe } + limit := maxEncodedPayloadForType(q.rrType) + if q.rrType == 0 { + limit = maxEncodedPayloadTXT + } + if len(p)+2 > limit { + errors.LogDebug(context.Background(), addr, " mask write err short write ", len(p), "+2 > ", limit) + return 0, nil + } buf := make([]byte, len(p)) copy(buf, p) @@ -406,7 +412,7 @@ func nextPacketServer(r *bytes.Reader) ([]byte, error) { } } -func responseFor(query *Message, domains []Name) (*Message, []byte) { +func responseFor(query *Message, domains []domainSpec) (*Message, []byte) { resp := &Message{ ID: query.ID, Flags: 0x8000, @@ -454,11 +460,15 @@ func responseFor(query *Message, domains []Name) (*Message, []byte) { } question := query.Question[0] - var prefix Name - var ok bool + var ( + prefix Name + ok bool + match domainSpec + ) for _, domain := range domains { - prefix, ok = question.Name.TrimSuffix(domain) + prefix, ok = question.Name.TrimSuffix(domain.name) if ok { + match = domain break } } @@ -473,7 +483,13 @@ func responseFor(query *Message, domains []Name) (*Message, []byte) { return resp, nil } - if question.Type != RRTypeTXT { + switch question.Type { + case RRTypeTXT, RRTypeA, RRTypeAAAA: + default: + resp.Flags |= RcodeNameError + return resp, nil + } + if match.rrType != 0 && question.Type != match.rrType { resp.Flags |= RcodeNameError return resp, nil } @@ -494,78 +510,3 @@ func responseFor(query *Message, domains []Name) (*Message, []byte) { return resp, payload } - -func computeMaxEncodedPayload(limit int) int { - maxLengthName, err := NewName([][]byte{ - []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), - []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), - []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), - []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), - }) - if err != nil { - panic(err) - } - { - n := 0 - for _, label := range maxLengthName { - n += len(label) + 1 - } - n += 1 - if n != 255 { - panic("computeMaxEncodedPayload n != 255") - } - } - - queryLimit := uint16(limit) - if int(queryLimit) != limit { - queryLimit = 0xffff - } - query := &Message{ - Question: []Question{ - { - Name: maxLengthName, - Type: RRTypeTXT, - Class: RRTypeTXT, - }, - }, - - Additional: []RR{ - { - Name: Name{}, - Type: RRTypeOPT, - Class: queryLimit, - TTL: 0, - Data: []byte{}, - }, - }, - } - resp, _ := responseFor(query, []Name{[][]byte{}}) - - resp.Answer = []RR{ - { - Name: query.Question[0].Name, - Type: query.Question[0].Type, - Class: query.Question[0].Class, - TTL: responseTTL, - Data: nil, - }, - } - - low := 0 - high := 32768 - for low+1 < high { - mid := (low + high) / 2 - resp.Answer[0].Data = EncodeRDataTXT(make([]byte, mid)) - buf, err := resp.WireFormat() - if err != nil { - panic(err) - } - if len(buf) <= limit { - low = mid - } else { - high = mid - } - } - - return low -} diff --git a/transport/internet/finalmask/xdns/spec.go b/transport/internet/finalmask/xdns/spec.go new file mode 100644 index 000000000..28461569f --- /dev/null +++ b/transport/internet/finalmask/xdns/spec.go @@ -0,0 +1,80 @@ +package xdns + +import ( + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +type domainSpec struct { + name Name + rrType uint16 +} + +func rrTypeFromMethod(method string) (uint16, error) { + switch strings.ToLower(method) { + case "", "txt": + return RRTypeTXT, nil + case "a": + return RRTypeA, nil + case "aaaa": + return RRTypeAAAA, nil + default: + return 0, errors.New("unsupported method") + } +} + +func parseDomainSpec(s string, defaultMethod string) (domainSpec, error) { + domainPart := s + method := "" + hasMethod := false + + if i := strings.LastIndex(s, ":"); i >= 0 { + domainPart = s[:i] + method = s[i+1:] + hasMethod = true + } else if defaultMethod != "" { + method = defaultMethod + hasMethod = true + } + + if domainPart == "" { + return domainSpec{}, errors.New("empty domain") + } + + name, err := ParseName(domainPart) + if err != nil { + return domainSpec{}, err + } + + rrType := uint16(0) + if hasMethod { + var err error + rrType, err = rrTypeFromMethod(method) + if err != nil { + return domainSpec{}, err + } + } + + return domainSpec{ + name: name, + rrType: rrType, + }, nil +} + +func parseResolver(s string) (Name, string, uint16, error) { + head, server, ok := strings.Cut(s, "+udp://") + if !ok { + return nil, "", 0, errors.New("invalid resolver scheme") + } + if server == "" { + return nil, "", 0, errors.New("empty resolver server") + } + + spec, err := parseDomainSpec(head, "txt") + if err != nil { + return nil, "", 0, err + } + + return spec.name, server, spec.rrType, nil +} From 56bb63668c391987e409fa12df42685fcf00d6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sat, 23 May 2026 20:23:52 +0800 Subject: [PATCH 05/78] Geodata: Cleanup unneeded shared matchers (weakly referenced in map) (#6139) https://github.com/XTLS/Xray-core/pull/6139#issuecomment-4525195265 --- common/geodata/domain_matcher.go | 17 +++++----- common/geodata/domain_matcher_test.go | 14 +++++---- common/geodata/ip_matcher.go | 9 +++--- common/utils/weak_cache.go | 45 +++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 common/utils/weak_cache.go diff --git a/common/geodata/domain_matcher.go b/common/geodata/domain_matcher.go index be9e62bf4..2c231d98c 100644 --- a/common/geodata/domain_matcher.go +++ b/common/geodata/domain_matcher.go @@ -8,6 +8,7 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/geodata/strmatcher" + "github.com/xtls/xray-core/common/utils" ) type DomainMatcher interface { @@ -25,7 +26,7 @@ type DomainMatcherFactory interface { type MphDomainMatcherFactory struct { sync.Mutex - shared map[string]strmatcher.MatcherGroup // TODO: cleanup + shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher] } func buildDomainRulesKey(rules []*DomainRule) string { @@ -65,7 +66,7 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch if key != "" { f.Lock() defer f.Unlock() - if g := f.shared[key]; g != nil { + if g, ok := f.shared.Load(key); ok { errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules") return g, nil } @@ -102,14 +103,14 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch return nil, err } if key != "" { - f.shared[key] = g + f.shared.Store(key, g) } return g, nil } type CompactDomainMatcherFactory struct { sync.Mutex - shared map[string]strmatcher.MatcherSet // TODO: cleanup + shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher] } func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) { @@ -118,7 +119,7 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat f.Lock() defer f.Unlock() - if s := f.shared[key]; s != nil { + if s, ok := f.shared.Load(key); ok { errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key) return s, nil } @@ -138,7 +139,7 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat } s.Add(m) } - f.shared[key] = s + f.shared.Store(key, s) return s, err } @@ -230,8 +231,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) { func newDomainMatcherFactory() DomainMatcherFactory { switch runtime.GOOS { case "ios", "android": - return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)} + return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()} default: - return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)} + return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()} } } diff --git a/common/geodata/domain_matcher_test.go b/common/geodata/domain_matcher_test.go index 83ae60ca9..0c0c5080c 100644 --- a/common/geodata/domain_matcher_test.go +++ b/common/geodata/domain_matcher_test.go @@ -7,10 +7,11 @@ import ( "testing" "github.com/xtls/xray-core/common/geodata/strmatcher" + "github.com/xtls/xray-core/common/utils" ) func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) { - factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)} + factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()} matcher, err := factory.BuildMatcher([]*DomainRule{ {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}}, {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}}, @@ -31,7 +32,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) { func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) { t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) - factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)} + factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()} matcher, err := factory.BuildMatcher([]*DomainRule{ {Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}}, {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}}, @@ -50,10 +51,11 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) { } func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) { - matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{ - {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}}, - {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}}, - }) + matcher, err := (&MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}). + BuildMatcher([]*DomainRule{ + {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}}, + {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}}, + }) if err != nil { t.Fatalf("BuildMatcher() failed: %v", err) } diff --git a/common/geodata/ip_matcher.go b/common/geodata/ip_matcher.go index 1eba5dbf9..703b64973 100644 --- a/common/geodata/ip_matcher.go +++ b/common/geodata/ip_matcher.go @@ -11,6 +11,7 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" "go4.org/netipx" ) @@ -806,7 +807,7 @@ func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) { type IPSetFactory struct { sync.Mutex - shared map[string]*IPSet // TODO: cleanup + shared *utils.WeakCacheMap[string, IPSet] } func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) { @@ -815,7 +816,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er f.Lock() defer f.Unlock() - if ipset := f.shared[key]; ipset != nil { + if ipset, ok := f.shared.Load(key); ok { errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key) return ipset, nil } @@ -835,7 +836,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er return nil }) if err == nil { - f.shared[key] = ipset + f.shared.Store(key, ipset) } return ipset, err } @@ -1018,5 +1019,5 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error } func newIPSetFactory() *IPSetFactory { - return &IPSetFactory{shared: make(map[string]*IPSet)} + return &IPSetFactory{shared: utils.NewWeakCacheMap[string, IPSet]()} } diff --git a/common/utils/weak_cache.go b/common/utils/weak_cache.go new file mode 100644 index 000000000..a9aaacca6 --- /dev/null +++ b/common/utils/weak_cache.go @@ -0,0 +1,45 @@ +package utils + +import ( + "runtime" + "sync" + "weak" +) + +// WeakCacheMap is a map that holds weak references to values. +// Use for shared expensive objects and automatic cleanup when no longer used. +// This object can be GC and no goroutine is used for cleanup. +type WeakCacheMap[K comparable, V any] struct { + mu sync.Mutex + m map[K]weak.Pointer[V] +} + +func NewWeakCacheMap[K comparable, V any]() *WeakCacheMap[K, V] { + return &WeakCacheMap[K, V]{ + m: make(map[K]weak.Pointer[V]), + } +} + +func (c *WeakCacheMap[K, V]) Load(key K) (value *V, ok bool) { + c.mu.Lock() + defer c.mu.Unlock() + weakPtr := c.m[key].Value() + if weakPtr != nil { + return weakPtr, true + } + return nil, false +} + +func (c *WeakCacheMap[K, V]) Store(key K, value *V) { + c.mu.Lock() + defer c.mu.Unlock() + weakPtr := weak.Make(value) + c.m[key] = weakPtr + runtime.AddCleanup(value, func(struct{}) { + c.mu.Lock() + defer c.mu.Unlock() + if c.m[key] == weakPtr { + delete(c.m, key) + } + }, struct{}{}) +} From ab69985fccb3206300c761cf1792258e53f82749 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Sat, 23 May 2026 21:50:01 +0800 Subject: [PATCH 06/78] Config: Warn when `sockopt.trustedXForwardedFor` is not set for XHTTP/WS/HU inbounds (#6159) https://github.com/XTLS/Xray-core/pull/6110#issuecomment-4470157219 Usage: https://github.com/XTLS/Xray-core/pull/5331#issue-3655317949 --- infra/conf/xray.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/infra/conf/xray.go b/infra/conf/xray.go index d01dac2bf..a077af5ed 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -173,6 +173,26 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { return nil, err } receiverSettings.StreamSettings = ss + // TODO: Actually implement this breaking change + protocol := ss.GetEffectiveProtocol() + if (protocol == "websocket" || protocol == "httpupgrade" || protocol == "splithttp") && + (c.StreamSetting.SocketSettings == nil || len(c.StreamSetting.SocketSettings.TrustedXForwardedFor) == 0) { + errors.LogWarning(context.Background(), + `====== SECURITY WARNING ======`, + "\n", + `inbound "`, c.Tag, `" using `, protocol, ` has not configured "sockopt.trustedXForwardedFor".`, + "\n", + `THIS IS VERY INSECURE!!!`, + "\n", + `For compatibility, Xray still allows this for now and still trusts X-Forwarded-For implicitly.`, + "\n", + `Please configure "sockopt.trustedXForwardedFor" immediately.`, + "\n", + `In future versions, this option must be explicitly set.`, + "\n", + `====== SECURITY WARNING ======`, + ) + } if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil || len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) { errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`) From 4c3842711dd6abc8401f83678f30bb56e3f8819b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 24 May 2026 20:05:40 +0800 Subject: [PATCH 07/78] XHTTP client: Fix packet-up `OpenUsage` counting and H3 `keepAlivePeriod` parameter (#6140) Fixes https://github.com/XTLS/Xray-core/pull/6140#issuecomment-4525409315 --- transport/internet/splithttp/dialer.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index f89c71ed9..1329713c5 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -183,6 +183,8 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea if quicParams.KeepAlivePeriod == 0 { if keepAlivePeriod == 0 { quicConfig.KeepAlivePeriod = net.QuicgoH3KeepAlivePeriod + } else if keepAlivePeriod > 0 { + quicConfig.KeepAlivePeriod = keepAlivePeriod } } if quicParams.MaxIncomingStreams == 0 { @@ -506,6 +508,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me var seq int64 var lastWrite time.Time + dynamicHTTPClient := httpClient + dynamicXmuxClient := xmuxClient for { // by offloading the uploads into a buffered pipe, multiple conn.Write // calls get automatically batched together into larger POST requests. @@ -540,13 +544,13 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me lastWrite = time.Now() - if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 || - (xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) { - httpClient, xmuxClient = getHTTPClient(ctx, dest, streamSettings) + if dynamicXmuxClient != nil && (dynamicXmuxClient.LeftRequests.Add(-1) <= 0 || + (dynamicXmuxClient.UnreusableAt != time.Time{} && lastWrite.After(dynamicXmuxClient.UnreusableAt))) { + dynamicHTTPClient, dynamicXmuxClient = getHTTPClient(ctx, dest, streamSettings) } - go func() { - err := httpClient.PostPacket( + go func(hClient DialerClient) { + err := hClient.PostPacket( ctx, requestURL.String(), sessionId, @@ -559,9 +563,9 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me uploadPipeReader.Interrupt() doSplit.Store(false) } - }() + }(dynamicHTTPClient) - if _, ok := httpClient.(*DefaultDialerClient); ok { + if _, ok := dynamicHTTPClient.(*DefaultDialerClient); ok { <-wroteRequest.Wait() } } From 81d993f49dc4c9a036bc285864f3ed348bbdd36c Mon Sep 17 00:00:00 2001 From: Whale Choi Date: Sun, 24 May 2026 20:19:00 +0800 Subject: [PATCH 08/78] README.md: Remove Xray4Magisk; Add AsteriskNG to Android in GUI Clients (#6153) https://github.com/XTLS/Xray-core/pull/6153#issuecomment-4522913987 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdde7bde6..a7dba7c3e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,6 @@ - [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless) - [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU) - Magisk - - [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk) - [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk) - Homebrew - `brew install xray` @@ -120,6 +119,7 @@ - [XrayFA](https://github.com/Q7DF1/XrayFA) - [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [OneXray](https://github.com/OneXray/OneXray) + - [AsteriskNG](https://github.com/Asterisk4Magisk/AsteriskNG) - iOS & macOS arm64 & tvOS - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274) - [Streisand](https://apps.apple.com/app/streisand/id6450534064) From ee2b2c5ab6eeb4b4cec4457d5f5aa46cf50d9c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 24 May 2026 20:27:12 +0800 Subject: [PATCH 09/78] leastLoad balancer: Implement missing `tolerance` (#6156) Closes https://github.com/XTLS/Xray-core/issues/6154 --- app/router/strategy_leastload.go | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/router/strategy_leastload.go b/app/router/strategy_leastload.go index 1bf3cbc09..289982a84 100644 --- a/app/router/strategy_leastload.go +++ b/app/router/strategy_leastload.go @@ -3,6 +3,7 @@ package router import ( "context" "math" + "slices" "sort" "time" @@ -77,7 +78,7 @@ func (s *LeastLoadStrategy) PickOutbound(candidates []string) string { } func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node { - qualified := s.getNodes(candidates, time.Duration(s.settings.MaxRTT)) + qualified := s.getNodes(candidates) selects := s.selectLeastLoad(qualified) return selects } @@ -138,7 +139,7 @@ func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node { return nodes[:count] } -func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) []*node { +func (s *LeastLoadStrategy) getNodes(candidates []string) []*node { if s.observer == nil { errors.LogError(s.ctx, "observer is nil") return make([]*node, 0) @@ -151,12 +152,10 @@ func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) results := observeResult.(*observatory.ObservationResult) - outboundlist := outboundList(candidates) - var ret []*node for _, v := range results.Status { - if v.Alive && (v.Delay < maxRTT.Milliseconds() || maxRTT == 0) && outboundlist.contains(v.OutboundTag) { + if s.shouldSelectNode(v, candidates) { record := &node{ Tag: v.OutboundTag, CountAll: 1, @@ -172,8 +171,8 @@ func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) record.RTTDeviationCost = time.Duration(s.costs.Apply(v.OutboundTag, float64(v.HealthPing.Deviation))) record.CountAll = int(v.HealthPing.All) record.CountFail = int(v.HealthPing.Fail) - } + ret = append(ret, record) } } @@ -182,6 +181,23 @@ func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) return ret } +func (s *LeastLoadStrategy) shouldSelectNode(v *observatory.OutboundStatus, candidates []string) bool { + maxRTT := time.Duration(s.settings.MaxRTT) + if !v.Alive { + return false + } + if maxRTT != 0 && v.Delay >= maxRTT.Milliseconds() { + return false + } + if !slices.Contains(candidates, v.OutboundTag) { + return false + } + if v.HealthPing != nil && v.HealthPing.All > 0 && s.settings.Tolerance > 0 && float64(v.HealthPing.Fail)/float64(v.HealthPing.All) > float64(s.settings.Tolerance) { + return false + } + return true +} + func leastloadSort(nodes []*node) { sort.Slice(nodes, func(i, j int) bool { left := nodes[i] From d878fc83f8be0f56cebbc5fb9a9baf0aa48d9990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 24 May 2026 20:50:02 +0800 Subject: [PATCH 10/78] SS2022 inbound: Fix potential panic when dispatching (#6162) https://github.com/XTLS/Xray-core/pull/6162#issuecomment-4496738737 --- common/singbridge/destination.go | 9 ++-- common/singbridge/dialer.go | 12 ++++- common/singbridge/handler.go | 12 ++++- common/singbridge/packet.go | 61 +++++++++++++------------ common/singbridge/pipe.go | 36 +++++++-------- proxy/shadowsocks_2022/inbound.go | 16 ++++++- proxy/shadowsocks_2022/inbound_multi.go | 17 +++++-- proxy/shadowsocks_2022/inbound_relay.go | 16 ++++++- proxy/shadowsocks_2022/outbound.go | 4 ++ 9 files changed, 118 insertions(+), 65 deletions(-) diff --git a/common/singbridge/destination.go b/common/singbridge/destination.go index 98aed2587..217c4d082 100644 --- a/common/singbridge/destination.go +++ b/common/singbridge/destination.go @@ -3,6 +3,7 @@ package singbridge import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" ) @@ -17,14 +18,14 @@ func ToNetwork(network string) net.Network { } } -func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination { +func ToDestination(socksaddr M.Socksaddr, network net.Network) (net.Destination, error) { // IsFqdn() implicitly checks if the domain name is valid if socksaddr.IsFqdn() { return net.Destination{ Network: network, Address: net.DomainAddress(socksaddr.Fqdn), Port: net.Port(socksaddr.Port), - } + }, nil } // IsIP() implicitly checks if the IP address is valid @@ -33,10 +34,10 @@ func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination { Network: network, Address: net.IPAddress(socksaddr.Addr.AsSlice()), Port: net.Port(socksaddr.Port), - } + }, nil } - return net.Destination{} + return net.Destination{}, errors.New("invalid socks address: ", socksaddr) } func ToSocksaddr(destination net.Destination) M.Socksaddr { diff --git a/common/singbridge/dialer.go b/common/singbridge/dialer.go index a6b321991..07c428813 100644 --- a/common/singbridge/dialer.go +++ b/common/singbridge/dialer.go @@ -26,7 +26,11 @@ func NewDialer(dialer internet.Dialer) *XrayDialer { } func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.Dialer.Dial(ctx, ToDestination(destination, ToNetwork(network))) + dest, err := ToDestination(destination, ToNetwork(network)) + if err != nil { + return nil, err + } + return d.Dialer.Dial(ctx, dest) } func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { @@ -43,13 +47,17 @@ func NewOutboundDialer(outbound proxy.Outbound, dialer internet.Dialer) *XrayOut } func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + dest, err := ToDestination(destination, ToNetwork(network)) + if err != nil { + return nil, err + } outbounds := session.OutboundsFromContext(ctx) if len(outbounds) == 0 { outbounds = []*session.Outbound{{}} ctx = session.ContextWithOutbounds(ctx, outbounds) } ob := outbounds[len(outbounds)-1] - ob.Target = ToDestination(destination, ToNetwork(network)) + ob.Target = dest opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)} uplinkReader, uplinkWriter := pipe.New(opts...) diff --git a/common/singbridge/handler.go b/common/singbridge/handler.go index f200075c2..ee4b7c15f 100644 --- a/common/singbridge/handler.go +++ b/common/singbridge/handler.go @@ -31,15 +31,23 @@ func NewDispatcher(dispatcher routing.Dispatcher, newErrorFunc func(values ...an } func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + dest, err := ToDestination(metadata.Destination, net.Network_TCP) + if err != nil { + return err + } xConn := NewConn(conn) - return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_TCP), &transport.Link{ + return d.upstream.DispatchLink(ctx, dest, &transport.Link{ Reader: xConn, Writer: xConn, }) } func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { - return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_UDP), &transport.Link{ + dest, err := ToDestination(metadata.Destination, net.Network_UDP) + if err != nil { + return err + } + return d.upstream.DispatchLink(ctx, dest, &transport.Link{ Reader: buf.NewPacketReader(conn.(io.Reader)), Writer: buf.NewWriter(conn.(io.Writer)), }) diff --git a/common/singbridge/packet.go b/common/singbridge/packet.go index d4dccb4ce..fde4bed3d 100644 --- a/common/singbridge/packet.go +++ b/common/singbridge/packet.go @@ -10,15 +10,21 @@ import ( "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/transport" ) func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error { + cancel := func() { + common.Interrupt(link.Reader) + common.Interrupt(serverConn) + } conn := &PacketConnWrapper{ Reader: link.Reader, Writer: link.Writer, Dest: destination, Conn: inboundConn, + T: signal.CancelAfterInactivity(ctx, cancel, 300*time.Second), } return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn))) } @@ -29,11 +35,19 @@ type PacketConnWrapper struct { net.Conn Dest net.Destination cached buf.MultiBuffer + + // A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err + T *signal.ActivityTimer } -// This ReadPacket implemented a timeout to avoid goroutine leak like PipeConnWrapper.Read() -// as a temporarily solution -func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) { +func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) { + w.T.Update() + defer func() { + if err != nil { + // uplinkonly + w.T.SetTimeout(2 * time.Second) + } + }() if w.cached != nil { mb, bb := buf.SplitFirst(w.cached) if bb == nil { @@ -51,30 +65,7 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) { return ToSocksaddr(destination), nil } } - - // timeout - type readResult struct { - mb buf.MultiBuffer - err error - } - c := make(chan readResult, 1) - go func() { - mb, err := w.ReadMultiBuffer() - c <- readResult{mb: mb, err: err} - }() - var mb buf.MultiBuffer - select { - case <-time.After(60 * time.Second): - common.Close(w.Reader) - common.Interrupt(w.Reader) - return M.Socksaddr{}, buf.ErrReadTimeout - case result := <-c: - if result.err != nil { - return M.Socksaddr{}, result.err - } - mb = result.mb - } - + mb, err := w.ReadMultiBuffer() nb, bb := buf.SplitFirst(mb) if bb == nil { return M.Socksaddr{}, nil @@ -92,12 +83,22 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) { } } -func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) error { +func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) (err error) { + w.T.Update() + defer func() { + if err != nil { + // downlinkonly + w.T.SetTimeout(5 * time.Second) + } + }() + endpoint, err := ToDestination(destination, net.Network_UDP) + if err != nil { + return err + } vBuf := buf.New() vBuf.Write(buffer.Bytes()) - endpoint := ToDestination(destination, net.Network_UDP) vBuf.UDP = &endpoint - return w.Writer.WriteMultiBuffer(buf.MultiBuffer{vBuf}) + return w.WriteMultiBuffer(buf.MultiBuffer{vBuf}) } func (w *PacketConnWrapper) Close() error { diff --git a/common/singbridge/pipe.go b/common/singbridge/pipe.go index d6c0f3d88..94c5ee0c1 100644 --- a/common/singbridge/pipe.go +++ b/common/singbridge/pipe.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing/common/bufio" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/transport" ) @@ -22,6 +23,11 @@ func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, s } else { conn.R = &buf.BufferedReader{Reader: link.Reader} } + cancel := func() { + common.Interrupt(link.Reader) + common.Interrupt(serverConn) + } + conn.T = signal.CancelAfterInactivity(ctx, cancel, 300*time.Second) return ReturnError(bufio.CopyConn(ctx, conn, serverConn)) } @@ -29,35 +35,27 @@ type PipeConnWrapper struct { R io.Reader W buf.Writer net.Conn + + // A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err + T *signal.ActivityTimer } func (w *PipeConnWrapper) Close() error { return nil } -// This Read implemented a timeout to avoid goroutine leak. -// as a temporarily solution func (w *PipeConnWrapper) Read(b []byte) (n int, err error) { - type readResult struct { - n int - err error - } - c := make(chan readResult, 1) - go func() { - n, err := w.R.Read(b) - c <- readResult{n: n, err: err} - }() - select { - case result := <-c: - return result.n, result.err - case <-time.After(300 * time.Second): - common.Close(w.R) - common.Interrupt(w.R) - return 0, buf.ErrReadTimeout + w.T.Update() + n, err = w.R.Read(b) + if err != nil { + // uplinkonly + w.T.SetTimeout(2 * time.Second) } + return } func (w *PipeConnWrapper) Write(p []byte) (n int, err error) { + w.T.Update() n = len(p) var mb buf.MultiBuffer pLen := len(p) @@ -76,6 +74,8 @@ func (w *PipeConnWrapper) Write(p []byte) (n int, err error) { if err != nil { n = 0 buf.ReleaseMulti(mb) + // downlinkonly + w.T.SetTimeout(5 * time.Second) } return } diff --git a/proxy/shadowsocks_2022/inbound.go b/proxy/shadowsocks_2022/inbound.go index a889fa0fd..edf9857c8 100644 --- a/proxy/shadowsocks_2022/inbound.go +++ b/proxy/shadowsocks_2022/inbound.go @@ -2,6 +2,7 @@ package shadowsocks_2022 import ( "context" + "time" shadowsocks "github.com/sagernet/sing-shadowsocks" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" @@ -18,6 +19,7 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/singbridge" "github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/transport/internet/stat" @@ -115,7 +117,11 @@ func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.M }) errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination) dispatcher := session.DispatcherFromContext(ctx) - link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP)) + destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP) + if err != nil { + return err + } + link, err := dispatcher.Dispatch(ctx, destination) if err != nil { return err } @@ -136,7 +142,10 @@ func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, me }) errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination) dispatcher := session.DispatcherFromContext(ctx) - destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + if err != nil { + return err + } link, err := dispatcher.Dispatch(ctx, destination) if err != nil { return err @@ -145,6 +154,9 @@ func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, me Reader: link.Reader, Writer: link.Writer, Dest: destination, + T: signal.CancelAfterInactivity(ctx, func() { + common.Interrupt(link.Reader) + }, 300*time.Second), } return bufio.CopyPacketConn(ctx, conn, outConn) } diff --git a/proxy/shadowsocks_2022/inbound_multi.go b/proxy/shadowsocks_2022/inbound_multi.go index 4bfa086aa..d6d68c09a 100644 --- a/proxy/shadowsocks_2022/inbound_multi.go +++ b/proxy/shadowsocks_2022/inbound_multi.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" C "github.com/sagernet/sing/common" @@ -22,6 +23,7 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/singbridge" "github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/features/routing" @@ -237,11 +239,10 @@ func (i *MultiUserInbound) NewConnection(ctx context.Context, conn net.Conn, met }) errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination) dispatcher := session.DispatcherFromContext(ctx) - destination := singbridge.ToDestination(metadata.Destination, net.Network_TCP) - if !destination.IsValid() { - return errors.New("invalid destination") + destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP) + if err != nil { + return err } - link, err := dispatcher.Dispatch(ctx, destination) if err != nil { return err @@ -262,7 +263,10 @@ func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.Packe }) errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination) dispatcher := session.DispatcherFromContext(ctx) - destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + if err != nil { + return err + } link, err := dispatcher.Dispatch(ctx, destination) if err != nil { return err @@ -271,6 +275,9 @@ func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.Packe Reader: link.Reader, Writer: link.Writer, Dest: destination, + T: signal.CancelAfterInactivity(ctx, func() { + common.Interrupt(link.Reader) + }, 300*time.Second), } return bufio.CopyPacketConn(ctx, conn, outConn) } diff --git a/proxy/shadowsocks_2022/inbound_relay.go b/proxy/shadowsocks_2022/inbound_relay.go index 19afd4620..4ca5e2075 100644 --- a/proxy/shadowsocks_2022/inbound_relay.go +++ b/proxy/shadowsocks_2022/inbound_relay.go @@ -4,6 +4,7 @@ import ( "context" "strconv" "strings" + "time" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" C "github.com/sagernet/sing/common" @@ -20,6 +21,7 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/singbridge" "github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/features/routing" @@ -138,7 +140,11 @@ func (i *RelayInbound) NewConnection(ctx context.Context, conn net.Conn, metadat }) errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination) dispatcher := session.DispatcherFromContext(ctx) - link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP)) + destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP) + if err != nil { + return err + } + link, err := dispatcher.Dispatch(ctx, destination) if err != nil { return err } @@ -161,7 +167,10 @@ func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketCon }) errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination) dispatcher := session.DispatcherFromContext(ctx) - destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + if err != nil { + return err + } link, err := dispatcher.Dispatch(ctx, destination) if err != nil { return err @@ -170,6 +179,9 @@ func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketCon Reader: link.Reader, Writer: link.Writer, Dest: destination, + T: signal.CancelAfterInactivity(ctx, func() { + common.Interrupt(link.Reader) + }, 300*time.Second), } return bufio.CopyPacketConn(ctx, conn, outConn) } diff --git a/proxy/shadowsocks_2022/outbound.go b/proxy/shadowsocks_2022/outbound.go index dd2e42598..bd3e4c4ec 100644 --- a/proxy/shadowsocks_2022/outbound.go +++ b/proxy/shadowsocks_2022/outbound.go @@ -16,6 +16,7 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/singbridge" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" @@ -142,6 +143,9 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int Writer: link.Writer, Conn: inboundConn, Dest: destination, + T: signal.CancelAfterInactivity(ctx, func() { + common.Interrupt(link.Reader) + }, 300*time.Second), } } From 787aa7677b47c24f19aae84111d50ef4123072be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 24 May 2026 22:20:53 +0800 Subject: [PATCH 11/78] Hysteria server: `tls.WithNextProto("h3")` by default (#6186) Client: https://github.com/XTLS/Xray-core/pull/6186#issuecomment-4528041838 --- transport/internet/hysteria/hub.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/hysteria/hub.go b/transport/internet/hysteria/hub.go index 17088938f..d20313b5d 100644 --- a/transport/internet/hysteria/hub.go +++ b/transport/internet/hysteria/hub.go @@ -309,7 +309,7 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti tr := &quic.Transport{Conn: pktConn} - listener, err := tr.Listen(tlsConfig.GetTLSConfig(), quicConfig) + listener, err := tr.Listen(tlsConfig.GetTLSConfig(tls.WithNextProto("h3")), quicConfig) if err != nil { _ = tr.Close() _ = pktConn.Close() From e26f5e95487a9c0064dd9f9e34ebef53f0018460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Thu, 28 May 2026 22:22:57 +0800 Subject: [PATCH 12/78] Socks5 server: More standard UDP ASSOCIATE (RFC 1928) (#6149) https://github.com/XTLS/Xray-core/pull/6149#issuecomment-4529069218 Fixes https://github.com/XTLS/Xray-core/issues/6145#issuecomment-4467482623 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- proxy/socks/protocol.go | 45 +++++++++++++++------ proxy/socks/server.go | 40 ++++++++----------- proxy/socks/temp_udp_listen.go | 72 ++++++++++++++++++++++++++++++++++ proxy/socks/udpfilter.go | 31 --------------- 4 files changed, 120 insertions(+), 68 deletions(-) create mode 100644 proxy/socks/temp_udp_listen.go delete mode 100644 proxy/socks/udpfilter.go diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 76ceb47c0..bf4f61a5b 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -1,14 +1,17 @@ package socks import ( + "context" "encoding/binary" "io" + gonet "net" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/transport/internet" ) const ( @@ -137,13 +140,13 @@ func (s *ServerSession) auth5(nMethod byte, reader io.Reader, writer io.Writer) return "", nil } -func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { +func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Conn) (*protocol.RequestHeader, *TempUDPConn, error) { var ( username string err error ) if username, err = s.auth5(nMethod, reader, writer); err != nil { - return nil, err + return nil, nil, err } var cmd byte @@ -151,7 +154,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri buffer := buf.StackNew() if _, err := buffer.ReadFullFrom(reader, 3); err != nil { buffer.Release() - return nil, errors.New("failed to read request").Base(err) + return nil, nil, errors.New("failed to read request").Base(err) } cmd = buffer.Byte(1) buffer.Release() @@ -168,28 +171,29 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri case cmdUDPAssociate: if !s.config.UdpEnabled { writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) - return nil, errors.New("UDP is not enabled.") + return nil, nil, errors.New("UDP is not enabled.") } request.Command = protocol.RequestCommandUDP case cmdTCPBind: writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) - return nil, errors.New("TCP bind is not supported.") + return nil, nil, errors.New("TCP bind is not supported.") default: writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) - return nil, errors.New("unknown command ", cmd) + return nil, nil, errors.New("unknown command ", cmd) } request.Version = socks5Version addr, port, err := addrParser.ReadAddressPort(nil, reader) if err != nil { - return nil, errors.New("failed to read address").Base(err) + return nil, nil, errors.New("failed to read address").Base(err) } request.Address = addr request.Port = port responseAddress := s.address responsePort := s.port + var tempUDPConn *TempUDPConn //nolint:gocritic // Use if else chain for clarity if request.Command == protocol.RequestCommandUDP { if s.config.Address != nil { @@ -199,20 +203,34 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri // Use conn.LocalAddr() IP as remote address in the response by default responseAddress = s.localAddress } + udpHub, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: responseAddress.IP(), Port: 0}, nil) + if err != nil { + return nil, nil, errors.New("failed to create UDP listener").Base(err) + } + responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) + expectedRemote := &gonet.UDPAddr{} + if request.Address.IP().IsUnspecified() { + expectedRemote.IP = writer.RemoteAddr().(*net.TCPAddr).IP // unix? + } else { + expectedRemote.IP = request.Address.IP() // panic? + expectedRemote.Port = int(request.Port) // 0 is allowed + } + tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemote) } if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { - return nil, err + common.CloseIfExists(tempUDPConn) + return nil, nil, err } - return request, nil + return request, tempUDPConn, nil } // Handshake performs a Socks4/4a/5 handshake. -func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { +func (s *ServerSession) Handshake(reader io.Reader, writer net.Conn) (*protocol.RequestHeader, *TempUDPConn, error) { buffer := buf.StackNew() if _, err := buffer.ReadFullFrom(reader, 2); err != nil { buffer.Release() - return nil, errors.New("insufficient header").Base(err) + return nil, nil, errors.New("insufficient header").Base(err) } version := buffer.Byte(0) @@ -221,11 +239,12 @@ func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol switch version { case socks4Version: - return s.handshake4(cmd, reader, writer) + header, err := s.handshake4(cmd, reader, writer) + return header, nil, err case socks5Version: return s.handshake5(cmd, reader, writer) default: - return nil, errors.New("unknown Socks version: ", version) + return nil, nil, errors.New("unknown Socks version: ", version) } } diff --git a/proxy/socks/server.go b/proxy/socks/server.go index 478410f35..fab664766 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -29,7 +29,6 @@ type Server struct { config *ServerConfig policyManager policy.Manager cone bool - udpFilter *UDPFilter httpServer *http.Server } @@ -46,7 +45,6 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { } if config.AuthType == AuthType_PASSWORD { httpConfig.Accounts = config.Accounts - s.udpFilter = new(UDPFilter) // We only use this when auth is enabled } s.httpServer, _ = http.NewServer(ctx, httpConfig) return s, nil @@ -60,11 +58,7 @@ func (s *Server) policy() policy.Session { // Network implements proxy.Inbound. func (s *Server) Network() []net.Network { - list := []net.Network{net.Network_TCP} - if s.config.UdpEnabled { - list = append(list, net.Network_UDP) - } - return list + return []net.Network{net.Network_TCP} } // Process implements proxy.Inbound. @@ -94,8 +88,6 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con return s.httpServer.ProcessWithFirstbyte(ctx, network, conn, dispatcher, firstbyte...) } return s.processTCP(ctx, conn, dispatcher, firstbyte) - case net.Network_UDP: - return s.handleUDPPayload(ctx, conn, dispatcher) default: return errors.New("unknown network: ", network) } @@ -126,7 +118,8 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche Reader: buf.NewReader(conn), Buffer: buf.MultiBuffer{buf.FromBytes(firstbyte)}, } - request, err := svrSession.Handshake(reader, conn) + request, tempUDPConn, err := svrSession.Handshake(reader, conn) + defer common.CloseIfExists(tempUDPConn) if err != nil { if inbound.Source.IsValid() { log.Record(&log.AccessMessage{ @@ -170,26 +163,25 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche } if request.Command == protocol.RequestCommandUDP { - if s.udpFilter != nil { - s.udpFilter.Add(conn.RemoteAddr()) + if tempUDPConn == nil { + return errors.New("UDP associate with listen port failed") } - return s.handleUDP(conn) + tempUDPConn.SetTimeout(plcy.Timeouts.ConnectionIdle) + errCh := make(chan error, 1) + go func() { + errCh <- s.handleUDPPayload(ctx, tempUDPConn, dispatcher) + }() + // Associated TCP keeps the UDP alive + // Close UDP if TCP connection is closed + // Or Close TCP if UDP is idle timeout + io.Copy(buf.DiscardBytes, conn) + tempUDPConn.Close() + return <-errCh } - return nil } -func (*Server) handleUDP(c io.Reader) error { - // The TCP connection closes after this method returns. We need to wait until - // the client closes it. - return common.Error2(io.Copy(buf.DiscardBytes, c)) -} - func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error { - if s.udpFilter != nil && !s.udpFilter.Check(conn.RemoteAddr()) { - errors.LogDebug(ctx, "Unauthorized UDP access from ", conn.RemoteAddr().String()) - return nil - } udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { payload := packet.Payload errors.LogDebug(ctx, "writing back UDP response with ", payload.Len(), " bytes") diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go new file mode 100644 index 000000000..5e0dab03d --- /dev/null +++ b/proxy/socks/temp_udp_listen.go @@ -0,0 +1,72 @@ +package socks + +import ( + "context" + "net" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common/signal" +) + +func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, expectedRemote *net.UDPAddr) *TempUDPConn { + t := &TempUDPConn{ + PacketConn: udpConn, + AssociatedTCPConn: tcpConn, + } + t.ExpectedRemote.Store(expectedRemote) + return t +} + +// TempUDPConn wait for the first packet to determine the remote address +// SetTimeout MUST be called before any read/write operation +type TempUDPConn struct { + net.PacketConn + AssociatedTCPConn net.Conn + ExpectedRemote atomic.Pointer[net.UDPAddr] + Timer *signal.ActivityTimer +} + +func (c *TempUDPConn) Read(b []byte) (n int, err error) { + var remote net.Addr + for { + n, remote, err = c.PacketConn.ReadFrom(b) + if err != nil { + return + } + remote := remote.(*net.UDPAddr) + expected := c.ExpectedRemote.Load() + if remote.IP.Equal(expected.IP) { + if remote.Port == expected.Port { + c.Timer.Update() + return + } + if expected.Port == 0 { + c.ExpectedRemote.Store(remote) + c.Timer.Update() + return + } + } + } +} + +func (c *TempUDPConn) Write(b []byte) (n int, err error) { + c.Timer.Update() + return c.PacketConn.WriteTo(b, c.ExpectedRemote.Load()) +} + +func (c *TempUDPConn) RemoteAddr() net.Addr { + return c.ExpectedRemote.Load() +} + +func (c *TempUDPConn) SetTimeout(d time.Duration) { + c.Timer = signal.CancelAfterInactivity(context.Background(), func() { + c.Close() + }, d) +} + +func (c *TempUDPConn) Close() error { + c.Timer.SetTimeout(0) + c.AssociatedTCPConn.Close() + return c.PacketConn.Close() +} diff --git a/proxy/socks/udpfilter.go b/proxy/socks/udpfilter.go deleted file mode 100644 index 9ae3e6971..000000000 --- a/proxy/socks/udpfilter.go +++ /dev/null @@ -1,31 +0,0 @@ -package socks - -import ( - "net" - "sync" -) - -/* -In the sock implementation of * ray, UDP authentication is flawed and can be bypassed. -Tracking a UDP connection may be a bit troublesome. -Here is a simple solution. -We create a filter, add remote IP to the pool when it try to establish a UDP connection with auth. -And drop UDP packets from unauthorized IP. -After discussion, we believe it is not necessary to add a timeout mechanism to this filter. -*/ - -type UDPFilter struct { - ips sync.Map -} - -func (f *UDPFilter) Add(addr net.Addr) bool { - ip, _, _ := net.SplitHostPort(addr.String()) - f.ips.Store(ip, true) - return true -} - -func (f *UDPFilter) Check(addr net.Addr) bool { - ip, _, _ := net.SplitHostPort(addr.String()) - _, ok := f.ips.Load(ip) - return ok -} From fa466f81746bd692ca2213413bf7dc854abec5c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 18:20:27 +0000 Subject: [PATCH 13/78] Bump golang.org/x/net from 0.54.0 to 0.55.0 (#6192) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0. - [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.55.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 283c72531..e4c4b8e2e 100644 --- a/go.mod +++ b/go.mod @@ -23,9 +23,9 @@ require ( go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.51.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/net v0.54.0 + golang.org/x/net v0.55.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v1.0.1 diff --git a/go.sum b/go.sum index 754a9524c..1e0d65bf6 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= @@ -111,8 +111,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From cb206ddc74766509f57321c48d08ec807b9dfbad Mon Sep 17 00:00:00 2001 From: Kirill Livanov <13822441+kirilllivanov@users.noreply.github.com> Date: Thu, 28 May 2026 21:27:08 +0300 Subject: [PATCH 14/78] DNS: Avoid passing domain to WriteTo func (#6163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/XTLS/Xray-core/pull/6163#issuecomment-4528771758 --------- Co-authored-by: 风扇滑翔翼 --- app/dns/nameserver_udp.go | 4 ---- proxy/wireguard/client.go | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index 55b825f71..e75f301ac 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -131,8 +131,6 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot newReq.msg = &newMsg s.addPendingRequest(&newReq) b, _ := dns.PackMessage(newReq.msg) - copyDest := net.UDPDestination(s.address.Address, s.address.Port) - b.UDP = ©Dest s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b) return } @@ -179,8 +177,6 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan< } return } - copyDest := net.UDPDestination(s.address.Address, s.address.Port) - b.UDP = ©Dest s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b) } } diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index 3ee3a2c5b..8a02d89f1 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -185,6 +185,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } addr = net.IPAddress(ips[dice.Roll(len(ips))]) } + destination.Address = addr var newCtx context.Context var newCancel context.CancelFunc From 09002ab763a8b7a8d8be2f7603b98024043f150d Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 29 May 2026 03:24:22 +0800 Subject: [PATCH 15/78] noise finalmask: Better `reset` (#6188) https://github.com/XTLS/Xray-core/pull/6170 Explanation: https://github.com/XTLS/Xray-core/pull/5657#issue-3901983829 --- transport/internet/finalmask/noise/conn.go | 78 +++++----------------- 1 file changed, 17 insertions(+), 61 deletions(-) diff --git a/transport/internet/finalmask/noise/conn.go b/transport/internet/finalmask/noise/conn.go index c3ab89ab4..8bb115bb4 100644 --- a/transport/internet/finalmask/noise/conn.go +++ b/transport/internet/finalmask/noise/conn.go @@ -12,85 +12,41 @@ type noiseConn struct { net.PacketConn config *Config m map[string]time.Time - stop chan struct{} - once sync.Once - mutex sync.RWMutex + mu sync.Mutex } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &noiseConn{ + return &noiseConn{ PacketConn: raw, config: c, m: make(map[string]time.Time), - stop: make(chan struct{}), - } - - if conn.config.ResetMax > 0 { - go conn.reset() - } - - return conn, nil + }, nil } func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { return NewConnClient(c, raw) } -func (c *noiseConn) reset() { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for { - select { - case <-ticker.C: - c.mutex.RLock() - now := time.Now() - timeOut := make([]string, 0, len(c.m)) - for key, last := range c.m { - if now.After(last) { - timeOut = append(timeOut, key) - } - } - c.mutex.RUnlock() - - for _, key := range timeOut { - c.mutex.Lock() - delete(c.m, key) - c.mutex.Unlock() - } - case <-c.stop: - return - } - } -} - func (c *noiseConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.mutex.RLock() - _, ready := c.m[addr.String()] - c.mutex.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() - if !ready { - c.mutex.Lock() - _, ready = c.m[addr.String()] - if !ready { - for _, item := range c.config.Items { - if item.RandMax > 0 { - item.Packet = make([]byte, crypto.RandBetween(item.RandMin, item.RandMax)) - crypto.RandBytesBetween(item.Packet, byte(item.RandRangeMin), byte(item.RandRangeMax)) - } + t := c.m[addr.String()] + + if t.IsZero() || (c.config.ResetMax > 0 && time.Now().After(t)) { + for _, item := range c.config.Items { + if item.RandMax > 0 { + buf := make([]byte, crypto.RandBetween(item.RandMin, item.RandMax)) + crypto.RandBytesBetween(buf, byte(item.RandRangeMin), byte(item.RandRangeMax)) + c.PacketConn.WriteTo(buf, addr) + } else { c.PacketConn.WriteTo(item.Packet, addr) - time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond) } - c.m[addr.String()] = time.Now().Add(time.Duration(crypto.RandBetween(c.config.ResetMin, c.config.ResetMax)) * time.Second) + time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond) } - c.mutex.Unlock() } + c.m[addr.String()] = time.Now().Add(time.Duration(crypto.RandBetween(c.config.ResetMin, c.config.ResetMax)) * time.Second) + return c.PacketConn.WriteTo(p, addr) } - -func (c *noiseConn) Close() error { - c.once.Do(func() { - close(c.stop) - }) - return c.PacketConn.Close() -} From 1cd7d25fecb71edac3dadffa8ca1255321253a29 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 29 May 2026 03:44:16 +0800 Subject: [PATCH 16/78] header-custom finalmask: Remove headerConnMode headerReadAddrAware interface (#6193) https://github.com/XTLS/Xray-core/pull/5920#issuecomment-4253108964 https://github.com/XTLS/Xray-core/pull/6193#issuecomment-4531875088 --- common/serial/typed_message_test.go | 29 ------ infra/conf/transport_internet.go | 16 +++- infra/conf/transport_test.go | 3 +- transport/internet/finalmask/finalmask.go | 39 +------- .../finalmask/header/custom/config.go | 23 ++--- .../finalmask/header/custom/config.pb.go | 91 ++++++++++++++----- .../finalmask/header/custom/config.proto | 6 +- .../finalmask/header/custom/metadata_test.go | 62 +++++++++---- .../internet/finalmask/header/custom/udp.go | 22 ++--- transport/internet/finalmask/udp_test.go | 17 ++-- 10 files changed, 156 insertions(+), 152 deletions(-) diff --git a/common/serial/typed_message_test.go b/common/serial/typed_message_test.go index 75d529fd7..726a7733d 100644 --- a/common/serial/typed_message_test.go +++ b/common/serial/typed_message_test.go @@ -4,7 +4,6 @@ import ( "testing" . "github.com/xtls/xray-core/common/serial" - "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" ) func TestGetInstance(t *testing.T) { @@ -23,31 +22,3 @@ func TestConvertingNilMessage(t *testing.T) { t.Error("expect nil, but actually not") } } - -func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) { - msg := &custom.UDPConfig{ - Mode: "standalone", - Client: []*custom.UDPItem{ - {Rand: 12, Save: "txid"}, - }, - } - - tm := ToTypedMessage(msg) - if tm == nil { - t.Fatal("expected typed message") - } - - roundTrip, err := tm.GetInstance() - if err != nil { - t.Fatalf("GetInstance() failed: %v", err) - } - - udp, ok := roundTrip.(*custom.UDPConfig) - if !ok { - t.Fatalf("unexpected round-trip type: %T", roundTrip) - } - - if udp.GetMode() != "standalone" { - t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode()) - } -} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 7bd481652..0fd1c7673 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1734,11 +1734,17 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) { }) } - return &custom.UDPConfig{ - Client: client, - Server: server, - Mode: c.Mode, - }, nil + if c.Mode == "standalone" { + return &custom.UDPStandaloneConfig{ + Client: client, + Server: server, + }, nil + } else { + return &custom.UDPConfig{ + Client: client, + Server: server, + }, nil + } } type Dns struct { diff --git a/infra/conf/transport_test.go b/infra/conf/transport_test.go index 15c9aba30..0814fe6f7 100644 --- a/infra/conf/transport_test.go +++ b/infra/conf/transport_test.go @@ -195,8 +195,7 @@ func TestHeaderCustomUDPBuild(t *testing.T) { "mode": "standalone" }`, Parser: parser, - Output: &finalmaskcustom.UDPConfig{ - Mode: "standalone", + Output: &finalmaskcustom.UDPStandaloneConfig{ Client: []*finalmaskcustom.UDPItem{ { RandMax: 255, diff --git a/transport/internet/finalmask/finalmask.go b/transport/internet/finalmask/finalmask.go index 2fca1215f..c732f477c 100644 --- a/transport/internet/finalmask/finalmask.go +++ b/transport/internet/finalmask/finalmask.go @@ -31,19 +31,6 @@ func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketCon var conns []net.PacketConn for i, mask := range m.udpmasks { if _, ok := mask.(headerConn); ok { - if mode, ok := mask.(headerConnMode); ok && !mode.UseHeaderConn() { - if len(conns) > 0 { - raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw} - sizes = nil - conns = nil - } - var err error - raw, err = mask.WrapPacketConnClient(raw, i, len(m.udpmasks)-1) - if err != nil { - return nil, err - } - continue - } conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1) if err != nil { return nil, err @@ -77,19 +64,6 @@ func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketCon var conns []net.PacketConn for i, mask := range m.udpmasks { if _, ok := mask.(headerConn); ok { - if mode, ok := mask.(headerConnMode); ok && !mode.UseHeaderConn() { - if len(conns) > 0 { - raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw} - sizes = nil - conns = nil - } - var err error - raw, err = mask.WrapPacketConnServer(raw, i, len(m.udpmasks)-1) - if err != nil { - return nil, err - } - continue - } conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1) if err != nil { return nil, err @@ -126,10 +100,6 @@ type headerConn interface { HeaderConn() } -type headerConnMode interface { - UseHeaderConn() bool -} - type headerSize interface { Size() int } @@ -143,10 +113,6 @@ type headerManagerConn struct { writeBuf [UDPSize]byte } -type headerReadAddrAware interface { - SetReadAddr(net.Addr) -} - func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { buf := p if len(buf) < UDPSize { @@ -173,9 +139,6 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) } for i := range c.conns { - if aware, ok := c.conns[i].(headerReadAddrAware); ok { - aware.SetReadAddr(addr) - } n, _, err = c.conns[i].ReadFrom(newBuf) if n == 0 || err != nil { errors.LogDebug(context.Background(), addr, " mask read err ", err) @@ -211,7 +174,7 @@ func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) n = copy(c.writeBuf[sum:], p) for i := len(c.conns) - 1; i >= 0; i-- { - n, err = c.conns[i].WriteTo(c.writeBuf[sum-c.sizes[i]:n+sum], addr) + n, err = c.conns[i].WriteTo(c.writeBuf[sum-c.sizes[i]:n+sum], nil) if n == 0 || err != nil { errors.LogDebug(context.Background(), addr, " mask write err ", err) return 0, nil diff --git a/transport/internet/finalmask/header/custom/config.go b/transport/internet/finalmask/header/custom/config.go index 0427051bd..23460111d 100644 --- a/transport/internet/finalmask/header/custom/config.go +++ b/transport/internet/finalmask/header/custom/config.go @@ -4,8 +4,7 @@ import ( "net" ) -func (c *TCPConfig) TCP() { -} +func (c *TCPConfig) TCP() {} func (c *TCPConfig) WrapConnClient(raw net.Conn) (net.Conn, error) { return NewConnClientTCP(c, raw) @@ -15,26 +14,24 @@ func (c *TCPConfig) WrapConnServer(raw net.Conn) (net.Conn, error) { return NewConnServerTCP(c, raw) } -func (c *UDPConfig) UDP() { -} +func (c *UDPConfig) UDP() {} + +func (c *UDPConfig) HeaderConn() {} func (c *UDPConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - if c.Mode == "standalone" { - return NewConnClientUDPStandalone(c, raw) - } return NewConnClientUDP(c, raw) } func (c *UDPConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - if c.Mode == "standalone" { - return NewConnServerUDPStandalone(c, raw) - } return NewConnServerUDP(c, raw) } -func (c *UDPConfig) HeaderConn() { +func (c *UDPStandaloneConfig) UDP() {} + +func (c *UDPStandaloneConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClientUDPStandalone(c, raw) } -func (c *UDPConfig) UseHeaderConn() bool { - return c.Mode != "standalone" +func (c *UDPStandaloneConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServerUDPStandalone(c, raw) } diff --git a/transport/internet/finalmask/header/custom/config.pb.go b/transport/internet/finalmask/header/custom/config.pb.go index 434238602..66ffab495 100644 --- a/transport/internet/finalmask/header/custom/config.pb.go +++ b/transport/internet/finalmask/header/custom/config.pb.go @@ -511,7 +511,6 @@ type UDPConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Client []*UDPItem `protobuf:"bytes,1,rep,name=client,proto3" json:"client,omitempty"` Server []*UDPItem `protobuf:"bytes,2,rep,name=server,proto3" json:"server,omitempty"` - Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -560,11 +559,56 @@ func (x *UDPConfig) GetServer() []*UDPItem { return nil } -func (x *UDPConfig) GetMode() string { +type UDPStandaloneConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Client []*UDPItem `protobuf:"bytes,1,rep,name=client,proto3" json:"client,omitempty"` + Server []*UDPItem `protobuf:"bytes,2,rep,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UDPStandaloneConfig) Reset() { + *x = UDPStandaloneConfig{} + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UDPStandaloneConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UDPStandaloneConfig) ProtoMessage() {} + +func (x *UDPStandaloneConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[7] if x != nil { - return x.Mode + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) +} + +// Deprecated: Use UDPStandaloneConfig.ProtoReflect.Descriptor instead. +func (*UDPStandaloneConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{7} +} + +func (x *UDPStandaloneConfig) GetClient() []*UDPItem { + if x != nil { + return x.Client + } + return nil +} + +func (x *UDPStandaloneConfig) GetServer() []*UDPItem { + if x != nil { + return x.Server + } + return nil } var File_transport_internet_finalmask_header_custom_config_proto protoreflect.FileDescriptor @@ -605,11 +649,13 @@ const file_transport_internet_finalmask_header_custom_config_proto_rawDesc = "" "\x06packet\x18\x04 \x01(\fR\x06packet\x12\x12\n" + "\x04save\x18\x05 \x01(\tR\x04save\x12\x10\n" + "\x03var\x18\x06 \x01(\tR\x03var\x12I\n" + - "\x04expr\x18\a \x01(\v25.xray.transport.internet.finalmask.header.custom.ExprR\x04expr\"\xc3\x01\n" + + "\x04expr\x18\a \x01(\v25.xray.transport.internet.finalmask.header.custom.ExprR\x04expr\"\xaf\x01\n" + "\tUDPConfig\x12P\n" + "\x06client\x18\x01 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06client\x12P\n" + - "\x06server\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06server\x12\x12\n" + - "\x04mode\x18\x03 \x01(\tR\x04modeB\xaf\x01\n" + + "\x06server\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06server\"\xb9\x01\n" + + "\x13UDPStandaloneConfig\x12P\n" + + "\x06client\x18\x01 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06client\x12P\n" + + "\x06server\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06serverB\xaf\x01\n" + "3com.xray.transport.internet.finalmask.header.customP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/header/custom\xaa\x02/Xray.Transport.Internet.Finalmask.Header.Customb\x06proto3" var ( @@ -624,15 +670,16 @@ func file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP() return file_transport_internet_finalmask_header_custom_config_proto_rawDescData } -var file_transport_internet_finalmask_header_custom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_transport_internet_finalmask_header_custom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_transport_internet_finalmask_header_custom_config_proto_goTypes = []any{ - (*Expr)(nil), // 0: xray.transport.internet.finalmask.header.custom.Expr - (*ExprArg)(nil), // 1: xray.transport.internet.finalmask.header.custom.ExprArg - (*TCPItem)(nil), // 2: xray.transport.internet.finalmask.header.custom.TCPItem - (*TCPSequence)(nil), // 3: xray.transport.internet.finalmask.header.custom.TCPSequence - (*TCPConfig)(nil), // 4: xray.transport.internet.finalmask.header.custom.TCPConfig - (*UDPItem)(nil), // 5: xray.transport.internet.finalmask.header.custom.UDPItem - (*UDPConfig)(nil), // 6: xray.transport.internet.finalmask.header.custom.UDPConfig + (*Expr)(nil), // 0: xray.transport.internet.finalmask.header.custom.Expr + (*ExprArg)(nil), // 1: xray.transport.internet.finalmask.header.custom.ExprArg + (*TCPItem)(nil), // 2: xray.transport.internet.finalmask.header.custom.TCPItem + (*TCPSequence)(nil), // 3: xray.transport.internet.finalmask.header.custom.TCPSequence + (*TCPConfig)(nil), // 4: xray.transport.internet.finalmask.header.custom.TCPConfig + (*UDPItem)(nil), // 5: xray.transport.internet.finalmask.header.custom.UDPItem + (*UDPConfig)(nil), // 6: xray.transport.internet.finalmask.header.custom.UDPConfig + (*UDPStandaloneConfig)(nil), // 7: xray.transport.internet.finalmask.header.custom.UDPStandaloneConfig } var file_transport_internet_finalmask_header_custom_config_proto_depIdxs = []int32{ 1, // 0: xray.transport.internet.finalmask.header.custom.Expr.args:type_name -> xray.transport.internet.finalmask.header.custom.ExprArg @@ -645,11 +692,13 @@ var file_transport_internet_finalmask_header_custom_config_proto_depIdxs = []int 0, // 7: xray.transport.internet.finalmask.header.custom.UDPItem.expr:type_name -> xray.transport.internet.finalmask.header.custom.Expr 5, // 8: xray.transport.internet.finalmask.header.custom.UDPConfig.client:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem 5, // 9: xray.transport.internet.finalmask.header.custom.UDPConfig.server:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 5, // 10: xray.transport.internet.finalmask.header.custom.UDPStandaloneConfig.client:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem + 5, // 11: xray.transport.internet.finalmask.header.custom.UDPStandaloneConfig.server:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_transport_internet_finalmask_header_custom_config_proto_init() } @@ -670,7 +719,7 @@ func file_transport_internet_finalmask_header_custom_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_custom_config_proto_rawDesc), len(file_transport_internet_finalmask_header_custom_config_proto_rawDesc)), NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/transport/internet/finalmask/header/custom/config.proto b/transport/internet/finalmask/header/custom/config.proto index d350e76f7..6a27c57e6 100644 --- a/transport/internet/finalmask/header/custom/config.proto +++ b/transport/internet/finalmask/header/custom/config.proto @@ -56,5 +56,9 @@ message UDPItem { message UDPConfig { repeated UDPItem client = 1; repeated UDPItem server = 2; - string mode = 3; } + +message UDPStandaloneConfig { + repeated UDPItem client = 1; + repeated UDPItem server = 2; +} \ No newline at end of file diff --git a/transport/internet/finalmask/header/custom/metadata_test.go b/transport/internet/finalmask/header/custom/metadata_test.go index 78633e61a..0c426cea8 100644 --- a/transport/internet/finalmask/header/custom/metadata_test.go +++ b/transport/internet/finalmask/header/custom/metadata_test.go @@ -3,6 +3,7 @@ package custom import ( "bytes" "encoding/binary" + "fmt" "io" "net" "strings" @@ -124,8 +125,8 @@ func TestMetadataAliasesExposeDstNames(t *testing.T) { } } -func TestMetadataUDPWriteUsesRemotePort(t *testing.T) { - cfg := &UDPConfig{ +func TestMetadataUDPStandaloneWriteUsesRemotePort(t *testing.T) { + cfg := &UDPStandaloneConfig{ Client: []*UDPItem{ { Expr: &Expr{ @@ -136,6 +137,11 @@ func TestMetadataUDPWriteUsesRemotePort(t *testing.T) { }, }, }, + Server: []*UDPItem{ + { + Packet: []byte{0xCA, 0xFE}, + }, + }, } clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0") @@ -156,26 +162,48 @@ func TestMetadataUDPWriteUsesRemotePort(t *testing.T) { } payload := []byte("meta") + errCh := make(chan error, 1) + go func() { + wire := make([]byte, 64) + _ = serverRaw.SetDeadline(time.Now().Add(time.Second)) + + n, addr, err := serverRaw.ReadFrom(wire) + if err != nil { + errCh <- err + return + } + wantPort := uint16(serverRaw.LocalAddr().(*net.UDPAddr).Port) + if n != 2 { + errCh <- fmt.Errorf("unexpected handshake size: %d", n) + return + } + if got := binary.BigEndian.Uint16(wire[:2]); got != wantPort { + errCh <- fmt.Errorf("unexpected encoded port: got=%d want=%d", got, wantPort) + return + } + if _, err := serverRaw.WriteTo([]byte{0xCA, 0xFE}, addr); err != nil { + errCh <- err + return + } + + n, _, err = serverRaw.ReadFrom(wire) + if err != nil { + errCh <- err + return + } + if !bytes.Equal(wire[:n], payload) { + errCh <- fmt.Errorf("unexpected payload: %q", wire[:n]) + return + } + errCh <- nil + }() + if _, err := client.WriteTo(payload, serverRaw.LocalAddr()); err != nil { t.Fatal(err) } - - wire := make([]byte, 64) - _ = serverRaw.SetDeadline(time.Now().Add(time.Second)) - n, _, err := serverRaw.ReadFrom(wire) - if err != nil { + if err := <-errCh; err != nil { t.Fatal(err) } - if n != len(payload)+2 { - t.Fatalf("unexpected wire size: %d", n) - } - wantPort := uint16(serverRaw.LocalAddr().(*net.UDPAddr).Port) - if got := binary.BigEndian.Uint16(wire[:2]); got != wantPort { - t.Fatalf("unexpected encoded port: got=%d want=%d", got, wantPort) - } - if !bytes.Equal(wire[2:n], payload) { - t.Fatalf("unexpected payload: %q", wire[2:n]) - } } func TestMetadataTCPHandshakeUsesEndpointPorts(t *testing.T) { diff --git a/transport/internet/finalmask/header/custom/udp.go b/transport/internet/finalmask/header/custom/udp.go index b16c7c9a2..dba73798b 100644 --- a/transport/internet/finalmask/header/custom/udp.go +++ b/transport/internet/finalmask/header/custom/udp.go @@ -16,7 +16,6 @@ type udpCustomClient struct { server []*UDPItem merged []byte read int - addr net.Addr state *stateStore vars map[string][]byte } @@ -33,13 +32,13 @@ func (h *udpCustomClient) Serialize(b []byte) { func (h *udpCustomClient) Match(b []byte) bool { var initial map[string][]byte if h.state != nil { - initial, _ = h.state.get(udpStateKey(h.addr)) + initial, _ = h.state.get(udpStateKey(nil)) } vars, ok := matchUDPItems(h.server, b, h.read, initial) if ok { h.vars = vars if h.state != nil { - h.state.set(udpStateKey(h.addr), vars) + h.state.set(udpStateKey(nil), vars) } } return ok @@ -110,16 +109,11 @@ func (c *udpCustomClientConn) WriteTo(p []byte, addr net.Addr) (n int, err error return len(p), nil } -func (c *udpCustomClientConn) SetReadAddr(addr net.Addr) { - c.header.addr = addr -} - type udpCustomServer struct { client []*UDPItem server []*UDPItem merged []byte read int - addr net.Addr state *stateStore vars map[string][]byte } @@ -136,13 +130,13 @@ func (h *udpCustomServer) Serialize(b []byte) { func (h *udpCustomServer) Match(b []byte) bool { var initial map[string][]byte if h.state != nil { - initial, _ = h.state.get(udpStateKey(h.addr)) + initial, _ = h.state.get(udpStateKey(nil)) } vars, ok := matchUDPItems(h.client, b, h.read, initial) if ok { h.vars = vars if h.state != nil { - h.state.set(udpStateKey(h.addr), vars) + h.state.set(udpStateKey(nil), vars) } } return ok @@ -213,10 +207,6 @@ func (c *udpCustomServerConn) WriteTo(p []byte, addr net.Addr) (n int, err error return len(p), nil } -func (c *udpCustomServerConn) SetReadAddr(addr net.Addr) { - c.header.addr = addr -} - func matchUDPItems(items []*UDPItem, data []byte, totalSize int, initial map[string][]byte) (map[string][]byte, bool) { if len(data) < totalSize { return nil, false @@ -294,7 +284,7 @@ type udpStandaloneWaiter struct { done chan error } -func NewConnClientUDPStandalone(c *UDPConfig, raw net.PacketConn) (net.PacketConn, error) { +func NewConnClientUDPStandalone(c *UDPStandaloneConfig, raw net.PacketConn) (net.PacketConn, error) { clientSavedSizes := collectSavedUDPSizes(c.Client) read, err := measureUDPItemsWithFallback(c.Server, clientSavedSizes) if err != nil { @@ -441,7 +431,7 @@ type udpCustomStandaloneServerConn struct { read int } -func NewConnServerUDPStandalone(c *UDPConfig, raw net.PacketConn) (net.PacketConn, error) { +func NewConnServerUDPStandalone(c *UDPStandaloneConfig, raw net.PacketConn) (net.PacketConn, error) { read, err := measureUDPItems(c.Client) if err != nil { return nil, err diff --git a/transport/internet/finalmask/udp_test.go b/transport/internet/finalmask/udp_test.go index 7b7b75663..0c0641323 100644 --- a/transport/internet/finalmask/udp_test.go +++ b/transport/internet/finalmask/udp_test.go @@ -154,9 +154,8 @@ func (h *captureUDPHandler) NewPacketConnection(_ context.Context, _ singN.Packe func (h *captureUDPHandler) NewError(_ context.Context, _ error) {} -func newStandaloneEchoUDPConfig() *custom.UDPConfig { - return &custom.UDPConfig{ - Mode: "standalone", +func newStandaloneEchoUDPConfig() *custom.UDPStandaloneConfig { + return &custom.UDPStandaloneConfig{ Client: []*custom.UDPItem{ {Packet: []byte{0xAA}}, {Rand: 2, Save: "txid"}, @@ -168,9 +167,8 @@ func newStandaloneEchoUDPConfig() *custom.UDPConfig { } } -func newStandaloneStunLikeUDPConfig() *custom.UDPConfig { - return &custom.UDPConfig{ - Mode: "standalone", +func newStandaloneStunLikeUDPConfig() *custom.UDPStandaloneConfig { + return &custom.UDPStandaloneConfig{ Client: []*custom.UDPItem{ {Packet: []byte{0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xA4, 0x42}}, {Rand: 12, RandMin: 0x2A, RandMax: 0x2A, Save: "txid"}, @@ -185,9 +183,8 @@ func newStandaloneStunLikeUDPConfig() *custom.UDPConfig { } } -func newStandaloneStunLikeUDPServerConfig() *custom.UDPConfig { - return &custom.UDPConfig{ - Mode: "standalone", +func newStandaloneStunLikeUDPServerConfig() *custom.UDPStandaloneConfig { + return &custom.UDPStandaloneConfig{ Client: []*custom.UDPItem{ {Packet: []byte{0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xA4, 0x42}}, {Rand: 12, RandMin: 0x2A, RandMax: 0x2A, Save: "txid"}, @@ -236,7 +233,7 @@ func newStandaloneStunLikeUDPServerConfig() *custom.UDPConfig { } } -func newUDPClientServerPair(t *testing.T, cfg *custom.UDPConfig) (net.PacketConn, net.PacketConn, net.PacketConn, net.PacketConn) { +func newUDPClientServerPair(t *testing.T, cfg *custom.UDPStandaloneConfig) (net.PacketConn, net.PacketConn, net.PacketConn, net.PacketConn) { t.Helper() clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0") From a2cec2e580e8ea1900701b11b425e0351de2ec71 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 29 May 2026 04:06:32 +0800 Subject: [PATCH 17/78] DNS: Avoid panic on domain too long (#6207) Fixes https://github.com/XTLS/Xray-core/issues/6204 --- app/dns/dnscommon.go | 13 +++++++++---- app/dns/dnscommon_test.go | 8 +++++++- app/dns/nameserver_doh.go | 22 ++++++++++++++++++++-- app/dns/nameserver_quic.go | 14 +++++++++++++- app/dns/nameserver_tcp.go | 14 +++++++++++++- app/dns/nameserver_udp.go | 14 +++++++++++++- 6 files changed, 75 insertions(+), 10 deletions(-) diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go index 2092a2fd7..d16316b88 100644 --- a/app/dns/dnscommon.go +++ b/app/dns/dnscommon.go @@ -127,15 +127,20 @@ func genEDNS0Options(clientIP net.IP, padding int) *dnsmessage.Resource { return opt } -func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest { +func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) ([]*dnsRequest, error) { + name, err := dnsmessage.NewName(domain) + if err != nil { + return nil, err + } + qA := dnsmessage.Question{ - Name: dnsmessage.MustNewName(domain), + Name: name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, } qAAAA := dnsmessage.Question{ - Name: dnsmessage.MustNewName(domain), + Name: name, Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET, } @@ -175,7 +180,7 @@ func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() ui }) } - return reqs + return reqs, nil } // parseResponse parses DNS answers from the returned payload diff --git a/app/dns/dnscommon_test.go b/app/dns/dnscommon_test.go index e77117c68..fc73d20d8 100644 --- a/app/dns/dnscommon_test.go +++ b/app/dns/dnscommon_test.go @@ -2,6 +2,7 @@ package dns import ( "math/rand" + "strings" "testing" "time" @@ -131,10 +132,15 @@ func Test_buildReqMsgs(t *testing.T) { IPv6Enable: false, FakeEnable: false, }, nil}, 0}, + {"name too long", args{strings.Repeat("a", 256), dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }, nil}, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) { + if got, _ := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) { t.Errorf("buildReqMsgs() = %v, want %v", got, tt.want) } }) diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go index 2849dbed3..ed7f18466 100644 --- a/app/dns/nameserver_doh.go +++ b/app/dns/nameserver_doh.go @@ -137,14 +137,32 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er if s.Name()+"." == "DOH//"+fqdn { errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead") if noResponseErrCh != nil { - noResponseErrCh <- errors.New("tries to resolve itself!", s.Name()) + err := errors.New("tries to resolve itself!", s.Name()) + if option.IPv4Enable { + noResponseErrCh <- err + } + if option.IPv6Enable { + noResponseErrCh <- err + } } return } // As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467 // Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all - reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300)))) + reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300)))) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn) + if noResponseErrCh != nil { + if option.IPv4Enable { + noResponseErrCh <- err + } + if option.IPv6Enable { + noResponseErrCh <- err + } + } + return + } var deadline time.Time if d, ok := ctx.Deadline(); ok { diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index bd0da1700..058d25bfd 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -78,7 +78,19 @@ func (s *QUICNameServer) getCacheController() *CacheController { return s.cacheC func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) - reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn) + if noResponseErrCh != nil { + if option.IPv4Enable { + noResponseErrCh <- err + } + if option.IPv6Enable { + noResponseErrCh <- err + } + } + return + } var deadline time.Time if d, ok := ctx.Deadline(); ok { diff --git a/app/dns/nameserver_tcp.go b/app/dns/nameserver_tcp.go index 283a42a70..185dbad3e 100644 --- a/app/dns/nameserver_tcp.go +++ b/app/dns/nameserver_tcp.go @@ -113,7 +113,19 @@ func (s *TCPNameServer) getCacheController() *CacheController { func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) - reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn) + if noResponseErrCh != nil { + if option.IPv4Enable { + noResponseErrCh <- err + } + if option.IPv6Enable { + noResponseErrCh <- err + } + } + return + } var deadline time.Time if d, ok := ctx.Deadline(); ok { diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index e75f301ac..7761662fb 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -161,7 +161,19 @@ func (s *ClassicNameServer) getCacheController() *CacheController { func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) - reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn) + if noResponseErrCh != nil { + if option.IPv4Enable { + noResponseErrCh <- err + } + if option.IPv6Enable { + noResponseErrCh <- err + } + } + return + } for _, req := range reqs { udpReq := &udpDnsRequest{ From 4dcf802ae3a7c51a6cac68724f4e0e17b447d861 Mon Sep 17 00:00:00 2001 From: Jason Fox <44783405+the-thirteenth-fox@users.noreply.github.com> Date: Thu, 28 May 2026 23:15:38 +0300 Subject: [PATCH 18/78] README.md: Add flutter_vless to Xray Wrapper in Others (#6197) https://pub.dev/packages/flutter_vless --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a7dba7c3e..cad3ce831 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ - [xtls-sdk](https://github.com/remnawave/xtls-sdk) - [xtlsapi](https://github.com/hiddify/xtlsapi) - [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite) + - [flutter_vless](https://github.com/XIIIFOX/flutter_vless) - [Xray-core-python](https://github.com/LorenEteval/Xray-core-python) - [xray-api](https://github.com/XVGuardian/xray-api) - [XrayR](https://github.com/XrayR-project/XrayR) From 36303694d1edfeb1b83bf6e0f79caebf4651c063 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 29 May 2026 16:29:53 +0800 Subject: [PATCH 19/78] Finalmask: Add Realm (UDP hole punching in Hysteria v2.9.1) (#6137) https://github.com/XTLS/Xray-core/pull/5657#issuecomment-4446406536 https://github.com/XTLS/Xray-core/pull/6137#issuecomment-4469822775 Example: https://github.com/XTLS/Xray-core/pull/6137#issue-4454013510 --- .github/workflows/release.yml | 3 + README.md | 7 + go.mod | 5 + go.sum | 10 + infra/conf/transport_internet.go | 88 ++++ transport/internet/finalmask/realm/client.go | 171 ++++++++ transport/internet/finalmask/realm/config.go | 27 ++ .../internet/finalmask/realm/config.pb.go | 181 ++++++++ .../internet/finalmask/realm/config.proto | 19 + transport/internet/finalmask/realm/http.go | 307 ++++++++++++++ transport/internet/finalmask/realm/punch.go | 145 +++++++ transport/internet/finalmask/realm/server.go | 401 ++++++++++++++++++ transport/internet/finalmask/realm/stun.go | 224 ++++++++++ .../congestion/bbr/bandwidth_sampler.go | 6 +- .../hysteria/congestion/bbr/bbr_sender.go | 3 +- 15 files changed, 1594 insertions(+), 3 deletions(-) create mode 100644 transport/internet/finalmask/realm/client.go create mode 100644 transport/internet/finalmask/realm/config.go create mode 100644 transport/internet/finalmask/realm/config.pb.go create mode 100644 transport/internet/finalmask/realm/config.proto create mode 100644 transport/internet/finalmask/realm/http.go create mode 100644 transport/internet/finalmask/realm/punch.go create mode 100644 transport/internet/finalmask/realm/server.go create mode 100644 transport/internet/finalmask/realm/stun.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 589eddb1d..b1b468751 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -208,6 +208,9 @@ jobs: go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main # The line below is for without running conhost.exe version. Commented for not being used. Provided for reference. # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + elif [[ ${GOOS} == 'android' ]]; then + echo 'Building Xray for Android...' + go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0" -v ./main else echo 'Building Xray...' if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then diff --git a/README.md b/README.md index cad3ce831..deeb29049 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,13 @@ Make sure that you are using the same Go version, and remember to set the git co CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main ``` +For Android: + +```bash +GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main +GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main +``` + If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead: ```bash diff --git a/go.mod b/go.mod index e4c4b8e2e..c5b21f68e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 + github.com/pion/stun/v3 v3.1.2 github.com/pires/go-proxyproto v0.12.0 github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/robfig/cron/v3 v3.0.1 @@ -43,9 +44,13 @@ require ( github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/kr/text v0.2.0 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect + github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.12.0 // indirect diff --git a/go.sum b/go.sum index 1e0d65bf6..317452b5a 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY= +github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -67,6 +75,8 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 0fd1c7673..02f94de0a 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -32,6 +32,7 @@ import ( "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" "github.com/xtls/xray-core/transport/internet/finalmask/noise" + "github.com/xtls/xray-core/transport/internet/finalmask/realm" "github.com/xtls/xray-core/transport/internet/finalmask/salamander" finalsudoku "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" "github.com/xtls/xray-core/transport/internet/finalmask/xdns" @@ -1255,6 +1256,7 @@ var ( "sudoku": func() interface{} { return new(Sudoku) }, "xdns": func() interface{} { return new(Xdns) }, "xicmp": func() interface{} { return new(Xicmp) }, + "realm": func() interface{} { return new(Realm) }, }, "type", "settings") ) @@ -1908,6 +1910,92 @@ func (c *Xicmp) Build() (proto.Message, error) { return config, nil } +type Realm struct { + Url string `json:"url"` + StunServers []string `json:"stunServers"` + TlsConfig *TLSConfig `json:"tlsConfig"` +} + +func (c *Realm) Build() (proto.Message, error) { + var scheme, host, port, token, id string + var stunServers []string + var tlsConfig *tls.Config + + u, err := url.Parse(c.Url) + if err != nil { + return nil, err + } + + switch u.Scheme { + case "realm": + scheme = "https" + case "realm+http": + scheme = "http" + default: + return nil, errors.New("invalid scheme", u.Scheme) + } + + host = u.Hostname() + if host == "" { + return nil, errors.New("invalid host", host) + } + + port = u.Port() + if port == "" { + port = "443" + if scheme == "http" { + port = "80" + } + } + + token, err = url.PathUnescape(u.User.String()) + if err != nil { + return nil, err + } + if token == "" { + return nil, errors.New("invalid token", token) + } + + id, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/")) + if err != nil { + return nil, err + } + if id == "" { + return nil, errors.New("invalid id", id) + } + + if len(c.StunServers) == 0 { + return nil, errors.New("empty stunServers") + } + + for _, s := range c.StunServers { + _, _, err = net.SplitHostPort(s) + if err != nil { + return nil, err + } + } + + stunServers = c.StunServers + + if c.TlsConfig != nil { + tc, err := c.TlsConfig.Build() + if err != nil { + return nil, err + } + tlsConfig = tc.(*tls.Config) + } + + return &realm.Config{ + Scheme: scheme, + Host: host, + Port: port, + Token: token, + ID: id, + StunServers: stunServers, + TlsConfig: tlsConfig, + }, nil +} + type Mask struct { Type string `json:"type"` Settings *json.RawMessage `json:"settings"` diff --git a/transport/internet/finalmask/realm/client.go b/transport/internet/finalmask/realm/client.go new file mode 100644 index 000000000..ab7ba6bb4 --- /dev/null +++ b/transport/internet/finalmask/realm/client.go @@ -0,0 +1,171 @@ +package realm + +import ( + "context" + goerrors "errors" + "net" + "net/netip" + "slices" + "strings" + "time" + + "github.com/pion/stun/v3" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +type realmConnClient struct { + net.PacketConn + peer *net.UDPAddr + + realmClient *Client + realmID string + stunServers []string + stunTimeout time.Duration + punchTimeout time.Duration + punchInterval time.Duration +} + +func NewConnClient(config *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &realmConnClient{ + PacketConn: raw, + + realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig), + realmID: config.ID, + stunServers: config.StunServers, + stunTimeout: defaultSTUNTimeout, + punchTimeout: defaultPunchTimeout, + punchInterval: defaultPunchInterval, + } + + return conn.getpeer() +} + +func (c *realmConnClient) getpeer() (net.PacketConn, error) { + start := time.Now() + servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers) + errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start)) + if len(servers) == 0 { + return nil, errors.New("empty locals") + } + + start = time.Now() + locals := c.discover(servers) + errors.LogDebug(context.Background(), "[realm] update stun locals ", locals, " with ", time.Since(start)) + if len(locals) == 0 { + return nil, errors.New("empty locals") + } + + meta := common.Must2(NewPunchMetadata()) + + start = time.Now() + resp, err := c.realmClient.Connect(context.Background(), c.realmID, ConnectRequest{ + Addresses: addrPortStrings(locals), + PunchMetadata: meta, + }) + if err != nil { + return nil, err + } + errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", meta.Nonce, " connect ", resp.Addresses, " with ", time.Since(start)) + + peers, _ := parseAddrPorts(resp.Addresses) + errors.LogDebug(context.Background(), "[realm] update peers ", peers) + filteredPeers, seen := candidatePunchAddrs(locals, peers) + errors.LogDebug(context.Background(), "[realm] filtered peers ", filteredPeers) + expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen) + errors.LogDebug(context.Background(), "[realm] expanded peers ", expandedPeers) + + if len(expandedPeers) == 0 { + return nil, errors.New("empty peers") + } + + start = time.Now() + peer, err := c.punch(meta, peers) + if err != nil { + return nil, errors.New("punch fail").Base(err) + } + errors.LogDebug(context.Background(), "[realm] punch peer ", peer, " with ", time.Since(start)) + + c.peer = peer + return c, nil +} + +func (c *realmConnClient) discover(servers []*net.UDPAddr) []netip.AddrPort { + var transactionIDs = make(map[[stun.TransactionIDSize]byte]struct{}, len(servers)) + for _, server := range servers { + msg := common.Must2(stun.Build(stun.TransactionID, stun.BindingRequest)) + transactionIDs[msg.TransactionID] = struct{}{} + _, _ = c.PacketConn.WriteTo(msg.Raw, server) + } + + var buf = make([]byte, 1500) + var results = make([]netip.AddrPort, 0, len(servers)) + c.PacketConn.SetReadDeadline(time.Now().Add(defaultSTUNTimeout)) + for len(transactionIDs) > 0 { + n, _, err := c.PacketConn.ReadFrom(buf) + if err != nil { + break + } + msg, addrPort, err := parseSTUNBindingResponse(buf[:n]) + if err != nil { + continue + } + if _, ok := transactionIDs[msg.TransactionID]; ok { + delete(transactionIDs, msg.TransactionID) + results = append(results, addrPort) + } + } + c.PacketConn.SetReadDeadline(time.Time{}) + slices.SortFunc(results, func(a, b netip.AddrPort) int { + return strings.Compare(a.String(), b.String()) + }) + + return results +} + +func (c *realmConnClient) punch(meta PunchMetadata, peers []netip.AddrPort) (*net.UDPAddr, error) { + defer c.PacketConn.SetReadDeadline(time.Time{}) + nextSend := time.Now() + deadline := nextSend.Add(c.punchTimeout) + buf := make([]byte, punchMaxWireLen) + for { + now := time.Now() + if now.After(deadline) { + return nil, errors.New("timeout") + } + if now.After(nextSend) { + for _, peer := range peers { + packet := common.Must2(EncodePunchPacket(PunchPacketHello, meta)) + _, _ = c.PacketConn.WriteTo(packet, net.UDPAddrFromAddrPort(peer)) + } + nextSend = now.Add(c.punchInterval) + } + + if nextSend.After(deadline) { + c.PacketConn.SetReadDeadline(deadline) + } else { + c.PacketConn.SetReadDeadline(nextSend) + } + n, addr, err := c.PacketConn.ReadFrom(buf) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + continue + } + return nil, err + } + packet, err := DecodePunchPacket(buf[:n], meta) + if err != nil { + continue + } + if packet.Type == PunchPacketHello { + packet := common.Must2(EncodePunchPacket(PunchPacketAck, meta)) + _, _ = c.PacketConn.WriteTo(packet, addr) + } + return addr.(*net.UDPAddr), nil + } +} + +func (c *realmConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return c.PacketConn.WriteTo(p, c.peer) +} diff --git a/transport/internet/finalmask/realm/config.go b/transport/internet/finalmask/realm/config.go new file mode 100644 index 000000000..2604fa8ab --- /dev/null +++ b/transport/internet/finalmask/realm/config.go @@ -0,0 +1,27 @@ +package realm + +import ( + "net" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" +) + +func (c *Config) UDP() {} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + _, ok1 := raw.(*internet.FakePacketConn) + _, ok2 := raw.(*udphop.UdpHopPacketConn) + if level != 0 || ok1 || ok2 { + return nil, errors.New("realm requires being at the outermost level") + } + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + if level != 0 { + return nil, errors.New("realm requires being at the outermost level") + } + return NewConnServer(c, raw) +} diff --git a/transport/internet/finalmask/realm/config.pb.go b/transport/internet/finalmask/realm/config.pb.go new file mode 100644 index 000000000..ee273e732 --- /dev/null +++ b/transport/internet/finalmask/realm/config.pb.go @@ -0,0 +1,181 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/realm/config.proto + +package realm + +import ( + tls "github.com/xtls/xray-core/transport/internet/tls" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"` + Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` + Port string `protobuf:"bytes,3,opt,name=port,proto3" json:"port,omitempty"` + Token string `protobuf:"bytes,4,opt,name=token,proto3" json:"token,omitempty"` + ID string `protobuf:"bytes,5,opt,name=ID,proto3" json:"ID,omitempty"` + StunServers []string `protobuf:"bytes,6,rep,name=stun_servers,json=stunServers,proto3" json:"stun_servers,omitempty"` + TlsConfig *tls.Config `protobuf:"bytes,7,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetScheme() string { + if x != nil { + return x.Scheme + } + return "" +} + +func (x *Config) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *Config) GetPort() string { + if x != nil { + return x.Port + } + return "" +} + +func (x *Config) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *Config) GetID() string { + if x != nil { + return x.ID + } + return "" +} + +func (x *Config) GetStunServers() []string { + if x != nil { + return x.StunServers + } + return nil +} + +func (x *Config) GetTlsConfig() *tls.Config { + if x != nil { + return x.TlsConfig + } + return nil +} + +var File_transport_internet_finalmask_realm_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" + + "\n" + + "/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"\xd5\x01\n" + + "\x06Config\x12\x16\n" + + "\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" + + "\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x03 \x01(\tR\x04port\x12\x14\n" + + "\x05token\x18\x04 \x01(\tR\x05token\x12\x0e\n" + + "\x02ID\x18\x05 \x01(\tR\x02ID\x12!\n" + + "\fstun_servers\x18\x06 \x03(\tR\vstunServers\x12B\n" + + "\n" + + "tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfigB\x97\x01\n" + + "+com.xray.transport.internet.finalmask.realmP\x01Z xray.transport.internet.tls.Config + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_realm_config_proto_init() } +func file_transport_internet_finalmask_realm_config_proto_init() { + if File_transport_internet_finalmask_realm_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_realm_config_proto_rawDesc), len(file_transport_internet_finalmask_realm_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_realm_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_realm_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_realm_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_realm_config_proto = out.File + file_transport_internet_finalmask_realm_config_proto_goTypes = nil + file_transport_internet_finalmask_realm_config_proto_depIdxs = nil +} diff --git a/transport/internet/finalmask/realm/config.proto b/transport/internet/finalmask/realm/config.proto new file mode 100644 index 000000000..62dce86e3 --- /dev/null +++ b/transport/internet/finalmask/realm/config.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.realm; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Realm"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/realm"; +option java_package = "com.xray.transport.internet.finalmask.realm"; +option java_multiple_files = true; + +import "transport/internet/tls/config.proto"; + +message Config { + string scheme = 1; + string host = 2; + string port = 3; + string token = 4; + string ID = 5; + repeated string stun_servers = 6; + xray.transport.internet.tls.Config tls_config = 7; +} \ No newline at end of file diff --git a/transport/internet/finalmask/realm/http.go b/transport/internet/finalmask/realm/http.go new file mode 100644 index 000000000..2f58ee89b --- /dev/null +++ b/transport/internet/finalmask/realm/http.go @@ -0,0 +1,307 @@ +package realm + +import ( + "bufio" + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + + "github.com/xtls/xray-core/transport/internet/tls" +) + +const maxErrorBodySize = 64 * 1024 + +const ( + PunchNonceSize = 16 + PunchObfsKeySize = 32 +) + +type Client struct { + scheme string + hostport string + token string + httpClient *http.Client +} + +type RegisterResponse struct { + SessionID string `json:"session_id"` + TTL int `json:"ttl"` +} + +type HeartbeatResponse struct { + TTL int `json:"ttl"` +} + +type HeartbeatRequest struct { + Addresses []string `json:"addresses,omitempty"` +} + +type PunchMetadata struct { + Nonce string `json:"nonce"` + Obfs string `json:"obfs"` +} + +type ConnectRequest struct { + Addresses []string `json:"addresses"` + PunchMetadata +} + +type ConnectResponse struct { + Addresses []string `json:"addresses"` + PunchMetadata +} + +type PunchEvent struct { + Addresses []string `json:"addresses"` + PunchMetadata +} + +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` +} + +type StatusError struct { + StatusCode int + Response ErrorResponse +} + +func (e *StatusError) Error() string { + if e.Response.Error != "" || e.Response.Message != "" { + return fmt.Sprintf("realm server returned %d: %s: %s", e.StatusCode, e.Response.Error, e.Response.Message) + } + return fmt.Sprintf("realm server returned %d", e.StatusCode) +} + +func NewClient(scheme, host, port, token string, tlsConfig *tls.Config) *Client { + client := http.DefaultClient + if tlsConfig != nil { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = tlsConfig.GetTLSConfig() + client = &http.Client{Transport: tr} + } + return &Client{ + scheme: scheme, + hostport: net.JoinHostPort(host, port), + token: token, + httpClient: client, + } +} + +func NewPunchMetadata() (PunchMetadata, error) { + nonce, err := randHex(PunchNonceSize) + if err != nil { + return PunchMetadata{}, err + } + obfs, err := randHex(PunchObfsKeySize) + if err != nil { + return PunchMetadata{}, err + } + return PunchMetadata{ + Nonce: nonce, + Obfs: obfs, + }, nil +} + +func (c *Client) Register(ctx context.Context, realmID string, addresses []string) (*RegisterResponse, error) { + var resp RegisterResponse + if err := c.doJSON(ctx, http.MethodPost, realmID, "", c.token, addressRequest{Addresses: addresses}, http.StatusOK, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +func (c *Client) Deregister(ctx context.Context, realmID, sessionID string) error { + return c.doJSON(ctx, http.MethodDelete, realmID, "", sessionID, nil, http.StatusNoContent, nil) +} + +func (c *Client) Heartbeat(ctx context.Context, realmID, sessionID string, req HeartbeatRequest) (*HeartbeatResponse, error) { + var resp HeartbeatResponse + if err := c.doJSON(ctx, http.MethodPost, realmID, "heartbeat", sessionID, req, http.StatusOK, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +func (c *Client) Connect(ctx context.Context, realmID string, req ConnectRequest) (*ConnectResponse, error) { + var resp ConnectResponse + if err := c.doJSON(ctx, http.MethodPost, realmID, "connect", c.token, req, http.StatusOK, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +type ConnectResponseRequest struct { + Addresses []string `json:"addresses"` +} + +func (c *Client) ConnectResponse(ctx context.Context, realmID, sessionID, nonce string, addresses []string) error { + subPath := "connects/" + url.PathEscape(nonce) + return c.doJSON(ctx, http.MethodPost, realmID, subPath, sessionID, + ConnectResponseRequest{Addresses: addresses}, http.StatusNoContent, nil) +} + +func (c *Client) Events(ctx context.Context, realmID, sessionID string) (*EventStream, error) { + req, err := c.newRequest(ctx, http.MethodGet, realmID, "events", sessionID, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + defer resp.Body.Close() + return nil, decodeStatusError(resp) + } + return newEventStream(resp), nil +} + +type addressRequest struct { + Addresses []string `json:"addresses"` +} + +func (c *Client) doJSON(ctx context.Context, method, realmID, subPath, token string, in any, expectedStatus int, out any) error { + var body io.Reader + if in != nil { + bs, err := json.Marshal(in) + if err != nil { + return err + } + body = bytes.NewReader(bs) + } + req, err := c.newRequest(ctx, method, realmID, subPath, token, body) + if err != nil { + return err + } + if in != nil { + req.Header.Set("Content-Type", "application/json") + } + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != expectedStatus { + return decodeStatusError(resp) + } + if out == nil || resp.Body == nil { + return nil + } + return json.NewDecoder(resp.Body).Decode(out) +} + +func (c *Client) newRequest(ctx context.Context, method, realmID, subPath, token string, body io.Reader) (*http.Request, error) { + u := &url.URL{ + Scheme: c.scheme, + Host: c.hostport, + Path: joinURLPath("v1", url.PathEscape(realmID), subPath), + } + req, err := http.NewRequestWithContext(ctx, method, u.String(), body) + if err != nil { + return nil, err + } + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + return req, nil +} + +func randHex(size int) (string, error) { + b := make([]byte, size) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +func joinURLPath(parts ...string) string { + var joined []string + for _, part := range parts { + part = strings.Trim(part, "/") + if part == "" { + continue + } + joined = append(joined, part) + } + return "/" + strings.Join(joined, "/") +} + +func decodeStatusError(resp *http.Response) error { + var errResp ErrorResponse + _ = json.NewDecoder(io.LimitReader(resp.Body, maxErrorBodySize)).Decode(&errResp) + return &StatusError{ + StatusCode: resp.StatusCode, + Response: errResp, + } +} + +type EventStream struct { + resp *http.Response + scanner *bufio.Scanner +} + +func newEventStream(resp *http.Response) *EventStream { + scanner := bufio.NewScanner(resp.Body) + scanner.Buffer(make([]byte, 1024), 1024*1024) + return &EventStream{ + resp: resp, + scanner: scanner, + } +} + +func (s *EventStream) Close() error { + return s.resp.Body.Close() +} + +func (s *EventStream) Next() (*PunchEvent, error) { + var eventName string + var data strings.Builder + for s.scanner.Scan() { + line := s.scanner.Text() + if line == "" { + if eventName == "" && data.Len() == 0 { + continue + } + if eventName != "punch" { + eventName = "" + data.Reset() + continue + } + var ev PunchEvent + if err := json.Unmarshal([]byte(data.String()), &ev); err != nil { + return nil, err + } + return &ev, nil + } + if strings.HasPrefix(line, ":") { + continue + } + field, value, ok := strings.Cut(line, ":") + if !ok { + continue + } + value = strings.TrimPrefix(value, " ") + switch field { + case "event": + eventName = value + case "data": + if data.Len() > 0 { + data.WriteByte('\n') + } + data.WriteString(value) + } + } + if err := s.scanner.Err(); err != nil { + return nil, err + } + return nil, io.EOF +} diff --git a/transport/internet/finalmask/realm/punch.go b/transport/internet/finalmask/realm/punch.go new file mode 100644 index 000000000..92174f6bf --- /dev/null +++ b/transport/internet/finalmask/realm/punch.go @@ -0,0 +1,145 @@ +package realm + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "math/big" +) + +const ( + MaxPunchPadding = 1024 + + punchSaltLen = 8 + // Plain punch payload before obfs: + // 8-byte magic, 1-byte type, 16-byte nonce, then 0..1024 random padding bytes. + punchHeaderLen = 25 + punchMinWireLen = punchSaltLen + punchHeaderLen + punchMaxWireLen = punchMinWireLen + MaxPunchPadding +) + +var ( + ErrInvalidPunchPacket = errors.New("invalid punch packet") + + punchMagic = [8]byte{'H', 'Y', 'R', 'L', 'M', 'v', '1', 0} +) + +type PunchPacketType byte + +const ( + PunchPacketHello PunchPacketType = 0x01 + PunchPacketAck PunchPacketType = 0x02 +) + +type PunchPacket struct { + Type PunchPacketType + PaddingLength int +} + +func EncodePunchPacket(packetType PunchPacketType, meta PunchMetadata) ([]byte, error) { + if !validPunchPacketType(packetType) { + return nil, fmt.Errorf("%w: unknown packet type", ErrInvalidPunchPacket) + } + nonce, obfsKey, err := decodePunchMetadata(meta) + if err != nil { + return nil, err + } + paddingLength, err := randomPaddingLength() + if err != nil { + return nil, err + } + plain := make([]byte, punchHeaderLen+paddingLength) + copy(plain[:len(punchMagic)], punchMagic[:]) + plain[len(punchMagic)] = byte(packetType) + copy(plain[len(punchMagic)+1:punchHeaderLen], nonce) + if paddingLength > 0 { + if _, err := rand.Read(plain[punchHeaderLen:]); err != nil { + return nil, err + } + } + packet := make([]byte, punchSaltLen+len(plain)) + if _, err := rand.Read(packet[:punchSaltLen]); err != nil { + return nil, err + } + copy(packet[punchSaltLen:], plain) + xorPunchPacket(packet[punchSaltLen:], obfsKey, packet[:punchSaltLen]) + return packet, nil +} + +func DecodePunchPacket(packet []byte, meta PunchMetadata) (PunchPacket, error) { + if len(packet) < punchMinWireLen { + return PunchPacket{}, fmt.Errorf("%w: packet too short", ErrInvalidPunchPacket) + } + if len(packet) > punchMaxWireLen { + return PunchPacket{}, fmt.Errorf("%w: packet too long", ErrInvalidPunchPacket) + } + nonce, obfsKey, err := decodePunchMetadata(meta) + if err != nil { + return PunchPacket{}, err + } + salt := packet[:punchSaltLen] + plain := append([]byte(nil), packet[punchSaltLen:]...) + xorPunchPacket(plain, obfsKey, salt) + if !bytes.Equal(plain[:len(punchMagic)], punchMagic[:]) { + return PunchPacket{}, fmt.Errorf("%w: bad magic", ErrInvalidPunchPacket) + } + packetType := PunchPacketType(plain[len(punchMagic)]) + if !validPunchPacketType(packetType) { + return PunchPacket{}, fmt.Errorf("%w: unknown packet type", ErrInvalidPunchPacket) + } + if !bytes.Equal(plain[len(punchMagic)+1:punchHeaderLen], nonce) { + return PunchPacket{}, fmt.Errorf("%w: nonce mismatch", ErrInvalidPunchPacket) + } + return PunchPacket{ + Type: packetType, + PaddingLength: len(plain) - punchHeaderLen, + }, nil +} + +func decodePunchMetadata(meta PunchMetadata) (nonce, obfsKey []byte, err error) { + nonce, err = decodeHexSize("nonce", meta.Nonce, PunchNonceSize) + if err != nil { + return nil, nil, err + } + obfsKey, err = decodeHexSize("obfs", meta.Obfs, PunchObfsKeySize) + if err != nil { + return nil, nil, err + } + return nonce, obfsKey, nil +} + +func decodeHexSize(name, value string, size int) ([]byte, error) { + b, err := hex.DecodeString(value) + if err != nil { + return nil, fmt.Errorf("%w: invalid %s", ErrInvalidPunchPacket, name) + } + if len(b) != size { + return nil, fmt.Errorf("%w: invalid %s length", ErrInvalidPunchPacket, name) + } + return b, nil +} + +func randomPaddingLength() (int, error) { + n, err := rand.Int(rand.Reader, big.NewInt(MaxPunchPadding+1)) + if err != nil { + return 0, err + } + return int(n.Int64()), nil +} + +func xorPunchPacket(packet, obfsKey, salt []byte) { + h := sha256.New() + _, _ = h.Write(obfsKey) + _, _ = h.Write(salt) + mask := h.Sum(nil) + for i := range packet { + packet[i] ^= mask[i%len(mask)] + } +} + +func validPunchPacketType(packetType PunchPacketType) bool { + return packetType == PunchPacketHello || packetType == PunchPacketAck +} diff --git a/transport/internet/finalmask/realm/server.go b/transport/internet/finalmask/realm/server.go new file mode 100644 index 000000000..544bf49db --- /dev/null +++ b/transport/internet/finalmask/realm/server.go @@ -0,0 +1,401 @@ +package realm + +import ( + "context" + go_errors "errors" + "net" + "net/http" + "net/netip" + "slices" + "strings" + "sync" + "time" + + "github.com/pion/stun/v3" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +const defaultEventBuffer = 16 +const defaultStunCacheTTL = time.Second * 10 +const defaultHeartbeatInterval = time.Second * 15 + +type PunchPacketEvent struct { + Addr netip.AddrPort + Packet PunchPacket +} + +type STUNPacketEvent struct { + Message *stun.Message + Addr netip.AddrPort +} + +type realmConnServer struct { + cleaned chan struct{} + ctx context.Context + cancel context.CancelFunc + net.PacketConn + + realmClient *Client + realmID string + stunServers []string + stunTimeout time.Duration + punchTimeout time.Duration + punchInterval time.Duration + + events map[PunchMetadata]chan PunchPacketEvent + stun chan STUNPacketEvent + mu sync.Mutex + + locals []netip.AddrPort + localsMu sync.Mutex + localsLast time.Time +} + +func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) { + ctx, cancel := context.WithCancel(context.Background()) + + conn := &realmConnServer{ + cleaned: make(chan struct{}), + ctx: ctx, + cancel: cancel, + PacketConn: raw, + + realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig), + realmID: config.ID, + stunServers: config.StunServers, + stunTimeout: defaultSTUNTimeout, + punchTimeout: defaultPunchTimeout, + punchInterval: defaultPunchInterval, + + events: make(map[PunchMetadata]chan PunchPacketEvent), + stun: make(chan STUNPacketEvent, defaultEventBuffer), + } + + go conn.run() + + return conn, nil +} + +func (c *realmConnServer) addSTUN(packet []byte) bool { + if !stun.IsMessage(packet) { + return false + } + msg, addr, err := parseSTUNBindingResponse(packet) + if err != nil { + return false + } + select { + case c.stun <- STUNPacketEvent{Message: msg, Addr: addr}: + default: + } + return true +} + +func (c *realmConnServer) addPunch(packet []byte, addr net.Addr) bool { + c.mu.Lock() + defer c.mu.Unlock() + for meta, ch := range c.events { + punchPacket, err := DecodePunchPacket(packet, meta) + if err != nil { + continue + } + select { + case ch <- PunchPacketEvent{ + Addr: addr.(*net.UDPAddr).AddrPort(), + Packet: punchPacket, + }: + default: + } + return true + } + return false +} + +func (c *realmConnServer) waitctx(ctx context.Context, t time.Duration) bool { + timer := time.NewTimer(t) + defer timer.Stop() + select { + case <-timer.C: + return false + case <-ctx.Done(): + return true + } +} + +func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort { + var transactionIDs = make(map[[stun.TransactionIDSize]byte]struct{}, len(servers)) + for _, server := range servers { + msg := common.Must2(stun.Build(stun.TransactionID, stun.BindingRequest)) + transactionIDs[msg.TransactionID] = struct{}{} + _, _ = c.PacketConn.WriteTo(msg.Raw, server) + } + + var deadline = time.NewTimer(c.stunTimeout) + var results = make([]netip.AddrPort, 0, len(servers)) + for len(transactionIDs) > 0 { + select { + case <-deadline.C: + goto end + case ev := <-c.stun: + if _, ok := transactionIDs[ev.Message.TransactionID]; ok { + delete(transactionIDs, ev.Message.TransactionID) + results = append(results, ev.Addr) + } + } + } +end: + deadline.Stop() + slices.SortFunc(results, func(a, b netip.AddrPort) int { + return strings.Compare(a.String(), b.String()) + }) + + return results +} + +func (c *realmConnServer) getlocals(force bool) []netip.AddrPort { + c.localsMu.Lock() + if force || time.Since(c.localsLast) > defaultStunCacheTTL { + start := time.Now() + servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers) + errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start)) + if len(servers) > 0 { + start = time.Now() + locals := c.discover(servers) + errors.LogDebug(context.Background(), "[realm] update stun locals ", locals, " with ", time.Since(start)) + if len(locals) > 0 { + c.locals = locals + c.localsLast = time.Now() + } + } + } + locals := append([]netip.AddrPort(nil), c.locals...) + c.localsMu.Unlock() + return locals +} + +func (c *realmConnServer) punch(ctx context.Context, meta PunchMetadata, peers []netip.AddrPort) { + c.mu.Lock() + if _, ok := c.events[meta]; ok { + c.mu.Unlock() + return + } + ch := make(chan PunchPacketEvent, defaultEventBuffer) + c.events[meta] = ch + c.mu.Unlock() + + start := time.Now() + for _, peer := range peers { + packet := common.Must2(EncodePunchPacket(PunchPacketHello, meta)) + _, _ = c.PacketConn.WriteTo(packet, net.UDPAddrFromAddrPort(peer)) + } + deadline := time.NewTimer(c.punchTimeout) + ticker := time.NewTicker(c.punchInterval) + for { + select { + case <-ctx.Done(): + errors.LogDebug(context.Background(), "[realm] punch ", meta.Nonce, " FAIL > session end") + goto end + case <-deadline.C: + errors.LogDebug(context.Background(), "[realm] punch ", meta.Nonce, " FAIL > timeout") + goto end + case <-ticker.C: + for _, peer := range peers { + packet := common.Must2(EncodePunchPacket(PunchPacketHello, meta)) + _, _ = c.PacketConn.WriteTo(packet, net.UDPAddrFromAddrPort(peer)) + } + case event := <-ch: + if event.Packet.Type == PunchPacketHello { + packet := common.Must2(EncodePunchPacket(PunchPacketAck, meta)) + _, _ = c.PacketConn.WriteTo(packet, net.UDPAddrFromAddrPort(event.Addr)) + } + errors.LogDebug(context.Background(), "[realm] punch ", meta.Nonce, " SUCCESS ", event.Addr, " with ", time.Since(start)) + goto end + } + } +end: + deadline.Stop() + ticker.Stop() + + c.mu.Lock() + delete(c.events, meta) + close(ch) + c.mu.Unlock() +} + +func (c *realmConnServer) run() { + backoff := time.Second +retry: + resp, err := c.realmClient.Register(c.ctx, c.realmID, addrPortStrings(c.getlocals(false))) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[realm] ", c.realmID, " register session err retry in ", backoff) + if c.waitctx(c.ctx, backoff) { + close(c.cleaned) + return + } + backoff *= 2 + if backoff > 30*time.Second { + backoff = 30 * time.Second + } + goto retry + } + backoff = time.Second + errors.LogDebug(context.Background(), "[realm] ", c.realmID, " sesssion ", resp.SessionID, " ", resp.TTL, " registered") + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error, 2) + go c.heartbeatLoop(ctx, resp.SessionID, resp.TTL, errCh) + go c.eventsLoop(ctx, resp.SessionID, resp.TTL, errCh) + select { + case <-c.ctx.Done(): + case err = <-errCh: + } + cancel() + errors.LogDebugInner(context.Background(), err, "[realm] session ", resp.SessionID, " end") + + select { + case <-c.ctx.Done(): + _ = c.realmClient.Deregister(context.Background(), c.realmID, resp.SessionID) + errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", resp.SessionID, " deregistered") + close(c.cleaned) + return + default: + goto retry + } +} + +func (c *realmConnServer) heartbeatLoop(ctx context.Context, sid string, ttl int, errCh chan<- error) { + interval := defaultHeartbeatInterval + if ttl > 0 { + interval = time.Second * time.Duration(ttl) / 2 + } + + last := time.Now() + cur := c.getlocals(false) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + errCh <- nil + return + case <-ticker.C: + req := HeartbeatRequest{} + if new := c.getlocals(false); !slices.Equal(cur, new) { + cur = new + req.Addresses = addrPortStrings(cur) + } + start := time.Now() + resp, err := c.realmClient.Heartbeat(ctx, c.realmID, sid, req) + if err != nil { + var statusErr *StatusError + if go_errors.As(err, &statusErr) && (statusErr.StatusCode == http.StatusUnauthorized || statusErr.StatusCode == http.StatusNotFound) { + errCh <- errors.New("session invalid") + return + } + if time.Since(last) > time.Second*time.Duration(ttl) { + errCh <- errors.New("session lost") + return + } + continue + } + last = start + errors.LogDebug(context.Background(), "[realm] heartbeat ", resp.TTL, " with ", time.Since(start)) + if resp.TTL > 0 && resp.TTL != ttl { + ttl = resp.TTL + ticker.Reset(time.Second * time.Duration(ttl) / 2) + } + } + } +} + +func (c *realmConnServer) eventsLoop(ctx context.Context, sid string, ttl int, errCh chan<- error) { + backoff := time.Second + last := time.Now() + for { + start := time.Now() + stream, err := c.realmClient.Events(ctx, c.realmID, sid) + if err != nil { + var statusErr *StatusError + if go_errors.As(err, &statusErr) && (statusErr.StatusCode == http.StatusUnauthorized || statusErr.StatusCode == http.StatusNotFound) { + errCh <- errors.New("session invalid") + return + } + if time.Since(last) > time.Second*time.Duration(ttl) { + errCh <- errors.New("session lost") + return + } + errors.LogDebugInner(context.Background(), err, "[realm] ", sid, " open stream err retry in ", backoff) + if c.waitctx(ctx, backoff) { + errCh <- nil + return + } + backoff *= 2 + if backoff > 30*time.Second { + backoff = 30 * time.Second + } + continue + } + backoff = time.Second + last = start + errors.LogDebug(context.Background(), "[realm] open stream with ", time.Since(start)) + for { + ev, err := stream.Next() + if err != nil { + _ = stream.Close() + break + } + last = time.Now() + go c.punchEvent(ctx, sid, ev) + } + } +} + +func (c *realmConnServer) punchEvent(ctx context.Context, sid string, ev *PunchEvent) { + errors.LogDebug(context.Background(), "[realm] start punch event ", ev.Nonce, " ", ev.Addresses) + + locals := c.getlocals(false) + + peers, _ := parseAddrPorts(ev.Addresses) + errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " update peers ", peers) + filteredPeers, seen := candidatePunchAddrs(locals, peers) + errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " filtered peers ", filteredPeers) + expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen) + errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " expanded peers ", expandedPeers) + + if len(expandedPeers) == 0 { + errors.LogDebug(context.Background(), "[realm] punch ", ev.Nonce, " FAIL > empty peers") + return + } + + start := time.Now() + err := c.realmClient.ConnectResponse(ctx, c.realmID, sid, ev.Nonce, addrPortStrings(locals)) + if err != nil { + errors.LogDebugInner(context.Background(), err, "[realm] ", ev.Nonce, " connect response err") + } + errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " connect response ", locals, " with ", time.Since(start)) + + c.punch(ctx, ev.PunchMetadata, expandedPeers) +} + +func (c *realmConnServer) ReadFrom(p []byte) (int, net.Addr, error) { + for { + n, addr, err := c.PacketConn.ReadFrom(p) + if err != nil { + return n, addr, err + } + if c.addSTUN(p[:n]) { + continue + } + if c.addPunch(p[:n], addr) { + continue + } + return n, addr, nil + } +} + +func (c *realmConnServer) Close() error { + c.cancel() + <-c.cleaned + return c.PacketConn.Close() +} diff --git a/transport/internet/finalmask/realm/stun.go b/transport/internet/finalmask/realm/stun.go new file mode 100644 index 000000000..5ff59bb3b --- /dev/null +++ b/transport/internet/finalmask/realm/stun.go @@ -0,0 +1,224 @@ +package realm + +import ( + "context" + "errors" + "net" + "net/netip" + "slices" + "strconv" + "strings" + "time" + + "github.com/pion/stun/v3" +) + +const ( + defaultSTUNTimeout = 4 * time.Second + defaultPunchTimeout = 10 * time.Second + defaultPunchInterval = 100 * time.Millisecond + + symmetricNATPortGap = 4 + symmetricNATExtraPorts = 4 + symmetricNATMaxPortsPerHost = 32 +) + +func resolveSTUNServers(local net.IP, servers []string) []*net.UDPAddr { + var network string + if local.IsUnspecified() { + network = "ip" + } else { + if local.To4() != nil { + network = "ip4" + } else { + network = "ip6" + } + } + + var seen = make(map[string]struct{}) + var addrs = make([]*net.UDPAddr, 0, len(servers)) + for _, server := range servers { + h, p, err := net.SplitHostPort(server) + if err != nil { + continue + } + port, err := strconv.Atoi(p) + if err != nil { + continue + } + ips, err := net.DefaultResolver.LookupIP(context.Background(), network, h) + if err != nil { + continue + } + for _, ip := range ips { + if _, ok := seen[net.JoinHostPort(ip.String(), p)]; !ok { + seen[net.JoinHostPort(ip.String(), p)] = struct{}{} + addrs = append(addrs, &net.UDPAddr{IP: ip, Port: port}) + break + } + } + } + + return addrs +} + +func parseSTUNBindingResponse(packet []byte) (*stun.Message, netip.AddrPort, error) { + msg := stun.New() + if err := stun.Decode(packet, msg); err != nil { + return nil, netip.AddrPort{}, err + } + if msg.Type != stun.BindingSuccess { + return nil, netip.AddrPort{}, errors.New("not a STUN binding success response") + } + + var xorMapped stun.XORMappedAddress + if err := xorMapped.GetFrom(msg); err == nil { + addr, err := netIPPortToAddrPort(xorMapped.IP, xorMapped.Port) + return msg, addr, err + } + + var mapped stun.MappedAddress + if err := mapped.GetFrom(msg); err == nil { + addr, err := netIPPortToAddrPort(mapped.IP, mapped.Port) + return msg, addr, err + } + + return nil, netip.AddrPort{}, errors.New("STUN mapped address not found") +} + +func netIPPortToAddrPort(ip net.IP, port int) (netip.AddrPort, error) { + if port <= 0 || port > 65535 { + return netip.AddrPort{}, errors.New("invalid STUN mapped port") + } + if ip4 := ip.To4(); ip4 != nil { + var addr [4]byte + copy(addr[:], ip4) + return netip.AddrPortFrom(netip.AddrFrom4(addr), uint16(port)), nil + } + ip16 := ip.To16() + if ip16 == nil { + return netip.AddrPort{}, errors.New("invalid STUN mapped IP") + } + var addr [16]byte + copy(addr[:], ip16) + return netip.AddrPortFrom(netip.AddrFrom16(addr), uint16(port)), nil +} + +func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[netip.AddrPort]struct{}) { + var allow4, allow6 bool + for _, local := range locals { + if local.Addr().Is4() { + allow4 = true + } else { + allow6 = true + } + if allow4 && allow6 { + break + } + } + var seen = make(map[netip.AddrPort]struct{}, len(peers)) + var candidates = make([]netip.AddrPort, 0, len(peers)) + for _, peer := range peers { + if _, ok := seen[peer]; ok { + continue + } + if peer.IsValid() { + if peer.Addr().Is4() { + if allow4 { + seen[peer] = struct{}{} + candidates = append(candidates, peer) + } + } else { + if allow6 { + seen[peer] = struct{}{} + candidates = append(candidates, peer) + } + } + } + } + return candidates, seen +} + +func expandSymmetricNATCandidates(candidates []netip.AddrPort, seen map[netip.AddrPort]struct{}) []netip.AddrPort { + portsByIP := make(map[netip.Addr][]uint16) + for _, addr := range candidates { + if addr.Addr().Is4() { + portsByIP[addr.Addr()] = append(portsByIP[addr.Addr()], addr.Port()) + } + } + for ip, ports := range portsByIP { + ports = uniqueSortedPorts(ports) + if !predictablePortGroup(ports) { + continue + } + start := int(ports[0]) + end := int(ports[len(ports)-1]) + symmetricNATExtraPorts + if end > 65535 { + end = 65535 + } + added := 0 + for port := start; port <= end && added < symmetricNATMaxPortsPerHost; port++ { + addr := netip.AddrPortFrom(ip, uint16(port)) + if _, ok := seen[addr]; ok { + continue + } + seen[addr] = struct{}{} + candidates = append(candidates, addr) + added++ + } + } + sortAddrPorts(candidates) + return candidates +} + +func uniqueSortedPorts(ports []uint16) []uint16 { + slices.Sort(ports) + out := ports[:0] + var last uint16 + for i, port := range ports { + if i > 0 && port == last { + continue + } + out = append(out, port) + last = port + } + return out +} + +func predictablePortGroup(ports []uint16) bool { + if len(ports) < 2 { + return false + } + for i := 1; i < len(ports); i++ { + if ports[i]-ports[i-1] > symmetricNATPortGap { + return false + } + } + return true +} + +func sortAddrPorts(addrs []netip.AddrPort) { + slices.SortFunc(addrs, func(a, b netip.AddrPort) int { + return strings.Compare(a.String(), b.String()) + }) +} + +func addrPortStrings(addrs []netip.AddrPort) []string { + out := make([]string, 0, len(addrs)) + for _, addr := range addrs { + out = append(out, addr.String()) + } + return out +} + +func parseAddrPorts(addrs []string) ([]netip.AddrPort, error) { + out := make([]netip.AddrPort, 0, len(addrs)) + for _, s := range addrs { + addr, err := netip.ParseAddrPort(s) + if err != nil { + return nil, err + } + out = append(out, addr) + } + return out, nil +} diff --git a/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go b/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go index 35b63c064..2bd66d091 100644 --- a/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go +++ b/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go @@ -801,7 +801,8 @@ func (b *bandwidthSampler) onPacketAcknowledged(ackTime monotime.Time, packetNum if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { sendRate = BandwidthFromDelta( sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, - sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) + sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime), + ) } var a0 ackPoint @@ -848,7 +849,8 @@ func (b *bandwidthSampler) onAckEventEnd( b.lastSentPacket, b.lastAckedPacket, b.lastAckedPacketAckTime, - newlyAckedBytes) + newlyAckedBytes, + ) // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack // aggregation epoch, save LessRecentPoint, which is the last ack point of the // previous epoch, as a A0 candidate. diff --git a/transport/internet/hysteria/congestion/bbr/bbr_sender.go b/transport/internet/hysteria/congestion/bbr/bbr_sender.go index bcbf8133d..26e7bb7f7 100644 --- a/transport/internet/hysteria/congestion/bbr/bbr_sender.go +++ b/transport/internet/hysteria/congestion/bbr/bbr_sender.go @@ -640,7 +640,8 @@ func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, even func (b *bbrSender) PacingRate() Bandwidth { if b.pacingRate == 0 { return Bandwidth(b.highGain * float64( - BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) + BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()), + )) } return b.pacingRate From 66a81007379656b803d55ac2c093f0ad2408f2b2 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 29 May 2026 16:36:45 +0800 Subject: [PATCH 20/78] Salamander finalmask: Support `packetSize` (Gecko in Hysteria v2.9.2) (#6198) And some other refinements https://github.com/XTLS/Xray-core/pull/6198#issuecomment-4567438813 Example: https://github.com/XTLS/Xray-core/pull/6198#issue-4522226670 --- infra/conf/transport_internet.go | 16 +- proxy/hysteria/protocol.go | 3 + transport/internet/finalmask/finalmask.go | 109 +++--- .../mkcp/aes128gcm/aes128gcm_test.go | 70 ---- .../finalmask/mkcp/aes128gcm/config.go | 8 +- .../internet/finalmask/mkcp/aes128gcm/conn.go | 37 +- .../finalmask/mkcp/original/config.go | 8 +- .../internet/finalmask/mkcp/original/conn.go | 12 +- .../finalmask/mkcp/original/simple_test.go | 35 -- .../internet/finalmask/salamander/config.go | 18 +- .../finalmask/salamander/config.pb.go | 73 +++- .../finalmask/salamander/config.proto | 5 + .../internet/finalmask/salamander/conn.go | 321 +++++++++++++++++- .../internet/finalmask/salamander/gecko.go | 86 +++++ .../finalmask/salamander/salamander.go | 22 +- .../finalmask/salamander/salamander_test.go | 81 ----- transport/internet/finalmask/udp_test.go | 44 --- 17 files changed, 587 insertions(+), 361 deletions(-) delete mode 100644 transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go delete mode 100644 transport/internet/finalmask/mkcp/original/simple_test.go create mode 100644 transport/internet/finalmask/salamander/gecko.go delete mode 100644 transport/internet/finalmask/salamander/salamander_test.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 02f94de0a..69a909226 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1811,13 +1811,21 @@ func (c *Aes128Gcm) Build() (proto.Message, error) { } type Salamander struct { - Password string `json:"password"` + Password string `json:"password"` + PacketSize *Int32Range `json:"packetSize"` } func (c *Salamander) Build() (proto.Message, error) { - config := &salamander.Config{} - config.Password = c.Password - return config, nil + if c.PacketSize != nil { + return &salamander.GeckoConfig{ + Password: c.Password, + MinPacketSize: c.PacketSize.From, + MaxPacketSize: c.PacketSize.To, + }, nil + } + return &salamander.Config{ + Password: c.Password, + }, nil } type Sudoku struct { diff --git a/proxy/hysteria/protocol.go b/proxy/hysteria/protocol.go index 5434bd00c..7209172e1 100644 --- a/proxy/hysteria/protocol.go +++ b/proxy/hysteria/protocol.go @@ -253,6 +253,9 @@ func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage { } fullPayload := m.Data maxPayloadSize := maxSize - m.HeaderSize() + if maxPayloadSize <= 0 { + return nil + } off := 0 fragID := uint8(0) fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up diff --git a/transport/internet/finalmask/finalmask.go b/transport/internet/finalmask/finalmask.go index c732f477c..d7697cfe1 100644 --- a/transport/internet/finalmask/finalmask.go +++ b/transport/internet/finalmask/finalmask.go @@ -3,9 +3,8 @@ package finalmask import ( "context" "net" - "sync" - "github.com/xtls/xray-core/common/bytespool" + "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" ) @@ -105,61 +104,60 @@ type headerSize interface { } type headerManagerConn struct { - sync.Mutex net.PacketConn - sizes []int - conns []net.PacketConn - writeBuf [UDPSize]byte + sizes []int + conns []net.PacketConn } func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - buf := p - if len(buf) < UDPSize { - b := bytespool.Alloc(UDPSize) - b = b[:UDPSize] - defer bytespool.Free(b) - buf = b + b := p + if len(b) < UDPSize { + buf := buf.New() + buf.Resize(0, UDPSize) + b = buf.Bytes() + defer buf.Release() } - n, addr, err = c.PacketConn.ReadFrom(buf) - if n == 0 || err != nil { - return 0, addr, err - } - newBuf := buf[:n] - - sum := 0 - for _, size := range c.sizes { - sum += size - } - - if n < sum { - errors.LogDebug(context.Background(), addr, " mask read err short length") - return 0, addr, nil - } - - for i := range c.conns { - n, _, err = c.conns[i].ReadFrom(newBuf) - if n == 0 || err != nil { - errors.LogDebug(context.Background(), addr, " mask read err ", err) - return 0, addr, nil + for { + n, addr, err = c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err } - newBuf = newBuf[c.sizes[i] : n+c.sizes[i]] + b = b[:n] + + sum := 0 + for _, size := range c.sizes { + sum += size + } + + if n < sum { + errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", len(b)) + continue + } + + for i := range c.conns { + n, _, err = c.conns[i].ReadFrom(b) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[mask] drop packet from ", addr, " with size ", len(b)) + break + } + b = b[c.sizes[i] : n+c.sizes[i]] + } + + if err != nil { + continue + } + + return copy(p, b), addr, nil } - - if len(p) < n { - errors.LogDebug(context.Background(), addr, " mask read err short buffer") - return 0, addr, nil - } - - copy(p, buf[sum:sum+n]) - - return n, addr, nil } func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.Lock() - defer c.Unlock() + buf := buf.New() + buf.Resize(0, UDPSize) + b := buf.Bytes() + defer buf.Release() sum := 0 for _, size := range c.sizes { @@ -167,24 +165,29 @@ func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) } if sum+len(p) > UDPSize { - errors.LogDebug(context.Background(), addr, " mask write err short write") + errors.LogError(context.Background(), "[mask] drop packet to ", addr, " with size ", len(p)) return 0, nil } - n = copy(c.writeBuf[sum:], p) + n = copy(b[sum:], p) for i := len(c.conns) - 1; i >= 0; i-- { - n, err = c.conns[i].WriteTo(c.writeBuf[sum-c.sizes[i]:n+sum], nil) - if n == 0 || err != nil { - errors.LogDebug(context.Background(), addr, " mask write err ", err) + n, err = c.conns[i].WriteTo(b[sum-c.sizes[i]:n+sum], nil) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[mask] drop packet to ", addr, " with size ", len(p)) return 0, nil } sum -= c.sizes[i] } - n, err = c.PacketConn.WriteTo(c.writeBuf[:n], addr) - if n == 0 || err != nil { - return n, err + if n > UDPSize { + errors.LogError(context.Background(), "[mask] drop packet to ", addr, " with size ", len(p)) + return 0, nil + } + + _, err = c.PacketConn.WriteTo(b[:n], addr) + if err != nil { + return 0, err } return len(p), nil diff --git a/transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go b/transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go deleted file mode 100644 index 4806dfc2c..000000000 --- a/transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package aes128gcm_test - -import ( - "crypto/rand" - "crypto/sha256" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/xtls/xray-core/common/crypto" -) - -func TestAes128GcmSealInPlace(t *testing.T) { - hashedPsk := sha256.Sum256([]byte("psk")) - aead := crypto.NewAesGcm(hashedPsk[:16]) - - text := []byte("0123456789012") - buf := make([]byte, 8192) - - nonceSize := aead.NonceSize() - nonce := buf[:nonceSize] - rand.Read(nonce) - copy(buf[nonceSize:], text) - plaintext := buf[nonceSize : nonceSize+len(text)] - - sealed := aead.Seal(nil, nonce, plaintext, nil) - - _ = aead.Seal(plaintext[:0], nonce, plaintext, nil) - - assert.Equal(t, sealed, buf[nonceSize:nonceSize+aead.Overhead()+len(text)]) -} - -func encrypted(plain []byte) ([]byte, []byte) { - hashedPsk := sha256.Sum256([]byte("psk")) - aead := crypto.NewAesGcm(hashedPsk[:16]) - - nonce := make([]byte, 12) - rand.Read(nonce) - - return nonce, aead.Seal(nil, nonce, plain, nil) -} - -func TestAes128GcmOpenInPlace(t *testing.T) { - a, b := encrypted([]byte("0123456789012")) - buf := make([]byte, 8192) - copy(buf, a) - copy(buf[len(a):], b) - - hashedPsk := sha256.Sum256([]byte("psk")) - aead := crypto.NewAesGcm(hashedPsk[:16]) - - nonceSize := aead.NonceSize() - nonce := buf[:nonceSize] - ciphertext := buf[nonceSize : nonceSize+len(b)] - - opened, _ := aead.Open(nil, nonce, ciphertext, nil) - _, _ = aead.Open(ciphertext[:0], nonce, ciphertext, nil) - - assert.Equal(t, opened, ciphertext[:len(ciphertext)-aead.Overhead()]) -} - -func TestAes128GcmBounce(t *testing.T) { - hashedPsk := sha256.Sum256([]byte("psk")) - aead := crypto.NewAesGcm(hashedPsk[:16]) - buf := make([]byte, aead.NonceSize()+aead.Overhead()) - for i := 0; i < 1000; i++ { - _, _ = rand.Read(buf) - _, err := aead.Open(buf[aead.NonceSize():aead.NonceSize()], buf[:aead.NonceSize()], buf[aead.NonceSize():], nil) - assert.NotEqual(t, err, nil) - } -} diff --git a/transport/internet/finalmask/mkcp/aes128gcm/config.go b/transport/internet/finalmask/mkcp/aes128gcm/config.go index a7160e2b0..70d87ae00 100644 --- a/transport/internet/finalmask/mkcp/aes128gcm/config.go +++ b/transport/internet/finalmask/mkcp/aes128gcm/config.go @@ -4,8 +4,9 @@ import ( "net" ) -func (c *Config) UDP() { -} +func (c *Config) UDP() {} + +func (c *Config) HeaderConn() {} func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnClient(c, raw) @@ -14,6 +15,3 @@ func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnServer(c, raw) } - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/mkcp/aes128gcm/conn.go b/transport/internet/finalmask/mkcp/aes128gcm/conn.go index 055803af2..6d1012a38 100644 --- a/transport/internet/finalmask/mkcp/aes128gcm/conn.go +++ b/transport/internet/finalmask/mkcp/aes128gcm/conn.go @@ -8,8 +8,6 @@ import ( "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/crypto" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/transport/internet/finalmask" ) type aes128gcmConn struct { @@ -19,13 +17,10 @@ type aes128gcmConn struct { func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { hashedPsk := sha256.Sum256([]byte(c.Password)) - - conn := &aes128gcmConn{ + return &aes128gcmConn{ PacketConn: raw, aead: crypto.NewAesGcm(hashedPsk[:16]), - } - - return conn, nil + }, nil } func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { @@ -37,31 +32,19 @@ func (c *aes128gcmConn) Size() int { } func (c *aes128gcmConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - if len(p) < c.aead.NonceSize()+c.aead.Overhead() { - return 0, addr, errors.New("aead short lenth") - } - nonceSize := c.aead.NonceSize() - nonce := p[:nonceSize] - ciphertext := p[nonceSize:] - _, err = c.aead.Open(ciphertext[:0], nonce, ciphertext, nil) + overhead := c.aead.Overhead() + _, err = c.aead.Open(p[nonceSize:nonceSize], p[:nonceSize], p[nonceSize:], nil) if err != nil { - return 0, addr, errors.New("aead open").Base(err) + return 0, nil, err } - - return len(p) - c.aead.NonceSize() - c.aead.Overhead(), addr, nil + return len(p) - nonceSize - overhead, nil, nil } func (c *aes128gcmConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if c.aead.Overhead()+len(p) > finalmask.UDPSize { - return 0, errors.New("aead short write") - } - nonceSize := c.aead.NonceSize() - nonce := p[:nonceSize] - common.Must2(rand.Read(nonce)) - plaintext := p[nonceSize:] - _ = c.aead.Seal(plaintext[:0], nonce, plaintext, nil) - - return len(p) + c.aead.Overhead(), nil + overhead := c.aead.Overhead() + common.Must2(rand.Read(p[:nonceSize])) + _ = c.aead.Seal(p[nonceSize:nonceSize], p[:nonceSize], p[nonceSize:], nil) + return len(p) + overhead, nil } diff --git a/transport/internet/finalmask/mkcp/original/config.go b/transport/internet/finalmask/mkcp/original/config.go index d18b13918..b6b42c73d 100644 --- a/transport/internet/finalmask/mkcp/original/config.go +++ b/transport/internet/finalmask/mkcp/original/config.go @@ -4,8 +4,9 @@ import ( "net" ) -func (c *Config) UDP() { -} +func (c *Config) UDP() {} + +func (c *Config) HeaderConn() {} func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnClient(c, raw) @@ -14,6 +15,3 @@ func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnServer(c, raw) } - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/mkcp/original/conn.go b/transport/internet/finalmask/mkcp/original/conn.go index 15abe99be..5fbe598df 100644 --- a/transport/internet/finalmask/mkcp/original/conn.go +++ b/transport/internet/finalmask/mkcp/original/conn.go @@ -77,12 +77,10 @@ type simpleConn struct { } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &simpleConn{ + return &simpleConn{ PacketConn: raw, aead: &simple{}, - } - - return conn, nil + }, nil } func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { @@ -96,14 +94,12 @@ func (c *simpleConn) Size() int { func (c *simpleConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { _, err = c.aead.Open(p[:0], nil, p, nil) if err != nil { - return 0, addr, errors.New("aead open").Base(err) + return 0, nil, err } - - return len(p) - c.aead.Overhead(), addr, nil + return len(p) - c.aead.Overhead(), nil, nil } func (c *simpleConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { _ = c.aead.Seal(p[:0], nil, p[c.aead.Overhead():], nil) - return len(p), nil } diff --git a/transport/internet/finalmask/mkcp/original/simple_test.go b/transport/internet/finalmask/mkcp/original/simple_test.go deleted file mode 100644 index be9d68398..000000000 --- a/transport/internet/finalmask/mkcp/original/simple_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package original_test - -import ( - "crypto/rand" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" -) - -func TestSimpleSealInPlace(t *testing.T) { - aead := original.NewSimple() - - text := []byte("0123456789012") - buf := make([]byte, 8192) - - copy(buf[aead.Overhead():], text) - plaintext := buf[aead.Overhead() : aead.Overhead()+len(text)] - - sealed := aead.Seal(nil, nil, plaintext, nil) - - _ = aead.Seal(buf[:0], nil, plaintext, nil) - - assert.Equal(t, sealed, buf[:aead.Overhead()+len(text)]) -} - -func TestOriginalBounce(t *testing.T) { - aead := original.NewSimple() - buf := make([]byte, aead.NonceSize()+aead.Overhead()) - for i := 0; i < 1000; i++ { - _, _ = rand.Read(buf) - _, err := aead.Open(buf[:0], nil, buf, nil) - assert.NotEqual(t, err, nil) - } -} diff --git a/transport/internet/finalmask/salamander/config.go b/transport/internet/finalmask/salamander/config.go index 8df1285d3..192c466b7 100644 --- a/transport/internet/finalmask/salamander/config.go +++ b/transport/internet/finalmask/salamander/config.go @@ -4,16 +4,24 @@ import ( "net" ) -func (c *Config) UDP() { -} +func (c *Config) UDP() {} + +func (c *Config) HeaderConn() {} func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw) + return NewSalamanderConnClient(c, raw) } func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw) + return NewSalamanderConnServer(c, raw) } -func (c *Config) HeaderConn() { +func (c *GeckoConfig) UDP() {} + +func (c *GeckoConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewGeckoConnClient(c, raw) +} + +func (c *GeckoConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewGeckoConnServer(c, raw) } diff --git a/transport/internet/finalmask/salamander/config.pb.go b/transport/internet/finalmask/salamander/config.pb.go index 949df60bd..4f4167be1 100644 --- a/transport/internet/finalmask/salamander/config.pb.go +++ b/transport/internet/finalmask/salamander/config.pb.go @@ -65,13 +65,77 @@ func (x *Config) GetPassword() string { return "" } +type GeckoConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + MinPacketSize int32 `protobuf:"varint,2,opt,name=MinPacketSize,proto3" json:"MinPacketSize,omitempty"` + MaxPacketSize int32 `protobuf:"varint,3,opt,name=MaxPacketSize,proto3" json:"MaxPacketSize,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GeckoConfig) Reset() { + *x = GeckoConfig{} + mi := &file_transport_internet_finalmask_salamander_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GeckoConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeckoConfig) ProtoMessage() {} + +func (x *GeckoConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_salamander_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeckoConfig.ProtoReflect.Descriptor instead. +func (*GeckoConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_salamander_config_proto_rawDescGZIP(), []int{1} +} + +func (x *GeckoConfig) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *GeckoConfig) GetMinPacketSize() int32 { + if x != nil { + return x.MinPacketSize + } + return 0 +} + +func (x *GeckoConfig) GetMaxPacketSize() int32 { + if x != nil { + return x.MaxPacketSize + } + return 0 +} + var File_transport_internet_finalmask_salamander_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_salamander_config_proto_rawDesc = "" + "\n" + "4transport/internet/finalmask/salamander/config.proto\x12,xray.transport.internet.finalmask.salamander\"$\n" + "\x06Config\x12\x1a\n" + - "\bpassword\x18\x01 \x01(\tR\bpasswordB\xa6\x01\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\"u\n" + + "\vGeckoConfig\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\x12$\n" + + "\rMinPacketSize\x18\x02 \x01(\x05R\rMinPacketSize\x12$\n" + + "\rMaxPacketSize\x18\x03 \x01(\x05R\rMaxPacketSizeB\xa6\x01\n" + "0com.xray.transport.internet.finalmask.salamanderP\x01ZAgithub.com/xtls/xray-core/transport/internet/finalmask/salamander\xaa\x02,Xray.Transport.Internet.Finalmask.Salamanderb\x06proto3" var ( @@ -86,9 +150,10 @@ func file_transport_internet_finalmask_salamander_config_proto_rawDescGZIP() []b return file_transport_internet_finalmask_salamander_config_proto_rawDescData } -var file_transport_internet_finalmask_salamander_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_salamander_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_transport_internet_finalmask_salamander_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.salamander.Config + (*Config)(nil), // 0: xray.transport.internet.finalmask.salamander.Config + (*GeckoConfig)(nil), // 1: xray.transport.internet.finalmask.salamander.GeckoConfig } var file_transport_internet_finalmask_salamander_config_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -109,7 +174,7 @@ func file_transport_internet_finalmask_salamander_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_salamander_config_proto_rawDesc), len(file_transport_internet_finalmask_salamander_config_proto_rawDesc)), NumEnums: 0, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/transport/internet/finalmask/salamander/config.proto b/transport/internet/finalmask/salamander/config.proto index 34bd4cefe..196391035 100644 --- a/transport/internet/finalmask/salamander/config.proto +++ b/transport/internet/finalmask/salamander/config.proto @@ -10,3 +10,8 @@ message Config { string password = 1; } +message GeckoConfig { + string password = 1; + int32 MinPacketSize = 2; + int32 MaxPacketSize = 3; +} \ No newline at end of file diff --git a/transport/internet/finalmask/salamander/conn.go b/transport/internet/finalmask/salamander/conn.go index bfb4934ac..c87150998 100644 --- a/transport/internet/finalmask/salamander/conn.go +++ b/transport/internet/finalmask/salamander/conn.go @@ -1,9 +1,16 @@ package salamander import ( + "crypto/rand" + "encoding/binary" + "errors" "net" + "sync" + "sync/atomic" + "time" - "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/transport/internet/finalmask" ) type salamanderConn struct { @@ -11,22 +18,19 @@ type salamanderConn struct { obfs *SalamanderObfuscator } -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { +func NewSalamanderConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { ob, err := NewSalamanderObfuscator([]byte(c.Password)) if err != nil { - return nil, errors.New("salamander err").Base(err) + return nil, err } - - conn := &salamanderConn{ + return &salamanderConn{ PacketConn: raw, obfs: ob, - } - - return conn, nil + }, nil } -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) +func NewSalamanderConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewSalamanderConnClient(c, raw) } func (c *salamanderConn) Size() int { @@ -35,12 +39,303 @@ func (c *salamanderConn) Size() int { func (c *salamanderConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { c.obfs.Deobfuscate(p, p[smSaltLen:]) - - return len(p) - smSaltLen, addr, nil + return len(p) - smSaltLen, nil, nil } func (c *salamanderConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { c.obfs.Obfuscate(p[smSaltLen:], p) - return len(p), nil } + +const ( + geckoReassemblyTTL = 8 * time.Second + geckoMaxReassembly = 4096 + geckoMaxPerSource = 8 + + geckoBufferSize = 2048 + + geckoDefaultMinPacket = 512 + geckoDefaultMaxPacket = 1200 +) + +type reassemblyKey struct { + addr string + msgID uint8 +} + +type reassemblyEntry struct { + chunks [][]byte + received int + total uint8 + deadline time.Time +} + +type geckoConn struct { + net.PacketConn + obfs *SalamanderObfuscator + minPkt, maxPkt int + + msgID atomic.Uint32 + + mu sync.Mutex + reassembly map[reassemblyKey]*reassemblyEntry + perSource map[string]int + + closeCh chan struct{} + closeOnce sync.Once +} + +func NewGeckoConnClient(c *GeckoConfig, raw net.PacketConn) (net.PacketConn, error) { + ob, err := NewSalamanderObfuscator([]byte(c.Password)) + if err != nil { + return nil, err + } + minPkt, maxPkt := c.MinPacketSize, c.MaxPacketSize + if minPkt == 0 { + minPkt = geckoDefaultMinPacket + } + if maxPkt == 0 { + maxPkt = geckoDefaultMaxPacket + } + if minPkt <= 0 || minPkt > maxPkt || maxPkt > geckoBufferSize { + return nil, errors.New("gecko: invalid min/max packet size") + } + g := &geckoConn{ + PacketConn: raw, + obfs: ob, + minPkt: int(minPkt), + maxPkt: int(maxPkt), + reassembly: make(map[reassemblyKey]*reassemblyEntry), + perSource: make(map[string]int), + closeCh: make(chan struct{}), + } + go g.gcLoop() + return g, nil +} + +func NewGeckoConnServer(c *GeckoConfig, raw net.PacketConn) (net.PacketConn, error) { + return NewGeckoConnClient(c, raw) +} + +func (c *geckoConn) readObfs(p []byte) (n int, addr net.Addr, err error) { + for { + n, addr, err = c.PacketConn.ReadFrom(p) + if err != nil { + return n, addr, err + } + if n < smSaltLen { + continue + } + c.obfs.Deobfuscate(p[:n], p) + return n - smSaltLen, addr, nil + } +} + +func (c *geckoConn) writeObfs(p []byte, addr net.Addr) (n int, err error) { + b := buf.New() + b.Resize(0, int32(len(p)+smSaltLen)) + defer b.Release() + c.obfs.Obfuscate(p, b.Bytes()) + return c.PacketConn.WriteTo(b.Bytes(), addr) +} + +func (g *geckoConn) WriteTo(p []byte, addr net.Addr) (int, error) { + if len(p) == 0 { + return 0, nil + } + if p[0]&0x80 != 0 { + // QUIC long header, do fragmentation. + return g.writeFragmented(p, addr) + } + // QUIC short header (data), pass through. + return g.writeObfs(p, addr) +} + +func (g *geckoConn) writeFragmented(p []byte, addr net.Addr) (int, error) { + chunks := randomFragmentChunks() + chunkSize := len(p) / chunks + msgID := uint8(g.msgID.Add(1)) + for i := range chunks { + start := i * chunkSize + end := len(p) + if i < chunks-1 { + end = start + chunkSize + } + chunk := p[start:end] + padLen := g.randomPadLen(len(chunk)) + buf := make([]byte, geckoHeaderSize+int(padLen)+len(chunk)) + n, err := encodeFrame(frameHeader{ + padLen: padLen, + msgID: msgID, + chunkIdx: uint8(i), + totalChunks: uint8(chunks), + }, chunk, buf) + if err != nil { + return 0, err + } + if _, err := g.writeObfs(buf[:n], addr); err != nil { + return 0, err + } + } + return len(p), nil +} + +func (g *geckoConn) randomPadLen(chunkLen int) uint16 { + base := smSaltLen + geckoHeaderSize + chunkLen + lo := max(g.minPkt, base) + if lo > g.maxPkt { + return 0 + } + return uint16(lo - base + randIntn(g.maxPkt-lo+1)) +} + +func randomFragmentChunks() int { + return geckoMinFragmentChunks + randIntn(geckoMaxFragmentChunks-geckoMinFragmentChunks+1) +} + +func randIntn(n int) int { + if n <= 1 { + return 0 + } + var b [4]byte + _, _ = rand.Read(b[:]) + return int(binary.BigEndian.Uint32(b[:]) % uint32(n)) +} + +func (g *geckoConn) ReadFrom(p []byte) (int, net.Addr, error) { + b := buf.New() + b.Resize(0, finalmask.UDPSize) + buf := b.Bytes() + defer b.Release() + for { + n, addr, err := g.readObfs(buf) + if err != nil { + return 0, addr, err + } + if n <= 0 { + continue + } + // Top bit set → Gecko fragment frame; clear → short-header packet + // or garbage, passed through for QUIC to handle. + if buf[0]&0x80 == 0 { + return copy(p, buf[:n]), addr, nil + } + h, payload, decErr := decodeFrame(buf[:n]) + if decErr != nil { + // Malformed frame; drop silently. + continue + } + out, ready := g.acceptChunk(addr, h, payload) + if !ready { + continue + } + return copy(p, out), addr, nil + } +} + +func (g *geckoConn) acceptChunk(addr net.Addr, h frameHeader, payload []byte) ([]byte, bool) { + key := reassemblyKey{addr: addr.String(), msgID: h.msgID} + + g.mu.Lock() + defer g.mu.Unlock() + + e, exists := g.reassembly[key] + if !exists { + // Per-source cap. + if g.perSource[key.addr] >= geckoMaxPerSource { + return nil, false + } + // Global cap with eviction. + if len(g.reassembly) >= geckoMaxReassembly { + g.evictOldestLocked() + } + e = &reassemblyEntry{ + chunks: make([][]byte, h.totalChunks), + total: h.totalChunks, + deadline: time.Now().Add(geckoReassemblyTTL), + } + g.reassembly[key] = e + g.perSource[key.addr]++ + } else if e.total != h.totalChunks { + // Inconsistent chunk count; drop. + return nil, false + } + if int(h.chunkIdx) >= len(e.chunks) || e.chunks[h.chunkIdx] != nil { + // Bad index or duplicate; drop. + return nil, false + } + cp := make([]byte, len(payload)) + copy(cp, payload) + e.chunks[h.chunkIdx] = cp + e.received++ + if e.received < int(e.total) { + return nil, false + } + + total := 0 + for _, c := range e.chunks { + total += len(c) + } + out := make([]byte, total) + off := 0 + for _, c := range e.chunks { + off += copy(out[off:], c) + } + g.dropEntryLocked(key) + return out, true +} + +func (g *geckoConn) gcLoop() { + t := time.NewTicker(geckoReassemblyTTL / 2) + defer t.Stop() + for { + select { + case <-g.closeCh: + return + case now := <-t.C: + g.gcExpired(now) + } + } +} + +func (g *geckoConn) gcExpired(now time.Time) { + g.mu.Lock() + defer g.mu.Unlock() + for k, e := range g.reassembly { + if now.After(e.deadline) { + g.dropEntryLocked(k) + } + } +} + +func (g *geckoConn) dropEntryLocked(k reassemblyKey) { + if _, ok := g.reassembly[k]; !ok { + return + } + delete(g.reassembly, k) + g.perSource[k.addr]-- + if g.perSource[k.addr] <= 0 { + delete(g.perSource, k.addr) + } +} + +func (g *geckoConn) evictOldestLocked() { + var oldestKey reassemblyKey + var oldestDeadline time.Time + first := true + for k, e := range g.reassembly { + if first || e.deadline.Before(oldestDeadline) { + oldestKey = k + oldestDeadline = e.deadline + first = false + } + } + if !first { + g.dropEntryLocked(oldestKey) + } +} + +func (g *geckoConn) Close() error { + g.closeOnce.Do(func() { close(g.closeCh) }) + return g.PacketConn.Close() +} diff --git a/transport/internet/finalmask/salamander/gecko.go b/transport/internet/finalmask/salamander/gecko.go new file mode 100644 index 000000000..e4db7049c --- /dev/null +++ b/transport/internet/finalmask/salamander/gecko.go @@ -0,0 +1,86 @@ +package salamander + +import ( + "crypto/rand" + "encoding/binary" + "errors" +) + +const ( + geckoFlagFragment = 0x80 + geckoHeaderSize = 5 + + geckoMinFragmentChunks = 2 + geckoMaxFragmentChunks = 8 +) + +var ( + errFrameTruncated = errors.New("gecko frame truncated") + errFrameInvalid = errors.New("gecko frame invalid") +) + +// frameHeader is a Gecko fragment frame header. +// Wire layout (after Salamander decryption): +// +// byte 0: 0x80 (fragment marker; low 7 bits reserved) +// byte 1: msgID +// byte 2: chunkIdx:4 | totalChunks:4 +// byte 3-4: padLen (uint16, big-endian) +// then padLen random padding bytes, then the chunk payload +type frameHeader struct { + padLen uint16 + msgID uint8 + chunkIdx uint8 // < totalChunks + totalChunks uint8 // [2, 8] +} + +// encodeFrame writes a frame into out, filling the padding region with random +// bytes. out must be at least geckoHeaderSize + h.padLen + len(payload) long. +func encodeFrame(h frameHeader, payload, out []byte) (int, error) { + if h.totalChunks < geckoMinFragmentChunks || h.totalChunks > geckoMaxFragmentChunks { + return 0, errFrameInvalid + } + if h.chunkIdx >= h.totalChunks { + return 0, errFrameInvalid + } + needed := geckoHeaderSize + int(h.padLen) + len(payload) + if len(out) < needed { + return 0, errFrameTruncated + } + out[0] = geckoFlagFragment + out[1] = h.msgID + out[2] = h.chunkIdx<<4 | h.totalChunks&0x0f + binary.BigEndian.PutUint16(out[3:5], h.padLen) + if _, err := rand.Read(out[geckoHeaderSize : geckoHeaderSize+int(h.padLen)]); err != nil { + return 0, err + } + copy(out[geckoHeaderSize+int(h.padLen):], payload) + return needed, nil +} + +// decodeFrame parses a frame from in. The returned payload is a sub-slice of +// in (zero-copy) covering the bytes after the header and padding. +func decodeFrame(in []byte) (frameHeader, []byte, error) { + if len(in) < geckoHeaderSize { + return frameHeader{}, nil, errFrameTruncated + } + if in[0]&geckoFlagFragment == 0 { + return frameHeader{}, nil, errFrameInvalid + } + h := frameHeader{ + msgID: in[1], + chunkIdx: in[2] >> 4, + totalChunks: in[2] & 0x0f, + padLen: binary.BigEndian.Uint16(in[3:5]), + } + if h.totalChunks < geckoMinFragmentChunks || h.totalChunks > geckoMaxFragmentChunks { + return frameHeader{}, nil, errFrameInvalid + } + if h.chunkIdx >= h.totalChunks { + return frameHeader{}, nil, errFrameInvalid + } + if geckoHeaderSize+int(h.padLen) > len(in) { + return frameHeader{}, nil, errFrameTruncated + } + return h, in[geckoHeaderSize+int(h.padLen):], nil +} diff --git a/transport/internet/finalmask/salamander/salamander.go b/transport/internet/finalmask/salamander/salamander.go index 86d92dcdf..d81863aa9 100644 --- a/transport/internet/finalmask/salamander/salamander.go +++ b/transport/internet/finalmask/salamander/salamander.go @@ -24,16 +24,21 @@ type SalamanderObfuscator struct { PSK []byte RandSrc *rand.Rand - lk sync.Mutex + lk sync.Mutex + keyInput []byte } func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) { if len(psk) < smPSKMinLen { return nil, ErrPSKTooShort } + pskCopy := append([]byte(nil), psk...) + keyInput := make([]byte, len(pskCopy)+smSaltLen) + copy(keyInput, pskCopy) return &SalamanderObfuscator{ - PSK: psk, - RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + PSK: pskCopy, + RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + keyInput: keyInput, }, nil } @@ -44,8 +49,8 @@ func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int { } o.lk.Lock() _, _ = o.RandSrc.Read(out[:smSaltLen]) + key := o.keyLocked(out[:smSaltLen]) o.lk.Unlock() - key := o.key(out[:smSaltLen]) for i, c := range in { out[i+smSaltLen] = c ^ key[i%smKeyLen] } @@ -57,13 +62,16 @@ func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int { if outLen <= 0 || len(out) < outLen { return 0 } - key := o.key(in[:smSaltLen]) + o.lk.Lock() + key := o.keyLocked(in[:smSaltLen]) + o.lk.Unlock() for i, c := range in[smSaltLen:] { out[i] = c ^ key[i%smKeyLen] } return outLen } -func (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte { - return blake2b.Sum256(append(o.PSK, salt...)) +func (o *SalamanderObfuscator) keyLocked(salt []byte) [smKeyLen]byte { + copy(o.keyInput[len(o.PSK):], salt[:smSaltLen]) + return blake2b.Sum256(o.keyInput) } diff --git a/transport/internet/finalmask/salamander/salamander_test.go b/transport/internet/finalmask/salamander/salamander_test.go deleted file mode 100644 index ffd508218..000000000 --- a/transport/internet/finalmask/salamander/salamander_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package salamander_test - -import ( - "crypto/rand" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/xtls/xray-core/transport/internet/finalmask/salamander" -) - -const ( - smSaltLen = 8 -) - -func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) { - o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) - in := make([]byte, 1200) - _, _ = rand.Read(in) - out := make([]byte, 2048) - b.ResetTimer() - for i := 0; i < b.N; i++ { - o.Obfuscate(in, out) - } -} - -func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) { - o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) - in := make([]byte, 1200) - _, _ = rand.Read(in) - out := make([]byte, 2048) - b.ResetTimer() - for i := 0; i < b.N; i++ { - o.Deobfuscate(in, out) - } -} - -func TestSalamanderObfuscator(t *testing.T) { - o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) - in := make([]byte, 1200) - oOut := make([]byte, 2048) - dOut := make([]byte, 2048) - for i := 0; i < 1000; i++ { - _, _ = rand.Read(in) - n := o.Obfuscate(in, oOut) - assert.Equal(t, len(in)+smSaltLen, n) - n = o.Deobfuscate(oOut[:n], dOut) - assert.Equal(t, len(in), n) - assert.Equal(t, in, dOut[:n]) - } -} - -func TestSalamanderInPlace(t *testing.T) { - o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) - - in := make([]byte, 1200) - out := make([]byte, 2048) - _, _ = rand.Read(in) - o.Obfuscate(in, out) - - out2 := make([]byte, 2048) - copy(out2[smSaltLen:], in) - o.Obfuscate(out2[smSaltLen:], out2) - - dOut := make([]byte, 2048) - o.Deobfuscate(out, dOut) - - o.Deobfuscate(out2, out2) - - assert.Equal(t, in, dOut[:1200]) - assert.Equal(t, in, out2[:1200]) -} - -func TestSalamanderBounce(t *testing.T) { - o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) - buf := make([]byte, 8) - for i := 0; i < 1000; i++ { - _, _ = rand.Read(buf) - n := o.Deobfuscate(buf, buf) - assert.Equal(t, 0, n) - } -} diff --git a/transport/internet/finalmask/udp_test.go b/transport/internet/finalmask/udp_test.go index 0c0641323..8759f9fbc 100644 --- a/transport/internet/finalmask/udp_test.go +++ b/transport/internet/finalmask/udp_test.go @@ -462,50 +462,6 @@ func TestUDPcustomStaticHeaderWireShape(t *testing.T) { } } -func TestUDPcustomServerRejectsMismatchedStaticHeader(t *testing.T) { - cfg := &custom.UDPConfig{ - Client: []*custom.UDPItem{ - {Packet: []byte{0x01, 0x02}}, - }, - Server: []*custom.UDPItem{ - {Packet: []byte{0x03}}, - }, - } - maskManager := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg}) - - clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer clientRaw.Close() - - serverRaw, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer serverRaw.Close() - - server, err := maskManager.WrapPacketConnServer(serverRaw) - if err != nil { - t.Fatal(err) - } - - _ = server.SetDeadline(time.Now().Add(200 * time.Millisecond)) - - if _, err := clientRaw.WriteTo([]byte{0x09, 0x09, 'b', 'a', 'd'}, server.LocalAddr()); err != nil { - t.Fatal(err) - } - - buf := make([]byte, 128) - n, _, err := server.ReadFrom(buf) - if n != 0 { - t.Fatalf("expected no payload on mismatched header, got %d bytes", n) - } - if err != nil { - t.Fatalf("expected mismatch to be dropped without surfaced error, got %v", err) - } -} - func TestUDPcustomStandaloneClientSendsDetachedHandshakeBeforePayload(t *testing.T) { _, serverRaw, client, _ := newUDPClientServerPair(t, newStandaloneEchoUDPConfig()) From 2b4269962360bdb4abc9c1d0117d52a92162a4a9 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 29 May 2026 17:44:06 +0800 Subject: [PATCH 21/78] XICMP finalmask: Refactor & Speed up; Add multi `ips` and `dgram` mode (client) (#6168) https://github.com/XTLS/Xray-core/pull/5872#issuecomment-4198514120 https://github.com/XTLS/Xray-core/pull/6168#issuecomment-4571606666 https://github.com/XTLS/Xray-core/pull/6168#issuecomment-4573294637 Example: https://github.com/XTLS/Xray-core/pull/6168#issue-4484980927 Closes https://github.com/XTLS/Xray-core/discussions/5879 --- infra/conf/transport_internet.go | 17 +- transport/internet/finalmask/xicmp/client.go | 508 +++++++++--------- .../internet/finalmask/xicmp/config.pb.go | 24 +- .../internet/finalmask/xicmp/config.proto | 4 +- transport/internet/finalmask/xicmp/server.go | 492 ++++++++--------- 5 files changed, 520 insertions(+), 525 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 69a909226..7ef91e522 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "math" + "net/netip" "net/url" "os" "regexp" @@ -1901,18 +1902,20 @@ func (c *Xdns) Build() (proto.Message, error) { } type Xicmp struct { - ListenIp string `json:"listenIp"` - Id uint16 `json:"id"` + DGRAM bool `json:"dgram"` + IPs []string `json:"ips"` } func (c *Xicmp) Build() (proto.Message, error) { - config := &xicmp.Config{ - Ip: c.ListenIp, - Id: int32(c.Id), + for _, ip := range c.IPs { + if _, err := netip.ParseAddr(ip); err != nil { + return nil, err + } } - if config.Ip == "" { - config.Ip = "0.0.0.0" + config := &xicmp.Config{ + DGRAM: c.DGRAM, + IPs: c.IPs, } return config, nil diff --git a/transport/internet/finalmask/xicmp/client.go b/transport/internet/finalmask/xicmp/client.go index d738b1250..3777bd879 100644 --- a/transport/internet/finalmask/xicmp/client.go +++ b/transport/internet/finalmask/xicmp/client.go @@ -1,14 +1,21 @@ package xicmp import ( + "bytes" "context" + "crypto/rand" + "encoding/binary" + goerrors "errors" + "fmt" "io" + mathrand "math/rand" "net" - "strings" + "net/netip" "sync" "time" + _ "unsafe" - "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/transport/internet/finalmask" "golang.org/x/net/icmp" @@ -16,149 +23,115 @@ import ( "golang.org/x/net/ipv6" ) -const ( - initPollDelay = 500 * time.Millisecond - maxPollDelay = 10 * time.Second - pollDelayMultiplier = 2.0 - pollLimit = 16 - windowSize = 1000 -) +var pool = sync.Pool{ + New: func() any { + return make([]byte, finalmask.UDPSize) + }, +} type packet struct { p []byte addr net.Addr -} - -type seqStatus struct { - needSeqByte bool - seqByte byte + err error } type xicmpConnClient struct { conn net.PacketConn - icmpConn *icmp.PacketConn - - typ icmp.Type - id int - seq int - proto int - seqStatus map[int]*seqStatus - - pollChan chan struct{} - readQueue chan *packet - writeQueue chan *packet - - closed bool - mutex sync.Mutex + icmp4 *icmp.PacketConn + icmp6 *icmp.PacketConn + udp bool + ips []netip.Addr + clientID [8]byte + id int + seq int + readCh chan packet + closedCh chan struct{} + mu sync.Mutex } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - network := "ip4:icmp" - typ := icmp.Type(ipv4.ICMPTypeEcho) - proto := 1 - if strings.Contains(c.Ip, ":") { - network = "ip6:ipv6-icmp" - typ = ipv6.ICMPTypeEchoRequest - proto = 58 + var icmp4, icmp6 *icmp.PacketConn + var err4, err6 error + if c.DGRAM { + icmp4, err4 = icmp.ListenPacket("udp4", "0.0.0.0") + icmp6, err6 = icmp.ListenPacket("udp6", "::") + } else { + icmp4, err4 = icmp.ListenPacket("ip4:icmp", "0.0.0.0") + icmp6, err6 = icmp.ListenPacket("ip6:ipv6-icmp", "::") + } + if err4 != nil || err6 != nil { + return nil, errors.Combine(err4, err6) } - icmpConn, err := icmp.ListenPacket(network, c.Ip) - if err != nil { - return nil, errors.New("xicmp listen err").Base(err) + ips := make([]netip.Addr, 0, len(c.IPs)) + for _, ip := range c.IPs { + ips = append(ips, netip.MustParseAddr(ip)) } - if c.Id == 0 { - c.Id = int32(crypto.RandBetween(0, 65535)) - } + var clientID [8]byte + common.Must2(rand.Read(clientID[:])) conn := &xicmpConnClient{ conn: raw, - icmpConn: icmpConn, - - typ: typ, - id: int(c.Id), - seq: 1, - proto: proto, - seqStatus: make(map[int]*seqStatus), - - pollChan: make(chan struct{}, pollLimit), - readQueue: make(chan *packet, 256), - writeQueue: make(chan *packet, 256), + icmp4: icmp4, + icmp6: icmp6, + udp: c.DGRAM, + ips: ips, + clientID: clientID, + id: mathrand.Intn(65536), + seq: 1, + readCh: make(chan packet), + closedCh: make(chan struct{}), } - go conn.recvLoop() - go conn.sendLoop() + go conn.recv4() + go conn.recv6() return conn, nil } -func (c *xicmpConnClient) encode(p []byte) ([]byte, error) { - c.mutex.Lock() - defer c.mutex.Unlock() - - needSeqByte := false - var seqByte byte - data := p - if len(p) > 0 { - needSeqByte = true - seqByte = p[0] - } - - msg := icmp.Message{ - Type: c.typ, - Code: 0, - Body: &icmp.Echo{ - ID: c.id, - Seq: c.seq, - Data: data, - }, - } - - buf, err := msg.Marshal(nil) - if err != nil { - return nil, err - } - - if len(buf) > finalmask.UDPSize { - return nil, errors.New("xicmp len(buf) > finalmask.UDPSize") - } - - c.seqStatus[c.seq] = &seqStatus{ - needSeqByte: needSeqByte, - seqByte: seqByte, - } - - delete(c.seqStatus, int(uint16(c.seq-windowSize))) - - c.seq++ - - if c.seq == 65536 { - delete(c.seqStatus, int(uint16(c.seq-windowSize))) - c.seq = 1 - } - - return buf, nil +func (c *xicmpConnClient) ring(a, b uint16) uint16 { + return min(a-b, b-a) } -func (c *xicmpConnClient) recvLoop() { - var buf [finalmask.UDPSize]byte +func (c *xicmpConnClient) closed() bool { + select { + case <-c.closedCh: + return true + default: + return false + } +} + +func (c *xicmpConnClient) recv4() { + var b [finalmask.UDPSize]byte for { - if c.closed { - break + if c.closed() { + return } - n, addr, err := c.icmpConn.ReadFrom(buf[:]) + n, addr, err := c.icmp4.ReadFrom(b[:]) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + select { + case c.readCh <- packet{ + err: err, + }: + case <-c.closedCh: + return + } + } + continue + } + + msg, err := icmp.ParseMessage(1, b[:n]) if err != nil { continue } - msg, err := icmp.ParseMessage(c.proto, buf[:n]) - if err != nil { - continue - } - - if msg.Type != ipv4.ICMPTypeEchoReply && msg.Type != ipv6.ICMPTypeEchoReply { + if msg.Type != ipv4.ICMPTypeEchoReply { continue } @@ -167,180 +140,223 @@ func (c *xicmpConnClient) recvLoop() { continue } - c.mutex.Lock() - seqStatus, ok := c.seqStatus[echo.Seq] - c.mutex.Unlock() + // errors.LogDebug(context.Background(), "id ", echo.ID, " seq ", echo.Seq, " addr ", addr) + if !c.udp && echo.ID != c.id { + continue + } + + if c.ring(uint16(echo.Seq), uint16(c.seq)) > 1000 { + continue + } + + if len(echo.Data) > 8 && bytes.Equal(echo.Data[:8], c.clientID[:]) { + continue + } + + p := pool.Get().([]byte)[:len(echo.Data)] + copy(p, echo.Data) + + if !c.udp { + addr = &net.UDPAddr{IP: addr.(*net.IPAddr).IP} + } + + select { + case c.readCh <- packet{ + p: p, + addr: addr, + }: + case <-c.closedCh: + pool.Put(p) + return + } + } +} + +func (c *xicmpConnClient) recv6() { + var b [finalmask.UDPSize]byte + + for { + if c.closed() { + break + } + + n, addr, err := c.icmp6.ReadFrom(b[:]) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + select { + case c.readCh <- packet{ + err: err, + }: + case <-c.closedCh: + return + } + } + continue + } + + msg, err := icmp.ParseMessage(58, b[:n]) + if err != nil { + continue + } + + if msg.Type != ipv6.ICMPTypeEchoReply { + continue + } + + echo, ok := msg.Body.(*icmp.Echo) if !ok { continue } - if seqStatus.needSeqByte { - if len(echo.Data) <= 1 { - continue - } - if echo.Data[0] == seqStatus.seqByte { - continue - } - echo.Data = echo.Data[1:] + // errors.LogDebug(context.Background(), "id ", echo.ID, " seq ", echo.Seq, " addr ", addr) + + if !c.udp && echo.ID != c.id { + continue } - if len(echo.Data) > 0 { - c.mutex.Lock() - delete(c.seqStatus, echo.Seq) - c.mutex.Unlock() - - buf := make([]byte, len(echo.Data)) - copy(buf, echo.Data) - select { - case c.readQueue <- &packet{ - p: buf, - addr: &net.UDPAddr{IP: addr.(*net.IPAddr).IP}, - }: - default: - errors.LogDebug(context.Background(), addr, " ", echo.Seq, " ", echo.ID, " mask read err queue full") - } - - select { - case c.pollChan <- struct{}{}: - default: - } + if c.ring(uint16(echo.Seq), uint16(c.seq)) > 1000 { + continue } - } - errors.LogDebug(context.Background(), "xicmp closed") + if len(echo.Data) > 8 && bytes.Equal(echo.Data[:8], c.clientID[:]) { + continue + } - close(c.pollChan) - close(c.readQueue) + p := pool.Get().([]byte)[:len(echo.Data)] + copy(p, echo.Data) - c.mutex.Lock() - defer c.mutex.Unlock() - - c.closed = true - close(c.writeQueue) -} - -func (c *xicmpConnClient) sendLoop() { - var addr net.Addr - - pollDelay := initPollDelay - pollTimer := time.NewTimer(pollDelay) - for { - var p *packet - pollTimerExpired := false + if !c.udp { + addr = &net.UDPAddr{IP: addr.(*net.IPAddr).IP} + } select { - case p = <-c.writeQueue: - default: - select { - case p = <-c.writeQueue: - case <-c.pollChan: - case <-pollTimer.C: - pollTimerExpired = true - } - } - - if p != nil { - addr = p.addr - - select { - case <-c.pollChan: - default: - } - } else if addr != nil { - encoded, _ := c.encode(nil) - p = &packet{ - p: encoded, - addr: addr, - } - } - - if pollTimerExpired { - pollDelay = time.Duration(float64(pollDelay) * pollDelayMultiplier) - if pollDelay > maxPollDelay { - pollDelay = maxPollDelay - } - } else { - if !pollTimer.Stop() { - <-pollTimer.C - } - pollDelay = initPollDelay - } - pollTimer.Reset(pollDelay) - - if c.closed { + case c.readCh <- packet{ + p: p, + addr: addr, + }: + case <-c.closedCh: + pool.Put(p) return } - - if p != nil { - _, err := c.icmpConn.WriteTo(p.p, p.addr) - if err != nil { - errors.LogDebug(context.Background(), p.addr, " xicmp writeto err ", err) - } - } } } func (c *xicmpConnClient) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - packet, ok := <-c.readQueue - if !ok { - return 0, nil, net.ErrClosed + select { + case packet := <-c.readCh: + if packet.p != nil { + n = copy(p, packet.p) + pool.Put(packet.p) + } + return n, packet.addr, packet.err + case <-c.closedCh: + return 0, nil, io.EOF } - if len(p) < len(packet.p) { - errors.LogDebug(context.Background(), packet.addr, " mask read err short buffer ", len(p), " ", len(packet.p)) - return 0, packet.addr, nil - } - copy(p, packet.p) - return len(packet.p), packet.addr, nil } func (c *xicmpConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { - encoded, err := c.encode(p) + if len(p)+16 > finalmask.UDPSize { + errors.LogError(context.Background(), "drop packet to ", addr, " with size ", len(p)) + return 0, nil + } + + c.mu.Lock() + seq := c.seq + c.seq += 1 + c.seq %= 65536 + c.mu.Unlock() + + ip := addr.(*net.UDPAddr).IP + if len(c.ips) > 0 { + ip = c.ips[mathrand.Intn(len(c.ips))].AsSlice() + } + + if c.udp { + addr = &net.UDPAddr{IP: ip} + } else { + addr = &net.IPAddr{IP: ip} + } + + b := pool.Get().([]byte)[:finalmask.UDPSize] + defer pool.Put(b) + + copy(b[8:], c.clientID[:]) + copy(b[16:], p) + + if ip.To4() != nil { + b = marshal(b, ipv4.ICMPTypeEcho, c.id, seq, 8+len(p)) + _, err = c.icmp4.WriteTo(b, addr) + } else { + b = marshal(b, ipv6.ICMPTypeEchoRequest, c.id, seq, 8+len(p)) + _, err = c.icmp6.WriteTo(b, addr) + } + if err != nil { - errors.LogDebug(context.Background(), addr, " xicmp wireformat err ", err) - return 0, nil + errors.LogErrorInner(context.Background(), err, "xicmp write") + return 0, err } - c.mutex.Lock() - defer c.mutex.Unlock() - - if c.closed { - return 0, io.ErrClosedPipe - } - - select { - case c.writeQueue <- &packet{ - p: encoded, - addr: &net.IPAddr{IP: addr.(*net.UDPAddr).IP}, - }: - return len(p), nil - default: - errors.LogDebug(context.Background(), addr, " mask write err queue full") - return 0, nil - } + return len(p), nil } func (c *xicmpConnClient) Close() error { - c.closed = true - _ = c.icmpConn.Close() - return c.conn.Close() + c.mu.Lock() + defer c.mu.Unlock() + if c.closed() { + return nil + } + close(c.closedCh) + _ = c.icmp4.Close() + _ = c.icmp6.Close() + _ = c.conn.Close() + return nil } func (c *xicmpConnClient) LocalAddr() net.Addr { - return &net.UDPAddr{ - IP: c.icmpConn.LocalAddr().(*net.IPAddr).IP, - Port: c.id, - } + return c.conn.LocalAddr() } func (c *xicmpConnClient) SetDeadline(t time.Time) error { - return c.icmpConn.SetDeadline(t) + _ = c.icmp4.SetDeadline(t) + _ = c.icmp6.SetDeadline(t) + return nil } func (c *xicmpConnClient) SetReadDeadline(t time.Time) error { - return c.icmpConn.SetReadDeadline(t) + _ = c.icmp4.SetReadDeadline(t) + _ = c.icmp6.SetReadDeadline(t) + return nil } func (c *xicmpConnClient) SetWriteDeadline(t time.Time) error { - return c.icmpConn.SetWriteDeadline(t) + _ = c.icmp4.SetWriteDeadline(t) + _ = c.icmp6.SetWriteDeadline(t) + return nil +} + +//go:linkname checksum golang.org/x/net/icmp.checksum +func checksum(b []byte) uint16 + +func marshal(b []byte, typ icmp.Type, id, seq int, dataLen int) []byte { + is4 := false + switch typ := typ.(type) { + case ipv4.ICMPType: + is4 = true + b[0] = byte(typ) + case ipv6.ICMPType: + b[0] = byte(typ) + default: + panic(fmt.Sprintf("%T %v", typ, typ)) + } + clear(b[1:4]) + binary.BigEndian.PutUint16(b[4:], uint16(id)) + binary.BigEndian.PutUint16(b[6:], uint16(seq)) + if is4 { + s := checksum(b[:8+dataLen]) + b[2] ^= byte(s) + b[3] ^= byte(s >> 8) + } + return b[:8+dataLen] } diff --git a/transport/internet/finalmask/xicmp/config.pb.go b/transport/internet/finalmask/xicmp/config.pb.go index be75dfe51..0f88ef1e9 100644 --- a/transport/internet/finalmask/xicmp/config.pb.go +++ b/transport/internet/finalmask/xicmp/config.pb.go @@ -23,8 +23,8 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` - Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` - Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` + DGRAM bool `protobuf:"varint,1,opt,name=DGRAM,proto3" json:"DGRAM,omitempty"` + IPs []string `protobuf:"bytes,2,rep,name=IPs,proto3" json:"IPs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -59,28 +59,28 @@ func (*Config) Descriptor() ([]byte, []int) { return file_transport_internet_finalmask_xicmp_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetIp() string { +func (x *Config) GetDGRAM() bool { if x != nil { - return x.Ip + return x.DGRAM } - return "" + return false } -func (x *Config) GetId() int32 { +func (x *Config) GetIPs() []string { if x != nil { - return x.Id + return x.IPs } - return 0 + return nil } var File_transport_internet_finalmask_xicmp_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_xicmp_config_proto_rawDesc = "" + "\n" + - "/transport/internet/finalmask/xicmp/config.proto\x12'xray.transport.internet.finalmask.xicmp\"(\n" + - "\x06Config\x12\x0e\n" + - "\x02ip\x18\x01 \x01(\tR\x02ip\x12\x0e\n" + - "\x02id\x18\x02 \x01(\x05R\x02idB\x97\x01\n" + + "/transport/internet/finalmask/xicmp/config.proto\x12'xray.transport.internet.finalmask.xicmp\"0\n" + + "\x06Config\x12\x14\n" + + "\x05DGRAM\x18\x01 \x01(\bR\x05DGRAM\x12\x10\n" + + "\x03IPs\x18\x02 \x03(\tR\x03IPsB\x97\x01\n" + "+com.xray.transport.internet.finalmask.xicmpP\x01Z= idleTimeout { - close(q.queue) - delete(c.writeQueueMap, key) - } - } - +func (c *xicmpConnServer) closed() bool { + select { + case <-c.closedCh: + return true + default: return false } +} +func (c *xicmpConnServer) clean() { + ticker := time.NewTicker(time.Minute / 2) + defer ticker.Stop() for { - time.Sleep(idleTimeout / 2) - if f() { + select { + case <-ticker.C: + now := time.Now() + c.mu.Lock() + for key, r := range c.rec { + if now.Sub(r.last) > time.Minute { + delete(c.rec, key) + } + } + c.mu.Unlock() + case <-c.closedCh: return } } } -func (c *xicmpConnServer) ensureQueue(addr net.Addr) *queue { - if c.closed { - return nil - } - - q, ok := c.writeQueueMap[addr.String()] - if !ok { - q = &queue{ - queue: make(chan []byte, 512), - } - c.writeQueueMap[addr.String()] = q - } - q.last = time.Now() - - return q -} - -func (c *xicmpConnServer) encode(p []byte, id int, seq int, needSeqByte bool, seqByte byte) ([]byte, error) { - data := p - if needSeqByte { - b2 := c.randUntil(seqByte) - data = append([]byte{b2}, p...) - } - - msg := icmp.Message{ - Type: c.typ, - Code: 0, - Body: &icmp.Echo{ - ID: id, - Seq: seq, - Data: data, - }, - } - - buf, err := msg.Marshal(nil) - if err != nil { - return nil, err - } - - if len(buf) > finalmask.UDPSize { - return nil, errors.New("xicmp len(buf) > finalmask.UDPSize") - } - - return buf, nil -} - -func (c *xicmpConnServer) randUntil(b1 byte) byte { - b2 := byte(crypto.RandBetween(0, 255)) - for { - if b2 != b1 { - return b2 - } - b2 = byte(crypto.RandBetween(0, 255)) - } -} - -func (c *xicmpConnServer) recvLoop() { - var buf [finalmask.UDPSize]byte +func (c *xicmpConnServer) recv4() { + var b [finalmask.UDPSize]byte for { - if c.closed { - break + if c.closed() { + return } - n, addr, err := c.icmpConn.ReadFrom(buf[:]) + n, addr, err := c.icmp4.ReadFrom(b[:]) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + select { + case c.readCh <- packet{ + err: err, + }: + case <-c.closedCh: + return + } + } + continue + } + + msg, err := icmp.ParseMessage(1, b[:n]) if err != nil { continue } - msg, err := icmp.ParseMessage(c.proto, buf[:n]) - if err != nil { - continue - } - - if msg.Type != ipv4.ICMPTypeEcho && msg.Type != ipv6.ICMPTypeEchoRequest { + if msg.Type != ipv4.ICMPTypeEcho { continue } @@ -197,179 +143,209 @@ func (c *xicmpConnServer) recvLoop() { continue } - if c.config.Id != 0 && echo.ID != int(c.config.Id) { + if len(echo.Data) <= 8 { continue } - needSeqByte := false - var seqByte byte + if len(c.ips) > 0 { + netipAddr, ok := netip.AddrFromSlice(addr.(*net.IPAddr).IP) + if !ok { + continue + } - if len(echo.Data) > 0 { - needSeqByte = true - seqByte = echo.Data[0] - - buf := make([]byte, len(echo.Data)) - copy(buf, echo.Data) - select { - case c.readQueue <- &packet{ - p: buf, - addr: &net.UDPAddr{ - IP: addr.(*net.IPAddr).IP, - Port: echo.ID, - }, - }: - default: - errors.LogDebug(context.Background(), addr, " ", echo.ID, " ", echo.Seq, " mask read err queue full") + if _, ok := c.ips[netipAddr]; !ok { + continue } } - select { - case c.ch <- &record{ - id: echo.ID, - seq: echo.Seq, - needSeqByte: needSeqByte, - seqByte: seqByte, - addr: &net.UDPAddr{ - IP: addr.(*net.IPAddr).IP, - Port: echo.ID, - }, - }: - default: - errors.LogDebug(context.Background(), addr, " ", echo.ID, " ", echo.Seq, " mask read err record queue full") + cAddr := clientIDToAddr([8]byte(echo.Data[:8])) + + c.mu.Lock() + c.rec[cAddr.String()] = record{ + id: echo.ID, + seq: echo.Seq, + addr: addr, + last: time.Now(), } - } + c.mu.Unlock() - errors.LogDebug(context.Background(), "xicmp closed") + p := pool.Get().([]byte)[:len(echo.Data[8:])] + copy(p, echo.Data[8:]) - close(c.ch) - close(c.readQueue) - - c.mutex.Lock() - defer c.mutex.Unlock() - - c.closed = true - for key, q := range c.writeQueueMap { - close(q.queue) - delete(c.writeQueueMap, key) + select { + case c.readCh <- packet{ + p: p, + addr: cAddr, + }: + case <-c.closedCh: + pool.Put(p) + return + } } } -func (c *xicmpConnServer) sendLoop() { - var nextRec *record - for { - rec := nextRec - nextRec = nil +func (c *xicmpConnServer) recv6() { + var b [finalmask.UDPSize]byte - if rec == nil { - var ok bool - rec, ok = <-c.ch + for { + if c.closed() { + return + } + + n, addr, err := c.icmp6.ReadFrom(b[:]) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + select { + case c.readCh <- packet{ + err: err, + }: + case <-c.closedCh: + return + } + } + continue + } + + msg, err := icmp.ParseMessage(58, b[:n]) + if err != nil { + continue + } + + if msg.Type != ipv6.ICMPTypeEchoRequest { + continue + } + + echo, ok := msg.Body.(*icmp.Echo) + if !ok { + continue + } + + if len(echo.Data) <= 8 { + continue + } + + if len(c.ips) > 0 { + netipAddr, ok := netip.AddrFromSlice(addr.(*net.IPAddr).IP) if !ok { - break + continue + } + + if _, ok := c.ips[netipAddr]; !ok { + continue } } - c.mutex.Lock() - q := c.ensureQueue(rec.addr) - if q == nil { - c.mutex.Unlock() - return + cAddr := clientIDToAddr([8]byte(echo.Data[:8])) + + c.mu.Lock() + c.rec[cAddr.String()] = record{ + id: echo.ID, + seq: echo.Seq, + addr: addr, + last: time.Now(), } - c.mutex.Unlock() + c.mu.Unlock() - var p []byte - - timer := time.NewTimer(maxResponseDelay) + p := pool.Get().([]byte)[:len(echo.Data[8:])] + copy(p, echo.Data[8:]) select { - case p = <-q.queue: - default: - select { - case p = <-q.queue: - case <-timer.C: - case nextRec = <-c.ch: - } - } - - timer.Stop() - - if len(p) == 0 { - continue - } - - buf, err := c.encode(p, rec.id, rec.seq, rec.needSeqByte, rec.seqByte) - if err != nil { - errors.LogDebug(context.Background(), rec.addr, " ", rec.id, " ", rec.seq, " xicmp wireformat err ", err) - continue - } - - if c.closed { + case c.readCh <- packet{ + p: p, + addr: cAddr, + }: + case <-c.closedCh: + pool.Put(p) return } - - _, err = c.icmpConn.WriteTo(buf, &net.IPAddr{IP: rec.addr.(*net.UDPAddr).IP}) - if err != nil { - errors.LogDebug(context.Background(), rec.addr, " ", rec.id, " ", rec.seq, " xicmp writeto err ", err) - } } } func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - packet, ok := <-c.readQueue - if !ok { - return 0, nil, net.ErrClosed + select { + case packet := <-c.readCh: + if packet.p != nil { + n = copy(p, packet.p) + pool.Put(packet.p) + } + return n, packet.addr, packet.err + case <-c.closedCh: + return 0, nil, io.EOF } - if len(p) < len(packet.p) { - errors.LogDebug(context.Background(), packet.addr, " mask read err short buffer ", len(p), " ", len(packet.p)) - return 0, packet.addr, nil - } - copy(p, packet.p) - return len(packet.p), packet.addr, nil } func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if len(p)+8+1 > finalmask.UDPSize { - errors.LogDebug(context.Background(), addr, " mask write err short write ", len(p), "+8+1 > ", finalmask.UDPSize) + if len(p)+8 > finalmask.UDPSize { + errors.LogError(context.Background(), "drop packet to ", addr, " with size ", len(p)) return 0, nil } - c.mutex.Lock() - defer c.mutex.Unlock() - - q := c.ensureQueue(addr) - if q == nil { - return 0, io.ErrClosedPipe - } - - buf := make([]byte, len(p)) - copy(buf, p) - - select { - case q.queue <- buf: - return len(p), nil - default: - // errors.LogDebug(context.Background(), addr, " mask write err queue full") + c.mu.Lock() + r, ok := c.rec[addr.String()] + if !ok { + errors.LogError(context.Background(), "drop packet to ", addr, " with size ", len(p)) + c.mu.Unlock() return 0, nil } + r.last = time.Now() + c.rec[addr.String()] = r + c.mu.Unlock() + + // errors.LogDebug(context.Background(), "id ", r.id, " seq ", r.seq, " addr ", r.addr) + + b := pool.Get().([]byte)[:finalmask.UDPSize] + defer pool.Put(b) + + copy(b[8:], p) + + if r.addr.(*net.IPAddr).IP.To4() != nil { + b = marshal(b, ipv4.ICMPTypeEchoReply, r.id, r.seq, len(p)) + _, err = c.icmp4.WriteTo(b, r.addr) + } else { + b = marshal(b, ipv6.ICMPTypeEchoReply, r.id, r.seq, len(p)) + _, err = c.icmp6.WriteTo(b, r.addr) + } + + if err != nil { + errors.LogErrorInner(context.Background(), err, "xicmp write") + return 0, err + } + + return len(p), nil } func (c *xicmpConnServer) Close() error { - c.closed = true - _ = c.icmpConn.Close() - return c.conn.Close() + c.mu.Lock() + defer c.mu.Unlock() + if c.closed() { + return nil + } + close(c.closedCh) + _ = c.icmp4.Close() + _ = c.icmp6.Close() + _ = c.conn.Close() + return nil } func (c *xicmpConnServer) LocalAddr() net.Addr { - return &net.UDPAddr{IP: c.icmpConn.LocalAddr().(*net.IPAddr).IP} + return c.conn.LocalAddr() } func (c *xicmpConnServer) SetDeadline(t time.Time) error { - return c.icmpConn.SetDeadline(t) + _ = c.icmp4.SetDeadline(t) + _ = c.icmp6.SetDeadline(t) + return nil } func (c *xicmpConnServer) SetReadDeadline(t time.Time) error { - return c.icmpConn.SetReadDeadline(t) + _ = c.icmp4.SetReadDeadline(t) + _ = c.icmp6.SetReadDeadline(t) + return nil } func (c *xicmpConnServer) SetWriteDeadline(t time.Time) error { - return c.icmpConn.SetWriteDeadline(t) + _ = c.icmp4.SetWriteDeadline(t) + _ = c.icmp6.SetWriteDeadline(t) + return nil } From 569459c54c30b2c375bf92141488daac6c4781e1 Mon Sep 17 00:00:00 2001 From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> Date: Fri, 29 May 2026 18:47:58 +0800 Subject: [PATCH 22/78] Sudoku finalmask: Harden UDP ReadFrom() against invalid packets (#6185) https://github.com/XTLS/Xray-core/issues/6184#issuecomment-4573949756 Fixes https://github.com/XTLS/Xray-core/issues/6184 --------- Co-authored-by: LjhAUMEM --- .../internet/finalmask/sudoku/conn_udp.go | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/transport/internet/finalmask/sudoku/conn_udp.go b/transport/internet/finalmask/sudoku/conn_udp.go index c2b2f4dda..4d610b50a 100644 --- a/transport/internet/finalmask/sudoku/conn_udp.go +++ b/transport/internet/finalmask/sudoku/conn_udp.go @@ -1,10 +1,12 @@ package sudoku import ( - "io" + "context" "net" "sync" "time" + + "github.com/xtls/xray-core/common/errors" ) type udpConn struct { @@ -39,26 +41,31 @@ func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { c.readMu.Lock() defer c.readMu.Unlock() - n, addr, err = c.conn.ReadFrom(c.readBuf) - if err != nil { - return n, addr, err - } + for { + n, addr, err = c.conn.ReadFrom(c.readBuf) + if err != nil { + return n, addr, err + } - decoded := make([]byte, 0, n/4+1) - hints := make([]byte, 0, 4) - tableIndex := 0 - hints, decoded, err = decodeBytes(c.tables, &tableIndex, c.readBuf[:n], hints, decoded) - if err != nil { - return 0, addr, err + decoded := make([]byte, 0, n/4+1) + hints := make([]byte, 0, 4) + tableIndex := 0 + hints, decoded, err = decodeBytes(c.tables, &tableIndex, c.readBuf[:n], hints, decoded) + if err != nil { + errors.LogErrorInner(context.Background(), err, "drop packet from ", addr, " with size ", n) + continue + } + if len(hints) != 0 { + errors.LogError(context.Background(), "drop packet from ", addr, " with size ", n) + continue + } + if len(p) < len(decoded) { + errors.LogError(context.Background(), "drop packet from ", addr, " with size ", n) + continue + } + copy(p, decoded) + return len(decoded), addr, nil } - if len(hints) != 0 { - return 0, addr, io.ErrUnexpectedEOF - } - if len(p) < len(decoded) { - return 0, addr, io.ErrShortBuffer - } - copy(p, decoded) - return len(decoded), addr, nil } func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { @@ -68,16 +75,15 @@ func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { // UDP decoding restarts at table 0 for every datagram, so encoding must do the same. encoded, err := newCodec(c.tables, c.pMin, c.pMax).encode(p) if err != nil { - return 0, err + errors.LogErrorInner(context.Background(), err, "drop packet to ", addr, " with size ", len(p)) + return 0, nil } - nn, err := c.conn.WriteTo(encoded, addr) + _, err = c.conn.WriteTo(encoded, addr) if err != nil { return 0, err } - if nn != len(encoded) { - return 0, io.ErrShortWrite - } + return len(p), nil } From aba22722a65d31c4c17ef5d9619e75a9ca474ce7 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Fri, 29 May 2026 19:48:22 +0800 Subject: [PATCH 23/78] Finalmask: Add mkcp-legacy (UDP) to replace mkcp-* and legacy header-* (#6201) https://github.com/XTLS/Xray-core/pull/6193#issuecomment-4543737699 https://github.com/XTLS/Xray-core/pull/6201#issuecomment-4574213827 Example: https://github.com/XTLS/Xray-core/pull/6201#issue-4525483699 --- infra/conf/transport_internet.go | 116 +++++---------- .../finalmask/header/custom/config.go | 2 - .../internet/finalmask/header/custom/udp.go | 119 ++++++++++----- .../header/custom/udp_runtime_test.go | 139 +----------------- .../finalmask/header/dns/config.pb.go | 123 ---------------- .../finalmask/header/dns/config.proto | 11 -- .../internet/finalmask/header/dtls/config.go | 19 --- .../finalmask/header/dtls/config.pb.go | 114 -------------- .../finalmask/header/dtls/config.proto | 9 -- .../internet/finalmask/header/dtls/conn.go | 74 ---------- .../internet/finalmask/header/srtp/config.go | 19 --- .../finalmask/header/srtp/config.pb.go | 114 -------------- .../finalmask/header/srtp/config.proto | 9 -- .../internet/finalmask/header/srtp/conn.go | 58 -------- .../internet/finalmask/header/utp/config.go | 19 --- .../finalmask/header/utp/config.pb.go | 114 -------------- .../finalmask/header/utp/config.proto | 9 -- .../internet/finalmask/header/utp/conn.go | 60 -------- .../finalmask/header/wechat/config.go | 19 --- .../finalmask/header/wechat/config.pb.go | 114 -------------- .../finalmask/header/wechat/config.proto | 9 -- .../internet/finalmask/header/wechat/conn.go | 64 -------- .../finalmask/header/wireguard/config.go | 19 --- .../finalmask/header/wireguard/config.pb.go | 114 -------------- .../finalmask/header/wireguard/config.proto | 9 -- .../finalmask/header/wireguard/conn.go | 50 ------- .../{header/dns => mkcp/header}/config.go | 10 +- .../finalmask/mkcp/header/config.pb.go | 132 +++++++++++++++++ .../finalmask/mkcp/header/config.proto | 12 ++ .../internet/finalmask/mkcp/header/conn.go | 57 +++++++ .../internet/finalmask/mkcp/header/header.go | 17 +++ .../dns/conn.go => mkcp/header/header_dns.go} | 100 +++++-------- .../finalmask/mkcp/header/header_dtls.go | 32 ++++ .../finalmask/mkcp/header/header_srtp.go | 18 +++ .../finalmask/mkcp/header/header_utp.go | 19 +++ .../finalmask/mkcp/header/header_wechat.go | 25 ++++ .../finalmask/mkcp/header/header_wireguard.go | 14 ++ transport/internet/finalmask/udp_test.go | 21 +-- 38 files changed, 499 insertions(+), 1484 deletions(-) delete mode 100644 transport/internet/finalmask/header/dns/config.pb.go delete mode 100644 transport/internet/finalmask/header/dns/config.proto delete mode 100644 transport/internet/finalmask/header/dtls/config.go delete mode 100644 transport/internet/finalmask/header/dtls/config.pb.go delete mode 100644 transport/internet/finalmask/header/dtls/config.proto delete mode 100644 transport/internet/finalmask/header/dtls/conn.go delete mode 100644 transport/internet/finalmask/header/srtp/config.go delete mode 100644 transport/internet/finalmask/header/srtp/config.pb.go delete mode 100644 transport/internet/finalmask/header/srtp/config.proto delete mode 100644 transport/internet/finalmask/header/srtp/conn.go delete mode 100644 transport/internet/finalmask/header/utp/config.go delete mode 100644 transport/internet/finalmask/header/utp/config.pb.go delete mode 100644 transport/internet/finalmask/header/utp/config.proto delete mode 100644 transport/internet/finalmask/header/utp/conn.go delete mode 100644 transport/internet/finalmask/header/wechat/config.go delete mode 100644 transport/internet/finalmask/header/wechat/config.pb.go delete mode 100644 transport/internet/finalmask/header/wechat/config.proto delete mode 100644 transport/internet/finalmask/header/wechat/conn.go delete mode 100644 transport/internet/finalmask/header/wireguard/config.go delete mode 100644 transport/internet/finalmask/header/wireguard/config.pb.go delete mode 100644 transport/internet/finalmask/header/wireguard/config.proto delete mode 100644 transport/internet/finalmask/header/wireguard/conn.go rename transport/internet/finalmask/{header/dns => mkcp/header}/config.go (80%) create mode 100644 transport/internet/finalmask/mkcp/header/config.pb.go create mode 100644 transport/internet/finalmask/mkcp/header/config.proto create mode 100644 transport/internet/finalmask/mkcp/header/conn.go create mode 100644 transport/internet/finalmask/mkcp/header/header.go rename transport/internet/finalmask/{header/dns/conn.go => mkcp/header/header_dns.go} (75%) create mode 100644 transport/internet/finalmask/mkcp/header/header_dtls.go create mode 100644 transport/internet/finalmask/mkcp/header/header_srtp.go create mode 100644 transport/internet/finalmask/mkcp/header/header_utp.go create mode 100644 transport/internet/finalmask/mkcp/header/header_wechat.go create mode 100644 transport/internet/finalmask/mkcp/header/header_wireguard.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 7ef91e522..25fe978ac 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -24,13 +24,8 @@ import ( "github.com/xtls/xray-core/transport/internet" "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" - "github.com/xtls/xray-core/transport/internet/finalmask/header/dtls" - "github.com/xtls/xray-core/transport/internet/finalmask/header/srtp" - "github.com/xtls/xray-core/transport/internet/finalmask/header/utp" - "github.com/xtls/xray-core/transport/internet/finalmask/header/wechat" - "github.com/xtls/xray-core/transport/internet/finalmask/header/wireguard" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" "github.com/xtls/xray-core/transport/internet/finalmask/noise" "github.com/xtls/xray-core/transport/internet/finalmask/realm" @@ -1243,21 +1238,14 @@ var ( }, "type", "settings") udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ - "header-custom": func() interface{} { return new(HeaderCustomUDP) }, - "header-dns": func() interface{} { return new(Dns) }, - "header-dtls": func() interface{} { return new(Dtls) }, - "header-srtp": func() interface{} { return new(Srtp) }, - "header-utp": func() interface{} { return new(Utp) }, - "header-wechat": func() interface{} { return new(Wechat) }, - "header-wireguard": func() interface{} { return new(Wireguard) }, - "mkcp-original": func() interface{} { return new(Original) }, - "mkcp-aes128gcm": func() interface{} { return new(Aes128Gcm) }, - "noise": func() interface{} { return new(NoiseMask) }, - "salamander": func() interface{} { return new(Salamander) }, - "sudoku": func() interface{} { return new(Sudoku) }, - "xdns": func() interface{} { return new(Xdns) }, - "xicmp": func() interface{} { return new(Xicmp) }, - "realm": func() interface{} { return new(Realm) }, + "header-custom": func() interface{} { return new(HeaderCustomUDP) }, + "mkcp-legacy": func() interface{} { return new(MkcpLegacy) }, + "noise": func() interface{} { return new(NoiseMask) }, + "salamander": func() interface{} { return new(Salamander) }, + "sudoku": func() interface{} { return new(Sudoku) }, + "xdns": func() interface{} { return new(Xdns) }, + "xicmp": func() interface{} { return new(Xicmp) }, + "realm": func() interface{} { return new(Realm) }, }, "type", "settings") ) @@ -1750,65 +1738,39 @@ func (c *HeaderCustomUDP) Build() (proto.Message, error) { } } -type Dns struct { - Domain string `json:"domain"` +type MkcpLegacy struct { + Header string `json:"header"` + Value string `json:"value"` } -func (c *Dns) Build() (proto.Message, error) { - config := &dns.Config{} - config.Domain = "www.baidu.com" - - if len(c.Domain) > 0 { - config.Domain = c.Domain +func (c *MkcpLegacy) Build() (proto.Message, error) { + if len(c.Header) == 0 { + if len(c.Value) == 0 { + return &original.Config{}, nil + } else { + return &aes128gcm.Config{Password: c.Value}, nil + } + } + switch strings.ToLower(c.Header) { + case "dns": + domain := c.Value + if len(domain) == 0 { + domain = "www.baidu.com" + } + return &header.Config{ID: 0, Domain: domain}, nil + case "dtls": + return &header.Config{ID: 1}, nil + case "srtp": + return &header.Config{ID: 2}, nil + case "utp": + return &header.Config{ID: 3}, nil + case "wechat": + return &header.Config{ID: 4}, nil + case "wireguard": + return &header.Config{ID: 5}, nil + default: + return nil, errors.New("invalid header ", c.Header) } - - return config, nil -} - -type Dtls struct{} - -func (c *Dtls) Build() (proto.Message, error) { - return &dtls.Config{}, nil -} - -type Srtp struct{} - -func (c *Srtp) Build() (proto.Message, error) { - return &srtp.Config{}, nil -} - -type Utp struct{} - -func (c *Utp) Build() (proto.Message, error) { - return &utp.Config{}, nil -} - -type Wechat struct{} - -func (c *Wechat) Build() (proto.Message, error) { - return &wechat.Config{}, nil -} - -type Wireguard struct{} - -func (c *Wireguard) Build() (proto.Message, error) { - return &wireguard.Config{}, nil -} - -type Original struct{} - -func (c *Original) Build() (proto.Message, error) { - return &original.Config{}, nil -} - -type Aes128Gcm struct { - Password string `json:"password"` -} - -func (c *Aes128Gcm) Build() (proto.Message, error) { - return &aes128gcm.Config{ - Password: c.Password, - }, nil } type Salamander struct { diff --git a/transport/internet/finalmask/header/custom/config.go b/transport/internet/finalmask/header/custom/config.go index 23460111d..c054718bf 100644 --- a/transport/internet/finalmask/header/custom/config.go +++ b/transport/internet/finalmask/header/custom/config.go @@ -16,8 +16,6 @@ func (c *TCPConfig) WrapConnServer(raw net.Conn) (net.Conn, error) { func (c *UDPConfig) UDP() {} -func (c *UDPConfig) HeaderConn() {} - func (c *UDPConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnClientUDP(c, raw) } diff --git a/transport/internet/finalmask/header/custom/udp.go b/transport/internet/finalmask/header/custom/udp.go index dba73798b..e3e78f5a2 100644 --- a/transport/internet/finalmask/header/custom/udp.go +++ b/transport/internet/finalmask/header/custom/udp.go @@ -2,11 +2,14 @@ package custom import ( "bytes" + "context" "net" "sync" "time" + "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet/finalmask" ) const udpStandaloneBufferSize = 4096 @@ -29,16 +32,16 @@ func (h *udpCustomClient) Serialize(b []byte) { copy(b, evaluated) } -func (h *udpCustomClient) Match(b []byte) bool { +func (h *udpCustomClient) Match(b []byte, addr net.Addr) bool { var initial map[string][]byte if h.state != nil { - initial, _ = h.state.get(udpStateKey(nil)) + initial, _ = h.state.get(udpStateKey(addr)) } vars, ok := matchUDPItems(h.server, b, h.read, initial) if ok { h.vars = vars if h.state != nil { - h.state.set(udpStateKey(nil), vars) + h.state.set(udpStateKey(addr), vars) } } return ok @@ -73,24 +76,38 @@ func NewConnClientUDP(c *UDPConfig, raw net.PacketConn) (net.PacketConn, error) return conn, nil } -func (c *udpCustomClientConn) Size() int { - return len(c.header.merged) -} - func (c *udpCustomClientConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - if !c.header.Match(p) { - return 0, addr, errors.New("header mismatch") + b := p + if len(b) < finalmask.UDPSize { + buf := buf.New() + buf.Resize(0, finalmask.UDPSize) + b = buf.Bytes() + defer buf.Release() } - return len(p) - c.header.read, addr, nil + for { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + + if !c.header.Match(b[:n], addr) { + errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", n, " > header mismatch") + continue + } + + copy(p, b[c.header.read:n]) + return n - c.header.read, addr, nil + } } func (c *udpCustomClientConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - var localAddr net.Addr - if c.PacketConn != nil { - localAddr = c.PacketConn.LocalAddr() - } - ctx := newEvalContextWithAddrs(localAddr, addr) + buf := buf.New() + buf.Resize(0, finalmask.UDPSize) + b := buf.Bytes() + defer buf.Release() + + ctx := newEvalContextWithAddrs(c.PacketConn.LocalAddr(), addr) if vars, ok := c.header.state.get(udpStateKey(addr)); ok { ctx.vars = cloneVars(vars) } else if len(c.header.vars) > 0 { @@ -98,13 +115,21 @@ func (c *udpCustomClientConn) WriteTo(p []byte, addr net.Addr) (n int, err error } evaluated, err := evaluateUDPItemsWithContext(c.header.client, ctx) if err != nil { - return 0, err + errors.LogErrorInner(context.Background(), err, "[mask] drop packet to ", addr, " with size ", len(p)) + return 0, nil } if len(evaluated) != len(c.header.merged) { - return 0, errors.New("header size mismatch") + errors.LogError(context.Background(), "[mask] drop packet to ", addr, " with size ", len(p), " > header size mismatch") + return 0, nil } c.header.state.set(udpStateKey(addr), ctx.vars) - copy(p, evaluated) + copy(b, evaluated) + copy(b[len(evaluated):], p) + _, err = c.PacketConn.WriteTo(b[:len(evaluated)+len(p)], addr) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[mask] drop packet to ", addr, " with size ", len(p)) + return 0, err + } return len(p), nil } @@ -127,16 +152,16 @@ func (h *udpCustomServer) Serialize(b []byte) { copy(b, evaluated) } -func (h *udpCustomServer) Match(b []byte) bool { +func (h *udpCustomServer) Match(b []byte, addr net.Addr) bool { var initial map[string][]byte if h.state != nil { - initial, _ = h.state.get(udpStateKey(nil)) + initial, _ = h.state.get(udpStateKey(addr)) } vars, ok := matchUDPItems(h.client, b, h.read, initial) if ok { h.vars = vars if h.state != nil { - h.state.set(udpStateKey(nil), vars) + h.state.set(udpStateKey(addr), vars) } } return ok @@ -171,24 +196,38 @@ func NewConnServerUDP(c *UDPConfig, raw net.PacketConn) (net.PacketConn, error) return conn, nil } -func (c *udpCustomServerConn) Size() int { - return len(c.header.merged) -} - func (c *udpCustomServerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - if !c.header.Match(p) { - return 0, addr, errors.New("header mismatch") + b := p + if len(b) < finalmask.UDPSize { + buf := buf.New() + buf.Resize(0, finalmask.UDPSize) + b = buf.Bytes() + defer buf.Release() } - return len(p) - c.header.read, addr, nil + for { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + + if !c.header.Match(b[:n], addr) { + errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", n, " > header mismatch") + continue + } + + copy(p, b[c.header.read:n]) + return n - c.header.read, addr, nil + } } func (c *udpCustomServerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - var localAddr net.Addr - if c.PacketConn != nil { - localAddr = c.PacketConn.LocalAddr() - } - ctx := newEvalContextWithAddrs(localAddr, addr) + buf := buf.New() + buf.Resize(0, finalmask.UDPSize) + b := buf.Bytes() + defer buf.Release() + + ctx := newEvalContextWithAddrs(c.PacketConn.LocalAddr(), addr) if vars, ok := c.header.state.get(udpStateKey(addr)); ok { ctx.vars = cloneVars(vars) } else if len(c.header.vars) > 0 { @@ -196,13 +235,21 @@ func (c *udpCustomServerConn) WriteTo(p []byte, addr net.Addr) (n int, err error } evaluated, err := evaluateUDPItemsWithContext(c.header.server, ctx) if err != nil { - return 0, err + errors.LogErrorInner(context.Background(), err, "[mask] drop packet to ", addr, " with size ", len(p)) + return 0, nil } if len(evaluated) != len(c.header.merged) { - return 0, errors.New("header size mismatch") + errors.LogError(context.Background(), "[mask] drop packet to ", addr, " with size ", len(p), " > header size mismatch") + return 0, nil } c.header.state.set(udpStateKey(addr), ctx.vars) - copy(p, evaluated) + copy(b, evaluated) + copy(b[len(evaluated):], p) + _, err = c.PacketConn.WriteTo(b[:len(evaluated)+len(p)], addr) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[mask] drop packet to ", addr, " with size ", len(p)) + return 0, err + } return len(p), nil } diff --git a/transport/internet/finalmask/header/custom/udp_runtime_test.go b/transport/internet/finalmask/header/custom/udp_runtime_test.go index e0a091f4d..3b726f07d 100644 --- a/transport/internet/finalmask/header/custom/udp_runtime_test.go +++ b/transport/internet/finalmask/header/custom/udp_runtime_test.go @@ -1,43 +1,9 @@ package custom import ( - "bytes" - "net" "testing" ) -func TestDSLUDPClientSizeTracksEvaluatedItems(t *testing.T) { - conn, err := NewConnClientUDP(&UDPConfig{ - Client: []*UDPItem{ - { - Rand: 2, - RandMin: 0x2A, - RandMax: 0x2A, - Save: "txid", - }, - { - Var: "txid", - }, - { - Expr: &Expr{ - Op: "concat", - Args: []*ExprArg{ - {Value: &ExprArg_Bytes{Bytes: []byte{0xAB}}}, - {Value: &ExprArg_Bytes{Bytes: []byte{0xCD}}}, - }, - }, - }, - }, - }, nil) - if err != nil { - t.Fatal(err) - } - - if got := conn.(*udpCustomClientConn).Size(); got != 6 { - t.Fatalf("unexpected header size: got=%d want=6", got) - } -} - func TestDSLUDPServerMatchCapturesSavedValues(t *testing.T) { conn, err := NewConnServerUDP(&UDPConfig{ Client: []*UDPItem{ @@ -55,7 +21,7 @@ func TestDSLUDPServerMatchCapturesSavedValues(t *testing.T) { } server := conn.(*udpCustomServerConn) - if !server.header.Match([]byte{0x01, 0x02, 0x01, 0x02}) { + if !server.header.Match([]byte{0x01, 0x02, 0x01, 0x02}, nil) { t.Fatal("expected packet to match") } @@ -81,108 +47,7 @@ func TestDSLUDPServerRejectsMalformedVarReference(t *testing.T) { } server := conn.(*udpCustomServerConn) - if server.header.Match([]byte{0x01, 0x02, 0x03, 0x04}) { + if server.header.Match([]byte{0x01, 0x02, 0x03, 0x04}, nil) { t.Fatal("expected packet mismatch") } } - -func TestDSLUDPClientWriteSupportsExtendedExprOps(t *testing.T) { - conn, err := NewConnClientUDP(&UDPConfig{ - Client: []*UDPItem{ - { - Expr: &Expr{ - Op: "le16", - Args: []*ExprArg{ - { - Value: &ExprArg_Expr{ - Expr: &Expr{ - Op: "add", - Args: []*ExprArg{ - {Value: &ExprArg_U64{U64: 1}}, - {Value: &ExprArg_U64{U64: 2}}, - }, - }, - }, - }, - }, - }, - }, - { - Expr: &Expr{ - Op: "pad", - Args: []*ExprArg{ - {Value: &ExprArg_Bytes{Bytes: []byte{0xAA}}}, - {Value: &ExprArg_U64{U64: 3}}, - {Value: &ExprArg_Bytes{Bytes: []byte{0xBB}}}, - }, - }, - }, - { - Expr: &Expr{ - Op: "truncate", - Args: []*ExprArg{ - {Value: &ExprArg_Bytes{Bytes: []byte{1, 2, 3, 4}}}, - {Value: &ExprArg_U64{U64: 2}}, - }, - }, - }, - { - Expr: &Expr{ - Op: "be16", - Args: []*ExprArg{ - { - Value: &ExprArg_Expr{ - Expr: &Expr{ - Op: "or", - Args: []*ExprArg{ - { - Value: &ExprArg_Expr{ - Expr: &Expr{ - Op: "shl", - Args: []*ExprArg{ - {Value: &ExprArg_U64{U64: 1}}, - {Value: &ExprArg_U64{U64: 8}}, - }, - }, - }, - }, - { - Value: &ExprArg_Expr{ - Expr: &Expr{ - Op: "shr", - Args: []*ExprArg{ - {Value: &ExprArg_U64{U64: 0x80}}, - {Value: &ExprArg_U64{U64: 7}}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, nil) - if err != nil { - t.Fatal(err) - } - - client := conn.(*udpCustomClientConn) - buf := make([]byte, client.Size()) - if _, err := client.WriteTo(buf, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53}); err != nil { - t.Fatal(err) - } - - want := []byte{ - 0x03, 0x00, - 0xAA, 0xBB, 0xBB, - 0x01, 0x02, - 0x01, 0x01, - } - if !bytes.Equal(buf, want) { - t.Fatalf("unexpected encoded header: %x", buf) - } -} diff --git a/transport/internet/finalmask/header/dns/config.pb.go b/transport/internet/finalmask/header/dns/config.pb.go deleted file mode 100644 index 156cfa63d..000000000 --- a/transport/internet/finalmask/header/dns/config.pb.go +++ /dev/null @@ -1,123 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v6.33.5 -// source: transport/internet/finalmask/header/dns/config.proto - -package dns - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Config) Reset() { - *x = Config{} - mi := &file_transport_internet_finalmask_header_dns_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_header_dns_config_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_header_dns_config_proto_rawDescGZIP(), []int{0} -} - -func (x *Config) GetDomain() string { - if x != nil { - return x.Domain - } - return "" -} - -var File_transport_internet_finalmask_header_dns_config_proto protoreflect.FileDescriptor - -const file_transport_internet_finalmask_header_dns_config_proto_rawDesc = "" + - "\n" + - "4transport/internet/finalmask/header/dns/config.proto\x12,xray.transport.internet.finalmask.header.dns\" \n" + - "\x06Config\x12\x16\n" + - "\x06domain\x18\x01 \x01(\tR\x06domainB\xa6\x01\n" + - "0com.xray.transport.internet.finalmask.header.dnsP\x01ZAgithub.com/xtls/xray-core/transport/internet/finalmask/header/dns\xaa\x02,Xray.Transport.Internet.Finalmask.Header.Dnsb\x06proto3" - -var ( - file_transport_internet_finalmask_header_dns_config_proto_rawDescOnce sync.Once - file_transport_internet_finalmask_header_dns_config_proto_rawDescData []byte -) - -func file_transport_internet_finalmask_header_dns_config_proto_rawDescGZIP() []byte { - file_transport_internet_finalmask_header_dns_config_proto_rawDescOnce.Do(func() { - file_transport_internet_finalmask_header_dns_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dns_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dns_config_proto_rawDesc))) - }) - return file_transport_internet_finalmask_header_dns_config_proto_rawDescData -} - -var file_transport_internet_finalmask_header_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_internet_finalmask_header_dns_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.header.dns.Config -} -var file_transport_internet_finalmask_header_dns_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_internet_finalmask_header_dns_config_proto_init() } -func file_transport_internet_finalmask_header_dns_config_proto_init() { - if File_transport_internet_finalmask_header_dns_config_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dns_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dns_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_internet_finalmask_header_dns_config_proto_goTypes, - DependencyIndexes: file_transport_internet_finalmask_header_dns_config_proto_depIdxs, - MessageInfos: file_transport_internet_finalmask_header_dns_config_proto_msgTypes, - }.Build() - File_transport_internet_finalmask_header_dns_config_proto = out.File - file_transport_internet_finalmask_header_dns_config_proto_goTypes = nil - file_transport_internet_finalmask_header_dns_config_proto_depIdxs = nil -} diff --git a/transport/internet/finalmask/header/dns/config.proto b/transport/internet/finalmask/header/dns/config.proto deleted file mode 100644 index 10e1baca2..000000000 --- a/transport/internet/finalmask/header/dns/config.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -package xray.transport.internet.finalmask.header.dns; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Dns"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/dns"; -option java_package = "com.xray.transport.internet.finalmask.header.dns"; -option java_multiple_files = true; - -message Config { - string domain = 1; -} diff --git a/transport/internet/finalmask/header/dtls/config.go b/transport/internet/finalmask/header/dtls/config.go deleted file mode 100644 index decb69e30..000000000 --- a/transport/internet/finalmask/header/dtls/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package dtls - -import ( - "net" -) - -func (c *Config) UDP() { -} - -func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw) -} - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/header/dtls/config.pb.go b/transport/internet/finalmask/header/dtls/config.pb.go deleted file mode 100644 index ef42f5c49..000000000 --- a/transport/internet/finalmask/header/dtls/config.pb.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v6.33.5 -// source: transport/internet/finalmask/header/dtls/config.proto - -package dtls - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Config) Reset() { - *x = Config{} - mi := &file_transport_internet_finalmask_header_dtls_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_header_dtls_config_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_header_dtls_config_proto_rawDescGZIP(), []int{0} -} - -var File_transport_internet_finalmask_header_dtls_config_proto protoreflect.FileDescriptor - -const file_transport_internet_finalmask_header_dtls_config_proto_rawDesc = "" + - "\n" + - "5transport/internet/finalmask/header/dtls/config.proto\x12-xray.transport.internet.finalmask.header.dtls\"\b\n" + - "\x06ConfigB\xa9\x01\n" + - "1com.xray.transport.internet.finalmask.header.dtlsP\x01ZBgithub.com/xtls/xray-core/transport/internet/finalmask/header/dtls\xaa\x02-Xray.Transport.Internet.Finalmask.Header.Dtlsb\x06proto3" - -var ( - file_transport_internet_finalmask_header_dtls_config_proto_rawDescOnce sync.Once - file_transport_internet_finalmask_header_dtls_config_proto_rawDescData []byte -) - -func file_transport_internet_finalmask_header_dtls_config_proto_rawDescGZIP() []byte { - file_transport_internet_finalmask_header_dtls_config_proto_rawDescOnce.Do(func() { - file_transport_internet_finalmask_header_dtls_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc))) - }) - return file_transport_internet_finalmask_header_dtls_config_proto_rawDescData -} - -var file_transport_internet_finalmask_header_dtls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_internet_finalmask_header_dtls_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.header.dtls.Config -} -var file_transport_internet_finalmask_header_dtls_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_internet_finalmask_header_dtls_config_proto_init() } -func file_transport_internet_finalmask_header_dtls_config_proto_init() { - if File_transport_internet_finalmask_header_dtls_config_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_internet_finalmask_header_dtls_config_proto_goTypes, - DependencyIndexes: file_transport_internet_finalmask_header_dtls_config_proto_depIdxs, - MessageInfos: file_transport_internet_finalmask_header_dtls_config_proto_msgTypes, - }.Build() - File_transport_internet_finalmask_header_dtls_config_proto = out.File - file_transport_internet_finalmask_header_dtls_config_proto_goTypes = nil - file_transport_internet_finalmask_header_dtls_config_proto_depIdxs = nil -} diff --git a/transport/internet/finalmask/header/dtls/config.proto b/transport/internet/finalmask/header/dtls/config.proto deleted file mode 100644 index 7f9f79718..000000000 --- a/transport/internet/finalmask/header/dtls/config.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -package xray.transport.internet.finalmask.header.dtls; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Dtls"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/dtls"; -option java_package = "com.xray.transport.internet.finalmask.header.dtls"; -option java_multiple_files = true; - -message Config {} diff --git a/transport/internet/finalmask/header/dtls/conn.go b/transport/internet/finalmask/header/dtls/conn.go deleted file mode 100644 index 3f875c2e1..000000000 --- a/transport/internet/finalmask/header/dtls/conn.go +++ /dev/null @@ -1,74 +0,0 @@ -package dtls - -import ( - "net" - - "github.com/xtls/xray-core/common/dice" -) - -type dtls struct { - epoch uint16 - length uint16 - sequence uint32 -} - -func (*dtls) Size() int { - return 1 + 2 + 2 + 6 + 2 -} - -func (h *dtls) Serialize(b []byte) { - b[0] = 23 - b[1] = 254 - b[2] = 253 - b[3] = byte(h.epoch >> 8) - b[4] = byte(h.epoch) - b[5] = 0 - b[6] = 0 - b[7] = byte(h.sequence >> 24) - b[8] = byte(h.sequence >> 16) - b[9] = byte(h.sequence >> 8) - b[10] = byte(h.sequence) - h.sequence++ - b[11] = byte(h.length >> 8) - b[12] = byte(h.length) - h.length += 17 - if h.length > 100 { - h.length -= 50 - } -} - -type dtlsConn struct { - net.PacketConn - header *dtls -} - -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &dtlsConn{ - PacketConn: raw, - header: &dtls{ - epoch: dice.RollUint16(), - sequence: 0, - length: 17, - }, - } - - return conn, nil -} - -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *dtlsConn) Size() int { - return c.header.Size() -} - -func (c *dtlsConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - return len(p) - c.header.Size(), addr, nil -} - -func (c *dtlsConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.header.Serialize(p) - - return len(p), nil -} diff --git a/transport/internet/finalmask/header/srtp/config.go b/transport/internet/finalmask/header/srtp/config.go deleted file mode 100644 index 006964d96..000000000 --- a/transport/internet/finalmask/header/srtp/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package srtp - -import ( - "net" -) - -func (c *Config) UDP() { -} - -func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw) -} - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/header/srtp/config.pb.go b/transport/internet/finalmask/header/srtp/config.pb.go deleted file mode 100644 index 2d0f32323..000000000 --- a/transport/internet/finalmask/header/srtp/config.pb.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v6.33.5 -// source: transport/internet/finalmask/header/srtp/config.proto - -package srtp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Config) Reset() { - *x = Config{} - mi := &file_transport_internet_finalmask_header_srtp_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_header_srtp_config_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_header_srtp_config_proto_rawDescGZIP(), []int{0} -} - -var File_transport_internet_finalmask_header_srtp_config_proto protoreflect.FileDescriptor - -const file_transport_internet_finalmask_header_srtp_config_proto_rawDesc = "" + - "\n" + - "5transport/internet/finalmask/header/srtp/config.proto\x12-xray.transport.internet.finalmask.header.srtp\"\b\n" + - "\x06ConfigB\xa9\x01\n" + - "1com.xray.transport.internet.finalmask.header.srtpP\x01ZBgithub.com/xtls/xray-core/transport/internet/finalmask/header/srtp\xaa\x02-Xray.Transport.Internet.Finalmask.Header.Srtpb\x06proto3" - -var ( - file_transport_internet_finalmask_header_srtp_config_proto_rawDescOnce sync.Once - file_transport_internet_finalmask_header_srtp_config_proto_rawDescData []byte -) - -func file_transport_internet_finalmask_header_srtp_config_proto_rawDescGZIP() []byte { - file_transport_internet_finalmask_header_srtp_config_proto_rawDescOnce.Do(func() { - file_transport_internet_finalmask_header_srtp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc))) - }) - return file_transport_internet_finalmask_header_srtp_config_proto_rawDescData -} - -var file_transport_internet_finalmask_header_srtp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_internet_finalmask_header_srtp_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.header.srtp.Config -} -var file_transport_internet_finalmask_header_srtp_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_internet_finalmask_header_srtp_config_proto_init() } -func file_transport_internet_finalmask_header_srtp_config_proto_init() { - if File_transport_internet_finalmask_header_srtp_config_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_internet_finalmask_header_srtp_config_proto_goTypes, - DependencyIndexes: file_transport_internet_finalmask_header_srtp_config_proto_depIdxs, - MessageInfos: file_transport_internet_finalmask_header_srtp_config_proto_msgTypes, - }.Build() - File_transport_internet_finalmask_header_srtp_config_proto = out.File - file_transport_internet_finalmask_header_srtp_config_proto_goTypes = nil - file_transport_internet_finalmask_header_srtp_config_proto_depIdxs = nil -} diff --git a/transport/internet/finalmask/header/srtp/config.proto b/transport/internet/finalmask/header/srtp/config.proto deleted file mode 100644 index 6792cb912..000000000 --- a/transport/internet/finalmask/header/srtp/config.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -package xray.transport.internet.finalmask.header.srtp; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Srtp"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/srtp"; -option java_package = "com.xray.transport.internet.finalmask.header.srtp"; -option java_multiple_files = true; - -message Config {} diff --git a/transport/internet/finalmask/header/srtp/conn.go b/transport/internet/finalmask/header/srtp/conn.go deleted file mode 100644 index 94de0f9f0..000000000 --- a/transport/internet/finalmask/header/srtp/conn.go +++ /dev/null @@ -1,58 +0,0 @@ -package srtp - -import ( - "encoding/binary" - "net" - - "github.com/xtls/xray-core/common/dice" -) - -type srtp struct { - header uint16 - number uint16 -} - -func (*srtp) Size() int { - return 4 -} - -func (h *srtp) Serialize(b []byte) { - h.number++ - binary.BigEndian.PutUint16(b, h.header) - binary.BigEndian.PutUint16(b[2:], h.number) -} - -type srtpConn struct { - net.PacketConn - header *srtp -} - -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &srtpConn{ - PacketConn: raw, - header: &srtp{ - header: 0xB5E8, - number: dice.RollUint16(), - }, - } - - return conn, nil -} - -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *srtpConn) Size() int { - return c.header.Size() -} - -func (c *srtpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - return len(p) - c.header.Size(), addr, nil -} - -func (c *srtpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.header.Serialize(p) - - return len(p), nil -} diff --git a/transport/internet/finalmask/header/utp/config.go b/transport/internet/finalmask/header/utp/config.go deleted file mode 100644 index 45d9a20c5..000000000 --- a/transport/internet/finalmask/header/utp/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package utp - -import ( - "net" -) - -func (c *Config) UDP() { -} - -func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw) -} - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/header/utp/config.pb.go b/transport/internet/finalmask/header/utp/config.pb.go deleted file mode 100644 index bd67647f5..000000000 --- a/transport/internet/finalmask/header/utp/config.pb.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v6.33.5 -// source: transport/internet/finalmask/header/utp/config.proto - -package utp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Config) Reset() { - *x = Config{} - mi := &file_transport_internet_finalmask_header_utp_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_header_utp_config_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_header_utp_config_proto_rawDescGZIP(), []int{0} -} - -var File_transport_internet_finalmask_header_utp_config_proto protoreflect.FileDescriptor - -const file_transport_internet_finalmask_header_utp_config_proto_rawDesc = "" + - "\n" + - "4transport/internet/finalmask/header/utp/config.proto\x12,xray.transport.internet.finalmask.header.utp\"\b\n" + - "\x06ConfigB\xa6\x01\n" + - "0com.xray.transport.internet.finalmask.header.utpP\x01ZAgithub.com/xtls/xray-core/transport/internet/finalmask/header/utp\xaa\x02,Xray.Transport.Internet.Finalmask.Header.Utpb\x06proto3" - -var ( - file_transport_internet_finalmask_header_utp_config_proto_rawDescOnce sync.Once - file_transport_internet_finalmask_header_utp_config_proto_rawDescData []byte -) - -func file_transport_internet_finalmask_header_utp_config_proto_rawDescGZIP() []byte { - file_transport_internet_finalmask_header_utp_config_proto_rawDescOnce.Do(func() { - file_transport_internet_finalmask_header_utp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_utp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_utp_config_proto_rawDesc))) - }) - return file_transport_internet_finalmask_header_utp_config_proto_rawDescData -} - -var file_transport_internet_finalmask_header_utp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_internet_finalmask_header_utp_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.header.utp.Config -} -var file_transport_internet_finalmask_header_utp_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_internet_finalmask_header_utp_config_proto_init() } -func file_transport_internet_finalmask_header_utp_config_proto_init() { - if File_transport_internet_finalmask_header_utp_config_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_utp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_utp_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_internet_finalmask_header_utp_config_proto_goTypes, - DependencyIndexes: file_transport_internet_finalmask_header_utp_config_proto_depIdxs, - MessageInfos: file_transport_internet_finalmask_header_utp_config_proto_msgTypes, - }.Build() - File_transport_internet_finalmask_header_utp_config_proto = out.File - file_transport_internet_finalmask_header_utp_config_proto_goTypes = nil - file_transport_internet_finalmask_header_utp_config_proto_depIdxs = nil -} diff --git a/transport/internet/finalmask/header/utp/config.proto b/transport/internet/finalmask/header/utp/config.proto deleted file mode 100644 index ce76ef2ea..000000000 --- a/transport/internet/finalmask/header/utp/config.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -package xray.transport.internet.finalmask.header.utp; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Utp"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/utp"; -option java_package = "com.xray.transport.internet.finalmask.header.utp"; -option java_multiple_files = true; - -message Config {} diff --git a/transport/internet/finalmask/header/utp/conn.go b/transport/internet/finalmask/header/utp/conn.go deleted file mode 100644 index 59005325e..000000000 --- a/transport/internet/finalmask/header/utp/conn.go +++ /dev/null @@ -1,60 +0,0 @@ -package utp - -import ( - "encoding/binary" - "net" - - "github.com/xtls/xray-core/common/dice" -) - -type utp struct { - header byte - extension byte - connectionID uint16 -} - -func (*utp) Size() int { - return 4 -} - -func (h *utp) Serialize(b []byte) { - binary.BigEndian.PutUint16(b, h.connectionID) - b[2] = h.header - b[3] = h.extension -} - -type utpConn struct { - net.PacketConn - header *utp -} - -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &utpConn{ - PacketConn: raw, - header: &utp{ - header: 1, - extension: 0, - connectionID: dice.RollUint16(), - }, - } - - return conn, nil -} - -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *utpConn) Size() int { - return c.header.Size() -} - -func (c *utpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - return len(p) - c.header.Size(), addr, nil -} - -func (c *utpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.header.Serialize(p) - - return len(p), nil -} diff --git a/transport/internet/finalmask/header/wechat/config.go b/transport/internet/finalmask/header/wechat/config.go deleted file mode 100644 index a433318ed..000000000 --- a/transport/internet/finalmask/header/wechat/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package wechat - -import ( - "net" -) - -func (c *Config) UDP() { -} - -func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw) -} - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/header/wechat/config.pb.go b/transport/internet/finalmask/header/wechat/config.pb.go deleted file mode 100644 index da8e428d5..000000000 --- a/transport/internet/finalmask/header/wechat/config.pb.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v6.33.5 -// source: transport/internet/finalmask/header/wechat/config.proto - -package wechat - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Config) Reset() { - *x = Config{} - mi := &file_transport_internet_finalmask_header_wechat_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_header_wechat_config_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_header_wechat_config_proto_rawDescGZIP(), []int{0} -} - -var File_transport_internet_finalmask_header_wechat_config_proto protoreflect.FileDescriptor - -const file_transport_internet_finalmask_header_wechat_config_proto_rawDesc = "" + - "\n" + - "7transport/internet/finalmask/header/wechat/config.proto\x12/xray.transport.internet.finalmask.header.wechat\"\b\n" + - "\x06ConfigB\xaf\x01\n" + - "3com.xray.transport.internet.finalmask.header.wechatP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/header/wechat\xaa\x02/Xray.Transport.Internet.Finalmask.Header.Wechatb\x06proto3" - -var ( - file_transport_internet_finalmask_header_wechat_config_proto_rawDescOnce sync.Once - file_transport_internet_finalmask_header_wechat_config_proto_rawDescData []byte -) - -func file_transport_internet_finalmask_header_wechat_config_proto_rawDescGZIP() []byte { - file_transport_internet_finalmask_header_wechat_config_proto_rawDescOnce.Do(func() { - file_transport_internet_finalmask_header_wechat_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc))) - }) - return file_transport_internet_finalmask_header_wechat_config_proto_rawDescData -} - -var file_transport_internet_finalmask_header_wechat_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_internet_finalmask_header_wechat_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.header.wechat.Config -} -var file_transport_internet_finalmask_header_wechat_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_internet_finalmask_header_wechat_config_proto_init() } -func file_transport_internet_finalmask_header_wechat_config_proto_init() { - if File_transport_internet_finalmask_header_wechat_config_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_internet_finalmask_header_wechat_config_proto_goTypes, - DependencyIndexes: file_transport_internet_finalmask_header_wechat_config_proto_depIdxs, - MessageInfos: file_transport_internet_finalmask_header_wechat_config_proto_msgTypes, - }.Build() - File_transport_internet_finalmask_header_wechat_config_proto = out.File - file_transport_internet_finalmask_header_wechat_config_proto_goTypes = nil - file_transport_internet_finalmask_header_wechat_config_proto_depIdxs = nil -} diff --git a/transport/internet/finalmask/header/wechat/config.proto b/transport/internet/finalmask/header/wechat/config.proto deleted file mode 100644 index 3127773a0..000000000 --- a/transport/internet/finalmask/header/wechat/config.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -package xray.transport.internet.finalmask.header.wechat; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Wechat"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/wechat"; -option java_package = "com.xray.transport.internet.finalmask.header.wechat"; -option java_multiple_files = true; - -message Config {} diff --git a/transport/internet/finalmask/header/wechat/conn.go b/transport/internet/finalmask/header/wechat/conn.go deleted file mode 100644 index cb1fff62a..000000000 --- a/transport/internet/finalmask/header/wechat/conn.go +++ /dev/null @@ -1,64 +0,0 @@ -package wechat - -import ( - "encoding/binary" - "net" - - "github.com/xtls/xray-core/common/dice" -) - -type wechat struct { - sn uint32 -} - -func (*wechat) Size() int { - return 13 -} - -func (h *wechat) Serialize(b []byte) { - h.sn++ - b[0] = 0xa1 - b[1] = 0x08 - binary.BigEndian.PutUint32(b[2:], h.sn) - b[6] = 0x00 - b[7] = 0x10 - b[8] = 0x11 - b[9] = 0x18 - b[10] = 0x30 - b[11] = 0x22 - b[12] = 0x30 -} - -type wechatConn struct { - net.PacketConn - header *wechat -} - -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &wechatConn{ - PacketConn: raw, - header: &wechat{ - sn: uint32(dice.RollUint16()), - }, - } - - return conn, nil -} - -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *wechatConn) Size() int { - return c.header.Size() -} - -func (c *wechatConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - return len(p) - c.header.Size(), addr, nil -} - -func (c *wechatConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.header.Serialize(p) - - return len(p), nil -} diff --git a/transport/internet/finalmask/header/wireguard/config.go b/transport/internet/finalmask/header/wireguard/config.go deleted file mode 100644 index dd3609d8b..000000000 --- a/transport/internet/finalmask/header/wireguard/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package wireguard - -import ( - "net" -) - -func (c *Config) UDP() { -} - -func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw) -} - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/header/wireguard/config.pb.go b/transport/internet/finalmask/header/wireguard/config.pb.go deleted file mode 100644 index fb49f3e64..000000000 --- a/transport/internet/finalmask/header/wireguard/config.pb.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v6.33.5 -// source: transport/internet/finalmask/header/wireguard/config.proto - -package wireguard - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Config) Reset() { - *x = Config{} - mi := &file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_header_wireguard_config_proto_rawDescGZIP(), []int{0} -} - -var File_transport_internet_finalmask_header_wireguard_config_proto protoreflect.FileDescriptor - -const file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc = "" + - "\n" + - ":transport/internet/finalmask/header/wireguard/config.proto\x122xray.transport.internet.finalmask.header.wireguard\"\b\n" + - "\x06ConfigB\xb8\x01\n" + - "6com.xray.transport.internet.finalmask.header.wireguardP\x01ZGgithub.com/xtls/xray-core/transport/internet/finalmask/header/wireguard\xaa\x022Xray.Transport.Internet.Finalmask.Header.Wireguardb\x06proto3" - -var ( - file_transport_internet_finalmask_header_wireguard_config_proto_rawDescOnce sync.Once - file_transport_internet_finalmask_header_wireguard_config_proto_rawDescData []byte -) - -func file_transport_internet_finalmask_header_wireguard_config_proto_rawDescGZIP() []byte { - file_transport_internet_finalmask_header_wireguard_config_proto_rawDescOnce.Do(func() { - file_transport_internet_finalmask_header_wireguard_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc))) - }) - return file_transport_internet_finalmask_header_wireguard_config_proto_rawDescData -} - -var file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_internet_finalmask_header_wireguard_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.transport.internet.finalmask.header.wireguard.Config -} -var file_transport_internet_finalmask_header_wireguard_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_internet_finalmask_header_wireguard_config_proto_init() } -func file_transport_internet_finalmask_header_wireguard_config_proto_init() { - if File_transport_internet_finalmask_header_wireguard_config_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_internet_finalmask_header_wireguard_config_proto_goTypes, - DependencyIndexes: file_transport_internet_finalmask_header_wireguard_config_proto_depIdxs, - MessageInfos: file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes, - }.Build() - File_transport_internet_finalmask_header_wireguard_config_proto = out.File - file_transport_internet_finalmask_header_wireguard_config_proto_goTypes = nil - file_transport_internet_finalmask_header_wireguard_config_proto_depIdxs = nil -} diff --git a/transport/internet/finalmask/header/wireguard/config.proto b/transport/internet/finalmask/header/wireguard/config.proto deleted file mode 100644 index 476cbfba1..000000000 --- a/transport/internet/finalmask/header/wireguard/config.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -package xray.transport.internet.finalmask.header.wireguard; -option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Wireguard"; -option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/wireguard"; -option java_package = "com.xray.transport.internet.finalmask.header.wireguard"; -option java_multiple_files = true; - -message Config {} diff --git a/transport/internet/finalmask/header/wireguard/conn.go b/transport/internet/finalmask/header/wireguard/conn.go deleted file mode 100644 index 4a38969f8..000000000 --- a/transport/internet/finalmask/header/wireguard/conn.go +++ /dev/null @@ -1,50 +0,0 @@ -package wireguard - -import ( - "net" -) - -type wireguare struct{} - -func (*wireguare) Size() int { - return 4 -} - -func (h *wireguare) Serialize(b []byte) { - b[0] = 0x04 - b[1] = 0x00 - b[2] = 0x00 - b[3] = 0x00 -} - -type wireguareConn struct { - net.PacketConn - header *wireguare -} - -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - conn := &wireguareConn{ - PacketConn: raw, - header: &wireguare{}, - } - - return conn, nil -} - -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *wireguareConn) Size() int { - return c.header.Size() -} - -func (c *wireguareConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - return len(p) - c.header.Size(), addr, nil -} - -func (c *wireguareConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.header.Serialize(p) - - return len(p), nil -} diff --git a/transport/internet/finalmask/header/dns/config.go b/transport/internet/finalmask/mkcp/header/config.go similarity index 80% rename from transport/internet/finalmask/header/dns/config.go rename to transport/internet/finalmask/mkcp/header/config.go index 48ede1267..17774871e 100644 --- a/transport/internet/finalmask/header/dns/config.go +++ b/transport/internet/finalmask/mkcp/header/config.go @@ -1,11 +1,12 @@ -package dns +package header import ( "net" ) -func (c *Config) UDP() { -} +func (c *Config) UDP() {} + +func (c *Config) HeaderConn() {} func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnClient(c, raw) @@ -14,6 +15,3 @@ func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { return NewConnServer(c, raw) } - -func (c *Config) HeaderConn() { -} diff --git a/transport/internet/finalmask/mkcp/header/config.pb.go b/transport/internet/finalmask/mkcp/header/config.pb.go new file mode 100644 index 000000000..7fc4e6f46 --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/config.pb.go @@ -0,0 +1,132 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/mkcp/header/config.proto + +package header + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ID int32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_mkcp_header_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_mkcp_header_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_mkcp_header_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetID() int32 { + if x != nil { + return x.ID + } + return 0 +} + +func (x *Config) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +var File_transport_internet_finalmask_mkcp_header_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_mkcp_header_config_proto_rawDesc = "" + + "\n" + + "5transport/internet/finalmask/mkcp/header/config.proto\x12-xray.transport.internet.finalmask.mkcp.header\"0\n" + + "\x06Config\x12\x0e\n" + + "\x02ID\x18\x01 \x01(\x05R\x02ID\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domainB\xa9\x01\n" + + "1com.xray.transport.internet.finalmask.mkcp.headerP\x01ZBgithub.com/xtls/xray-core/transport/internet/finalmask/mkcp/header\xaa\x02-Xray.Transport.Internet.Finalmask.Mkcp.Headerb\x06proto3" + +var ( + file_transport_internet_finalmask_mkcp_header_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_mkcp_header_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_mkcp_header_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_mkcp_header_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_mkcp_header_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_mkcp_header_config_proto_rawDesc), len(file_transport_internet_finalmask_mkcp_header_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_mkcp_header_config_proto_rawDescData +} + +var file_transport_internet_finalmask_mkcp_header_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_mkcp_header_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.mkcp.header.Config +} +var file_transport_internet_finalmask_mkcp_header_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_mkcp_header_config_proto_init() } +func file_transport_internet_finalmask_mkcp_header_config_proto_init() { + if File_transport_internet_finalmask_mkcp_header_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_mkcp_header_config_proto_rawDesc), len(file_transport_internet_finalmask_mkcp_header_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_mkcp_header_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_mkcp_header_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_mkcp_header_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_mkcp_header_config_proto = out.File + file_transport_internet_finalmask_mkcp_header_config_proto_goTypes = nil + file_transport_internet_finalmask_mkcp_header_config_proto_depIdxs = nil +} diff --git a/transport/internet/finalmask/mkcp/header/config.proto b/transport/internet/finalmask/mkcp/header/config.proto new file mode 100644 index 000000000..d5907cfdb --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/config.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.mkcp.header; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Mkcp.Header"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header"; +option java_package = "com.xray.transport.internet.finalmask.mkcp.header"; +option java_multiple_files = true; + +message Config { + int32 ID = 1; + string domain = 2; +} diff --git a/transport/internet/finalmask/mkcp/header/conn.go b/transport/internet/finalmask/mkcp/header/conn.go new file mode 100644 index 000000000..b17df3218 --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/conn.go @@ -0,0 +1,57 @@ +package header + +import ( + "net" + + "github.com/xtls/xray-core/common/errors" +) + +type headerConn struct { + net.PacketConn + header Header +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + var header Header + switch HeaderID(c.ID) { + case DNS: + var err error + header, err = NewHeaderDNS(c.Domain) + if err != nil { + return nil, err + } + case DTLS: + header = &dtls{} + case SRTP: + header = &srtp{} + case UTP: + header = &utp{} + case WECHAT: + header = &wechat{} + case WIREGUARD: + header = &wireguard{} + default: + return nil, errors.New("invalid id ", c.ID) + } + return &headerConn{ + PacketConn: raw, + header: header, + }, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *headerConn) Size() int { + return c.header.Size() +} + +func (c *headerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), nil, nil +} + +func (c *headerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + return len(p), nil +} diff --git a/transport/internet/finalmask/mkcp/header/header.go b/transport/internet/finalmask/mkcp/header/header.go new file mode 100644 index 000000000..0e7f25b71 --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/header.go @@ -0,0 +1,17 @@ +package header + +type Header interface { + Size() int + Serialize(b []byte) +} + +type HeaderID int + +const ( + DNS HeaderID = iota + DTLS + SRTP + UTP + WECHAT + WIREGUARD +) diff --git a/transport/internet/finalmask/header/dns/conn.go b/transport/internet/finalmask/mkcp/header/header_dns.go similarity index 75% rename from transport/internet/finalmask/header/dns/conn.go rename to transport/internet/finalmask/mkcp/header/header_dns.go index 263ca7efc..bcbb0e004 100644 --- a/transport/internet/finalmask/header/dns/conn.go +++ b/transport/internet/finalmask/mkcp/header/header_dns.go @@ -1,13 +1,44 @@ -package dns +package header import ( "encoding/binary" - "net" + "errors" "github.com/xtls/xray-core/common/dice" - "github.com/xtls/xray-core/common/errors" ) +type dns struct { + header []byte +} + +func (h *dns) Size() int { + return len(h.header) +} + +func (h *dns) Serialize(b []byte) { + copy(b, h.header) + binary.BigEndian.PutUint16(b[0:], dice.RollUint16()) +} + +func NewHeaderDNS(domain string) (*dns, error) { + var header []byte + header = binary.BigEndian.AppendUint16(header, 0x0000) // Transaction ID + header = binary.BigEndian.AppendUint16(header, 0x0100) // Flags: Standard query + header = binary.BigEndian.AppendUint16(header, 0x0001) // Questions + header = binary.BigEndian.AppendUint16(header, 0x0000) // Answer RRs + header = binary.BigEndian.AppendUint16(header, 0x0000) // Authority RRs + header = binary.BigEndian.AppendUint16(header, 0x0000) // Additional RRs + buf := make([]byte, 0x100) + off1, err := packDomainName(domain+".", buf) + if err != nil { + return nil, err + } + header = append(header, buf[:off1]...) + header = binary.BigEndian.AppendUint16(header, 0x0001) // Type: A + header = binary.BigEndian.AppendUint16(header, 0x0001) // Class: IN + return &dns{header: header}, nil +} + func packDomainName(s string, msg []byte) (off1 int, err error) { off := 0 ls := len(s) @@ -73,66 +104,3 @@ func packDomainName(s string, msg []byte) (off1 int, err error) { return off + 1, nil } - -type dns struct { - header []byte -} - -func (h *dns) Size() int { - return len(h.header) -} - -func (h *dns) Serialize(b []byte) { - copy(b, h.header) - binary.BigEndian.PutUint16(b[0:], dice.RollUint16()) -} - -type dnsConn struct { - net.PacketConn - header *dns -} - -func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { - var header []byte - header = binary.BigEndian.AppendUint16(header, 0x0000) // Transaction ID - header = binary.BigEndian.AppendUint16(header, 0x0100) // Flags: Standard query - header = binary.BigEndian.AppendUint16(header, 0x0001) // Questions - header = binary.BigEndian.AppendUint16(header, 0x0000) // Answer RRs - header = binary.BigEndian.AppendUint16(header, 0x0000) // Authority RRs - header = binary.BigEndian.AppendUint16(header, 0x0000) // Additional RRs - buf := make([]byte, 0x100) - off1, err := packDomainName(c.Domain+".", buf) - if err != nil { - return nil, err - } - header = append(header, buf[:off1]...) - header = binary.BigEndian.AppendUint16(header, 0x0001) // Type: A - header = binary.BigEndian.AppendUint16(header, 0x0001) // Class: IN - - conn := &dnsConn{ - PacketConn: raw, - header: &dns{ - header: header, - }, - } - - return conn, nil -} - -func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - return NewConnClient(c, raw) -} - -func (c *dnsConn) Size() int { - return c.header.Size() -} - -func (c *dnsConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - return len(p) - c.header.Size(), addr, nil -} - -func (c *dnsConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.header.Serialize(p) - - return len(p), nil -} diff --git a/transport/internet/finalmask/mkcp/header/header_dtls.go b/transport/internet/finalmask/mkcp/header/header_dtls.go new file mode 100644 index 000000000..286c6871e --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/header_dtls.go @@ -0,0 +1,32 @@ +package header + +type dtls struct { + epoch uint16 + length uint16 + sequence uint32 +} + +func (*dtls) Size() int { + return 1 + 2 + 2 + 6 + 2 +} + +func (h *dtls) Serialize(b []byte) { + b[0] = 23 + b[1] = 254 + b[2] = 253 + b[3] = byte(h.epoch >> 8) + b[4] = byte(h.epoch) + b[5] = 0 + b[6] = 0 + b[7] = byte(h.sequence >> 24) + b[8] = byte(h.sequence >> 16) + b[9] = byte(h.sequence >> 8) + b[10] = byte(h.sequence) + h.sequence++ + b[11] = byte(h.length >> 8) + b[12] = byte(h.length) + h.length += 17 + if h.length > 100 { + h.length -= 50 + } +} diff --git a/transport/internet/finalmask/mkcp/header/header_srtp.go b/transport/internet/finalmask/mkcp/header/header_srtp.go new file mode 100644 index 000000000..f90d9329b --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/header_srtp.go @@ -0,0 +1,18 @@ +package header + +import "encoding/binary" + +type srtp struct { + header uint16 + number uint16 +} + +func (*srtp) Size() int { + return 4 +} + +func (h *srtp) Serialize(b []byte) { + h.number++ + binary.BigEndian.PutUint16(b, h.header) + binary.BigEndian.PutUint16(b[2:], h.number) +} diff --git a/transport/internet/finalmask/mkcp/header/header_utp.go b/transport/internet/finalmask/mkcp/header/header_utp.go new file mode 100644 index 000000000..209a6e46f --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/header_utp.go @@ -0,0 +1,19 @@ +package header + +import "encoding/binary" + +type utp struct { + header byte + extension byte + connectionID uint16 +} + +func (*utp) Size() int { + return 4 +} + +func (h *utp) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, h.connectionID) + b[2] = h.header + b[3] = h.extension +} diff --git a/transport/internet/finalmask/mkcp/header/header_wechat.go b/transport/internet/finalmask/mkcp/header/header_wechat.go new file mode 100644 index 000000000..a4520b595 --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/header_wechat.go @@ -0,0 +1,25 @@ +package header + +import "encoding/binary" + +type wechat struct { + sn uint32 +} + +func (*wechat) Size() int { + return 13 +} + +func (h *wechat) Serialize(b []byte) { + h.sn++ + b[0] = 0xa1 + b[1] = 0x08 + binary.BigEndian.PutUint32(b[2:], h.sn) + b[6] = 0x00 + b[7] = 0x10 + b[8] = 0x11 + b[9] = 0x18 + b[10] = 0x30 + b[11] = 0x22 + b[12] = 0x30 +} diff --git a/transport/internet/finalmask/mkcp/header/header_wireguard.go b/transport/internet/finalmask/mkcp/header/header_wireguard.go new file mode 100644 index 000000000..8c9836777 --- /dev/null +++ b/transport/internet/finalmask/mkcp/header/header_wireguard.go @@ -0,0 +1,14 @@ +package header + +type wireguard struct{} + +func (*wireguard) Size() int { + return 4 +} + +func (h *wireguard) Serialize(b []byte) { + b[0] = 0x04 + b[1] = 0x00 + b[2] = 0x00 + b[3] = 0x00 +} diff --git a/transport/internet/finalmask/udp_test.go b/transport/internet/finalmask/udp_test.go index 8759f9fbc..c829d1add 100644 --- a/transport/internet/finalmask/udp_test.go +++ b/transport/internet/finalmask/udp_test.go @@ -15,12 +15,8 @@ import ( "github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/transport/internet/finalmask" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" - "github.com/xtls/xray-core/transport/internet/finalmask/header/dns" - "github.com/xtls/xray-core/transport/internet/finalmask/header/srtp" - "github.com/xtls/xray-core/transport/internet/finalmask/header/utp" - "github.com/xtls/xray-core/transport/internet/finalmask/header/wechat" - "github.com/xtls/xray-core/transport/internet/finalmask/header/wireguard" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" "github.com/xtls/xray-core/transport/internet/finalmask/salamander" "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" @@ -276,27 +272,32 @@ func TestPacketConnReadWrite(t *testing.T) { }, { name: "dns", - mask: &dns.Config{Domain: "www.baidu.com"}, + mask: &header.Config{ID: 0, Domain: "www.baidu.com"}, + layers: 2, + }, + { + name: "dtls", + mask: &header.Config{ID: 1}, layers: 2, }, { name: "srtp", - mask: &srtp.Config{}, + mask: &header.Config{ID: 2}, layers: 2, }, { name: "utp", - mask: &utp.Config{}, + mask: &header.Config{ID: 3}, layers: 2, }, { name: "wechat", - mask: &wechat.Config{}, + mask: &header.Config{ID: 4}, layers: 2, }, { name: "wireguard", - mask: &wireguard.Config{}, + mask: &header.Config{ID: 5}, layers: 2, }, { From ca4b156b57b11d77545039b4000ed3bbc2fdabdd Mon Sep 17 00:00:00 2001 From: Jesus <75259437+Jolymmiles@users.noreply.github.com> Date: Fri, 29 May 2026 15:59:24 +0400 Subject: [PATCH 24/78] Burst observatory: Fix time compare, cancel pending ping on instance close or new schedule started (#6106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/XTLS/Xray-core/pull/6106#issuecomment-4448117773 --------- Co-authored-by: 风扇滑翔翼 --- app/observatory/burst/healthping.go | 72 ++++++++++++---------- app/observatory/burst/healthping_result.go | 4 +- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/app/observatory/burst/healthping.go b/app/observatory/burst/healthping.go index cb1c3402d..d57f51e88 100644 --- a/app/observatory/burst/healthping.go +++ b/app/observatory/burst/healthping.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "sync" + "sync/atomic" "time" "github.com/xtls/xray-core/common/dice" @@ -24,11 +25,12 @@ type HealthPingSettings struct { // HealthPing is the health checker for balancers type HealthPing struct { - ctx context.Context - dispatcher routing.Dispatcher - access sync.Mutex - ticker *time.Ticker - tickerClose chan struct{} + ctx context.Context + cancelCtx context.CancelFunc + cancelPending atomic.Pointer[context.CancelFunc] + dispatcher routing.Dispatcher + access sync.Mutex + ticker *time.Ticker Settings *HealthPingSettings Results map[string]*HealthPingRTTS @@ -62,10 +64,10 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H settings.Destination = "https://connectivitycheck.gstatic.com/generate_204" } if settings.Interval == 0 { - settings.Interval = time.Duration(1) * time.Minute - } else if settings.Interval < 10 { + settings.Interval = 1 * time.Minute + } else if settings.Interval < 10*time.Second { errors.LogWarning(ctx, "health check interval is too small, 10s is applied") - settings.Interval = time.Duration(10) * time.Second + settings.Interval = 10 * time.Second } if settings.SamplingCount <= 0 { settings.SamplingCount = 10 @@ -73,10 +75,12 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H if settings.Timeout <= 0 { // results are saved after all health pings finish, // a larger timeout could possibly makes checks run longer - settings.Timeout = time.Duration(5) * time.Second + settings.Timeout = 5 * time.Second } + ctx, cancel := context.WithCancel(ctx) return &HealthPing{ ctx: ctx, + cancelCtx: cancel, dispatcher: dispatcher, Settings: settings, Results: nil, @@ -90,17 +94,7 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) { } interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount) ticker := time.NewTicker(interval) - tickerClose := make(chan struct{}) h.ticker = ticker - h.tickerClose = tickerClose - go func() { - tags, err := selector() - if err != nil { - errors.LogWarning(h.ctx, "error select outbounds for initial health check: ", err) - return - } - h.Check(tags) - }() go func() { for { @@ -110,13 +104,20 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) { errors.LogWarning(h.ctx, "error select outbounds for scheduled health check: ", err) return } - h.doCheck(tags, interval, h.Settings.SamplingCount) + subCtx, cancel := context.WithCancel(h.ctx) + old := h.cancelPending.Swap(&cancel) + if old != nil { + errors.LogDebug(h.ctx, "scheduled health check not finished before next round, canceling previous one") + (*old)() + } + h.doCheck(subCtx, tags, interval, h.Settings.SamplingCount) + h.cancelPending.CompareAndSwap(&cancel, nil) h.Cleanup(tags) }() select { case <-ticker.C: continue - case <-tickerClose: + case <-h.ctx.Done(): return } } @@ -130,8 +131,7 @@ func (h *HealthPing) StopScheduler() { } h.ticker.Stop() h.ticker = nil - close(h.tickerClose) - h.tickerClose = nil + h.cancelCtx() } // Check implements the HealthChecker @@ -140,7 +140,7 @@ func (h *HealthPing) Check(tags []string) error { return nil } errors.LogInfo(h.ctx, "perform one-time health check for tags ", tags) - h.doCheck(tags, 0, 1) + h.doCheck(h.ctx, tags, 0, 1) return nil } @@ -151,13 +151,14 @@ type rtt struct { // doCheck performs the 'rounds' amount checks in given 'duration'. You should make // sure all tags are valid for current balancer -func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) { +// cancel ctx will stop all pending checks +func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.Duration, rounds int) { count := len(tags) * rounds if count == 0 { return } ch := make(chan *rtt, count) - + timers := make([]*time.Timer, 0, count) for _, tag := range tags { handler := tag client := newPingClient( @@ -172,7 +173,7 @@ func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) if duration > 0 { delay = time.Duration(dice.RollInt63n(int64(duration))) } - time.AfterFunc(delay, func() { + timers = append(timers, time.AfterFunc(delay, func() { errors.LogDebug(h.ctx, "checking ", handler) delay, err := client.MeasureDelay(h.Settings.HttpMethod) if err == nil { @@ -200,14 +201,21 @@ func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) handler: handler, value: rttFailed, } - }) + })) } } for i := 0; i < count; i++ { - rtt := <-ch - if rtt.value > 0 { - // should not put results when network is down - h.PutResult(rtt.handler, rtt.value) + select { + case rtt := <-ch: + if rtt.value > 0 { + // should not put results when network is down + h.PutResult(rtt.handler, rtt.value) + } + case <-ctx.Done(): + for _, timer := range timers { + timer.Stop() + } + return } } } diff --git a/app/observatory/burst/healthping_result.go b/app/observatory/burst/healthping_result.go index f48d37b60..4759c5e32 100644 --- a/app/observatory/burst/healthping_result.go +++ b/app/observatory/burst/healthping_result.go @@ -59,7 +59,7 @@ func (h *HealthPingRTTS) Put(d time.Duration) { if h.rtts == nil { h.rtts = make([]*pingRTT, h.cap) for i := 0; i < h.cap; i++ { - h.rtts[i] = &pingRTT{} + h.rtts[i] = &pingRTT{value: rttUntested} } h.idx = -1 } @@ -88,7 +88,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats { validRTTs := make([]time.Duration, 0) for _, rtt := range h.rtts { switch { - case rtt.value == 0 || time.Since(rtt.time) > h.validity: + case rtt.value == rttUntested || time.Since(rtt.time) > h.validity: continue case rtt.value == rttFailed: stats.Fail++ From d43a808ea50265383646d17321426b193594fb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Fri, 29 May 2026 23:04:59 +0800 Subject: [PATCH 25/78] GitHub Action CI: Add Go source file format check (#6090) https://github.com/XTLS/Xray-core/pull/6057#issuecomment-4364819830 And https://github.com/XTLS/Xray-core/pull/6149#issuecomment-4546876261 --- .github/workflows/test.yml | 24 +- app/commander/commander.go | 11 +- app/dns/cache_controller.go | 7 +- app/dns/dns.go | 4 +- app/dns/dnscommon_test.go | 6 +- app/dns/fakedns/fakedns_test.go | 19 +- app/dns/nameserver.go | 2 +- app/dns/nameserver_local.go | 1 - app/metrics/metrics.go | 1 - app/observatory/burst/burstobserver.go | 2 - app/observatory/observer.go | 2 +- app/proxyman/command/command.go | 2 +- app/proxyman/inbound/inbound.go | 6 +- app/proxyman/outbound/handler.go | 3 +- app/proxyman/outbound/handler_test.go | 9 +- app/proxyman/outbound/outbound.go | 1 - app/router/command/command.go | 1 - app/router/router.go | 2 - app/router/strategy_leastload_test.go | 5 + app/router/webhook.go | 2 - app/stats/stats_test.go | 2 +- app/version/version.go | 5 +- common/buf/multi_buffer.go | 4 +- common/buf/reader_test.go | 12 +- common/buf/readv_posix.go | 2 +- common/geodata/ip_matcher_test.go | 6 +- common/log/logger.go | 1 - common/platform/platform_test.go | 5 +- common/protocol/id.go | 2 +- common/protocol/quic/sniff_test.go | 2 +- common/protocol/server_spec.go | 4 +- common/reflect/marshal.go | 1 - common/reflect/marshal_test.go | 4 +- common/units/bytesize_test.go | 21 +- common/utils/browser.go | 120 ++++--- common/utils/typed_sync_map.go | 2 +- common/utils/unixsocket.go | 39 ++- features/routing/session/context.go | 1 - infra/conf/blackhole.go | 3 +- infra/conf/router_strategy.go | 20 +- infra/conf/serial/loader.go | 19 +- infra/conf/transport_authenticators.go | 16 +- infra/conf/transport_internet.go | 14 +- infra/conf/version.go | 3 +- infra/conf/xray.go | 6 +- infra/vformat/main.go | 81 +++-- main/commands/all/api/balancer_info.go | 1 - main/commands/all/api/inbound_user_add.go | 3 +- main/commands/all/api/inbound_user_remove.go | 3 +- main/commands/all/api/rules_add.go | 5 +- main/commands/all/api/rules_remove.go | 1 - main/commands/all/api/source_ip_block.go | 1 - main/commands/all/convert/json.go | 1 - main/commands/all/convert/protobuf.go | 3 +- main/commands/all/tls/ech.go | 6 +- main/commands/all/x25519.go | 6 +- main/confloader/confloader.go | 4 +- main/confloader/external/external.go | 22 +- proxy/dokodemo/dokodemo.go | 8 +- proxy/freedom/freedom.go | 16 +- proxy/http/server.go | 8 +- proxy/hysteria/account/config.go | 2 +- proxy/proxy.go | 2 +- proxy/socks/server.go | 8 +- proxy/trojan/config.go | 1 + proxy/trojan/server.go | 1 - proxy/trojan/validator.go | 2 +- proxy/tun/stack_gvisor.go | 2 +- proxy/tun/stack_gvisor_endpoint.go | 2 - proxy/tun/tun_darwin.go | 6 +- proxy/tun/tun_default.go | 3 +- proxy/tun/tun_freebsd.go | 6 +- proxy/vless/encoding/encoding_test.go | 4 +- proxy/vless/encryption/client.go | 2 +- proxy/vless/encryption/server.go | 2 +- proxy/vless/inbound/inbound.go | 8 +- proxy/vless/validator.go | 2 +- proxy/vmess/account.go | 5 +- proxy/vmess/inbound/inbound.go | 3 +- proxy/vmess/outbound/command.go | 1 - proxy/vmess/outbound/outbound.go | 4 +- proxy/wireguard/bind.go | 1 - proxy/wireguard/server.go | 1 - proxy/wireguard/server_test.go | 3 +- proxy/wireguard/tun.go | 2 +- testing/scenarios/command_test.go | 9 +- transport/internet/browser_dialer/dialer.go | 8 +- transport/internet/config.go | 4 +- transport/internet/dialer.go | 1 - transport/internet/finalmask/realm/client.go | 6 +- transport/internet/finalmask/realm/server.go | 14 +- transport/internet/finalmask/realm/stun.go | 8 +- .../internet/finalmask/sudoku/sudoku_test.go | 3 +- transport/internet/finalmask/tcp_test.go | 2 +- transport/internet/finalmask/udp_test.go | 2 + transport/internet/finalmask/xdns/client.go | 2 +- transport/internet/finalmask/xdns/dns_test.go | 304 ++++++++++++++++-- transport/internet/grpc/dial.go | 2 +- transport/internet/happy_eyeballs.go | 11 +- transport/internet/hysteria/dialer.go | 6 +- transport/internet/kcp/connection.go | 6 +- transport/internet/kcp/connection_test.go | 8 +- transport/internet/reality/config.go | 2 +- transport/internet/sockopt_darwin.go | 6 +- transport/internet/sockopt_linux.go | 4 +- transport/internet/sockopt_windows.go | 4 +- transport/internet/splithttp/hub.go | 3 +- transport/internet/system_dialer.go | 8 +- transport/internet/tls/config.go | 2 +- transport/internet/tls/ech.go | 4 +- transport/internet/tls/ech_test.go | 1 - transport/internet/tls/tls.go | 6 +- transport/pipe/pipe.go | 2 +- transport/pipe/pipe_test.go | 10 +- 114 files changed, 736 insertions(+), 378 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c07983a46..ccc2fab60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test +name: Tests and Checkings on: push: @@ -8,6 +8,7 @@ on: jobs: check-assets: runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Restore Geodat Cache uses: actions/cache/restore@v5 @@ -36,6 +37,7 @@ jobs: check-proto: runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Checkout codebase uses: actions/checkout@v6 @@ -50,8 +52,28 @@ jobs: fi done + check-format: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + permissions: + contents: read + steps: + - name: Checkout codebase + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + cache: false + - name: Check Format + run: | + go install -v mvdan.cc/gofumpt@latest + go run ./infra/vformat/main.go -mode check -pwd ./ + test: needs: check-assets + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name permissions: contents: read runs-on: ${{ matrix.os }} diff --git a/app/commander/commander.go b/app/commander/commander.go index 3b541f92e..ac7bd835b 100644 --- a/app/commander/commander.go +++ b/app/commander/commander.go @@ -3,15 +3,15 @@ package commander import ( "context" "net" + "strings" "sync" - "strings" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/signal/done" core "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/outbound" - "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet" "google.golang.org/grpc" ) @@ -69,16 +69,15 @@ func (c *Commander) Start() error { } c.Unlock() - var listen = func(listener net.Listener) { + listen := func(listener net.Listener) { if err := c.server.Serve(listener); err != nil { errors.LogErrorInner(context.Background(), err, "failed to start grpc server") } } - if len(c.listen) > 0 { var addr net.Addr - + if strings.HasPrefix(c.listen, "/") || strings.HasPrefix(c.listen, "@") { addr = &net.UnixAddr{Name: c.listen, Net: "unix"} } else { @@ -89,7 +88,7 @@ func (c *Commander) Start() error { } addr = tcpAddr } - l, err := internet.ListenSystem(context.Background(), addr, nil) + l, err := internet.ListenSystem(context.Background(), addr, nil) if err != nil { errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen) return err diff --git a/app/dns/cache_controller.go b/app/dns/cache_controller.go index a303b264a..6e2fd6c97 100644 --- a/app/dns/cache_controller.go +++ b/app/dns/cache_controller.go @@ -139,7 +139,8 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) { if lenAfter == 0 { if c.highWatermark >= minSizeForEmptyRebuild { - errors.LogDebug(context.Background(), c.name, + errors.LogDebug( + context.Background(), c.name, " rebuilding empty cache map to reclaim memory.", " size_before_cleanup=", lenBefore, " peak_size_before_rebuild=", c.highWatermark, @@ -153,7 +154,8 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) { if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold && float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold { - errors.LogDebug(context.Background(), c.name, + errors.LogDebug( + context.Background(), c.name, " shrinking cache map to reclaim memory.", " new_size=", lenAfter, " peak_size_before_shrink=", c.highWatermark, @@ -165,7 +167,6 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) { c.highWatermark = lenAfter go c.migrate() } - } type migrationEntry struct { diff --git a/app/dns/dns.go b/app/dns/dns.go index abf6894b8..b750fce26 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -86,7 +86,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) { return nil, errors.New("failed to create hosts").Base(err) } - var defaultTag = config.Tag + defaultTag := config.Tag if len(config.Tag) == 0 { defaultTag = generateRandomTag() } @@ -139,7 +139,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) { serveExpiredTTL = *ns.ServeExpiredTTL } - var tag = defaultTag + tag := defaultTag if len(ns.Tag) > 0 { tag = ns.Tag } diff --git a/app/dns/dnscommon_test.go b/app/dns/dnscommon_test.go index fc73d20d8..64e987600 100644 --- a/app/dns/dnscommon_test.go +++ b/app/dns/dnscommon_test.go @@ -25,7 +25,8 @@ func Test_parseResponse(t *testing.T) { ans = new(dns.Msg) ans.Id = 1 - ans.Answer = append(ans.Answer, + ans.Answer = append( + ans.Answer, common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")), common.Must2(dns.NewRR("google.com. IN A 8.8.8.8")), @@ -35,7 +36,8 @@ func Test_parseResponse(t *testing.T) { ans = new(dns.Msg) ans.Id = 2 - ans.Answer = append(ans.Answer, + ans.Answer = append( + ans.Answer, common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), diff --git a/app/dns/fakedns/fakedns_test.go b/app/dns/fakedns/fakedns_test.go index f9a8449c4..0cea3919a 100644 --- a/app/dns/fakedns/fakedns_test.go +++ b/app/dns/fakedns/fakedns_test.go @@ -129,15 +129,16 @@ func TestFakeDnsHolderCreateMappingAndRollOver(t *testing.T) { } func TestFakeDNSMulti(t *testing.T) { - fakeMulti, err := NewFakeDNSHolderMulti(&FakeDnsPoolMulti{ - Pools: []*FakeDnsPool{{ - IpPool: "240.0.0.0/12", - LruSize: 256, - }, { - IpPool: "fddd:c5b4:ff5f:f4f0::/64", - LruSize: 256, - }}, - }, + fakeMulti, err := NewFakeDNSHolderMulti( + &FakeDnsPoolMulti{ + Pools: []*FakeDnsPool{{ + IpPool: "240.0.0.0/12", + LruSize: 256, + }, { + IpPool: "fddd:c5b4:ff5f:f4f0::/64", + LruSize: 256, + }}, + }, ) common.Must(err) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index 918624535..06882611f 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -135,7 +135,7 @@ func NewClient( } } - var timeoutMs = 4000 * time.Millisecond + timeoutMs := 4000 * time.Millisecond if ns.TimeoutMs > 0 { timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond } diff --git a/app/dns/nameserver_local.go b/app/dns/nameserver_local.go index 576c259b7..4369f89ed 100644 --- a/app/dns/nameserver_local.go +++ b/app/dns/nameserver_local.go @@ -18,7 +18,6 @@ type LocalNameServer struct { // QueryIP implements Server. func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) { - start := time.Now() ips, ttl, err = s.client.LookupIP(domain, option) diff --git a/app/metrics/metrics.go b/app/metrics/metrics.go index a37373842..a2ae2f30c 100644 --- a/app/metrics/metrics.go +++ b/app/metrics/metrics.go @@ -85,7 +85,6 @@ func (p *MetricsHandler) Type() interface{} { } func (p *MetricsHandler) Start() error { - // direct listen a port if listen is set if p.listen != "" { TCPlistener, err := net.Listen("tcp", p.listen) diff --git a/app/observatory/burst/burstobserver.go b/app/observatory/burst/burstobserver.go index 9af96a1de..fbcbfdccc 100644 --- a/app/observatory/burst/burstobserver.go +++ b/app/observatory/burst/burstobserver.go @@ -2,7 +2,6 @@ package burst import ( "context" - "sync" "github.com/xtls/xray-core/app/observatory" @@ -72,7 +71,6 @@ func (o *Observer) Start() error { o.hp.StartScheduler(func() ([]string, error) { hs, ok := o.ohm.(outbound.HandlerSelector) if !ok { - return nil, errors.New("outbound.Manager is not a HandlerSelector") } diff --git a/app/observatory/observer.go b/app/observatory/observer.go index 02dbaf138..80d81e227 100644 --- a/app/observatory/observer.go +++ b/app/observatory/observer.go @@ -186,7 +186,7 @@ func (o *Observer) probe(outbound string) ProbeResult { return nil }) if err != nil { - var errorMessage = "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed" + errorMessage := "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed" errors.LogInfoInner(o.ctx, errorCollectorForRequest.UnderlyingError(), errorMessage) return ProbeResult{Alive: false, LastErrorReason: errorMessage} } diff --git a/app/proxyman/command/command.go b/app/proxyman/command/command.go index a86f09212..217b2b4f7 100644 --- a/app/proxyman/command/command.go +++ b/app/proxyman/command/command.go @@ -138,7 +138,7 @@ func (s *handlerServer) GetInboundUsers(ctx context.Context, request *GetInbound if len(request.Email) > 0 { return &GetInboundUserResponse{Users: []*protocol.User{protocol.ToProtoUser(um.GetUser(ctx, request.Email))}}, nil } - var result = make([]*protocol.User, 0, 100) + result := make([]*protocol.User, 0, 100) users := um.GetUsers(ctx) for _, u := range users { result = append(result, protocol.ToProtoUser(u)) diff --git a/app/proxyman/inbound/inbound.go b/app/proxyman/inbound/inbound.go index 374a04286..cec93dd48 100644 --- a/app/proxyman/inbound/inbound.go +++ b/app/proxyman/inbound/inbound.go @@ -16,10 +16,10 @@ import ( // Manager manages all inbound handlers. type Manager struct { - access sync.RWMutex + access sync.RWMutex untaggedHandlers []inbound.Handler - taggedHandlers map[string]inbound.Handler - running bool + taggedHandlers map[string]inbound.Handler + running bool } // New returns a new Manager for inbound handlers. diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 62902c60d..7284a848d 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -348,7 +348,7 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String()) } } - //case addr.Family().IsDomain(): + // case addr.Family().IsDomain(): default: ob.Gateway = addr @@ -396,7 +396,6 @@ func (h *Handler) ProxySettings() *serial.TypedMessage { } func ParseRandomIP(addr net.Address, prefix string) net.Address { - _, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix) ones, bits := ipnet.Mask.Size() diff --git a/app/proxyman/outbound/handler_test.go b/app/proxyman/outbound/handler_test.go index b90224f8f..702944298 100644 --- a/app/proxyman/outbound/handler_test.go +++ b/app/proxyman/outbound/handler_test.go @@ -22,8 +22,8 @@ import ( ) func TestInterfaces(t *testing.T) { - _ = (outbound.Handler)(new(Handler)) - _ = (outbound.Manager)(new(Manager)) + _ = outbound.Handler(new(Handler)) + _ = outbound.Manager(new(Manager)) } const xrayKey core.XrayKey = 1 @@ -43,7 +43,7 @@ func TestOutboundWithoutStatCounter(t *testing.T) { } v, _ := core.New(config) - v.AddFeature((outbound.Manager)(new(Manager))) + v.AddFeature(outbound.Manager(new(Manager))) ctx := context.WithValue(context.Background(), xrayKey, v) ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ @@ -73,7 +73,7 @@ func TestOutboundWithStatCounter(t *testing.T) { } v, _ := core.New(config) - v.AddFeature((outbound.Manager)(new(Manager))) + v.AddFeature(outbound.Manager(new(Manager))) ctx := context.WithValue(context.Background(), xrayKey, v) ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ @@ -88,7 +88,6 @@ func TestOutboundWithStatCounter(t *testing.T) { } func TestTagsCache(t *testing.T) { - test_duration := 10 * time.Second threads_num := 50 delay := 10 * time.Millisecond diff --git a/app/proxyman/outbound/outbound.go b/app/proxyman/outbound/outbound.go index 244fcffe8..d86542888 100644 --- a/app/proxyman/outbound/outbound.go +++ b/app/proxyman/outbound/outbound.go @@ -162,7 +162,6 @@ func (m *Manager) ListHandlers(ctx context.Context) []outbound.Handler { // Select implements outbound.HandlerSelector. func (m *Manager) Select(selectors []string) []string { - key := strings.Join(selectors, ",") if cache, ok := m.tagsCache.Load(key); ok { return cache.([]string) diff --git a/app/router/command/command.go b/app/router/command/command.go index c09745533..8274d3b44 100644 --- a/app/router/command/command.go +++ b/app/router/command/command.go @@ -58,7 +58,6 @@ func (s *routingServer) AddRule(ctx context.Context, request *AddRuleRequest) (* return &AddRuleResponse{}, bo.AddRule(request.Config, request.ShouldAppend) } return nil, errors.New("unsupported router implementation") - } func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) { diff --git a/app/router/router.go b/app/router/router.go index df770a3a4..97a1d0d57 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -110,7 +110,6 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) { // AddRule implements routing.Router. func (r *Router) AddRule(config *serial.TypedMessage, shouldAppend bool) error { - inst, err := config.GetInstance() if err != nil { return err @@ -227,7 +226,6 @@ func (r *Router) RemoveRule(tag string) error { return nil } return errors.New("empty tag name!") - } // ListRule implements routing.Router diff --git a/app/router/strategy_leastload_test.go b/app/router/strategy_leastload_test.go index 832e0a874..5f7fab414 100644 --- a/app/router/strategy_leastload_test.go +++ b/app/router/strategy_leastload_test.go @@ -85,6 +85,7 @@ func TestSelectLeastExpected(t *testing.T) { t.Errorf("expected: %v, actual: %v", expected, len(ns)) } } + func TestSelectLeastExpected2(t *testing.T) { strategy := &LeastLoadStrategy{ settings: &StrategyLeastLoadConfig{ @@ -102,6 +103,7 @@ func TestSelectLeastExpected2(t *testing.T) { t.Errorf("expected: %v, actual: %v", expected, len(ns)) } } + func TestSelectLeastExpectedAndBaselines(t *testing.T) { strategy := &LeastLoadStrategy{ settings: &StrategyLeastLoadConfig{ @@ -122,6 +124,7 @@ func TestSelectLeastExpectedAndBaselines(t *testing.T) { t.Errorf("expected: %v, actual: %v", expected, len(ns)) } } + func TestSelectLeastExpectedAndBaselines2(t *testing.T) { strategy := &LeastLoadStrategy{ settings: &StrategyLeastLoadConfig{ @@ -142,6 +145,7 @@ func TestSelectLeastExpectedAndBaselines2(t *testing.T) { t.Errorf("expected: %v, actual: %v", expected, len(ns)) } } + func TestSelectLeastLoadBaselines(t *testing.T) { strategy := &LeastLoadStrategy{ settings: &StrategyLeastLoadConfig{ @@ -160,6 +164,7 @@ func TestSelectLeastLoadBaselines(t *testing.T) { t.Errorf("expected: %v, actual: %v", expected, len(ns)) } } + func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) { strategy := &LeastLoadStrategy{ settings: &StrategyLeastLoadConfig{ diff --git a/app/router/webhook.go b/app/router/webhook.go index 535ed294f..0c7a3e4d6 100644 --- a/app/router/webhook.go +++ b/app/router/webhook.go @@ -16,8 +16,6 @@ import ( routing_session "github.com/xtls/xray-core/features/routing/session" ) - - func ptr[T any](v T) *T { return &v } type event struct { diff --git a/app/stats/stats_test.go b/app/stats/stats_test.go index cdbd404ea..a38f554d0 100644 --- a/app/stats/stats_test.go +++ b/app/stats/stats_test.go @@ -11,7 +11,7 @@ import ( ) func TestInterface(t *testing.T) { - _ = (stats.Manager)(new(Manager)) + _ = stats.Manager(new(Manager)) } func TestStatsChannelRunnable(t *testing.T) { diff --git a/app/version/version.go b/app/version/version.go index 25d7e6ffa..03856051e 100644 --- a/app/version/version.go +++ b/app/version/version.go @@ -2,10 +2,11 @@ package version import ( "context" - "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/errors" "strconv" "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" ) type Version struct { diff --git a/common/buf/multi_buffer.go b/common/buf/multi_buffer.go index fcac84d72..a5cc38bca 100644 --- a/common/buf/multi_buffer.go +++ b/common/buf/multi_buffer.go @@ -38,8 +38,8 @@ func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) { // MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer. func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer { n := len(dest) - if n > 0 && !(dest)[n-1].IsFull() { - nBytes, _ := (dest)[n-1].Write(src) + if n > 0 && !dest[n-1].IsFull() { + nBytes, _ := dest[n-1].Write(src) src = src[nBytes:] } diff --git a/common/buf/reader_test.go b/common/buf/reader_test.go index d2e951693..58b6c72f6 100644 --- a/common/buf/reader_test.go +++ b/common/buf/reader_test.go @@ -121,11 +121,11 @@ func TestPacketReader_ReadMultiBuffer(t *testing.T) { } func TestReaderInterface(t *testing.T) { - _ = (io.Reader)(new(ReadVReader)) - _ = (Reader)(new(ReadVReader)) + _ = io.Reader(new(ReadVReader)) + _ = Reader(new(ReadVReader)) - _ = (Reader)(new(BufferedReader)) - _ = (io.Reader)(new(BufferedReader)) - _ = (io.ByteReader)(new(BufferedReader)) - _ = (io.WriterTo)(new(BufferedReader)) + _ = Reader(new(BufferedReader)) + _ = io.Reader(new(BufferedReader)) + _ = io.ByteReader(new(BufferedReader)) + _ = io.WriterTo(new(BufferedReader)) } diff --git a/common/buf/readv_posix.go b/common/buf/readv_posix.go index e8b3bd7fd..860030edc 100644 --- a/common/buf/readv_posix.go +++ b/common/buf/readv_posix.go @@ -19,7 +19,7 @@ func (r *posixReader) Init(bs []*Buffer) { } for idx, b := range bs { iovecs = append(iovecs, syscall.Iovec{ - Base: &(b.v[0]), + Base: &b.v[0], }) iovecs[idx].SetLen(int(Size)) } diff --git a/common/geodata/ip_matcher_test.go b/common/geodata/ip_matcher_test.go index 829c455d0..fc6a016dd 100644 --- a/common/geodata/ip_matcher_test.go +++ b/common/geodata/ip_matcher_test.go @@ -315,7 +315,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) { } if !matcher.AnyMatch([]net.IP{ - net.IP{}, + {}, ip("1.1.1.1"), ip("8.8.8.8"), }) { @@ -345,7 +345,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) { if matcher.Matches([]net.IP{ ip("8.8.8.8"), - net.IP{}, + {}, }) { t.Fatal("expect Matches to be false when any IP is invalid") } @@ -362,7 +362,7 @@ func TestIPMatcherFilterIPs(t *testing.T) { } matched, unmatched := matcher.FilterIPs([]net.IP{ - net.IP{}, + {}, ip("8.8.8.8"), ip("91.108.255.254"), ip("1.1.1.1"), diff --git a/common/log/logger.go b/common/log/logger.go index 538eda2d0..1b4eda864 100644 --- a/common/log/logger.go +++ b/common/log/logger.go @@ -98,7 +98,6 @@ func (l *generalLogger) run() { } func (l *generalLogger) Handle(msg Message) { - select { case l.buffer <- msg: default: diff --git a/common/platform/platform_test.go b/common/platform/platform_test.go index ae823217c..854c397f6 100644 --- a/common/platform/platform_test.go +++ b/common/platform/platform_test.go @@ -36,9 +36,10 @@ func TestNormalizeEnvName(t *testing.T) { } func TestEnvFlag(t *testing.T) { - if v := (EnvFlag{ + v := EnvFlag{ Name: "xxxxx.y", - }.GetValueAsInt(10)); v != 10 { + }.GetValueAsInt(10) + if v != 10 { t.Error("env value: ", v) } } diff --git a/common/protocol/id.go b/common/protocol/id.go index 211fc5782..5dd7a2f0a 100644 --- a/common/protocol/id.go +++ b/common/protocol/id.go @@ -19,7 +19,7 @@ type ID struct { // Equals returns true if this ID equals to the other one. func (id *ID) Equals(another *ID) bool { - return id.uuid.Equals(&(another.uuid)) + return id.uuid.Equals(&another.uuid) } func (id *ID) Bytes() []byte { diff --git a/common/protocol/quic/sniff_test.go b/common/protocol/quic/sniff_test.go index 8a85ea87c..8c6fbef42 100644 --- a/common/protocol/quic/sniff_test.go +++ b/common/protocol/quic/sniff_test.go @@ -229,7 +229,7 @@ func TestSniffQUICComplex(t *testing.T) { t.Errorf("SniffQUIC() error = %v, wantErr %v", err, tt.wantErr) return } - if (errors.Is(err, protocol.ErrProtoNeedMoreData)) != tt.needsMoreData { + if errors.Is(err, protocol.ErrProtoNeedMoreData) != tt.needsMoreData { t.Errorf("SniffQUIC() error = %v, expectsNoClue %v", err, tt.needsMoreData) return } diff --git a/common/protocol/server_spec.go b/common/protocol/server_spec.go index d3ab00898..5c1b99f47 100644 --- a/common/protocol/server_spec.go +++ b/common/protocol/server_spec.go @@ -5,8 +5,8 @@ import ( ) type ServerSpec struct { - Destination net.Destination - User *MemoryUser + Destination net.Destination + User *MemoryUser } func NewServerSpec(dest net.Destination, user *MemoryUser) *ServerSpec { diff --git a/common/reflect/marshal.go b/common/reflect/marshal.go index 127dc8e0b..a2888010f 100644 --- a/common/reflect/marshal.go +++ b/common/reflect/marshal.go @@ -228,7 +228,6 @@ func isValueKind(kind reflect.Kind) bool { } func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} { - if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok { return r } diff --git a/common/reflect/marshal_test.go b/common/reflect/marshal_test.go index 1a897ef7c..d560f1b4e 100644 --- a/common/reflect/marshal_test.go +++ b/common/reflect/marshal_test.go @@ -27,7 +27,6 @@ func TestMashalAccount(t *testing.T) { j, ok := MarshalToJson(user, false) if !ok || strings.Contains(j, "_TypedMessage_") { - t.Error("marshal account failed") } @@ -79,13 +78,12 @@ func TestMashalStruct(t *testing.T) { v := (*f2.Arr)[0]["foo"]["hello"] - if f1.N != f2.N || *(f1.Np) != *(f2.Np) || f1.S != f2.S || v != "world" { + if f1.N != f2.N || *f1.Np != *f2.Np || f1.S != f2.S || v != "world" { t.Error("f1 not equal to f2") } } func TestMarshalConfigJson(t *testing.T) { - buf := bytes.NewBufferString(getConfig()) config, err := iserial.DecodeJSONConfig(buf) if err != nil { diff --git a/common/units/bytesize_test.go b/common/units/bytesize_test.go index 3b8b9c88e..4631231f9 100644 --- a/common/units/bytesize_test.go +++ b/common/units/bytesize_test.go @@ -10,37 +10,44 @@ func TestByteSizes(t *testing.T) { size := units.ByteSize(0) assertSizeString(t, size, "0") size++ - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00B"), size, ) size <<= 10 - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00KB"), size, ) size <<= 10 - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00MB"), size, ) size <<= 10 - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00GB"), size, ) size <<= 10 - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00TB"), size, ) size <<= 10 - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00PB"), size, ) size <<= 10 - assertSizeValue(t, + assertSizeValue( + t, assertSizeString(t, size, "1.00EB"), size, ) diff --git a/common/utils/browser.go b/common/utils/browser.go index 957f49b72..12640564d 100644 --- a/common/utils/browser.go +++ b/common/utils/browser.go @@ -4,10 +4,10 @@ import ( "hash/fnv" "math" "math/rand" - "strconv" - "time" "net/http" + "strconv" "strings" + "time" "github.com/klauspost/cpuid/v2" ) @@ -18,6 +18,7 @@ func GetRandomizer() *rand.Rand { fnvHash.Write([]byte(strconv.Itoa(cpuid.CPU.Family) + strconv.Itoa(cpuid.CPU.Model) + strconv.Itoa(cpuid.CPU.PhysicalCores) + strconv.Itoa(cpuid.CPU.LogicalCores) + strconv.Itoa(cpuid.CPU.CacheLine) + strconv.Itoa(cpuid.CPU.ThreadsPerCore))) return rand.New(rand.NewSource(int64(fnvHash.Sum64()))) } + var globalRng *rand.Rand = GetRandomizer() // The Chrome version generator will suffer from deviation of a normal distribution. @@ -26,83 +27,114 @@ func ChromeVersion() int { var startVersion int = 144 var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeCurrent int64 = time.Now().Unix() / 86400 - var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 105)) + var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*105)) return startVersion + (timeDiff / 35) // It's 31.15 currently. } -var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1, +var safariMinorMap [25]int = [25]int{ + 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, - 4, 5, 5, 5, 5, 5, 6, 6, 6, 6} + 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, +} // The following version generators use deterministic generators, but with the distribution scaled by a curve. func CurlVersion() string { // curl 8.0.0 was released on 20/03/2023. var timeCurrent int64 = time.Now().Unix() / 86400 var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400 - var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 165)) + var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*165)) var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days. return "8." + strconv.Itoa(minorValue) + ".0" } + func FirefoxVersion() int { // Firefox 128 ESR was released on 09/07/2023. var timeCurrent int64 = time.Now().Unix() / 86400 var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400 - var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2) * 50)) - return int(timeDiff / 30) + 128 + timeDiff := timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2)*50)) + return int(timeDiff/30) + 128 } + func SafariVersion() string { var anchoredTime time.Time = time.Now() var releaseYear int = anchoredTime.Year() var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) - var delayedDays = int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75)) + delayedDays := int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75)) splitPoint = splitPoint.AddDate(0, 0, delayedDays) - if (anchoredTime.Compare(splitPoint) < 0) { - releaseYear -- + if anchoredTime.Compare(splitPoint) < 0 { + releaseYear-- splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) splitPoint = splitPoint.AddDate(0, 0, delayedDays) } - var minorVersion = safariMinorMap[(anchoredTime.Unix() - splitPoint.Unix()) / 1296000] - return strconv.Itoa(releaseYear - 1999) + "." + strconv.Itoa(minorVersion) + minorVersion := safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000] + return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion) } // The full Chromium brand GREASE implementation -var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} -var clientHintVersionNA = []string{"8", "99", "24"} -var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} -var clientHintShuffle4 = [][4]int{ - {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1}, - {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0}, - {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0}, - {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}} +var ( + clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} + clientHintVersionNA = []string{"8", "99", "24"} + clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} + clientHintShuffle4 = [][4]int{ + {0, 1, 2, 3}, + {0, 1, 3, 2}, + {0, 2, 1, 3}, + {0, 2, 3, 1}, + {0, 3, 1, 2}, + {0, 3, 2, 1}, + {1, 0, 2, 3}, + {1, 0, 3, 2}, + {1, 2, 0, 3}, + {1, 2, 3, 0}, + {1, 3, 0, 2}, + {1, 3, 2, 0}, + {2, 0, 1, 3}, + {2, 0, 3, 1}, + {2, 1, 0, 3}, + {2, 1, 3, 0}, + {2, 3, 0, 1}, + {2, 3, 1, 0}, + {3, 0, 1, 2}, + {3, 0, 2, 1}, + {3, 1, 0, 2}, + {3, 1, 2, 0}, + {3, 2, 0, 1}, + {3, 2, 1, 0}, + } +) + func getGreasedChInvalidBrand(seed int) string { - return "\"Not" + clientHintGreaseNA[seed % len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed + 1) % len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed % len(clientHintVersionNA)] + "\""; + return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\"" } + func getGreasedChOrder(brandLength int, seed int) []int { switch brandLength { - case 1: - return []int{0} - case 2: - return []int{seed % brandLength, (seed + 1) % brandLength} - case 3: - return clientHintShuffle3[seed % len(clientHintShuffle3)][:] - default: - return clientHintShuffle4[seed % len(clientHintShuffle4)][:] + case 1: + return []int{0} + case 2: + return []int{seed % brandLength, (seed + 1) % brandLength} + case 3: + return clientHintShuffle3[seed%len(clientHintShuffle3)][:] + default: + return clientHintShuffle4[seed%len(clientHintShuffle4)][:] } //return []int{} } + func getUngreasedChUa(majorVersion int, forkName string) []string { // Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice baseChUa := make([]string, 0, 4) baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion), - "\"Chromium\";v=\"" + strconv.Itoa(majorVersion) + "\"") + "\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"") switch forkName { case "chrome": - baseChUa = append(baseChUa, "\"Google Chrome\";v=\"" + strconv.Itoa(majorVersion) + "\"") + baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"") case "edge": - baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\"" + strconv.Itoa(majorVersion) + "\"") + baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"") } return baseChUa } + func getGreasedChUa(majorVersion int, forkName string) string { ungreasedCh := getUngreasedChUa(majorVersion, forkName) shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion) @@ -114,16 +146,18 @@ func getGreasedChUa(majorVersion int, forkName string) string { } // The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG. -var CurlUA = "curl/" + CurlVersion() -var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion()) -var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0" -var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15" -// Chromium browsers. -var AnchoredChromeVersion = ChromeVersion() -var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" -var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") -var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" -var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge") +var ( + CurlUA = "curl/" + CurlVersion() + AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion()) + FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0" + SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15" + // Chromium browsers. + AnchoredChromeVersion = ChromeVersion() + ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" + ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") + MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" + MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge") +) func applyMasqueradedHeaders(header http.Header, browser string, variant string) { // Browser-specific. diff --git a/common/utils/typed_sync_map.go b/common/utils/typed_sync_map.go index 39524a6f4..72d425f7c 100644 --- a/common/utils/typed_sync_map.go +++ b/common/utils/typed_sync_map.go @@ -109,4 +109,4 @@ func (m *TypedSyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) { previous = anyPrevious.(V) } return previous, loaded -} \ No newline at end of file +} diff --git a/common/utils/unixsocket.go b/common/utils/unixsocket.go index 52d36256e..8bb156c6b 100644 --- a/common/utils/unixsocket.go +++ b/common/utils/unixsocket.go @@ -19,18 +19,18 @@ import ( // Filesystem paths and abstract sockets on other platforms are returned // unchanged. func ResolveSocketPath(path string) string { - if len(path) == 0 || path[0] != '@' { - return path - } - if runtime.GOOS != "linux" && runtime.GOOS != "android" { - return path - } - if len(path) > 1 && path[1] == '@' { - fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) - copy(fullAddr, path[1:]) - return string(fullAddr) - } - return path + if len(path) == 0 || path[0] != '@' { + return path + } + if runtime.GOOS != "linux" && runtime.GOOS != "android" { + return path + } + if len(path) > 1 && path[1] == '@' { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) + copy(fullAddr, path[1:]) + return string(fullAddr) + } + return path } // SplitHTTPUnixURL splits a target into an HTTP URL and an optional Unix @@ -44,12 +44,11 @@ func ResolveSocketPath(path string) string { // The :/ separator delimits the socket path from the HTTP request path. // If omitted, "/" is used. func SplitHTTPUnixURL(raw string) (httpURL, socketPath string) { - if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { - return raw, "" - } - if idx := strings.Index(raw, ":/"); idx >= 0 { - return "http://localhost" + raw[idx+1:], raw[:idx] - } - return "http://localhost/", raw + if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { + return raw, "" + } + if idx := strings.Index(raw, ":/"); idx >= 0 { + return "http://localhost" + raw[idx+1:], raw[:idx] + } + return "http://localhost/", raw } - diff --git a/features/routing/session/context.go b/features/routing/session/context.go index 45fa8d8f4..e331c4347 100644 --- a/features/routing/session/context.go +++ b/features/routing/session/context.go @@ -34,7 +34,6 @@ func (ctx *Context) GetSourceIPs() []net.IP { } return nil - } // GetSourcePort implements routing.Context. diff --git a/infra/conf/blackhole.go b/infra/conf/blackhole.go index 7b78742f7..19ee1a6e6 100644 --- a/infra/conf/blackhole.go +++ b/infra/conf/blackhole.go @@ -48,4 +48,5 @@ var configLoader = NewJSONConfigLoader( "http": func() interface{} { return new(HTTPResponse) }, }, "type", - "") + "", +) diff --git a/infra/conf/router_strategy.go b/infra/conf/router_strategy.go index 464cbcfb6..6c426c510 100644 --- a/infra/conf/router_strategy.go +++ b/infra/conf/router_strategy.go @@ -1,9 +1,10 @@ package conf import ( - "google.golang.org/protobuf/proto" "strings" + "google.golang.org/protobuf/proto" + "github.com/xtls/xray-core/app/observatory/burst" "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/infra/conf/cfgcommon/duration" @@ -16,17 +17,14 @@ const ( strategyLeastLoad string = "leastload" ) -var ( - strategyConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ - strategyRandom: func() interface{} { return new(strategyEmptyConfig) }, - strategyLeastPing: func() interface{} { return new(strategyEmptyConfig) }, - strategyRoundRobin: func() interface{} { return new(strategyEmptyConfig) }, - strategyLeastLoad: func() interface{} { return new(strategyLeastLoadConfig) }, - }, "type", "settings") -) +var strategyConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ + strategyRandom: func() interface{} { return new(strategyEmptyConfig) }, + strategyLeastPing: func() interface{} { return new(strategyEmptyConfig) }, + strategyRoundRobin: func() interface{} { return new(strategyEmptyConfig) }, + strategyLeastLoad: func() interface{} { return new(strategyLeastLoadConfig) }, +}, "type", "settings") -type strategyEmptyConfig struct { -} +type strategyEmptyConfig struct{} func (v *strategyEmptyConfig) Build() (proto.Message, error) { return nil, nil diff --git a/infra/conf/serial/loader.go b/infra/conf/serial/loader.go index 912f105c7..d7d0d4651 100644 --- a/infra/conf/serial/loader.go +++ b/infra/conf/serial/loader.go @@ -78,18 +78,17 @@ func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) { // byte-by-byte comment stripper and TeeReader, which are significant overhead on // large configs. func DecodeJSONConfigStrict(reader io.Reader) (*conf.Config, error) { - data, err := io.ReadAll(reader) - if err != nil { - return nil, errors.New("failed to read config file").Base(err) - } - jsonConfig := &conf.Config{} - if err := json.Unmarshal(data, jsonConfig); err != nil { - return nil, errors.New("failed to parse remote JSON config").Base(err) - } - return jsonConfig, nil + data, err := io.ReadAll(reader) + if err != nil { + return nil, errors.New("failed to read config file").Base(err) + } + jsonConfig := &conf.Config{} + if err := json.Unmarshal(data, jsonConfig); err != nil { + return nil, errors.New("failed to parse remote JSON config").Base(err) + } + return jsonConfig, nil } - func LoadJSONConfig(reader io.Reader) (*core.Config, error) { jsonConfig, err := DecodeJSONConfig(reader) if err != nil { diff --git a/infra/conf/transport_authenticators.go b/infra/conf/transport_authenticators.go index a9590af90..9afdd4f04 100644 --- a/infra/conf/transport_authenticators.go +++ b/infra/conf/transport_authenticators.go @@ -45,31 +45,31 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { Value: []string{utils.ChromeUA}, }, { - Name: "Sec-CH-UA", + Name: "Sec-CH-UA", Value: []string{utils.ChromeUACH}, }, { - Name: "Sec-CH-UA-Mobile", + Name: "Sec-CH-UA-Mobile", Value: []string{"?0"}, }, { - Name: "Sec-CH-UA-Platform", + Name: "Sec-CH-UA-Platform", Value: []string{"Windows"}, }, { - Name: "Sec-Fetch-Mode", + Name: "Sec-Fetch-Mode", Value: []string{"no-cors", "cors", "same-origin"}, }, { - Name: "Sec-Fetch-Dest", + Name: "Sec-Fetch-Dest", Value: []string{"empty"}, }, { - Name: "Sec-Fetch-Site", + Name: "Sec-Fetch-Site", Value: []string{"none"}, }, { - Name: "Sec-Fetch-User", + Name: "Sec-Fetch-User", Value: []string{"?1"}, }, { @@ -96,7 +96,7 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { } if len(v.Path) > 0 { - config.Uri = append([]string(nil), (v.Path)...) + config.Uri = append([]string(nil), v.Path...) } if len(v.Headers) > 0 { diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 25fe978ac..7a86a08ba 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -45,12 +45,10 @@ import ( "google.golang.org/protobuf/proto" ) -var ( - tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ - "none": func() interface{} { return new(NoOpConnectionAuthenticator) }, - "http": func() interface{} { return new(Authenticator) }, - }, "type", "") -) +var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "none": func() interface{} { return new(NoOpConnectionAuthenticator) }, + "http": func() interface{} { return new(Authenticator) }, +}, "type", "") type KCPConfig struct { Mtu *uint32 `json:"mtu"` @@ -1034,7 +1032,7 @@ type HappyEyeballsConfig struct { } func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error { - var innerHappyEyeballsConfig = struct { + innerHappyEyeballsConfig := struct { PrioritizeIPv6 bool `json:"prioritizeIPv6"` TryDelayMs uint64 `json:"tryDelayMs"` Interleave uint32 `json:"interleave"` @@ -1162,7 +1160,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) { return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy) } - var happyEyeballs = &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4} + happyEyeballs := &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4} if c.HappyEyeballsSettings != nil { happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6 happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave diff --git a/infra/conf/version.go b/infra/conf/version.go index 5fedeb08e..3cbe92151 100644 --- a/infra/conf/version.go +++ b/infra/conf/version.go @@ -1,9 +1,10 @@ package conf import ( + "strconv" + "github.com/xtls/xray-core/app/version" "github.com/xtls/xray-core/core" - "strconv" ) type VersionConfig struct { diff --git a/infra/conf/xray.go b/infra/conf/xray.go index a077af5ed..3e6e43710 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -177,7 +177,8 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { protocol := ss.GetEffectiveProtocol() if (protocol == "websocket" || protocol == "httpupgrade" || protocol == "splithttp") && (c.StreamSetting.SocketSettings == nil || len(c.StreamSetting.SocketSettings.TrustedXForwardedFor) == 0) { - errors.LogWarning(context.Background(), + errors.LogWarning( + context.Background(), `====== SECURITY WARNING ======`, "\n", `inbound "`, c.Tag, `" using `, protocol, ` has not configured "sockopt.trustedXForwardedFor".`, @@ -285,7 +286,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { if c.SendThrough != nil { address := ParseSendThough(c.SendThrough) - //Check if CIDR exists + // Check if CIDR exists if strings.Contains(*c.SendThrough, "/") { senderSettings.ViaCidr = strings.Split(*c.SendThrough, "/")[1] } else { @@ -469,7 +470,6 @@ func (c *Config) Override(o *Config, fn string) { c.InboundConfigs = append(c.InboundConfigs, o.InboundConfigs[i]) errors.LogInfo(context.Background(), "[", fn, "] appended inbound with tag: ", o.InboundConfigs[i].Tag) } - } } diff --git a/infra/vformat/main.go b/infra/vformat/main.go index 84c63a420..533a5a147 100644 --- a/infra/vformat/main.go +++ b/infra/vformat/main.go @@ -12,7 +12,16 @@ import ( "strings" ) -var directory = flag.String("pwd", "", "Working directory of Xray vformat.") +var ( + directory = flag.String("pwd", "", "Working directory of Xray vformat.") + action = flag.String("mode", "format", "Execution mode. Default is 'format'.\n'format' formatting source files and save changes to files.\n'check' list all paths of improper formatted file.\n'dryrun' formatting source files and shows all diffs, but will not make any changes to files.") +) + +var ( + isCheck bool + isDryrun bool + isFormat bool +) // envFile returns the name of the Go environment configuration file. // Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 @@ -90,9 +99,10 @@ func Run(binary string, args []string) ([]byte, error) { return output, nil } -func RunMany(binary string, args, files []string) { - fmt.Println("Processing...") +func RunMany(binary string, args, files []string) bool { + fmt.Println("Processing with", binary, args, "...") + formatRequired := false maxTasks := make(chan struct{}, runtime.NumCPU()) for _, file := range files { maxTasks <- struct{}{} @@ -102,10 +112,12 @@ func RunMany(binary string, args, files []string) { fmt.Println(err) } else if len(output) > 0 { fmt.Println(string(output)) + formatRequired = true } <-maxTasks }(file) } + return formatRequired } func main() { @@ -124,6 +136,19 @@ func main() { *directory = filepath.Join(pwd, *directory) } + switch *action { + case "format": + isFormat = true + case "check": + isCheck = true + case "dryrun": + isCheck = true + isDryrun = true + default: + fmt.Println("Unrecognized 'mode'. Will format all source files and save changes.") + isFormat = true + } + pwd := *directory GOBIN := GetGOBIN() binPath := os.Getenv("PATH") @@ -136,7 +161,6 @@ func main() { suffix = ".exe" } gofmt := "gofumpt" + suffix - goimports := "gci" + suffix if gofmtPath, err := exec.LookPath(gofmt); err != nil { fmt.Println("Can not find", gofmt, "in system path or current working directory.") @@ -145,13 +169,6 @@ func main() { gofmt = gofmtPath } - if goimportsPath, err := exec.LookPath(goimports); err != nil { - fmt.Println("Can not find", goimports, "in system path or current working directory.") - os.Exit(1) - } else { - goimports = goimportsPath - } - rawFilesSlice := make([]string, 0, 1000) walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -179,15 +196,41 @@ func main() { os.Exit(1) } - gofmtArgs := []string{ - "-s", "-l", "-e", "-w", + if isFormat { + gofmtArgs := []string{ + "-l", "-e", "-w", + } + + fmt.Println("Formatting Go source files...") + RunMany(gofmt, gofmtArgs, rawFilesSlice) + fmt.Println("Do NOT forget to commit file changes.") } - goimportsArgs := []string{ - "write", - } + if isCheck { + gofmtListArgs := []string{ + "-l", "-e", + } - RunMany(gofmt, gofmtArgs, rawFilesSlice) - RunMany(goimports, goimportsArgs, rawFilesSlice) - fmt.Println("Do NOT forget to commit file changes.") + fmt.Println("Checking files thar are not properly formatted...") + formatRequired := RunMany(gofmt, gofmtListArgs, rawFilesSlice) + if formatRequired { + fmt.Println("Format problem(s) found.") + } + + if isDryrun { + if formatRequired { + gofmtShowArgs := []string{ + "-d", "-e", + } + RunMany(gofmt, gofmtShowArgs, rawFilesSlice) + } + } + + if formatRequired { + fmt.Println("Please run 'go install -v mvdan.cc/gofumpt@latest', then run 'go run ./infra/vformat/main.go' to format the Go source files.") + os.Exit(1) + } else { + fmt.Println("All Go source file format check has been passed.") + } + } } diff --git a/main/commands/all/api/balancer_info.go b/main/commands/all/api/balancer_info.go index f5b6804c6..cabc66d98 100644 --- a/main/commands/all/api/balancer_info.go +++ b/main/commands/all/api/balancer_info.go @@ -59,7 +59,6 @@ func executeBalancerInfo(cmd *base.Command, args []string) { } showBalancerInfo(resp.Balancer) - } func showBalancerInfo(b *routerService.BalancerMsg) { diff --git a/main/commands/all/api/inbound_user_add.go b/main/commands/all/api/inbound_user_add.go index 759bf5965..81afc0d29 100644 --- a/main/commands/all/api/inbound_user_add.go +++ b/main/commands/all/api/inbound_user_add.go @@ -62,7 +62,8 @@ func addInboundUserAction(ctx context.Context, client handlerService.HandlerServ Operation: cserial.ToTypedMessage( &handlerService.AddUserOperation{ User: user, - }), + }, + ), }) return err } diff --git a/main/commands/all/api/inbound_user_remove.go b/main/commands/all/api/inbound_user_remove.go index d12617001..d6e8a4b31 100644 --- a/main/commands/all/api/inbound_user_remove.go +++ b/main/commands/all/api/inbound_user_remove.go @@ -50,7 +50,8 @@ func executeRemoveUsers(cmd *base.Command, args []string) { Operation: cserial.ToTypedMessage( &handlerService.RemoveUserOperation{ Email: email, - }), + }, + ), }) if err == nil { success += 1 diff --git a/main/commands/all/api/rules_add.go b/main/commands/all/api/rules_add.go index 4ff3d0b80..fc0fda6c0 100644 --- a/main/commands/all/api/rules_add.go +++ b/main/commands/all/api/rules_add.go @@ -38,9 +38,7 @@ Example: } func executeAddRules(cmd *base.Command, args []string) { - var ( - shouldAppend bool - ) + var shouldAppend bool setSharedFlags(cmd) cmd.Flag.BoolVar(&shouldAppend, "append", false, "") cmd.Flag.Parse(args) @@ -96,5 +94,4 @@ func executeAddRules(cmd *base.Command, args []string) { } showJSONResponse(resp) } - } diff --git a/main/commands/all/api/rules_remove.go b/main/commands/all/api/rules_remove.go index ac9a8d097..7be62f714 100644 --- a/main/commands/all/api/rules_remove.go +++ b/main/commands/all/api/rules_remove.go @@ -56,5 +56,4 @@ func executeRemoveRules(cmd *base.Command, args []string) { } showJSONResponse(resp) } - } diff --git a/main/commands/all/api/source_ip_block.go b/main/commands/all/api/source_ip_block.go index 9479ef35a..caff15a4c 100644 --- a/main/commands/all/api/source_ip_block.go +++ b/main/commands/all/api/source_ip_block.go @@ -136,5 +136,4 @@ func executeSourceIpBlock(cmd *base.Command, args []string) { base.Fatalf("failed to perform AddRule: %s", err) } showJSONResponse(resp) - } diff --git a/main/commands/all/convert/json.go b/main/commands/all/convert/json.go index 580667953..9da7527b2 100644 --- a/main/commands/all/convert/json.go +++ b/main/commands/all/convert/json.go @@ -38,7 +38,6 @@ Examples: } func executeTypedMessageToJson(cmd *base.Command, args []string) { - var injectTypeInfo bool cmd.Flag.BoolVar(&injectTypeInfo, "t", false, "") cmd.Flag.BoolVar(&injectTypeInfo, "type", false, "") diff --git a/main/commands/all/convert/protobuf.go b/main/commands/all/convert/protobuf.go index 2f4296e1f..d56acc2dc 100644 --- a/main/commands/all/convert/protobuf.go +++ b/main/commands/all/convert/protobuf.go @@ -41,7 +41,6 @@ Examples: } func executeConvertConfigsToProtobuf(cmd *base.Command, args []string) { - var optFile string var optDump bool var optType bool @@ -60,7 +59,7 @@ func executeConvertConfigsToProtobuf(cmd *base.Command, args []string) { } if len(optFile) > 0 { - switch core.GetFormat(optFile){ + switch core.GetFormat(optFile) { case "protobuf", "": fmt.Println("Output ProtoBuf file is ", optFile) default: diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index b2b532ffe..c31ddcc65 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -34,8 +34,10 @@ func init() { var input_echServerKeys = cmdECH.Flag.String("i", "", "ECHServerKeys (base64.StdEncoding)") // var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "") -var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "") -var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output") +var ( + input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "") + input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output") +) func executeECH(cmd *base.Command, args []string) { var kem uint16 diff --git a/main/commands/all/x25519.go b/main/commands/all/x25519.go index 784a17904..c9a823ac2 100644 --- a/main/commands/all/x25519.go +++ b/main/commands/all/x25519.go @@ -21,8 +21,10 @@ func init() { cmdX25519.Run = executeX25519 // break init loop } -var input_stdEncoding = cmdX25519.Flag.Bool("std-encoding", false, "") -var input_x25519 = cmdX25519.Flag.String("i", "", "") +var ( + input_stdEncoding = cmdX25519.Flag.Bool("std-encoding", false, "") + input_x25519 = cmdX25519.Flag.String("i", "", "") +) func executeX25519(cmd *base.Command, args []string) { Curve25519Genkey(false, *input_x25519) diff --git a/main/confloader/confloader.go b/main/confloader/confloader.go index b9652a67e..a4b42513d 100644 --- a/main/confloader/confloader.go +++ b/main/confloader/confloader.go @@ -12,9 +12,7 @@ type ( configFileLoader func(string) (io.Reader, error) ) -var ( - EffectiveConfigFileLoader configFileLoader -) +var EffectiveConfigFileLoader configFileLoader // LoadConfig reads from a path/url/stdin // actual work is in external module diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index f9ea874c8..02066109b 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -52,7 +52,7 @@ func ConfigLoader(arg string) (out io.Reader, err error) { // When the ":/" separator is omitted on a socket target, the request is // made to "/". func FetchHTTPContent(target string) ([]byte, error) { - httpURL, socketPath := utils.SplitHTTPUnixURL(target) + httpURL, socketPath := utils.SplitHTTPUnixURL(target) parsedTarget, err := url.Parse(httpURL) if err != nil { @@ -63,15 +63,15 @@ func FetchHTTPContent(target string) ([]byte, error) { Timeout: 30 * time.Second, } - if socketPath != "" { - dialAddr := utils.ResolveSocketPath(socketPath) - client.Transport = &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, "unix", dialAddr) - }, - } - } + if socketPath != "" { + dialAddr := utils.ResolveSocketPath(socketPath) + client.Transport = &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", dialAddr) + }, + } + } resp, err := client.Do(&http.Request{ Method: "GET", @@ -95,7 +95,6 @@ func FetchHTTPContent(target string) ([]byte, error) { return content, nil } - // isRemoteSource reports whether arg should be fetched via HTTP (regular // network or Unix socket) rather than read from the local filesystem. // Recognized forms: @@ -124,7 +123,6 @@ func isRemoteSource(arg string) bool { return err == nil && info.Mode()&os.ModeSocket != 0 } - // httpUnixToCanonical converts the deprecated http+unix:///path/to/socket.sock/api // URL into the canonical /path/to/socket.sock:/api form by inserting ":" // between the ".sock" extension and the HTTP path. Inputs without a path diff --git a/proxy/dokodemo/dokodemo.go b/proxy/dokodemo/dokodemo.go index 5d51c22ef..39c790c7e 100644 --- a/proxy/dokodemo/dokodemo.go +++ b/proxy/dokodemo/dokodemo.go @@ -190,9 +190,11 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st } } - if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{ - Reader: reader, - Writer: writer}, + if err := dispatcher.DispatchLink( + ctx, dest, &transport.Link{ + Reader: reader, + Writer: writer, + }, ); err != nil { return errors.New("failed to dispatch request").Base(err) } diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index f8181a34a..9cdce38a3 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -30,10 +30,12 @@ import ( "github.com/xtls/xray-core/transport/internet/stat" ) -var useSplice bool -var allNetworks [8]bool -var defaultBlockPrivateRule *FinalRule -var defaultBlockAllRule *FinalRule +var ( + useSplice bool + allNetworks [8]bool + defaultBlockPrivateRule *FinalRule + defaultBlockAllRule *FinalRule +) func init() { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { @@ -677,7 +679,7 @@ type NoisePacketWriter struct { func (w *NoisePacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { if w.firstWrite { w.firstWrite = false - //Do not send Noise for dns requests(just to be safe) + // Do not send Noise for dns requests(just to be safe) if w.UDPOverride.Port == 53 { return w.Writer.WriteMultiBuffer(mb) } @@ -700,11 +702,11 @@ func (w *NoisePacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { default: panic("unreachable, applyTo is ip/ipv4/ipv6") } - //User input string or base64 encoded string or hex string + // User input string or base64 encoded string or hex string if n.Packet != nil { noise = n.Packet } else { - //Random noise + // Random noise noise, err = GenerateRandomBytes(crypto.RandBetween(int64(n.LengthMin), int64(n.LengthMax))) } diff --git a/proxy/http/server.go b/proxy/http/server.go index 90a07b38a..054e40bf4 100644 --- a/proxy/http/server.go +++ b/proxy/http/server.go @@ -192,9 +192,11 @@ func (s *Server) handleConnect(ctx context.Context, _ *http.Request, buffer *buf if inbound.CanSpliceCopy == 2 { inbound.CanSpliceCopy = 1 } - if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{ - Reader: reader, - Writer: buf.NewWriter(conn)}, + if err := dispatcher.DispatchLink( + ctx, dest, &transport.Link{ + Reader: reader, + Writer: buf.NewWriter(conn), + }, ); err != nil { return errors.New("failed to dispatch request").Base(err) } diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index 0e50dcc9e..63ecaf652 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -113,7 +113,7 @@ func (v *Validator) GetAll() []*protocol.MemoryUser { v.mutex.Lock() defer v.mutex.Unlock() - var users = make([]*protocol.MemoryUser, 0, len(v.users)) + users := make([]*protocol.MemoryUser, 0, len(v.users)) for _, user := range v.users { users = append(users, user) } diff --git a/proxy/proxy.go b/proxy/proxy.go index dbd81d6b0..ab7d7abc5 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -743,7 +743,7 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net for { inbound := session.InboundFromContext(ctx) outbounds := session.OutboundsFromContext(ctx) - var splice = inbound.CanSpliceCopy == 1 + splice := inbound.CanSpliceCopy == 1 for _, ob := range outbounds { if ob.CanSpliceCopy != 1 { splice = false diff --git a/proxy/socks/server.go b/proxy/socks/server.go index fab664766..53049dfe2 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -153,9 +153,11 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche if inbound.CanSpliceCopy == 2 { inbound.CanSpliceCopy = 1 } - if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{ - Reader: reader, - Writer: buf.NewWriter(conn)}, + if err := dispatcher.DispatchLink( + ctx, dest, &transport.Link{ + Reader: reader, + Writer: buf.NewWriter(conn), + }, ); err != nil { return errors.New("failed to dispatch request").Base(err) } diff --git a/proxy/trojan/config.go b/proxy/trojan/config.go index b7591996f..00d94bc91 100644 --- a/proxy/trojan/config.go +++ b/proxy/trojan/config.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "google.golang.org/protobuf/proto" "github.com/xtls/xray-core/common" diff --git a/proxy/trojan/server.go b/proxy/trojan/server.go index d66219c48..d5979e529 100644 --- a/proxy/trojan/server.go +++ b/proxy/trojan/server.go @@ -312,7 +312,6 @@ func (s *Server) handleUDPPayload(ctx context.Context, sessionPolicy policy.Sess } } } - } if err := task.Run(ctx, requestDone); err != nil { diff --git a/proxy/trojan/validator.go b/proxy/trojan/validator.go index 7841a2498..026649f66 100644 --- a/proxy/trojan/validator.go +++ b/proxy/trojan/validator.go @@ -63,7 +63,7 @@ func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { // Get all users func (v *Validator) GetAll() []*protocol.MemoryUser { - var u = make([]*protocol.MemoryUser, 0, 100) + u := make([]*protocol.MemoryUser, 0, 100) v.email.Range(func(key, value interface{}) bool { u = append(u, value.(*protocol.MemoryUser)) return true diff --git a/proxy/tun/stack_gvisor.go b/proxy/tun/stack_gvisor.go index 73f65f192..8584616e9 100644 --- a/proxy/tun/stack_gvisor.go +++ b/proxy/tun/stack_gvisor.go @@ -69,7 +69,7 @@ func (t *stackGVisor) Start() error { tcpForwarder := tcp.NewForwarder(ipStack, 0, 65535, func(r *tcp.ForwarderRequest) { go func(r *tcp.ForwarderRequest) { var wq waiter.Queue - var id = r.ID() + id := r.ID() // Perform a TCP three-way handshake. ep, err := r.CreateEndpoint(&wq) diff --git a/proxy/tun/stack_gvisor_endpoint.go b/proxy/tun/stack_gvisor_endpoint.go index 31def35ea..26d8ecabd 100644 --- a/proxy/tun/stack_gvisor_endpoint.go +++ b/proxy/tun/stack_gvisor_endpoint.go @@ -68,7 +68,6 @@ func (e *LinkEndpoint) IsAttached() bool { } func (e *LinkEndpoint) Wait() { - } func (e *LinkEndpoint) ARPHardwareType() header.ARPHardwareType { @@ -91,7 +90,6 @@ func (e *LinkEndpoint) Close() { } func (e *LinkEndpoint) SetOnCloseAction(_ func()) { - } func (e *LinkEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index 955f36141..81eaece0e 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -44,8 +44,10 @@ type DarwinTun struct { ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) } -var _ Tun = (*DarwinTun)(nil) -var _ GVisorDevice = (*DarwinTun)(nil) +var ( + _ Tun = (*DarwinTun)(nil) + _ GVisorDevice = (*DarwinTun)(nil) +) func NewTun(options *Config) (Tun, error) { // Check if fd is provided via environment (iOS mode) diff --git a/proxy/tun/tun_default.go b/proxy/tun/tun_default.go index 24e6d08bc..3ab6c9610 100644 --- a/proxy/tun/tun_default.go +++ b/proxy/tun/tun_default.go @@ -9,8 +9,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" ) -type DefaultTun struct { -} +type DefaultTun struct{} // DefaultTun implements Tun var _ Tun = (*DefaultTun)(nil) diff --git a/proxy/tun/tun_freebsd.go b/proxy/tun/tun_freebsd.go index 8833a7532..07469ca2b 100644 --- a/proxy/tun/tun_freebsd.go +++ b/proxy/tun/tun_freebsd.go @@ -27,8 +27,10 @@ type FreeBSDTun struct { mtu uint32 } -var _ Tun = (*FreeBSDTun)(nil) -var _ GVisorDevice = (*FreeBSDTun)(nil) +var ( + _ Tun = (*FreeBSDTun)(nil) + _ GVisorDevice = (*FreeBSDTun)(nil) +) // NewTun builds new tun interface handler func NewTun(options *Config) (Tun, error) { diff --git a/proxy/vless/encoding/encoding_test.go b/proxy/vless/encoding/encoding_test.go index ae5588651..48a64797b 100644 --- a/proxy/vless/encoding/encoding_test.go +++ b/proxy/vless/encoding/encoding_test.go @@ -53,7 +53,7 @@ func TestRequestSerialization(t *testing.T) { } addonsComparer := func(x, y *Addons) bool { - return (x.Flow == y.Flow) && (cmp.Equal(x.Seed, y.Seed)) + return (x.Flow == y.Flow) && cmp.Equal(x.Seed, y.Seed) } if r := cmp.Diff(actualAddons, expectedAddons, cmp.Comparer(addonsComparer)); r != "" { t.Error(r) @@ -125,7 +125,7 @@ func TestMuxRequest(t *testing.T) { } addonsComparer := func(x, y *Addons) bool { - return (x.Flow == y.Flow) && (cmp.Equal(x.Seed, y.Seed)) + return (x.Flow == y.Flow) && cmp.Equal(x.Seed, y.Seed) } if r := cmp.Diff(actualAddons, expectedAddons, cmp.Comparer(addonsComparer)); r != "" { t.Error(r) diff --git a/proxy/vless/encryption/client.go b/proxy/vless/encryption/client.go index 54959d0ef..2eb43409d 100644 --- a/proxy/vless/encryption/client.go +++ b/proxy/vless/encryption/client.go @@ -79,7 +79,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { var nfsKey []byte var lastCTR cipher.Stream for j, k := range i.NfsPKeys { - var index = 32 + index := 32 if k, ok := k.(*ecdh.PublicKey); ok { privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader) copy(relays, privateKey.PublicKey().Bytes()) diff --git a/proxy/vless/encryption/server.go b/proxy/vless/encryption/server.go index e1d73716e..41c969c75 100644 --- a/proxy/vless/encryption/server.go +++ b/proxy/vless/encryption/server.go @@ -135,7 +135,7 @@ func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn if lastCTR != nil { lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay } - var index = 32 + index := 32 if _, ok := k.(*mlkem.DecapsulationKey768); ok { index = 1088 } diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 0301fb788..2d10a0060 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -630,9 +630,11 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s return r.NewMux(ctx, dispatcher.WrapLink(ctx, h.policyManager, h.stats, &transport.Link{Reader: clientReader, Writer: clientWriter}), h.observer) } - if err := dispatch.DispatchLink(ctx, request.Destination(), &transport.Link{ - Reader: clientReader, - Writer: clientWriter}, + if err := dispatch.DispatchLink( + ctx, request.Destination(), &transport.Link{ + Reader: clientReader, + Writer: clientWriter, + }, ); err != nil { return errors.New("failed to dispatch request").Base(err) } diff --git a/proxy/vless/validator.go b/proxy/vless/validator.go index 35a4469a0..fc5dc95e4 100644 --- a/proxy/vless/validator.go +++ b/proxy/vless/validator.go @@ -79,7 +79,7 @@ func (v *MemoryValidator) GetByEmail(email string) *protocol.MemoryUser { // Get all users func (v *MemoryValidator) GetAll() []*protocol.MemoryUser { - var u = make([]*protocol.MemoryUser, 0, 100) + u := make([]*protocol.MemoryUser, 0, 100) v.email.Range(func(key, value interface{}) bool { u = append(u, value.(*protocol.MemoryUser)) return true diff --git a/proxy/vmess/account.go b/proxy/vmess/account.go index df8ba52be..a7a1b78ef 100644 --- a/proxy/vmess/account.go +++ b/proxy/vmess/account.go @@ -1,9 +1,10 @@ package vmess import ( - "google.golang.org/protobuf/proto" "strings" + "google.golang.org/protobuf/proto" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/uuid" @@ -30,7 +31,7 @@ func (a *MemoryAccount) Equals(account protocol.Account) bool { } func (a *MemoryAccount) ToProto() proto.Message { - var test = "" + test := "" if a.AuthenticatedLengthExperiment { test = "AuthenticatedLength|" } diff --git a/proxy/vmess/inbound/inbound.go b/proxy/vmess/inbound/inbound.go index 6a8591ad6..e084a881e 100644 --- a/proxy/vmess/inbound/inbound.go +++ b/proxy/vmess/inbound/inbound.go @@ -138,7 +138,8 @@ func New(ctx context.Context, config *Config) (*Handler, error) { func (h *Handler) Close() error { return errors.Combine( h.sessionHistory.Close(), - common.Close(h.usersByEmail)) + common.Close(h.usersByEmail), + ) } // Network implements proxy.Inbound.Network(). diff --git a/proxy/vmess/outbound/command.go b/proxy/vmess/outbound/command.go index c284bfb6e..5e07b0f38 100644 --- a/proxy/vmess/outbound/command.go +++ b/proxy/vmess/outbound/command.go @@ -1,7 +1,6 @@ package outbound import ( - "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" ) diff --git a/proxy/vmess/outbound/outbound.go b/proxy/vmess/outbound/outbound.go index 4850e7287..903e73ce3 100644 --- a/proxy/vmess/outbound/outbound.go +++ b/proxy/vmess/outbound/outbound.go @@ -224,9 +224,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return nil } -var ( - enablePadding = false -) +var enablePadding = false func shouldEnablePadding(s protocol.SecurityType) bool { return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO diff --git a/proxy/wireguard/bind.go b/proxy/wireguard/bind.go index ddbc2178e..c5c0f8c8e 100644 --- a/proxy/wireguard/bind.go +++ b/proxy/wireguard/bind.go @@ -134,7 +134,6 @@ func (bind *netBindClient) connectTo(endpoint *netEndpoint) error { for { buff := buf.NewWithSize(device.MaxMessageSize) n, err := buff.ReadFrom(c) - if err != nil { buff.Release() endpoint.conn = nil diff --git a/proxy/wireguard/server.go b/proxy/wireguard/server.go index 1f358a381..876d749f7 100644 --- a/proxy/wireguard/server.go +++ b/proxy/wireguard/server.go @@ -162,7 +162,6 @@ func (s *Server) forwardConnection(dest net.Destination, conn net.Conn) { Reader: buf.NewReader(conn), Writer: buf.NewWriter(conn), }) - if err != nil { errors.LogInfoInner(ctx, err, "connection ends") } diff --git a/proxy/wireguard/server_test.go b/proxy/wireguard/server_test.go index 057b508ed..1cb4697ce 100644 --- a/proxy/wireguard/server_test.go +++ b/proxy/wireguard/server_test.go @@ -2,10 +2,11 @@ package wireguard_test import ( "context" - "github.com/stretchr/testify/assert" "runtime/debug" "testing" + "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/proxy/wireguard" ) diff --git a/proxy/wireguard/tun.go b/proxy/wireguard/tun.go index 4168e175a..2f9c1ab16 100644 --- a/proxy/wireguard/tun.go +++ b/proxy/wireguard/tun.go @@ -156,7 +156,7 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) { go func(r *tcp.ForwarderRequest) { var wq waiter.Queue - var id = r.ID() + id := r.ID() ep, err := r.CreateEndpoint(&wq) if err != nil { diff --git a/testing/scenarios/command_test.go b/testing/scenarios/command_test.go index 37686e2eb..8f82bb2b4 100644 --- a/testing/scenarios/command_test.go +++ b/testing/scenarios/command_test.go @@ -292,7 +292,8 @@ func TestCommanderListHandlers(t *testing.T) { protocmp.Transform(), cmpopts.SortSlices(func(a, b *core.InboundHandlerConfig) bool { return a.Tag < b.Tag - })); diff != "" { + }), + ); diff != "" { t.Fatalf("inbound response doesn't match config (-want +got):\n%s", diff) } @@ -308,7 +309,8 @@ func TestCommanderListHandlers(t *testing.T) { protocmp.Transform(), cmpopts.SortSlices(func(a, b *core.InboundHandlerConfig) bool { return a.Tag < b.Tag - })); diff != "" { + }), + ); diff != "" { t.Fatalf("outbound response doesn't match config (-want +got):\n%s", diff) } } @@ -467,7 +469,8 @@ func TestCommanderAddRemoveUser(t *testing.T) { Id: u2.String(), }), }, - }), + }, + ), }) common.Must(err) if resp == nil { diff --git a/transport/internet/browser_dialer/dialer.go b/transport/internet/browser_dialer/dialer.go index 3be712d7b..5705f9503 100644 --- a/transport/internet/browser_dialer/dialer.go +++ b/transport/internet/browser_dialer/dialer.go @@ -26,9 +26,11 @@ type task struct { StreamResponse bool `json:"streamResponse"` } -var conns chan *websocket.Conn -var server *http.Server -var mu sync.Mutex +var ( + conns chan *websocket.Conn + server *http.Server + mu sync.Mutex +) var upgrader = &websocket.Upgrader{ ReadBufferSize: 0, diff --git a/transport/internet/config.go b/transport/internet/config.go index 0b6f93245..460cdc4fa 100644 --- a/transport/internet/config.go +++ b/transport/internet/config.go @@ -8,9 +8,7 @@ import ( type ConfigCreator func() interface{} -var ( - globalTransportConfigCreatorCache = make(map[string]ConfigCreator) -) +var globalTransportConfigCreatorCache = make(map[string]ConfigCreator) var strategy = [][]byte{ // name strategy, prefer, fallback diff --git a/transport/internet/dialer.go b/transport/internet/dialer.go index 9342f26f5..475d0ff95 100644 --- a/transport/internet/dialer.go +++ b/transport/internet/dialer.go @@ -133,7 +133,6 @@ func redirect(ctx context.Context, dst net.Destination, obt string, h outbound.H cnc.ConnectionOnClose(common.ChainedClosable{uw, dw}), ) return nc - } func checkAddressPortStrategy(ctx context.Context, dest net.Destination, sockopt *SocketConfig) (*net.Destination, error) { diff --git a/transport/internet/finalmask/realm/client.go b/transport/internet/finalmask/realm/client.go index ab7ba6bb4..f2c0fb7c2 100644 --- a/transport/internet/finalmask/realm/client.go +++ b/transport/internet/finalmask/realm/client.go @@ -91,15 +91,15 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) { } func (c *realmConnClient) discover(servers []*net.UDPAddr) []netip.AddrPort { - var transactionIDs = make(map[[stun.TransactionIDSize]byte]struct{}, len(servers)) + transactionIDs := make(map[[stun.TransactionIDSize]byte]struct{}, len(servers)) for _, server := range servers { msg := common.Must2(stun.Build(stun.TransactionID, stun.BindingRequest)) transactionIDs[msg.TransactionID] = struct{}{} _, _ = c.PacketConn.WriteTo(msg.Raw, server) } - var buf = make([]byte, 1500) - var results = make([]netip.AddrPort, 0, len(servers)) + buf := make([]byte, 1500) + results := make([]netip.AddrPort, 0, len(servers)) c.PacketConn.SetReadDeadline(time.Now().Add(defaultSTUNTimeout)) for len(transactionIDs) > 0 { n, _, err := c.PacketConn.ReadFrom(buf) diff --git a/transport/internet/finalmask/realm/server.go b/transport/internet/finalmask/realm/server.go index 544bf49db..aacd4d0f5 100644 --- a/transport/internet/finalmask/realm/server.go +++ b/transport/internet/finalmask/realm/server.go @@ -16,9 +16,11 @@ import ( "github.com/xtls/xray-core/common/errors" ) -const defaultEventBuffer = 16 -const defaultStunCacheTTL = time.Second * 10 -const defaultHeartbeatInterval = time.Second * 15 +const ( + defaultEventBuffer = 16 + defaultStunCacheTTL = time.Second * 10 + defaultHeartbeatInterval = time.Second * 15 +) type PunchPacketEvent struct { Addr netip.AddrPort @@ -124,15 +126,15 @@ func (c *realmConnServer) waitctx(ctx context.Context, t time.Duration) bool { } func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort { - var transactionIDs = make(map[[stun.TransactionIDSize]byte]struct{}, len(servers)) + transactionIDs := make(map[[stun.TransactionIDSize]byte]struct{}, len(servers)) for _, server := range servers { msg := common.Must2(stun.Build(stun.TransactionID, stun.BindingRequest)) transactionIDs[msg.TransactionID] = struct{}{} _, _ = c.PacketConn.WriteTo(msg.Raw, server) } - var deadline = time.NewTimer(c.stunTimeout) - var results = make([]netip.AddrPort, 0, len(servers)) + deadline := time.NewTimer(c.stunTimeout) + results := make([]netip.AddrPort, 0, len(servers)) for len(transactionIDs) > 0 { select { case <-deadline.C: diff --git a/transport/internet/finalmask/realm/stun.go b/transport/internet/finalmask/realm/stun.go index 5ff59bb3b..07b07dc4b 100644 --- a/transport/internet/finalmask/realm/stun.go +++ b/transport/internet/finalmask/realm/stun.go @@ -35,8 +35,8 @@ func resolveSTUNServers(local net.IP, servers []string) []*net.UDPAddr { } } - var seen = make(map[string]struct{}) - var addrs = make([]*net.UDPAddr, 0, len(servers)) + seen := make(map[string]struct{}) + addrs := make([]*net.UDPAddr, 0, len(servers)) for _, server := range servers { h, p, err := net.SplitHostPort(server) if err != nil { @@ -116,8 +116,8 @@ func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[ break } } - var seen = make(map[netip.AddrPort]struct{}, len(peers)) - var candidates = make([]netip.AddrPort, 0, len(peers)) + seen := make(map[netip.AddrPort]struct{}, len(peers)) + candidates := make([]netip.AddrPort, 0, len(peers)) for _, peer := range peers { if _, ok := seen[peer]; ok { continue diff --git a/transport/internet/finalmask/sudoku/sudoku_test.go b/transport/internet/finalmask/sudoku/sudoku_test.go index f0780e2b1..1a01de923 100644 --- a/transport/internet/finalmask/sudoku/sudoku_test.go +++ b/transport/internet/finalmask/sudoku/sudoku_test.go @@ -934,7 +934,8 @@ func cloneConfig(cfg *Config) *Config { } func defaultApps(cfg *core.Config) *core.Config { - cfg.App = append(cfg.App, + cfg.App = append( + cfg.App, serial.ToTypedMessage(&log.Config{ ErrorLogLevel: clog.Severity_Warning, ErrorLogType: log.LogType_Console, diff --git a/transport/internet/finalmask/tcp_test.go b/transport/internet/finalmask/tcp_test.go index 4c07d9833..2ae888d0b 100644 --- a/transport/internet/finalmask/tcp_test.go +++ b/transport/internet/finalmask/tcp_test.go @@ -49,7 +49,7 @@ type layerMaskTcp struct { type failingWrapMask struct{} -func (failingWrapMask) TCP() {} +func (failingWrapMask) TCP() {} func (f failingWrapMask) WrapConnClient(raw net.Conn) (net.Conn, error) { return raw, nil } func (f failingWrapMask) WrapConnServer(raw net.Conn) (net.Conn, error) { return nil, io.ErrClosedPipe diff --git a/transport/internet/finalmask/udp_test.go b/transport/internet/finalmask/udp_test.go index c829d1add..8a507d2ec 100644 --- a/transport/internet/finalmask/udp_test.go +++ b/transport/internet/finalmask/udp_test.go @@ -123,10 +123,12 @@ func (c *scriptedPacketConn) SetDeadline(t time.Time) error { c.deadline.Store(t.UnixNano()) return nil } + func (c *scriptedPacketConn) SetReadDeadline(t time.Time) error { c.deadline.Store(t.UnixNano()) return nil } + func (c *scriptedPacketConn) SetWriteDeadline(t time.Time) error { c.deadline.Store(t.UnixNano()) return nil diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 7513a0a9d..557347f21 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -73,7 +73,7 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { } var resolverAddrs []*net.UDPAddr - var resolverSend = make(map[string]*atomic.Uint32) + resolverSend := make(map[string]*atomic.Uint32) for _, rs := range servers { h, p, err := net.SplitHostPort(rs) if err != nil { diff --git a/transport/internet/finalmask/xdns/dns_test.go b/transport/internet/finalmask/xdns/dns_test.go index 45da317d4..7eac084e6 100644 --- a/transport/internet/finalmask/xdns/dns_test.go +++ b/transport/internet/finalmask/xdns/dns_test.go @@ -35,19 +35,25 @@ func TestName(t *testing.T) { {[][]byte{[]byte("a"), {}, []byte("c")}, ErrZeroLengthLabel, ""}, // 63 octets. - {[][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE")}, nil, - "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"}, + { + [][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE")}, + nil, + "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE", + }, // 64 octets. {[][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDEF")}, ErrLabelTooLong, ""}, // 64+64+64+62 octets. - {[][]byte{ - []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), - []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), - []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), - []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"), - }, nil, - "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"}, + { + [][]byte{ + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"), + }, + nil, + "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC", + }, // 64+64+64+63 octets. {[][]byte{ []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), @@ -56,27 +62,269 @@ func TestName(t *testing.T) { []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCD"), }, ErrNameTooLong, ""}, // 127 one-octet labels. - {[][]byte{ - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, - }, nil, - "0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E"}, + { + [][]byte{ + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + }, + nil, + "0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E", + }, // 128 one-octet labels. {[][]byte{ - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, - {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'a'}, + {'b'}, + {'c'}, + {'d'}, + {'e'}, + {'f'}, + {'0'}, + {'1'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {'6'}, + {'7'}, + {'8'}, + {'9'}, + {'A'}, + {'B'}, + {'C'}, + {'D'}, + {'E'}, + {'F'}, }, ErrNameTooLong, ""}, } { // Test that NewName returns proper error codes, and otherwise diff --git a/transport/internet/grpc/dial.go b/transport/internet/grpc/dial.go index c8b8423c6..9ffbbfd1d 100644 --- a/transport/internet/grpc/dial.go +++ b/transport/internet/grpc/dial.go @@ -10,8 +10,8 @@ import ( c "github.com/xtls/xray-core/common/ctx" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/grpc/encoding" "github.com/xtls/xray-core/transport/internet/reality" diff --git a/transport/internet/happy_eyeballs.go b/transport/internet/happy_eyeballs.go index d7f30227a..08a8a0e3b 100644 --- a/transport/internet/happy_eyeballs.go +++ b/transport/internet/happy_eyeballs.go @@ -2,9 +2,10 @@ package internet import ( "context" + "time" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" - "time" ) type result struct { @@ -26,7 +27,7 @@ func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Po ips = sortIPs(ips, prioritizeIPv6, interleave) newCtx, cancel := context.WithCancel(ctx) defer cancel() - var resultCh = make(chan *result, len(ips)) + resultCh := make(chan *result, len(ips)) nextTryIndex := 0 activeNum := uint32(0) timer := time.NewTimer(0) @@ -101,8 +102,8 @@ func sortIPs(ips []net.IP, prioritizeIPv6 bool, interleave uint32) []net.IP { if len(ips) == 0 { return ips } - var ip4 = make([]net.IP, 0, len(ips)) - var ip6 = make([]net.IP, 0, len(ips)) + ip4 := make([]net.IP, 0, len(ips)) + ip6 := make([]net.IP, 0, len(ips)) for _, ip := range ips { parsedIp := net.IPAddress(ip).IP() if len(parsedIp) == net.IPv4len { @@ -116,7 +117,7 @@ func sortIPs(ips []net.IP, prioritizeIPv6 bool, interleave uint32) []net.IP { return ips } - var newIPs = make([]net.IP, 0, len(ips)) + newIPs := make([]net.IP, 0, len(ips)) consumeIP4 := 0 consumeIP6 := 0 consumeTurn := uint32(0) diff --git a/transport/internet/hysteria/dialer.go b/transport/internet/hysteria/dialer.go index dfaac92f2..3cca93563 100644 --- a/transport/internet/hysteria/dialer.go +++ b/transport/internet/hysteria/dialer.go @@ -317,8 +317,10 @@ func (m *clientManager) clean() { } } -var manager *clientManager -var initmanager sync.Once +var ( + manager *clientManager + initmanager sync.Once +) func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { tlsConfig := tls.ConfigFromStreamSettings(streamSettings) diff --git a/transport/internet/kcp/connection.go b/transport/internet/kcp/connection.go index f31e54f0b..72e3229ab 100644 --- a/transport/internet/kcp/connection.go +++ b/transport/internet/kcp/connection.go @@ -237,12 +237,14 @@ func NewConnection(meta ConnMetadata, writer io.Writer, closer io.Closer, config return !isTerminating() && (conn.sendingWorker.UpdateNecessary() || conn.receivingWorker.UpdateNecessary()) }, isTerminating, - conn.updateTask) + conn.updateTask, + ) conn.pingUpdater = NewUpdater( 5000, // 5 seconds func() bool { return !isTerminated() }, isTerminated, - conn.updateTask) + conn.updateTask, + ) conn.pingUpdater.WakeUp() return conn diff --git a/transport/internet/kcp/connection_test.go b/transport/internet/kcp/connection_test.go index cf1a529d6..702d273a4 100644 --- a/transport/internet/kcp/connection_test.go +++ b/transport/internet/kcp/connection_test.go @@ -36,8 +36,8 @@ func TestConnectionReadTimeout(t *testing.T) { } func TestConnectionInterface(t *testing.T) { - _ = (io.Writer)(new(Connection)) - _ = (io.Reader)(new(Connection)) - _ = (buf.Reader)(new(Connection)) - _ = (buf.Writer)(new(Connection)) + _ = io.Writer(new(Connection)) + _ = io.Reader(new(Connection)) + _ = buf.Reader(new(Connection)) + _ = buf.Writer(new(Connection)) } diff --git a/transport/internet/reality/config.go b/transport/internet/reality/config.go index 9bc6ee151..0b26621d8 100644 --- a/transport/internet/reality/config.go +++ b/transport/internet/reality/config.go @@ -63,7 +63,7 @@ func KeyLogWriterFromConfig(c *Config) io.Writer { return nil } - writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) + writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") } diff --git a/transport/internet/sockopt_darwin.go b/transport/internet/sockopt_darwin.go index 469957114..73a85d8ff 100644 --- a/transport/internet/sockopt_darwin.go +++ b/transport/internet/sockopt_darwin.go @@ -135,7 +135,6 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf if config.Interface != "" { iface, err := net.InterfaceByName(config.Interface) - if err != nil { return errors.New("failed to get interface ", config.Interface).Base(err) } @@ -163,7 +162,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf if !strings.HasPrefix(network, custom.Network) { continue } - var level = 0x6 // default TCP + level := 0x6 // default TCP var opt int if len(custom.Opt) == 0 { return errors.New("No opt!") @@ -226,7 +225,6 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) if config.Interface != "" { iface, err := net.InterfaceByName(config.Interface) - if err != nil { return errors.New("failed to get interface ", config.Interface).Base(err) } @@ -260,7 +258,7 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) if !strings.HasPrefix(network, custom.Network) { continue } - var level = 0x6 // default TCP + level := 0x6 // default TCP var opt int if len(custom.Opt) == 0 { return errors.New("No opt!") diff --git a/transport/internet/sockopt_linux.go b/transport/internet/sockopt_linux.go index f1fb3f81c..bf100350e 100644 --- a/transport/internet/sockopt_linux.go +++ b/transport/internet/sockopt_linux.go @@ -76,7 +76,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf if !strings.HasPrefix(network, custom.Network) { continue } - var level = 0x6 // default TCP + level := 0x6 // default TCP var opt int if len(custom.Opt) == 0 { return errors.New("No opt!") @@ -182,7 +182,7 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) if !strings.HasPrefix(network, custom.Network) { continue } - var level = 0x6 // default TCP + level := 0x6 // default TCP var opt int if len(custom.Opt) == 0 { return errors.New("No opt!") diff --git a/transport/internet/sockopt_windows.go b/transport/internet/sockopt_windows.go index 3555b3999..dd1df0bd2 100644 --- a/transport/internet/sockopt_windows.go +++ b/transport/internet/sockopt_windows.go @@ -94,7 +94,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf if !strings.HasPrefix(network, custom.Network) { continue } - var level = 0x6 // default TCP + level := 0x6 // default TCP var opt int if len(custom.Opt) == 0 { return errors.New("No opt!") @@ -155,7 +155,7 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) if !strings.HasPrefix(network, custom.Network) { continue } - var level = 0x6 // default TCP + level := 0x6 // default TCP var opt int if len(custom.Opt) == 0 { return errors.New("No opt!") diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index 9744a9857..535549bf4 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -344,7 +344,6 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req Payload: payload, Seq: seq, }) - if err != nil { errors.LogInfoInner(context.Background(), err, "failed to upload (PushPayload)") writer.WriteHeader(http.StatusInternalServerError) @@ -604,6 +603,7 @@ func (ln *Listener) Close() error { } return errors.New("listener does not have an HTTP/3 server or a net.listener") } + func getTLSConfig(streamSettings *internet.MemoryStreamConfig) *gotls.Config { config := tls.ConfigFromStreamSettings(streamSettings) if config == nil { @@ -611,6 +611,7 @@ func getTLSConfig(streamSettings *internet.MemoryStreamConfig) *gotls.Config { } return config.GetTLSConfig() } + func init() { common.Must(internet.RegisterTransportListener(protocolName, ListenXH)) } diff --git a/transport/internet/system_dialer.go b/transport/internet/system_dialer.go index 35dc55fa8..05adc778a 100644 --- a/transport/internet/system_dialer.go +++ b/transport/internet/system_dialer.go @@ -12,9 +12,11 @@ import ( "github.com/xtls/xray-core/features/outbound" ) -var Controllers []func(network, address string, c syscall.RawConn) error -var ControllersLock sync.Mutex -var effectiveSystemDialer SystemDialer = &DefaultSystemDialer{} +var ( + Controllers []func(network, address string, c syscall.RawConn) error + ControllersLock sync.Mutex + effectiveSystemDialer SystemDialer = &DefaultSystemDialer{} +) type SystemDialer interface { Dial(ctx context.Context, source net.Address, destination net.Destination, sockopt *SocketConfig) (net.Conn, error) diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index cfb360c0a..0c8878045 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -459,7 +459,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { } if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" { - writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) + writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") } else { diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 1a691f97a..9de124c1c 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -326,9 +326,7 @@ func ConvertToGoECHKeys(data []byte) ([]tls.EncryptedClientHelloKey, error) { return keys, ErrInvalidLen } child := cryptobyte.String(s[:2+keyLength+2+configLength]) - var ( - sk, config cryptobyte.String - ) + var sk, config cryptobyte.String if !child.ReadUint16LengthPrefixed(&sk) || !child.ReadUint16LengthPrefixed(&config) || !child.Empty() { return keys, ErrInvalidLen } diff --git a/transport/internet/tls/ech_test.go b/transport/internet/tls/ech_test.go index 69db965b4..ed67cedec 100644 --- a/transport/internet/tls/ech_test.go +++ b/transport/internet/tls/ech_test.go @@ -44,7 +44,6 @@ func TestECHDial(t *testing.T) { echConfigCache, ok := GlobalECHConfigCache.Load(ECHCacheKey("udp://1.1.1.1", "encryptedsni.com", nil)) if !ok { t.Error("ECH config cache not found") - } ok = echConfigCache.UpdateLock.TryLock() if !ok { diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index 7fa3c25be..f9d368d8c 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -22,8 +22,10 @@ type Interface interface { NegotiatedProtocol() string } -var _ buf.Writer = (*Conn)(nil) -var _ Interface = (*Conn)(nil) +var ( + _ buf.Writer = (*Conn)(nil) + _ Interface = (*Conn)(nil) +) type Conn struct { *tls.Conn diff --git a/transport/pipe/pipe.go b/transport/pipe/pipe.go index f4b783036..6343adb7f 100644 --- a/transport/pipe/pipe.go +++ b/transport/pipe/pipe.go @@ -59,7 +59,7 @@ func New(opts ...Option) (*Reader, *Writer) { } for _, opt := range opts { - opt(&(p.option)) + opt(&p.option) } return &Reader{ diff --git a/transport/pipe/pipe_test.go b/transport/pipe/pipe_test.go index a5cb25c58..79e369aab 100644 --- a/transport/pipe/pipe_test.go +++ b/transport/pipe/pipe_test.go @@ -129,12 +129,12 @@ func TestPipeWriteMultiThread(t *testing.T) { } func TestInterfaces(t *testing.T) { - _ = (buf.Reader)(new(Reader)) - _ = (buf.TimeoutReader)(new(Reader)) + _ = buf.Reader(new(Reader)) + _ = buf.TimeoutReader(new(Reader)) - _ = (common.Interruptible)(new(Reader)) - _ = (common.Interruptible)(new(Writer)) - _ = (common.Closable)(new(Writer)) + _ = common.Interruptible(new(Reader)) + _ = common.Interruptible(new(Writer)) + _ = common.Closable(new(Writer)) } func BenchmarkPipeReadWrite(b *testing.B) { From ba538619eb8a21ec0e44fc97357ac9e142bbd3bd Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Sat, 30 May 2026 17:30:23 +0800 Subject: [PATCH 26/78] Realm finalmask: Fix client punch peers (#6213) Fixes https://github.com/XTLS/Xray-core/pull/6137 --- transport/internet/finalmask/realm/client.go | 2 +- transport/internet/finalmask/realm/stun.go | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/transport/internet/finalmask/realm/client.go b/transport/internet/finalmask/realm/client.go index f2c0fb7c2..5b2354a58 100644 --- a/transport/internet/finalmask/realm/client.go +++ b/transport/internet/finalmask/realm/client.go @@ -80,7 +80,7 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) { } start = time.Now() - peer, err := c.punch(meta, peers) + peer, err := c.punch(meta, expandedPeers) if err != nil { return nil, errors.New("punch fail").Base(err) } diff --git a/transport/internet/finalmask/realm/stun.go b/transport/internet/finalmask/realm/stun.go index 07b07dc4b..f490cbe03 100644 --- a/transport/internet/finalmask/realm/stun.go +++ b/transport/internet/finalmask/realm/stun.go @@ -167,7 +167,9 @@ func expandSymmetricNATCandidates(candidates []netip.AddrPort, seen map[netip.Ad added++ } } - sortAddrPorts(candidates) + slices.SortFunc(candidates, func(a, b netip.AddrPort) int { + return strings.Compare(a.String(), b.String()) + }) return candidates } @@ -197,12 +199,6 @@ func predictablePortGroup(ports []uint16) bool { return true } -func sortAddrPorts(addrs []netip.AddrPort) { - slices.SortFunc(addrs, func(a, b netip.AddrPort) int { - return strings.Compare(a.String(), b.String()) - }) -} - func addrPortStrings(addrs []netip.AddrPort) []string { out := make([]string, 0, len(addrs)) for _, addr := range addrs { From 455f6bc2d5915be0465d66fe6d7d06974c2729d3 Mon Sep 17 00:00:00 2001 From: Davoyan <20373889+Davoyan@users.noreply.github.com> Date: Sat, 30 May 2026 20:29:33 +0300 Subject: [PATCH 27/78] uTLS: Update `ModernFingerprints` map and `OtherFingerprints` map (#6181) Paying attention to https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533 And a real issue: https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4583018017 --- transport/internet/tls/tls.go | 75 ++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index f9d368d8c..df5d1cbd7 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -218,54 +218,57 @@ var PresetFingerprints = map[string]*utls.ClientHelloID{ var ModernFingerprints = map[string]*utls.ClientHelloID{ // One of these will be chosen as `random` at startup + "hellofirefox_120": &utls.HelloFirefox_120, + "hellofirefox_148": &utls.HelloFirefox_148, + "hellochrome_120": &utls.HelloChrome_120, + "hellochrome_131": &utls.HelloChrome_131, + "hellochrome_133": &utls.HelloChrome_133, + "helloios_13": &utls.HelloIOS_13, + "helloios_14": &utls.HelloIOS_14, + "helloedge_106": &utls.HelloEdge_106, + "hellosafari_26_3": &utls.HelloSafari_26_3, + "hello360_11_0": &utls.Hello360_11_0, + "helloqq_11_1": &utls.HelloQQ_11_1, +} + +var OtherFingerprints = map[string]*utls.ClientHelloID{ + // Golang, randomized, auto, and fingerprints that are too old + "hellogolang": &utls.HelloGolang, + "hellorandomized": &utls.HelloRandomized, + "hellorandomizedalpn": &utls.HelloRandomizedALPN, + "hellorandomizednoalpn": &utls.HelloRandomizedNoALPN, + "hellofirefox_auto": &utls.HelloFirefox_Auto, + "hellofirefox_55": &utls.HelloFirefox_55, + "hellofirefox_56": &utls.HelloFirefox_56, + "hellofirefox_63": &utls.HelloFirefox_63, + "hellofirefox_65": &utls.HelloFirefox_65, "hellofirefox_99": &utls.HelloFirefox_99, "hellofirefox_102": &utls.HelloFirefox_102, "hellofirefox_105": &utls.HelloFirefox_105, - "hellofirefox_120": &utls.HelloFirefox_120, + "hellochrome_auto": &utls.HelloChrome_Auto, + "hellochrome_58": &utls.HelloChrome_58, + "hellochrome_62": &utls.HelloChrome_62, + "hellochrome_70": &utls.HelloChrome_70, + "hellochrome_72": &utls.HelloChrome_72, "hellochrome_83": &utls.HelloChrome_83, "hellochrome_87": &utls.HelloChrome_87, "hellochrome_96": &utls.HelloChrome_96, "hellochrome_100": &utls.HelloChrome_100, "hellochrome_102": &utls.HelloChrome_102, "hellochrome_106_shuffle": &utls.HelloChrome_106_Shuffle, - "hellochrome_120": &utls.HelloChrome_120, - "hellochrome_131": &utls.HelloChrome_131, - "helloios_13": &utls.HelloIOS_13, - "helloios_14": &utls.HelloIOS_14, + "helloios_auto": &utls.HelloIOS_Auto, + "helloios_11_1": &utls.HelloIOS_11_1, + "helloios_12_1": &utls.HelloIOS_12_1, + "helloandroid_11_okhttp": &utls.HelloAndroid_11_OkHttp, "helloedge_85": &utls.HelloEdge_85, - "helloedge_106": &utls.HelloEdge_106, + "helloedge_auto": &utls.HelloEdge_Auto, "hellosafari_16_0": &utls.HelloSafari_16_0, - "hello360_11_0": &utls.Hello360_11_0, - "helloqq_11_1": &utls.HelloQQ_11_1, -} + "hellosafari_auto": &utls.HelloSafari_Auto, + "hello360_auto": &utls.Hello360_Auto, + "hello360_7_5": &utls.Hello360_7_5, + "helloqq_auto": &utls.HelloQQ_Auto, -var OtherFingerprints = map[string]*utls.ClientHelloID{ - // Golang, randomized, auto, and fingerprints that are too old - "hellogolang": &utls.HelloGolang, - "hellorandomized": &utls.HelloRandomized, - "hellorandomizedalpn": &utls.HelloRandomizedALPN, - "hellorandomizednoalpn": &utls.HelloRandomizedNoALPN, - "hellofirefox_auto": &utls.HelloFirefox_Auto, - "hellofirefox_55": &utls.HelloFirefox_55, - "hellofirefox_56": &utls.HelloFirefox_56, - "hellofirefox_63": &utls.HelloFirefox_63, - "hellofirefox_65": &utls.HelloFirefox_65, - "hellochrome_auto": &utls.HelloChrome_Auto, - "hellochrome_58": &utls.HelloChrome_58, - "hellochrome_62": &utls.HelloChrome_62, - "hellochrome_70": &utls.HelloChrome_70, - "hellochrome_72": &utls.HelloChrome_72, - "helloios_auto": &utls.HelloIOS_Auto, - "helloios_11_1": &utls.HelloIOS_11_1, - "helloios_12_1": &utls.HelloIOS_12_1, - "helloandroid_11_okhttp": &utls.HelloAndroid_11_OkHttp, - "helloedge_auto": &utls.HelloEdge_Auto, - "hellosafari_auto": &utls.HelloSafari_Auto, - "hello360_auto": &utls.Hello360_Auto, - "hello360_7_5": &utls.Hello360_7_5, - "helloqq_auto": &utls.HelloQQ_Auto, - - // Chrome betas' + // Chrome betas "hellochrome_100_psk": &utls.HelloChrome_100_PSK, "hellochrome_112_psk_shuf": &utls.HelloChrome_112_PSK_Shuf, "hellochrome_114_padding_psk_shuf": &utls.HelloChrome_114_Padding_PSK_Shuf, From cb8cd048c12f902b673a0e4ed6e4c51017a85439 Mon Sep 17 00:00:00 2001 From: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:25:47 +0800 Subject: [PATCH 28/78] DNS outbound: Replace "reject" with "return" (`rCode` is 0 by default) (#6214) https://github.com/XTLS/Xray-core/pull/6214#issuecomment-4587988752 Example: https://github.com/XTLS/Xray-core/pull/6214#issue-4553786283 --------- Co-authored-by: Meo597 <197331664+Meo597@users.noreply.github.com> --- infra/conf/common.go | 3 +- infra/conf/dns_proxy.go | 42 +++++++++--------- infra/conf/dns_proxy_test.go | 43 ++++++++++-------- proxy/dns/config.pb.go | 31 ++++++++----- proxy/dns/config.proto | 5 ++- proxy/dns/dns.go | 84 ++++++++++++++++++------------------ proxy/dns/dns_test.go | 7 +-- 7 files changed, 116 insertions(+), 99 deletions(-) diff --git a/infra/conf/common.go b/infra/conf/common.go index ab3cfba79..0de3568cd 100644 --- a/infra/conf/common.go +++ b/infra/conf/common.go @@ -3,6 +3,7 @@ package conf import ( "encoding/json" "fmt" + "math" "strconv" "strings" @@ -199,7 +200,7 @@ func (v *PortRange) UnmarshalJSON(data []byte) error { if err == nil { v.From = uint32(from) v.To = uint32(to) - if v.From > v.To { + if v.From > v.To || v.To > math.MaxUint16 { return errors.New("invalid port range ", v.From, " -> ", v.To) } return nil diff --git a/infra/conf/dns_proxy.go b/infra/conf/dns_proxy.go index 734175e33..aabdea86c 100644 --- a/infra/conf/dns_proxy.go +++ b/infra/conf/dns_proxy.go @@ -12,8 +12,9 @@ import ( type DNSOutboundRuleConfig struct { Action string `json:"action"` - QType *PortList `json:"qtype"` + QType *PortList `json:"qType"` Domain *StringList `json:"domain"` + RCode uint32 `json:"rCode"` } func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { @@ -24,8 +25,8 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { rule.Action = dns.RuleAction_Direct case "drop": rule.Action = dns.RuleAction_Drop - case "reject": - rule.Action = dns.RuleAction_Reject + case "return": + rule.Action = dns.RuleAction_Return case "hijack": rule.Action = dns.RuleAction_Hijack default: @@ -34,14 +35,8 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { if c.QType != nil { for _, r := range c.QType.Range { - if r.From > r.To { - return nil, errors.New("invalid qtype range: ", r.String()) - } - if r.To > 65535 { - return nil, errors.New("dns rule qtype out of range: ", r.String()) - } - for qtype := r.From; qtype <= r.To; qtype++ { - rule.Qtype = append(rule.Qtype, int32(qtype)) + for qType := r.From; qType <= r.To; qType++ { + rule.QType = append(rule.QType, int32(qType)) } } } @@ -54,6 +49,11 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { rule.Domain = rules } + if c.RCode > 65535 { + return nil, errors.New("rCode out of range: ", c.RCode) + } + rule.RCode = c.RCode + return rule, nil } @@ -133,28 +133,30 @@ func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) if c.BlockTypes != nil && len(*c.BlockTypes) > 0 { rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop} if mode == "reject" { - rule.Action = dns.RuleAction_Reject + rule.Action = dns.RuleAction_Return + rule.RCode = 5 } - for _, qtype := range *c.BlockTypes { - if qtype < 0 || qtype > 65535 { - return nil, errors.New("legacy blockTypes qtype out of range: ", qtype) + for _, qType := range *c.BlockTypes { + if qType < 0 || qType > 65535 { + return nil, errors.New("legacy blockTypes qType out of range: ", qType) } - rule.Qtype = append(rule.Qtype, qtype) + rule.QType = append(rule.QType, qType) } rules = append(rules, rule) } { rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack} - rule.Qtype = append(rule.Qtype, 1) - rule.Qtype = append(rule.Qtype, 28) + rule.QType = append(rule.QType, 1) + rule.QType = append(rule.QType, 28) rules = append(rules, rule) } { - rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject} + rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Return} if mode == "reject" { - rule.Action = dns.RuleAction_Reject + rule.Action = dns.RuleAction_Return + rule.RCode = 5 } else if mode == "drop" { rule.Action = dns.RuleAction_Drop } else if mode == "skip" { diff --git a/infra/conf/dns_proxy_test.go b/infra/conf/dns_proxy_test.go index 1656e2d6e..b88b0cfef 100644 --- a/infra/conf/dns_proxy_test.go +++ b/infra/conf/dns_proxy_test.go @@ -35,10 +35,10 @@ func TestDnsProxyConfig(t *testing.T) { Input: `{ "rules": [{ "action": "direct", - "qtype": "1,3,23-24" + "qType": "1,3,23-24" }, { "action": "drop", - "qtype": 28, + "qType": 28, "domain": ["domain:example.com", "full:example.com"] }] }`, @@ -48,11 +48,11 @@ func TestDnsProxyConfig(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Direct, - Qtype: []int32{1, 3, 23, 24}, + QType: []int32{1, 3, 23, 24}, }, { Action: dns.RuleAction_Drop, - Qtype: []int32{28}, + QType: []int32{28}, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -78,7 +78,8 @@ func TestDnsProxyConfig(t *testing.T) { { Input: `{ "rules": [{ - "action": "reject", + "action": "return", + "rCode": 5, "domain": "keyword:example" }] }`, @@ -87,7 +88,8 @@ func TestDnsProxyConfig(t *testing.T) { RewriteServer: &net.Endpoint{}, Rule: []*dns.DNSRuleConfig{ { - Action: dns.RuleAction_Reject, + Action: dns.RuleAction_Return, + RCode: 5, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -106,7 +108,7 @@ func TestDnsProxyConfig(t *testing.T) { Input: `{ "rules": [{ "action": "drop", - "qtype": 257 + "qType": 257 }] }`, Parser: loadJSON(creator), @@ -115,7 +117,7 @@ func TestDnsProxyConfig(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Drop, - Qtype: []int32{257}, + QType: []int32{257}, }, }, }, @@ -140,10 +142,11 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { - Action: dns.RuleAction_Reject, + Action: dns.RuleAction_Return, + RCode: 5, }, }, }, @@ -157,15 +160,17 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { RewriteServer: &net.Endpoint{}, Rule: []*dns.DNSRuleConfig{ { - Action: dns.RuleAction_Reject, - Qtype: []int32{1, 65}, + Action: dns.RuleAction_Return, + QType: []int32{1, 65}, + RCode: 5, }, { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { - Action: dns.RuleAction_Reject, + Action: dns.RuleAction_Return, + RCode: 5, }, }, }, @@ -181,11 +186,11 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Drop, - Qtype: []int32{1}, + QType: []int32{1}, }, { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { Action: dns.RuleAction_Drop, @@ -204,11 +209,11 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Drop, - Qtype: []int32{65, 28}, + QType: []int32{65, 28}, }, { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { Action: dns.RuleAction_Direct, @@ -228,7 +233,7 @@ func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) { _, err := loadJSON(creator)(`{ "rules": [{ "action": "direct", - "qtype": 65 + "qType": 65 }], "blockTypes": [65] }`) diff --git a/proxy/dns/config.pb.go b/proxy/dns/config.pb.go index b8712719a..249fd0222 100644 --- a/proxy/dns/config.pb.go +++ b/proxy/dns/config.pb.go @@ -28,7 +28,7 @@ type RuleAction int32 const ( RuleAction_Direct RuleAction = 0 RuleAction_Drop RuleAction = 1 - RuleAction_Reject RuleAction = 2 + RuleAction_Return RuleAction = 2 RuleAction_Hijack RuleAction = 3 ) @@ -37,13 +37,13 @@ var ( RuleAction_name = map[int32]string{ 0: "Direct", 1: "Drop", - 2: "Reject", + 2: "Return", 3: "Hijack", } RuleAction_value = map[string]int32{ "Direct": 0, "Drop": 1, - "Reject": 2, + "Return": 2, "Hijack": 3, } ) @@ -78,8 +78,9 @@ func (RuleAction) EnumDescriptor() ([]byte, []int) { type DNSRuleConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.dns.RuleAction" json:"action,omitempty"` - Qtype []int32 `protobuf:"varint,2,rep,packed,name=qtype,proto3" json:"qtype,omitempty"` + QType []int32 `protobuf:"varint,2,rep,packed,name=q_type,json=qType,proto3" json:"q_type,omitempty"` Domain []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domain,proto3" json:"domain,omitempty"` + RCode uint32 `protobuf:"varint,4,opt,name=r_code,json=rCode,proto3" json:"r_code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -121,9 +122,9 @@ func (x *DNSRuleConfig) GetAction() RuleAction { return RuleAction_Direct } -func (x *DNSRuleConfig) GetQtype() []int32 { +func (x *DNSRuleConfig) GetQType() []int32 { if x != nil { - return x.Qtype + return x.QType } return nil } @@ -135,6 +136,13 @@ func (x *DNSRuleConfig) GetDomain() []*geodata.DomainRule { return nil } +func (x *DNSRuleConfig) GetRCode() uint32 { + if x != nil { + return x.RCode + } + return 0 +} + type Config struct { state protoimpl.MessageState `protogen:"open.v1"` UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` @@ -199,11 +207,12 @@ var File_proxy_dns_config_proto protoreflect.FileDescriptor const file_proxy_dns_config_proto_rawDesc = "" + "\n" + - "\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" + + "\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\xaa\x01\n" + "\rDNSRuleConfig\x122\n" + - "\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" + - "\x05qtype\x18\x02 \x03(\x05R\x05qtype\x127\n" + - "\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\"\x9c\x01\n" + + "\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x15\n" + + "\x06q_type\x18\x02 \x03(\x05R\x05qType\x127\n" + + "\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12\x15\n" + + "\x06r_code\x18\x04 \x01(\rR\x05rCode\"\x9c\x01\n" + "\x06Config\x12\x1d\n" + "\n" + "user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" + @@ -215,7 +224,7 @@ const file_proxy_dns_config_proto_rawDesc = "" + "\x06Direct\x10\x00\x12\b\n" + "\x04Drop\x10\x01\x12\n" + "\n" + - "\x06Reject\x10\x02\x12\n" + + "\x06Return\x10\x02\x12\n" + "\n" + "\x06Hijack\x10\x03BL\n" + "\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3" diff --git a/proxy/dns/config.proto b/proxy/dns/config.proto index 3be782d19..cfa3e28d3 100644 --- a/proxy/dns/config.proto +++ b/proxy/dns/config.proto @@ -12,14 +12,15 @@ import "common/geodata/geodat.proto"; enum RuleAction { Direct = 0; Drop = 1; - Reject = 2; + Return = 2; Hijack = 3; } message DNSRuleConfig { RuleAction action = 1; - repeated int32 qtype = 2; + repeated int32 q_type = 2; repeated xray.common.geodata.DomainRule domain = 3; + uint32 r_code = 4; } message Config { diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go index 2d678dd2b..efeb56cda 100644 --- a/proxy/dns/dns.go +++ b/proxy/dns/dns.go @@ -45,6 +45,7 @@ type DNSRule struct { action RuleAction qTypes []uint16 domains geodata.DomainMatcher + rCode dnsmessage.RCode } func (r *DNSRule) matchQType(qType uint16) bool { @@ -95,9 +96,10 @@ func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager polic for _, r := range config.Rule { rule := &DNSRule{ action: r.Action, - qTypes: make([]uint16, 0, len(r.Qtype)), + qTypes: make([]uint16, 0, len(r.QType)), + rCode: dnsmessage.RCode(r.RCode), } - for _, t := range r.Qtype { + for _, t := range r.QType { rule.qTypes = append(rule.qTypes, uint16(t)) } if len(r.Domain) > 0 { @@ -136,17 +138,17 @@ func parseQuery(b []byte) (id uint16, qType dnsmessage.Type, domain string, ok b return } -func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction { +func (h *Handler) applyRules(qType dnsmessage.Type, domain string) (RuleAction, dnsmessage.RCode) { qCode := uint16(qType) for _, r := range h.rules { if r.Apply(qCode, domain) { - return r.action + return r.action, r.rCode } } if qType == dnsmessage.TypeA || qType == dnsmessage.TypeAAAA { - return RuleAction_Hijack + return RuleAction_Hijack, dnsmessage.RCodeSuccess } - return RuleAction_Reject + return RuleAction_Return, dnsmessage.RCodeSuccess } // Process implements proxy.Outbound. @@ -213,7 +215,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. } if session.TimeoutOnlyFromContext(ctx) { - ctx, _ = context.WithCancel(context.Background()) + ctx = context.Background() } ctx, cancel := context.WithCancel(ctx) @@ -250,21 +252,22 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. continue } - switch h.applyRules(qType, domain) { + action, rCode := h.applyRules(qType, domain) + switch action { case RuleAction_Drop: b.Release() errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain) - case RuleAction_Reject: + case RuleAction_Return: b.Release() errors.LogInfo(ctx, "rejected type ", qType, " query for domain ", domain) - if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil { + if err := h.rejectNonIPQuery(id, qType, domain, writer, rCode); err != nil { return err } case RuleAction_Hijack: b.Release() if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA { errors.LogError(ctx, "can only hijack A/AAAA records") - if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil { + if err := h.rejectNonIPQuery(id, qType, domain, writer, rCode); err != nil { return err } } else { @@ -309,48 +312,35 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter, timer *signal.ActivityTimer) { var ips []net.IP + var ttl uint32 var err error - var ttl4 uint32 - var ttl6 uint32 - switch qType { case dnsmessage.TypeA: - ips, ttl4, err = h.client.LookupIP(domain, dns.IPOption{ + ips, ttl, err = h.client.LookupIP(domain, dns.IPOption{ IPv4Enable: true, IPv6Enable: false, FakeEnable: true, }) case dnsmessage.TypeAAAA: - ips, ttl6, err = h.client.LookupIP(domain, dns.IPOption{ + ips, ttl, err = h.client.LookupIP(domain, dns.IPOption{ IPv4Enable: false, IPv6Enable: true, FakeEnable: true, }) } - rcode := dns.RCodeFromError(err) - if rcode == 0 && len(ips) == 0 && !go_errors.Is(err, dns.ErrEmptyResponse) { + rCode := dns.RCodeFromError(err) + if rCode == 0 && len(ips) == 0 && !go_errors.Is(err, dns.ErrEmptyResponse) { errors.LogInfoInner(context.Background(), err, "ip query") return } - switch qType { - case dnsmessage.TypeA: - for i, ip := range ips { - ips[i] = ip.To4() - } - case dnsmessage.TypeAAAA: - for i, ip := range ips { - ips[i] = ip.To16() - } - } - b := buf.New() rawBytes := b.Extend(buf.Size) builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ ID: id, - RCode: dnsmessage.RCode(rcode), + RCode: dnsmessage.RCode(rCode), RecursionAvailable: true, RecursionDesired: true, Response: true, @@ -365,17 +355,25 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, })) common.Must(builder.StartAnswers()) - rHeader4 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl4} - rHeader6 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl6} - for _, ip := range ips { - if len(ip) == net.IPv4len { - var r dnsmessage.AResource - copy(r.A[:], ip) - common.Must(builder.AResource(rHeader4, r)) - } else { - var r dnsmessage.AAAAResource - copy(r.AAAA[:], ip) - common.Must(builder.AAAAResource(rHeader6, r)) + rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl} + switch qType { + case dnsmessage.TypeA: + for _, ip := range ips { + ip = ip.To4() + if len(ip) == net.IPv4len { + var r dnsmessage.AResource + copy(r.A[:], ip) + common.Must(builder.AResource(rHeader, r)) + } + } + case dnsmessage.TypeAAAA: + for _, ip := range ips { + ip = ip.To16() + if len(ip) == net.IPv6len { + var r dnsmessage.AAAAResource + copy(r.AAAA[:], ip) + common.Must(builder.AAAAResource(rHeader, r)) + } } } msgBytes, err := builder.Finish() @@ -392,7 +390,7 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, } } -func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter) error { +func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter, rCode dnsmessage.RCode) error { domainT := strings.TrimSuffix(domain, ".") if domainT == "" { return errors.New("empty domain name") @@ -401,7 +399,7 @@ func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain stri rawBytes := b.Extend(buf.Size) builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ ID: id, - RCode: dnsmessage.RCodeRefused, + RCode: rCode, RecursionAvailable: true, RecursionDesired: true, Response: true, diff --git a/proxy/dns/dns_test.go b/proxy/dns/dns_test.go index 6e4b468e1..f93ff8dd0 100644 --- a/proxy/dns/dns_test.go +++ b/proxy/dns/dns_test.go @@ -424,7 +424,7 @@ func TestDNSRules(t *testing.T) { ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{ Rule: []*dns_proxy.DNSRuleConfig{ { - Qtype: []int32{int32(dns.TypeA)}, + QType: []int32{int32(dns.TypeA)}, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -438,7 +438,7 @@ func TestDNSRules(t *testing.T) { Action: dns_proxy.RuleAction_Direct, }, { - Qtype: []int32{int32(dns.TypeA)}, + QType: []int32{int32(dns.TypeA)}, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -449,7 +449,8 @@ func TestDNSRules(t *testing.T) { }, }, }, - Action: dns_proxy.RuleAction_Reject, + Action: dns_proxy.RuleAction_Return, + RCode: 5, }, }, }), From c4dfcd4c1a998cc7eb5be61ef7c2e5dbb65dfcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Mon, 1 Jun 2026 09:30:40 +0800 Subject: [PATCH 29/78] Burst observatory: Fix init check (#6221) https://github.com/XTLS/Xray-core/pull/6106#issuecomment-4587975865 --- app/observatory/burst/healthping.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/observatory/burst/healthping.go b/app/observatory/burst/healthping.go index d57f51e88..b5cc021fb 100644 --- a/app/observatory/burst/healthping.go +++ b/app/observatory/burst/healthping.go @@ -96,6 +96,16 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) { ticker := time.NewTicker(interval) h.ticker = ticker + // init run to get a fast check result + go func() { + tags, err := selector() + if err != nil { + errors.LogWarning(h.ctx, "error select outbounds for initial health check: ", err) + return + } + h.Check(tags) + }() + go func() { for { go func() { From 94ffd50060f1cfd5d7482ec90a23a92bdefdff68 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 1 Jun 2026 02:11:09 +0000 Subject: [PATCH 30/78] Xray-core v26.6.1 Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668 Project X Channel: https://t.me/projectXtls Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633 Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1 VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067 VLESS NFT: https://opensea.io/collection/vless XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113 REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2 --- core/core.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/core.go b/core/core.go index 1e274f5a6..bb66da8ec 100644 --- a/core/core.go +++ b/core/core.go @@ -19,8 +19,8 @@ import ( var ( Version_x byte = 26 - Version_y byte = 5 - Version_z byte = 9 + Version_y byte = 6 + Version_z byte = 1 ) var ( From 55956f8d70f0f92e861cea57957f62afffda31d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:36:42 +0800 Subject: [PATCH 31/78] TLS config: Remove some deprecated fields (#6226) https://t.me/projectXtls/1490 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- infra/conf/transport_internet.go | 21 +-------------------- transport/internet/tls/config.go | 1 - transport/internet/tls/config.pb.go | 26 ++++---------------------- transport/internet/tls/config.proto | 5 ++--- 4 files changed, 7 insertions(+), 46 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 7a86a08ba..31701fe0a 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -14,7 +14,6 @@ import ( "strconv" "strings" "syscall" - "time" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" @@ -651,10 +650,8 @@ type TLSConfig struct { MasterKeyLog string `json:"masterKeyLog"` PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` VerifyPeerCertByName string `json:"verifyPeerCertByName"` - VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHServerKeys string `json:"echServerKeys"` ECHConfigList string `json:"echConfigList"` - ECHForceQuery string `json:"echForceQuery"` ECHSocketSettings *SocketConfig `json:"echSockopt"` } @@ -699,12 +696,7 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.MasterKeyLog = c.MasterKeyLog if c.AllowInsecure { - if time.Now().After(time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) { - return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`) - } else { - errors.LogWarning(context.Background(), `"allowInsecure" will be removed automatically after 2026-06-01, please use "pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn) instead, PLEASE CONTACT YOUR SERVICE PROVIDER (AIRPORT)`) - config.AllowInsecure = true - } + return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`) } if c.PinnedPeerCertSha256 != "" { for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") { @@ -723,10 +715,6 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue) } } - - if c.VerifyPeerCertInNames != nil { - return nil, errors.PrintRemovedFeatureError(`"verifyPeerCertInNames"`, `"verifyPeerCertByName"`) - } if c.VerifyPeerCertByName != "" { for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") { v = strings.TrimSpace(v) @@ -744,13 +732,6 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.EchServerKeys = EchPrivateKey } - switch c.ECHForceQuery { - case "none", "half", "full", "": - config.EchForceQuery = c.ECHForceQuery - default: - return nil, errors.New(`invalid "echForceQuery": `, c.ECHForceQuery) - } - config.EchForceQuery = c.ECHForceQuery config.EchConfigList = c.ECHConfigList if c.ECHSocketSettings != nil { ss, err := c.ECHSocketSettings.Build() diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 0c8878045..9c8604699 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -381,7 +381,6 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { PinnedPeerCertSha256: c.PinnedPeerCertSha256, } config := &tls.Config{ - InsecureSkipVerify: c.AllowInsecure, Rand: randCarrier, ClientSessionCache: globalSessionCache, RootCAs: root, diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 37628755e..b622d5a61 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -177,8 +177,7 @@ func (x *Certificate) GetBuildChain() bool { } type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` // List of certificates to be served on server. Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"` // Override server name. @@ -205,7 +204,6 @@ type Config struct { VerifyPeerCertByName []string `protobuf:"bytes,17,rep,name=verify_peer_cert_by_name,json=verifyPeerCertByName,proto3" json:"verify_peer_cert_by_name,omitempty"` EchServerKeys []byte `protobuf:"bytes,18,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"` EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` - EchForceQuery string `protobuf:"bytes,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"` EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"` PinnedPeerCertSha256 [][]byte `protobuf:"bytes,22,rep,name=pinned_peer_cert_sha256,json=pinnedPeerCertSha256,proto3" json:"pinned_peer_cert_sha256,omitempty"` unknownFields protoimpl.UnknownFields @@ -242,13 +240,6 @@ func (*Config) Descriptor() ([]byte, []int) { return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{1} } -func (x *Config) GetAllowInsecure() bool { - if x != nil { - return x.AllowInsecure - } - return false -} - func (x *Config) GetCertificate() []*Certificate { if x != nil { return x.Certificate @@ -354,13 +345,6 @@ func (x *Config) GetEchConfigList() string { return "" } -func (x *Config) GetEchForceQuery() string { - if x != nil { - return x.EchForceQuery - } - return "" -} - func (x *Config) GetEchSocketSettings() *internet.SocketConfig { if x != nil { return x.EchSocketSettings @@ -393,9 +377,8 @@ const file_transport_internet_tls_config_proto_rawDesc = "" + "\x05Usage\x12\x10\n" + "\fENCIPHERMENT\x10\x00\x12\x14\n" + "\x10AUTHORITY_VERIFY\x10\x01\x12\x13\n" + - "\x0fAUTHORITY_ISSUE\x10\x02\"\xf5\x06\n" + - "\x06Config\x12%\n" + - "\x0eallow_insecure\x18\x01 \x01(\bR\rallowInsecure\x12J\n" + + "\x0fAUTHORITY_ISSUE\x10\x02\"\xa6\x06\n" + + "\x06Config\x12J\n" + "\vcertificate\x18\x02 \x03(\v2(.xray.transport.internet.tls.CertificateR\vcertificate\x12\x1f\n" + "\vserver_name\x18\x03 \x01(\tR\n" + "serverName\x12#\n" + @@ -413,8 +396,7 @@ const file_transport_internet_tls_config_proto_rawDesc = "" + "\x11curve_preferences\x18\x10 \x03(\tR\x10curvePreferences\x126\n" + "\x18verify_peer_cert_by_name\x18\x11 \x03(\tR\x14verifyPeerCertByName\x12&\n" + "\x0fech_server_keys\x18\x12 \x01(\fR\rechServerKeys\x12&\n" + - "\x0fech_config_list\x18\x13 \x01(\tR\rechConfigList\x12&\n" + - "\x0fech_force_query\x18\x14 \x01(\tR\rechForceQuery\x12U\n" + + "\x0fech_config_list\x18\x13 \x01(\tR\rechConfigList\x12U\n" + "\x13ech_socket_settings\x18\x15 \x01(\v2%.xray.transport.internet.SocketConfigR\x11echSocketSettings\x125\n" + "\x17pinned_peer_cert_sha256\x18\x16 \x03(\fR\x14pinnedPeerCertSha256Bs\n" + "\x1fcom.xray.transport.internet.tlsP\x01Z0github.com/xtls/xray-core/transport/internet/tls\xaa\x02\x1bXray.Transport.Internet.Tlsb\x06proto3" diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 459282264..a05cc0494 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -38,7 +38,7 @@ message Certificate { } message Config { - bool allow_insecure = 1; + // Number 1 was assigned and used by an legacy option. // List of certificates to be served on server. repeated Certificate certificate = 2; @@ -81,8 +81,7 @@ message Config { string ech_config_list = 19; - // Deprecated - string ech_force_query = 20; + // Number 20 was assigned and used by an legacy option. SocketConfig ech_socket_settings = 21; From d792fba59cd423f50da19460fdc82107de4a1875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:40:17 +0800 Subject: [PATCH 32/78] GitHub Actions CI: Builders run only once in pull requests in the same repository (#6222) https://github.com/XTLS/Xray-core/pull/6221#issuecomment-4588919827 Completes https://github.com/XTLS/Xray-core/pull/6090#issuecomment-4572094191 --- .github/workflows/release-win7.yml | 2 ++ .github/workflows/release.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 9d777bc05..ac6b628f0 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -11,6 +11,7 @@ on: jobs: check-assets: runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Restore Geodat Cache uses: actions/cache/restore@v5 @@ -75,6 +76,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name env: GOOS: ${{ matrix.goos}} GOARCH: ${{ matrix.goarch }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1b468751..8f25b7827 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ on: jobs: check-assets: runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Restore Geodat Cache uses: actions/cache/restore@v5 @@ -161,6 +162,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} From fdb9b616fc0edf8fb4c3285870388947b92669fc Mon Sep 17 00:00:00 2001 From: lmlm Date: Wed, 3 Jun 2026 07:58:56 +0800 Subject: [PATCH 33/78] README.md: Add Hey to HarmonyOS in GUI Clients (#6244) https://github.com/popsiclelmlm/Hey/blob/main/entry/src/main/cpp/README.md --------- Co-authored-by: liumin01 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index deeb29049..d04cd359c 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,8 @@ - [v2rayN](https://github.com/2dust/v2rayN) - [GenyConnect](https://github.com/genyleap/GenyConnect) - [OneXray](https://github.com/OneXray/OneXray) +- HarmonyOS + - [Hey](https://github.com/popsiclelmlm/Hey) ## Others that support VLESS, XTLS, REALITY, XUDP, PLUX... From 2249f8b5c67531a96063921c7ae69c6a748e7363 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:29:14 +0000 Subject: [PATCH 34/78] Bump github.com/pion/stun/v3 from 3.1.2 to 3.1.5 (#6291) Bumps [github.com/pion/stun/v3](https://github.com/pion/stun) from 3.1.2 to 3.1.5. - [Release notes](https://github.com/pion/stun/releases) - [Commits](https://github.com/pion/stun/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: github.com/pion/stun/v3 dependency-version: 3.1.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index c5b21f68e..b1a64c93e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 - github.com/pion/stun/v3 v3.1.2 + github.com/pion/stun/v3 v3.1.5 github.com/pires/go-proxyproto v0.12.0 github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/robfig/cron/v3 v3.0.1 @@ -44,16 +44,16 @@ require ( github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/kr/text v0.2.0 // indirect - github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/dtls/v3 v3.1.4 // indirect github.com/pion/logging v0.2.4 // indirect - github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/transport/v4 v4.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/text v0.37.0 // indirect - golang.org/x/time v0.12.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 317452b5a..e292923ef 100644 --- a/go.sum +++ b/go.sum @@ -45,14 +45,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= -github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= +github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY= -github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA= -github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= -github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= +github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -129,8 +129,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= From 83cf229909dfb8f0513db5a407abb190f60ab2c4 Mon Sep 17 00:00:00 2001 From: IconHHw <138437673+IconHHw@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:55:06 +0800 Subject: [PATCH 35/78] Salamander finalmask: Replace `math/rand` with `crypto/rand` in salt generation (#6228) And https://github.com/XTLS/Xray-core/pull/6228#issuecomment-4612712100 Fixes https://github.com/XTLS/Xray-core/pull/6228#issuecomment-4599037015 --- .../internet/finalmask/salamander/salamander.go | 10 ++++------ transport/internet/finalmask/sudoku/codec.go | 12 ++++++------ .../internet/finalmask/sudoku/conn_tcp_packed.go | 2 +- transport/internet/finalmask/sudoku/table.go | 15 +++++++-------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/transport/internet/finalmask/salamander/salamander.go b/transport/internet/finalmask/salamander/salamander.go index d81863aa9..3c7188c3a 100644 --- a/transport/internet/finalmask/salamander/salamander.go +++ b/transport/internet/finalmask/salamander/salamander.go @@ -1,11 +1,11 @@ package salamander import ( + "crypto/rand" "fmt" - "math/rand" "sync" - "time" + "github.com/xtls/xray-core/common" "golang.org/x/crypto/blake2b" ) @@ -21,8 +21,7 @@ var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen) // the BLAKE2b-256 hash of a pre-shared key combined with a random salt. // Packet format: [8-byte salt][payload] type SalamanderObfuscator struct { - PSK []byte - RandSrc *rand.Rand + PSK []byte lk sync.Mutex keyInput []byte @@ -37,7 +36,6 @@ func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) { copy(keyInput, pskCopy) return &SalamanderObfuscator{ PSK: pskCopy, - RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), keyInput: keyInput, }, nil } @@ -47,8 +45,8 @@ func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int { if len(out) < outLen { return 0 } + common.Must2(rand.Read(out[:smSaltLen])) o.lk.Lock() - _, _ = o.RandSrc.Read(out[:smSaltLen]) key := o.keyLocked(out[:smSaltLen]) o.lk.Unlock() for i, c := range in { diff --git a/transport/internet/finalmask/sudoku/codec.go b/transport/internet/finalmask/sudoku/codec.go index e748b8b7b..eaf879b92 100644 --- a/transport/internet/finalmask/sudoku/codec.go +++ b/transport/internet/finalmask/sudoku/codec.go @@ -2,7 +2,7 @@ package sudoku import ( "fmt" - "math/rand" + "math/rand/v2" ) var perm4 = [24][4]byte{ @@ -67,7 +67,7 @@ func pickPaddingChance(rng *rand.Rand, pMin, pMax int) int { if pMax == pMin { return pMin } - return pMin + rng.Intn(pMax-pMin+1) + return pMin + rng.IntN(pMax-pMin+1) } func (c *codec) shouldPad() bool { @@ -77,7 +77,7 @@ func (c *codec) shouldPad() bool { if c.paddingChance >= 100 { return true } - return c.rng.Intn(100) < c.paddingChance + return c.rng.IntN(100) < c.paddingChance } func (c *codec) currentTable() *table { @@ -89,7 +89,7 @@ func (c *codec) currentTable() *table { func (c *codec) randomPadding(t *table) byte { pool := t.layout.paddingPool - return pool[c.rng.Intn(len(pool))] + return pool[c.rng.IntN(len(pool))] } func (c *codec) encode(in []byte) ([]byte, error) { @@ -112,8 +112,8 @@ func (c *codec) encode(in []byte) ([]byte, error) { return nil, fmt.Errorf("sudoku encode table missing for byte %d", b) } - hints := enc[c.rng.Intn(len(enc))] - perm := perm4[c.rng.Intn(len(perm4))] + hints := enc[c.rng.IntN(len(enc))] + perm := perm4[c.rng.IntN(len(perm4))] for _, idx := range perm { if c.shouldPad() { out = append(out, c.randomPadding(t)) diff --git a/transport/internet/finalmask/sudoku/conn_tcp_packed.go b/transport/internet/finalmask/sudoku/conn_tcp_packed.go index fa3c4c867..182e7343c 100644 --- a/transport/internet/finalmask/sudoku/conn_tcp_packed.go +++ b/transport/internet/finalmask/sudoku/conn_tcp_packed.go @@ -72,7 +72,7 @@ func (e *packedEncoder) maybePad(out []byte, layout *byteLayout) []byte { return append(out, layout.paddingPool[0]) } for { - b := layout.paddingPool[e.codec.rng.Intn(len(layout.paddingPool))] + b := layout.paddingPool[e.codec.rng.IntN(len(layout.paddingPool))] if b != layout.padMarker { return append(out, b) } diff --git a/transport/internet/finalmask/sudoku/table.go b/transport/internet/finalmask/sudoku/table.go index 6396d8645..464d718cf 100644 --- a/transport/internet/finalmask/sudoku/table.go +++ b/transport/internet/finalmask/sudoku/table.go @@ -7,10 +7,12 @@ import ( "fmt" "math/bits" "math/rand" + rand_v2 "math/rand/v2" "sort" "strings" "sync" - "time" + + "github.com/xtls/xray-core/common" ) type table struct { @@ -570,11 +572,8 @@ func sort4(in [4]byte) [4]byte { return in } -func newSeededRand() *rand.Rand { - seed := time.Now().UnixNano() - var seedBytes [8]byte - if _, err := crypto_rand.Read(seedBytes[:]); err == nil { - seed = int64(binary.BigEndian.Uint64(seedBytes[:])) - } - return rand.New(rand.NewSource(seed)) +func newSeededRand() *rand_v2.Rand { + var seedBytes [32]byte + common.Must2(crypto_rand.Read(seedBytes[:])) + return rand_v2.New(rand_v2.NewChaCha8(seedBytes)) } From a0e9347f1b4f8d76444d975ab4d3696ecf3e84a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 9 Jun 2026 17:03:24 +0800 Subject: [PATCH 36/78] TLS ECH: Handle "h2c://" query correctly (#6261) Fixes https://github.com/XTLS/Xray-core/issues/6259#issuecomment-4614984919 --------- Co-authored-by: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com> --- transport/internet/tls/ech.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 9de124c1c..4755cd4fd 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -234,6 +234,9 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b if err != nil { return nil, 0, err } + // h2c: in config is just for claim + // change scheme to https and expect outbound to handle TLS (freedom + tlsSetting) + req.URL.Scheme = "https" req.Header.Set("Accept", "application/dns-message") req.Header.Set("Content-Type", "application/dns-message") utils.TryDefaultHeadersWith(req.Header, "fetch") From 6189d2bfd564171d4e1cc0f4e31b43b0e1032d85 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Tue, 9 Jun 2026 17:14:54 +0800 Subject: [PATCH 37/78] XICMP finalmask: Refine Linux sever (#6272) Fixes https://github.com/XTLS/Xray-core/pull/6168 --- transport/internet/finalmask/xicmp/server.go | 2 + .../internet/finalmask/xicmp/server_oob.go | 364 ++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 transport/internet/finalmask/xicmp/server_oob.go diff --git a/transport/internet/finalmask/xicmp/server.go b/transport/internet/finalmask/xicmp/server.go index 2e95d842d..05ba48bbb 100644 --- a/transport/internet/finalmask/xicmp/server.go +++ b/transport/internet/finalmask/xicmp/server.go @@ -1,3 +1,5 @@ +//go:build !linux + package xicmp import ( diff --git a/transport/internet/finalmask/xicmp/server_oob.go b/transport/internet/finalmask/xicmp/server_oob.go new file mode 100644 index 000000000..0a04b0802 --- /dev/null +++ b/transport/internet/finalmask/xicmp/server_oob.go @@ -0,0 +1,364 @@ +//go:build linux + +package xicmp + +import ( + "context" + goerrors "errors" + "io" + "net" + "net/netip" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet/finalmask" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +func clientIDToAddr(clientID [8]byte) *net.UDPAddr { + ip := make(net.IP, 16) + + ip[0] = 0xfd + ip[1] = 0x00 + + copy(ip[8:], clientID[:]) + + return &net.UDPAddr{IP: ip} +} + +type record struct { + id int + seq int + addr net.Addr + dst net.IP + last time.Time +} + +type xicmpConnServer struct { + conn net.PacketConn + icmp4 *icmp.PacketConn + icmp6 *icmp.PacketConn + ipv4PC *ipv4.PacketConn + ipv6PC *ipv6.PacketConn + ips map[netip.Addr]struct{} + rec map[string]record + readCh chan packet + closedCh chan struct{} + mu sync.Mutex +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + icmp4, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + return nil, err + } + icmp6, err := icmp.ListenPacket("ip6:ipv6-icmp", "::") + if err != nil { + return nil, err + } + + ips := make(map[netip.Addr]struct{}) + for _, ip := range c.IPs { + ips[netip.MustParseAddr(ip)] = struct{}{} + } + + conn := &xicmpConnServer{ + conn: raw, + icmp4: icmp4, + icmp6: icmp6, + ipv4PC: icmp4.IPv4PacketConn(), + ipv6PC: icmp6.IPv6PacketConn(), + ips: ips, + rec: make(map[string]record), + readCh: make(chan packet), + closedCh: make(chan struct{}), + } + + common.Must(conn.ipv4PC.SetControlMessage(ipv4.FlagDst, true)) + common.Must(conn.ipv6PC.SetControlMessage(ipv6.FlagDst, true)) + + go conn.clean() + go conn.recv4() + go conn.recv6() + + return conn, nil +} + +func (c *xicmpConnServer) closed() bool { + select { + case <-c.closedCh: + return true + default: + return false + } +} + +func (c *xicmpConnServer) clean() { + ticker := time.NewTicker(time.Minute / 2) + defer ticker.Stop() + for { + select { + case <-ticker.C: + now := time.Now() + c.mu.Lock() + for key, r := range c.rec { + if now.Sub(r.last) > time.Minute { + delete(c.rec, key) + } + } + c.mu.Unlock() + case <-c.closedCh: + return + } + } +} + +func (c *xicmpConnServer) recv4() { + var b [finalmask.UDPSize]byte + + for { + if c.closed() { + return + } + + n, cm, addr, err := c.ipv4PC.ReadFrom(b[:]) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + select { + case c.readCh <- packet{ + err: err, + }: + case <-c.closedCh: + return + } + } + continue + } + + msg, err := icmp.ParseMessage(1, b[:n]) + if err != nil { + continue + } + + if msg.Type != ipv4.ICMPTypeEcho { + continue + } + + echo, ok := msg.Body.(*icmp.Echo) + if !ok { + continue + } + + if len(echo.Data) <= 8 { + continue + } + + if len(c.ips) > 0 { + netipAddr, ok := netip.AddrFromSlice(addr.(*net.IPAddr).IP) + if !ok { + continue + } + + if _, ok := c.ips[netipAddr]; !ok { + continue + } + } + + cAddr := clientIDToAddr([8]byte(echo.Data[:8])) + + c.mu.Lock() + c.rec[cAddr.String()] = record{ + id: echo.ID, + seq: echo.Seq, + addr: addr, + dst: cm.Dst, + last: time.Now(), + } + c.mu.Unlock() + + p := pool.Get().([]byte)[:len(echo.Data[8:])] + copy(p, echo.Data[8:]) + + select { + case c.readCh <- packet{ + p: p, + addr: cAddr, + }: + case <-c.closedCh: + pool.Put(p) + return + } + } +} + +func (c *xicmpConnServer) recv6() { + var b [finalmask.UDPSize]byte + + for { + if c.closed() { + return + } + + n, cm, addr, err := c.ipv6PC.ReadFrom(b[:]) + if err != nil { + var netErr net.Error + if goerrors.As(err, &netErr) && netErr.Timeout() { + select { + case c.readCh <- packet{ + err: err, + }: + case <-c.closedCh: + return + } + } + continue + } + + msg, err := icmp.ParseMessage(58, b[:n]) + if err != nil { + continue + } + + if msg.Type != ipv6.ICMPTypeEchoRequest { + continue + } + + echo, ok := msg.Body.(*icmp.Echo) + if !ok { + continue + } + + if len(echo.Data) <= 8 { + continue + } + + if len(c.ips) > 0 { + netipAddr, ok := netip.AddrFromSlice(addr.(*net.IPAddr).IP) + if !ok { + continue + } + + if _, ok := c.ips[netipAddr]; !ok { + continue + } + } + + cAddr := clientIDToAddr([8]byte(echo.Data[:8])) + + c.mu.Lock() + c.rec[cAddr.String()] = record{ + id: echo.ID, + seq: echo.Seq, + addr: addr, + dst: cm.Dst, + last: time.Now(), + } + c.mu.Unlock() + + p := pool.Get().([]byte)[:len(echo.Data[8:])] + copy(p, echo.Data[8:]) + + select { + case c.readCh <- packet{ + p: p, + addr: cAddr, + }: + case <-c.closedCh: + pool.Put(p) + return + } + } +} + +func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + select { + case packet := <-c.readCh: + if packet.p != nil { + n = copy(p, packet.p) + pool.Put(packet.p) + } + return n, packet.addr, packet.err + case <-c.closedCh: + return 0, nil, io.EOF + } +} + +func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if len(p)+8 > finalmask.UDPSize { + errors.LogError(context.Background(), "drop packet to ", addr, " with size ", len(p)) + return 0, nil + } + + c.mu.Lock() + r, ok := c.rec[addr.String()] + if !ok { + errors.LogError(context.Background(), "drop packet to ", addr, " with size ", len(p)) + c.mu.Unlock() + return 0, nil + } + r.last = time.Now() + c.rec[addr.String()] = r + c.mu.Unlock() + + // errors.LogDebug(context.Background(), "id ", r.id, " seq ", r.seq, " addr ", r.addr) + + b := pool.Get().([]byte)[:finalmask.UDPSize] + defer pool.Put(b) + + copy(b[8:], p) + + if r.addr.(*net.IPAddr).IP.To4() != nil { + b = marshal(b, ipv4.ICMPTypeEchoReply, r.id, r.seq, len(p)) + _, err = c.ipv4PC.WriteTo(b, &ipv4.ControlMessage{Src: r.dst}, r.addr) + } else { + b = marshal(b, ipv6.ICMPTypeEchoReply, r.id, r.seq, len(p)) + _, err = c.ipv6PC.WriteTo(b, &ipv6.ControlMessage{Src: r.dst}, r.addr) + } + + if err != nil { + errors.LogErrorInner(context.Background(), err, "xicmp write") + return 0, err + } + + return len(p), nil +} + +func (c *xicmpConnServer) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed() { + return nil + } + close(c.closedCh) + _ = c.icmp4.Close() + _ = c.icmp6.Close() + _ = c.conn.Close() + return nil +} + +func (c *xicmpConnServer) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *xicmpConnServer) SetDeadline(t time.Time) error { + _ = c.icmp4.SetDeadline(t) + _ = c.icmp6.SetDeadline(t) + return nil +} + +func (c *xicmpConnServer) SetReadDeadline(t time.Time) error { + _ = c.icmp4.SetReadDeadline(t) + _ = c.icmp6.SetReadDeadline(t) + return nil +} + +func (c *xicmpConnServer) SetWriteDeadline(t time.Time) error { + _ = c.icmp4.SetWriteDeadline(t) + _ = c.icmp6.SetWriteDeadline(t) + return nil +} From 06b4931743b2ee0487faa9739ae8be535d2d0a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 9 Jun 2026 17:22:33 +0800 Subject: [PATCH 38/78] TUN inbound: Start TUN by AlwaysOnInboundHandler (#6275) Fixes https://github.com/XTLS/Xray-core/issues/6274 --- app/proxyman/inbound/always.go | 6 ++++++ proxy/tun/handler.go | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/proxyman/inbound/always.go b/app/proxyman/inbound/always.go index 02b841985..f26a30c2e 100644 --- a/app/proxyman/inbound/always.go +++ b/app/proxyman/inbound/always.go @@ -170,6 +170,12 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig * // Start implements common.Runnable. func (h *AlwaysOnInboundHandler) Start() error { + // for inbound without worker (TUN) + if run, ok := h.proxy.(common.Runnable); ok { + if err := run.Start(); err != nil { + return errors.New("failed to start proxy").Base(err) + } + } for _, worker := range h.workers { if err := worker.Start(); err != nil { return err diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 1e8e030e6..c26f14d52 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -40,10 +40,11 @@ type ConnectionHandler interface { // Handler implements ConnectionHandler var _ ConnectionHandler = (*Handler)(nil) +// Handler implements common.Runnable +var _ common.Runnable = (*Handler)(nil) + // Init the Handler instance with necessary parameters func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routing.Dispatcher) error { - var err error - // Retrieve tag and sniffing config from context (set by AlwaysOnInboundHandler) if inbound := session.InboundFromContext(ctx); inbound != nil { t.tag = inbound.Tag @@ -56,6 +57,10 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin t.policyManager = pm t.dispatcher = dispatcher + return nil +} + +func (t *Handler) Start() error { tunName := t.config.Name tunInterface, err := NewTun(t.config) if err != nil { @@ -92,7 +97,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin tunStackOptions := StackOptions{ Tun: tunInterface, - IdleTimeout: pm.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle, + IdleTimeout: t.policyManager.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle, } tunStack, err := NewStack(t.ctx, tunStackOptions, t) if err != nil { @@ -167,7 +172,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { // Close implements common.Closable. func (t *Handler) Close() error { - return errors.Combine(t.stack.Close(), t.tun.Close()) + return errors.Combine(common.CloseIfExists(t.stack), common.CloseIfExists(t.tun)) } // Network implements proxy.Inbound From 3239d21168c8731b0100570e17a904394be8f947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 9 Jun 2026 17:40:55 +0800 Subject: [PATCH 39/78] TUN inbound: `autoOutboundsInterface` bypasses loopback addresses (#6276) Fixes https://github.com/XTLS/Xray-core/issues/6269 --- proxy/tun/handler.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index c26f14d52..d552eca53 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -2,6 +2,8 @@ package tun import ( "context" + "net/netip" + "strings" "syscall" "github.com/xtls/xray-core/common" @@ -85,6 +87,11 @@ func (t *Handler) Start() error { return nil } return c.Control(func(fd uintptr) { + addrPort, _ := netip.ParseAddrPort(address) + // skip loopback + if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") { + return + } err := setinterface(network, address, fd, iface) if err != nil { errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface") From 95e9816223faa154a8f86c43b565a1e203b61937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:55:42 +0800 Subject: [PATCH 40/78] Chore: Limit sing* dependencies to shadowsocks_2022 only (#6286) https://github.com/XTLS/Xray-core/pull/6286#issuecomment-4658968225 --- app/proxyman/outbound/handler.go | 6 ---- app/proxyman/outbound/uot.go | 35 ------------------ infra/conf/shadowsocks.go | 46 ++++++++++-------------- proxy/shadowsocks_2022/config.pb.go | 37 +++++-------------- proxy/shadowsocks_2022/config.proto | 2 -- proxy/shadowsocks_2022/outbound.go | 29 ++++----------- transport/internet/finalmask/udp_test.go | 21 ----------- 7 files changed, 34 insertions(+), 142 deletions(-) delete mode 100644 app/proxyman/outbound/uot.go diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 7284a848d..578b2ca16 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -6,7 +6,6 @@ import ( goerrors "errors" "io" "math/big" - "os" "github.com/xtls/xray-core/common/dice" @@ -306,11 +305,6 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti ob := outbounds[len(outbounds)-1] h.SetOutboundGateway(ctx, ob) } - - } - - if conn, err := h.getUoTConnection(ctx, dest); err != os.ErrInvalid { - return conn, err } conn, err := internet.Dial(ctx, dest, h.streamSettings) diff --git a/app/proxyman/outbound/uot.go b/app/proxyman/outbound/uot.go deleted file mode 100644 index 659f65a11..000000000 --- a/app/proxyman/outbound/uot.go +++ /dev/null @@ -1,35 +0,0 @@ -package outbound - -import ( - "context" - "os" - - "github.com/sagernet/sing/common/uot" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/transport/internet" - "github.com/xtls/xray-core/transport/internet/stat" -) - -func (h *Handler) getUoTConnection(ctx context.Context, dest net.Destination) (stat.Connection, error) { - if dest.Address == nil { - return nil, errors.New("nil destination address") - } - if !dest.Address.Family().IsDomain() { - return nil, os.ErrInvalid - } - var uotVersion int - if dest.Address.Domain() == uot.MagicAddress { - uotVersion = uot.Version - } else if dest.Address.Domain() == uot.LegacyMagicAddress { - uotVersion = uot.LegacyVersion - } else { - return nil, os.ErrInvalid - } - packetConn, err := internet.ListenSystemPacket(ctx, &net.UDPAddr{IP: net.AnyIP.IP(), Port: 0}, h.streamSettings.SocketSettings) - if err != nil { - return nil, errors.New("unable to listen socket").Base(err) - } - conn := uot.NewServerConn(packetConn, uotVersion) - return h.getStatCouterConnection(conn), nil -} diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index 30a7fd656..2cfc09539 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -179,26 +179,22 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) { } type ShadowsocksServerTarget struct { - Address *Address `json:"address"` - Port uint16 `json:"port"` - Level byte `json:"level"` - Email string `json:"email"` - Cipher string `json:"method"` - Password string `json:"password"` - UoT bool `json:"uot"` - UoTVersion int `json:"uotVersion"` + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Cipher string `json:"method"` + Password string `json:"password"` } type ShadowsocksClientConfig struct { - Address *Address `json:"address"` - Port uint16 `json:"port"` - Level byte `json:"level"` - Email string `json:"email"` - Cipher string `json:"method"` - Password string `json:"password"` - UoT bool `json:"uot"` - UoTVersion int `json:"uotVersion"` - Servers []*ShadowsocksServerTarget `json:"servers"` + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Cipher string `json:"method"` + Password string `json:"password"` + Servers []*ShadowsocksServerTarget `json:"servers"` } func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { @@ -207,14 +203,12 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { if v.Address != nil { v.Servers = []*ShadowsocksServerTarget{ { - Address: v.Address, - Port: v.Port, - Level: v.Level, - Email: v.Email, - Cipher: v.Cipher, - Password: v.Password, - UoT: v.UoT, - UoTVersion: v.UoTVersion, + Address: v.Address, + Port: v.Port, + Level: v.Level, + Email: v.Email, + Cipher: v.Cipher, + Password: v.Password, }, } } @@ -240,8 +234,6 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { config.Port = uint32(server.Port) config.Method = server.Cipher config.Key = server.Password - config.UdpOverTcp = server.UoT - config.UdpOverTcpVersion = uint32(server.UoTVersion) return config, nil } } diff --git a/proxy/shadowsocks_2022/config.pb.go b/proxy/shadowsocks_2022/config.pb.go index df2ee7cb9..7213bd2d3 100644 --- a/proxy/shadowsocks_2022/config.pb.go +++ b/proxy/shadowsocks_2022/config.pb.go @@ -356,15 +356,13 @@ func (x *Account) GetKey() string { } type ClientConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` - Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` - UdpOverTcp bool `protobuf:"varint,5,opt,name=udp_over_tcp,json=udpOverTcp,proto3" json:"udp_over_tcp,omitempty"` - UdpOverTcpVersion uint32 `protobuf:"varint,6,opt,name=udp_over_tcp_version,json=udpOverTcpVersion,proto3" json:"udp_over_tcp_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ClientConfig) Reset() { @@ -425,20 +423,6 @@ func (x *ClientConfig) GetKey() string { return "" } -func (x *ClientConfig) GetUdpOverTcp() bool { - if x != nil { - return x.UdpOverTcp - } - return false -} - -func (x *ClientConfig) GetUdpOverTcpVersion() uint32 { - if x != nil { - return x.UdpOverTcpVersion - } - return 0 -} - var File_proxy_shadowsocks_2022_config_proto protoreflect.FileDescriptor const file_proxy_shadowsocks_2022_config_proto_rawDesc = "" + @@ -467,15 +451,12 @@ const file_proxy_shadowsocks_2022_config_proto_rawDesc = "" + "\fdestinations\x18\x03 \x03(\v2-.xray.proxy.shadowsocks_2022.RelayDestinationR\fdestinations\x122\n" + "\anetwork\x18\x04 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"\x1b\n" + "\aAccount\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\"\xd6\x01\n" + + "\x03key\x18\x01 \x01(\tR\x03key\"\x83\x01\n" + "\fClientConfig\x125\n" + "\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x16\n" + "\x06method\x18\x03 \x01(\tR\x06method\x12\x10\n" + - "\x03key\x18\x04 \x01(\tR\x03key\x12 \n" + - "\fudp_over_tcp\x18\x05 \x01(\bR\n" + - "udpOverTcp\x12/\n" + - "\x14udp_over_tcp_version\x18\x06 \x01(\rR\x11udpOverTcpVersionBr\n" + + "\x03key\x18\x04 \x01(\tR\x03keyBr\n" + "\x1fcom.xray.proxy.shadowsocks_2022P\x01Z0github.com/xtls/xray-core/proxy/shadowsocks_2022\xaa\x02\x1aXray.Proxy.Shadowsocks2022b\x06proto3" var ( diff --git a/proxy/shadowsocks_2022/config.proto b/proxy/shadowsocks_2022/config.proto index 140066486..c73943d4a 100644 --- a/proxy/shadowsocks_2022/config.proto +++ b/proxy/shadowsocks_2022/config.proto @@ -49,6 +49,4 @@ message ClientConfig { uint32 port = 2; string method = 3; string key = 4; - bool udp_over_tcp = 5; - uint32 udp_over_tcp_version = 6; } diff --git a/proxy/shadowsocks_2022/outbound.go b/proxy/shadowsocks_2022/outbound.go index bd3e4c4ec..f1195950d 100644 --- a/proxy/shadowsocks_2022/outbound.go +++ b/proxy/shadowsocks_2022/outbound.go @@ -10,7 +10,6 @@ import ( B "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/uot" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -29,10 +28,9 @@ func init() { } type Outbound struct { - ctx context.Context - server net.Destination - method shadowsocks.Method - uotClient *uot.Client + ctx context.Context + server net.Destination + method shadowsocks.Method } func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { @@ -56,9 +54,6 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { } else { return nil, errors.New("unknown method ", config.Method) } - if config.UdpOverTcp { - o.uotClient = &uot.Client{Version: uint8(config.UdpOverTcpVersion)} - } return o, nil } @@ -82,11 +77,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int errors.LogInfo(ctx, "tunneling request to ", destination, " via ", o.server.NetAddr()) serverDestination := o.server - if o.uotClient != nil { - serverDestination.Network = net.Network_TCP - } else { - serverDestination.Network = network - } + serverDestination.Network = network connection, err := dialer.Dial(ctx, serverDestination) if err != nil { return errors.New("failed to connect to server").Base(err) @@ -149,15 +140,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int } } - if o.uotClient != nil { - uConn, err := o.uotClient.DialEarlyConn(o.method.DialEarlyConn(connection, uot.RequestDestination(o.uotClient.Version)), false, singbridge.ToSocksaddr(destination)) - if err != nil { - return err - } - return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, uConn)) - } else { - serverConn := o.method.DialPacketConn(connection) - return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn)) - } + serverConn := o.method.DialPacketConn(connection) + return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn)) } } diff --git a/transport/internet/finalmask/udp_test.go b/transport/internet/finalmask/udp_test.go index 8a507d2ec..95233e02e 100644 --- a/transport/internet/finalmask/udp_test.go +++ b/transport/internet/finalmask/udp_test.go @@ -2,7 +2,6 @@ package finalmask_test import ( "bytes" - "context" "encoding/binary" "io" "net" @@ -10,8 +9,6 @@ import ( "testing" "time" - singM "github.com/sagernet/sing/common/metadata" - singN "github.com/sagernet/sing/common/network" "github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/transport/internet/finalmask" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" @@ -134,24 +131,6 @@ func (c *scriptedPacketConn) SetWriteDeadline(t time.Time) error { return nil } -type captureUDPHandler struct { - gotMetadata chan singM.Metadata -} - -func (h *captureUDPHandler) NewConnection(_ context.Context, _ net.Conn, _ singM.Metadata) error { - return nil -} - -func (h *captureUDPHandler) NewPacketConnection(_ context.Context, _ singN.PacketConn, metadata singM.Metadata) error { - select { - case h.gotMetadata <- metadata: - default: - } - return nil -} - -func (h *captureUDPHandler) NewError(_ context.Context, _ error) {} - func newStandaloneEchoUDPConfig() *custom.UDPStandaloneConfig { return &custom.UDPStandaloneConfig{ Client: []*custom.UDPItem{ From 26a022c90534323704ca456cf640eeec94aabf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:08:38 +0800 Subject: [PATCH 41/78] GitHub Action CI, README.md: Refinements and add compliance contents (#6283) And https://github.com/XTLS/Xray-core/pull/6283#issuecomment-4640162879 --- .github/build/windows/xray_no_window.cmd | 1 + .github/build/windows/xray_no_window.ps1 | 1 + .github/build/windows/xray_no_window.vbs | 1 + .github/workflows/release-win7.yml | 8 ++++--- .github/workflows/release.yml | 8 ++++--- .github/workflows/scheduled-assets-update.yml | 17 ++++++++++----- README.md | 21 +++++++++++++++++++ 7 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 .github/build/windows/xray_no_window.cmd create mode 100644 .github/build/windows/xray_no_window.ps1 create mode 100644 .github/build/windows/xray_no_window.vbs diff --git a/.github/build/windows/xray_no_window.cmd b/.github/build/windows/xray_no_window.cmd new file mode 100644 index 000000000..0fb15283b --- /dev/null +++ b/.github/build/windows/xray_no_window.cmd @@ -0,0 +1 @@ +powershell.exe -ExecutionPolicy Bypass -File ".\xray_no_window.ps1" diff --git a/.github/build/windows/xray_no_window.ps1 b/.github/build/windows/xray_no_window.ps1 new file mode 100644 index 000000000..6342a4ec5 --- /dev/null +++ b/.github/build/windows/xray_no_window.ps1 @@ -0,0 +1 @@ +Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden diff --git a/.github/build/windows/xray_no_window.vbs b/.github/build/windows/xray_no_window.vbs new file mode 100644 index 000000000..8aa8ae40f --- /dev/null +++ b/.github/build/windows/xray_no_window.vbs @@ -0,0 +1 @@ +CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0 diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index ac6b628f0..d6bce6076 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -134,15 +134,17 @@ jobs: run: | mv -f resources/geo* build_assets/ if [[ ${GOOS} == 'windows' ]]; then - echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs - echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 + cp .github/build/windows/* build_assets/ + fi + if [[ ${GOOS} == 'windows' ]]; then + echo 'Adding Wintun into packages' if [[ ${GOARCH} == 'amd64' ]]; then mv resources/wintun/bin/amd64/wintun.dll build_assets/ fi if [[ ${GOARCH} == '386' ]]; then mv resources/wintun/bin/x86/wintun.dll build_assets/ fi - mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt + mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun fi - name: Copy README.md & LICENSE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f25b7827..6ff09d52c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -241,8 +241,10 @@ jobs: run: | mv -f resources/geo* build_assets/ if [[ ${GOOS} == 'windows' ]]; then - echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs - echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 + cp .github/build/windows/* build_assets/ + fi + if [[ ${GOOS} == 'windows' ]]; then + echo 'Adding Wintun into packages' if [[ ${GOARCH} == 'amd64' ]]; then mv resources/wintun/bin/amd64/wintun.dll build_assets/ fi @@ -252,7 +254,7 @@ jobs: if [[ ${GOARCH} == 'arm64' ]]; then mv resources/wintun/bin/arm64/wintun.dll build_assets/ fi - mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt + mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun fi - name: Copy README.md & LICENSE diff --git a/.github/workflows/scheduled-assets-update.yml b/.github/workflows/scheduled-assets-update.yml index 44d4cb5b9..3e20fc433 100644 --- a/.github/workflows/scheduled-assets-update.yml +++ b/.github/workflows/scheduled-assets-update.yml @@ -68,6 +68,9 @@ jobs: wintun: if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + env: + ASSETVER: 0.14.1 + ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51 steps: - name: Restore Wintun Cache uses: actions/cache/restore@v5 @@ -96,7 +99,6 @@ jobs: echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..." if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then echo -e "wintun.dll for ${ARCHITECTURE} exists" - continue else echo -e "wintun.dll for ${ARCHITECTURE} is missing" missing=true @@ -113,12 +115,17 @@ jobs: fi if [[ "$missing" == true ]]; then FILENAME=wintun.zip - DOWNLOAD_FILE=wintun-0.14.1.zip + DOWNLOAD_FILE=wintun-${ASSETVER}.zip echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..." curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}" - echo -e "Unpacking wintun..." - unzip -u ${FILENAME} -d resources/ - echo "unhit=true" >> $GITHUB_OUTPUT + if [[ "$(sha256sum "./${FILENAME}" | awk -F ' ' '{print $1}')" == "${ASSETHASH}" ]]; then + echo -e "Unpacking wintun..." + unzip -u ${FILENAME} -d resources/ + echo "unhit=true" >> $GITHUB_OUTPUT + else + echo -e "Digest of ${FILENAME} mismatch." + exit 1 + fi fi - name: Save Wintun Cache diff --git a/README.md b/README.md index d04cd359c..daf82368e 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,27 @@ - [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases). - For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod). +### Bundled Third-Party Components Redistribution + +**Certain optional features dynamically load third-party components. These optional components are separate works distributed under their own licenses, and are bundled into the ZIP package for ease of use. Users may replace these components under the licenses from these components.** + +These components include: + +#### Wintun + +This distribution contains unmodified official precompiled and pre-signed Wintun binaries. + +- Project: Wintun +- Copyright: Copyright (C) 2018-2021 WireGuard LLC. All Rights Reserved. +- Redistribution License: Prebuilt Binaries License (PBL) bundled with official precompiled and pre-signed binaries from wintun.net +- Component(s): wintun.dll +- Source: https://www.wintun.net/ +- Included in: + - Windows x86 (windows-32, win7-32) + - Windows x86-64 (windows-64, win7-64) + - Windows AArch64 (windows-arm64) +- Notes: Wintun is an optional runtime-loaded component only used for TUN inbound functionality on supported Windows platforms. + ## One-line Compilation ### Windows (PowerShell) From e10347bf01f28bca118002963ee29bbcf529cb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 9 Jun 2026 23:58:02 +0800 Subject: [PATCH 42/78] XHTTP transport: Add `sessionIDTable` and `sessionIDLength`; Rename `session*` to `sessionID*` (#6258) https://github.com/XTLS/Xray-core/pull/6258#issuecomment-4658534046 https://github.com/XTLS/Xray-core/pull/6253#issuecomment-4657704004 https://github.com/XTLS/Xray-core/pull/6251#issuecomment-4612756220 Usage: https://github.com/XTLS/Xray-core/pull/6258#issue-4580617110 Closes https://github.com/XTLS/Xray-core/issues/6264 --------- Co-authored-by: XXcipherX --- infra/conf/transport_internet.go | 57 +++++++++--- transport/internet/splithttp/config.go | 104 +++++++++++++++------- transport/internet/splithttp/config.pb.go | 53 +++++++---- transport/internet/splithttp/config.proto | 6 +- transport/internet/splithttp/dialer.go | 4 +- transport/internet/splithttp/xpadding.go | 6 +- 6 files changed, 159 insertions(+), 71 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 31701fe0a..2c5650b0d 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "math" + "math/big" "net/netip" "net/url" "os" @@ -219,8 +220,10 @@ type SplitHTTPConfig struct { XPaddingPlacement string `json:"xPaddingPlacement"` XPaddingMethod string `json:"xPaddingMethod"` UplinkHTTPMethod string `json:"uplinkHTTPMethod"` - SessionPlacement string `json:"sessionPlacement"` - SessionKey string `json:"sessionKey"` + SessionIDPlacement string `json:"sessionIDPlacement"` + SessionIDKey string `json:"sessionIDKey"` + SessionIDTable string `json:"sessionIDTable"` + SessionIDLength Int32Range `json:"sessionIDLength"` SeqPlacement string `json:"seqPlacement"` SeqKey string `json:"seqKey"` UplinkDataPlacement string `json:"uplinkDataPlacement"` @@ -331,12 +334,12 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode") } - switch c.SessionPlacement { + switch c.SessionIDPlacement { case "": - c.SessionPlacement = "path" + c.SessionIDPlacement = "path" case "path", "cookie", "header", "query": default: - return nil, errors.New("unsupported session placement: " + c.SessionPlacement) + return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement) } switch c.SeqPlacement { @@ -347,12 +350,31 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) } - if c.SessionPlacement != "path" && c.SessionKey == "" { - switch c.SessionPlacement { + if c.SessionIDPlacement != "path" && c.SessionIDKey == "" { + switch c.SessionIDPlacement { case "cookie", "query": - c.SessionKey = "x_session" + c.SessionIDKey = "x_session" case "header": - c.SessionKey = "X-Session" + c.SessionIDKey = "X-Session" + } + } + + if c.SessionIDTable != "" { + if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok { + c.SessionIDTable = predefined + } + room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To) + // 2.1B possiblities should be enough + if room.Cmp(big.NewInt(2<<30)) < 0 { + return nil, errors.New("sessionIDTable or sessionIDLength is too small") + } + if c.SessionIDLength.From <= 0 { + return nil, errors.New("sessionIDLength.from must be greater than 0") + } + for i := 0; i < len(c.SessionIDTable); i++ { + if c.SessionIDTable[i] >= 0x80 { + return nil, errors.New("sessionIDTable must contain only ASCII characters") + } } } @@ -402,9 +424,9 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { XPaddingPlacement: c.XPaddingPlacement, XPaddingMethod: c.XPaddingMethod, UplinkHTTPMethod: c.UplinkHTTPMethod, - SessionPlacement: c.SessionPlacement, + SessionIDPlacement: c.SessionIDPlacement, SeqPlacement: c.SeqPlacement, - SessionKey: c.SessionKey, + SessionIDKey: c.SessionIDKey, SeqKey: c.SeqKey, UplinkDataPlacement: c.UplinkDataPlacement, UplinkDataKey: c.UplinkDataKey, @@ -416,6 +438,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { ScMaxBufferedPosts: c.ScMaxBufferedPosts, ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, + SessionIDTable: c.SessionIDTable, + SessionIDLength: newRangeConfig(c.SessionIDLength), Xmux: &splithttp.XmuxConfig{ MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), MaxConnections: newRangeConfig(c.Xmux.MaxConnections), @@ -439,6 +463,17 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return config, nil } +func roomSize(tableSize int, min, max int32) *big.Int { + base := big.NewInt(int64(tableSize)) + sum := new(big.Int) + term := new(big.Int) + for k := min; k <= max; k++ { + term.Exp(base, big.NewInt(int64(k)), nil) + sum.Add(sum, term) + } + return sum +} + const ( Byte = 1 Kilobyte = 1024 * Byte diff --git a/transport/internet/splithttp/config.go b/transport/internet/splithttp/config.go index 61f861a33..9acf5e4ff 100644 --- a/transport/internet/splithttp/config.go +++ b/transport/internet/splithttp/config.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "io" + "math/rand/v2" "net/http" "strings" @@ -11,6 +12,7 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/transport/internet" ) @@ -131,26 +133,26 @@ func (c *Config) GetNormalizedUplinkHTTPMethod() string { return c.UplinkHTTPMethod } -func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig { +func (c *Config) GetNormalizedScMaxEachPostBytes() *RangeConfig { if c.ScMaxEachPostBytes == nil || c.ScMaxEachPostBytes.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 1000000, To: 1000000, } } - return *c.ScMaxEachPostBytes + return c.ScMaxEachPostBytes } -func (c *Config) GetNormalizedScMinPostsIntervalMs() RangeConfig { +func (c *Config) GetNormalizedScMinPostsIntervalMs() *RangeConfig { if c.ScMinPostsIntervalMs == nil || c.ScMinPostsIntervalMs.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 30, To: 30, } } - return *c.ScMinPostsIntervalMs + return c.ScMinPostsIntervalMs } func (c *Config) GetNormalizedScMaxBufferedPosts() int { @@ -161,27 +163,27 @@ func (c *Config) GetNormalizedScMaxBufferedPosts() int { return int(c.ScMaxBufferedPosts) } -func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig { +func (c *Config) GetNormalizedScStreamUpServerSecs() *RangeConfig { if c.ScStreamUpServerSecs == nil || c.ScStreamUpServerSecs.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 20, To: 80, } } - return *c.ScStreamUpServerSecs + return c.ScStreamUpServerSecs } -func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig { +func (c *Config) GetNormalizedUplinkChunkSize() *RangeConfig { if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 { switch c.UplinkDataPlacement { case PlacementCookie: - return RangeConfig{ + return &RangeConfig{ From: 2 * 1024, // 2 KiB To: 3 * 1024, // 3 KiB } case PlacementHeader: - return RangeConfig{ + return &RangeConfig{ From: 3 * 1000, // 3 KB To: 4 * 1000, // 4 KB } @@ -189,13 +191,13 @@ func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig { return c.GetNormalizedScMaxEachPostBytes() } } else if c.UplinkChunkSize.From < 64 { - return RangeConfig{ + return &RangeConfig{ From: 64, To: max(64, c.UplinkChunkSize.To), } } - return *c.UplinkChunkSize + return c.UplinkChunkSize } func (c *Config) GetNormalizedServerMaxHeaderBytes() int { @@ -207,10 +209,10 @@ func (c *Config) GetNormalizedServerMaxHeaderBytes() int { } func (c *Config) GetNormalizedSessionPlacement() string { - if c.SessionPlacement == "" { + if c.SessionIDPlacement == "" { return PlacementPath } - return c.SessionPlacement + return c.SessionIDPlacement } func (c *Config) GetNormalizedSeqPlacement() string { @@ -228,8 +230,8 @@ func (c *Config) GetNormalizedUplinkDataPlacement() string { } func (c *Config) GetNormalizedSessionKey() string { - if c.SessionKey != "" { - return c.SessionKey + if c.SessionIDKey != "" { + return c.SessionIDKey } switch c.GetNormalizedSessionPlacement() { case PlacementHeader: @@ -417,59 +419,59 @@ func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (session return sessionId, seqStr } -func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig { +func (m *XmuxConfig) GetNormalizedMaxConcurrency() *RangeConfig { if m.MaxConcurrency == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.MaxConcurrency + return m.MaxConcurrency } -func (m *XmuxConfig) GetNormalizedMaxConnections() RangeConfig { +func (m *XmuxConfig) GetNormalizedMaxConnections() *RangeConfig { if m.MaxConnections == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.MaxConnections + return m.MaxConnections } -func (m *XmuxConfig) GetNormalizedCMaxReuseTimes() RangeConfig { +func (m *XmuxConfig) GetNormalizedCMaxReuseTimes() *RangeConfig { if m.CMaxReuseTimes == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.CMaxReuseTimes + return m.CMaxReuseTimes } -func (m *XmuxConfig) GetNormalizedHMaxRequestTimes() RangeConfig { +func (m *XmuxConfig) GetNormalizedHMaxRequestTimes() *RangeConfig { if m.HMaxRequestTimes == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.HMaxRequestTimes + return m.HMaxRequestTimes } -func (m *XmuxConfig) GetNormalizedHMaxReusableSecs() RangeConfig { +func (m *XmuxConfig) GetNormalizedHMaxReusableSecs() *RangeConfig { if m.HMaxReusableSecs == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.HMaxReusableSecs + return m.HMaxReusableSecs } func init() { @@ -478,10 +480,44 @@ func init() { })) } -func (c RangeConfig) rand() int32 { +func (c *RangeConfig) rand() int32 { + if c == nil { + return 0 + } return int32(crypto.RandBetween(int64(c.From), int64(c.To))) } +// predefined +var PredefinedTable = map[string]string{ + "ALPHABET": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "BASE36": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Base62": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "HEX": "0123456789ABCDEF", + "alphabet": "abcdefghijklmnopqrstuvwxyz", + "base36": "0123456789abcdefghijklmnopqrstuvwxyz", + "hex": "0123456789abcdef", + "number": "0123456789", +} + +func (c *Config) GenerateSessionID() string { + length := c.SessionIDLength.rand() + table := c.SessionIDTable + if predefined, ok := PredefinedTable[table]; ok { + table = predefined + } + if table != "" && length > 0 { + id := make([]byte, length) + for i := range id { + id[i] = table[rand.N(len(table))] + } + return string(id) + } else { + uuid := uuid.New() + return uuid.String() + } +} + func appendToPath(path, value string) string { if strings.HasSuffix(path, "/") { return path + value diff --git a/transport/internet/splithttp/config.pb.go b/transport/internet/splithttp/config.pb.go index 4e99d8a8c..c7dfe4ce8 100644 --- a/transport/internet/splithttp/config.pb.go +++ b/transport/internet/splithttp/config.pb.go @@ -179,14 +179,16 @@ type Config struct { XPaddingPlacement string `protobuf:"bytes,17,opt,name=xPaddingPlacement,proto3" json:"xPaddingPlacement,omitempty"` XPaddingMethod string `protobuf:"bytes,18,opt,name=xPaddingMethod,proto3" json:"xPaddingMethod,omitempty"` UplinkHTTPMethod string `protobuf:"bytes,19,opt,name=uplinkHTTPMethod,proto3" json:"uplinkHTTPMethod,omitempty"` - SessionPlacement string `protobuf:"bytes,20,opt,name=sessionPlacement,proto3" json:"sessionPlacement,omitempty"` - SessionKey string `protobuf:"bytes,21,opt,name=sessionKey,proto3" json:"sessionKey,omitempty"` + SessionIDPlacement string `protobuf:"bytes,20,opt,name=sessionIDPlacement,proto3" json:"sessionIDPlacement,omitempty"` + SessionIDKey string `protobuf:"bytes,21,opt,name=sessionIDKey,proto3" json:"sessionIDKey,omitempty"` SeqPlacement string `protobuf:"bytes,22,opt,name=seqPlacement,proto3" json:"seqPlacement,omitempty"` SeqKey string `protobuf:"bytes,23,opt,name=seqKey,proto3" json:"seqKey,omitempty"` UplinkDataPlacement string `protobuf:"bytes,24,opt,name=uplinkDataPlacement,proto3" json:"uplinkDataPlacement,omitempty"` UplinkDataKey string `protobuf:"bytes,25,opt,name=uplinkDataKey,proto3" json:"uplinkDataKey,omitempty"` UplinkChunkSize *RangeConfig `protobuf:"bytes,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"` ServerMaxHeaderBytes int32 `protobuf:"varint,27,opt,name=serverMaxHeaderBytes,proto3" json:"serverMaxHeaderBytes,omitempty"` + SessionIDTable string `protobuf:"bytes,28,opt,name=sessionIDTable,proto3" json:"sessionIDTable,omitempty"` + SessionIDLength *RangeConfig `protobuf:"bytes,29,opt,name=sessionIDLength,proto3" json:"sessionIDLength,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -354,16 +356,16 @@ func (x *Config) GetUplinkHTTPMethod() string { return "" } -func (x *Config) GetSessionPlacement() string { +func (x *Config) GetSessionIDPlacement() string { if x != nil { - return x.SessionPlacement + return x.SessionIDPlacement } return "" } -func (x *Config) GetSessionKey() string { +func (x *Config) GetSessionIDKey() string { if x != nil { - return x.SessionKey + return x.SessionIDKey } return "" } @@ -410,6 +412,20 @@ func (x *Config) GetServerMaxHeaderBytes() int32 { return 0 } +func (x *Config) GetSessionIDTable() string { + if x != nil { + return x.SessionIDTable + } + return "" +} + +func (x *Config) GetSessionIDLength() *RangeConfig { + if x != nil { + return x.SessionIDLength + } + return nil +} + var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor const file_transport_internet_splithttp_config_proto_rawDesc = "" + @@ -425,7 +441,7 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" + "\x0ecMaxReuseTimes\x18\x03 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0ecMaxReuseTimes\x12Z\n" + "\x10hMaxRequestTimes\x18\x04 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxRequestTimes\x12Z\n" + "\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" + - "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xc2\v\n" + + "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xcc\f\n" + "\x06Config\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + @@ -446,17 +462,17 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" + "\x0exPaddingHeader\x18\x10 \x01(\tR\x0exPaddingHeader\x12,\n" + "\x11xPaddingPlacement\x18\x11 \x01(\tR\x11xPaddingPlacement\x12&\n" + "\x0exPaddingMethod\x18\x12 \x01(\tR\x0exPaddingMethod\x12*\n" + - "\x10uplinkHTTPMethod\x18\x13 \x01(\tR\x10uplinkHTTPMethod\x12*\n" + - "\x10sessionPlacement\x18\x14 \x01(\tR\x10sessionPlacement\x12\x1e\n" + - "\n" + - "sessionKey\x18\x15 \x01(\tR\n" + - "sessionKey\x12\"\n" + + "\x10uplinkHTTPMethod\x18\x13 \x01(\tR\x10uplinkHTTPMethod\x12.\n" + + "\x12sessionIDPlacement\x18\x14 \x01(\tR\x12sessionIDPlacement\x12\"\n" + + "\fsessionIDKey\x18\x15 \x01(\tR\fsessionIDKey\x12\"\n" + "\fseqPlacement\x18\x16 \x01(\tR\fseqPlacement\x12\x16\n" + "\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" + "\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" + "\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12X\n" + "\x0fuplinkChunkSize\x18\x1a \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fuplinkChunkSize\x122\n" + - "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x1a:\n" + + "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x12&\n" + + "\x0esessionIDTable\x18\x1c \x01(\tR\x0esessionIDTable\x12X\n" + + "\x0fsessionIDLength\x18\x1d \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fsessionIDLength\x1a:\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" + @@ -496,11 +512,12 @@ var file_transport_internet_splithttp_config_proto_depIdxs = []int32{ 1, // 10: xray.transport.internet.splithttp.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig 4, // 11: xray.transport.internet.splithttp.Config.downloadSettings:type_name -> xray.transport.internet.StreamConfig 0, // 12: xray.transport.internet.splithttp.Config.uplinkChunkSize:type_name -> xray.transport.internet.splithttp.RangeConfig - 13, // [13:13] is the sub-list for method output_type - 13, // [13:13] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 0, // 13: xray.transport.internet.splithttp.Config.sessionIDLength:type_name -> xray.transport.internet.splithttp.RangeConfig + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_transport_internet_splithttp_config_proto_init() } diff --git a/transport/internet/splithttp/config.proto b/transport/internet/splithttp/config.proto index 4c303d293..efe6de03f 100644 --- a/transport/internet/splithttp/config.proto +++ b/transport/internet/splithttp/config.proto @@ -42,12 +42,14 @@ message Config { string xPaddingPlacement = 17; string xPaddingMethod = 18; string uplinkHTTPMethod = 19; - string sessionPlacement = 20; - string sessionKey = 21; + string sessionIDPlacement = 20; + string sessionIDKey = 21; string seqPlacement = 22; string seqKey = 23; string uplinkDataPlacement = 24; string uplinkDataKey = 25; RangeConfig uplinkChunkSize = 26; int32 serverMaxHeaderBytes = 27; + string sessionIDTable = 28; + RangeConfig sessionIDLength = 29; } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 1329713c5..5092a9dbf 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -24,7 +24,6 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/signal/done" - "github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/browser_dialer" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" @@ -376,8 +375,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me sessionId := "" if mode != "stream-one" { - sessionIdUuid := uuid.New() - sessionId = sessionIdUuid.String() + sessionId = transportConfiguration.GenerateSessionID() } errors.LogInfo(ctx, fmt.Sprintf("XHTTP is dialing to %s, mode %s, HTTP version %s, host %s", dest, mode, httpVersion, requestURL.Host)) diff --git a/transport/internet/splithttp/xpadding.go b/transport/internet/splithttp/xpadding.go index 1dcbc3e2f..f2f99340b 100644 --- a/transport/internet/splithttp/xpadding.go +++ b/transport/internet/splithttp/xpadding.go @@ -176,15 +176,15 @@ func ApplyPaddingToQuery(u *url.URL, key, value string) { u.RawQuery = q.Encode() } -func (c *Config) GetNormalizedXPaddingBytes() RangeConfig { +func (c *Config) GetNormalizedXPaddingBytes() *RangeConfig { if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 100, To: 1000, } } - return *c.XPaddingBytes + return c.XPaddingBytes } func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) { From da21a8f77f31b295e77e1b55e9ef408ca3bf9402 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Wed, 10 Jun 2026 04:53:55 +0800 Subject: [PATCH 43/78] TUN & WireGuard inbounds: Ignore b.UDP's domain when receiving it from outbound (#6285) Fixes https://github.com/XTLS/Xray-core/issues/6279 --- proxy/tun/udp_fullcone.go | 30 +++++++++++++++++++----------- proxy/wireguard/tun.go | 34 +++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/proxy/tun/udp_fullcone.go b/proxy/tun/udp_fullcone.go index ab59e97f4..1a1cb0843 100644 --- a/proxy/tun/udp_fullcone.go +++ b/proxy/tun/udp_fullcone.go @@ -98,18 +98,22 @@ type udpConn struct { } func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { - e, ok := <-c.egress - if !ok { - return nil, io.EOF - } + for { + e, ok := <-c.egress + if !ok { + return nil, io.EOF + } - b := buf.New() - if _, err := b.Write(e.data); err != nil { - return nil, err - } - b.UDP = e.dest + b := buf.New() + if _, err := b.Write(e.data); err != nil { + errors.LogErrorInner(context.Background(), err, "drop packet to ", e.dest, " with size ", len(e.data)) + b.Release() + continue + } + b.UDP = e.dest - return buf.MultiBuffer{b}, nil + return buf.MultiBuffer{b}, nil + } } // Read packets from the connection @@ -129,7 +133,11 @@ func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { for i, b := range mb { dst := c.dst if b.UDP != nil { - dst = *b.UDP + if b.UDP.Address.Family().IsDomain() { + errors.LogError(context.Background(), "impossible domain packet ", b.UDP, " reply via original target ", dst) + } else { + dst = *b.UDP + } } err := c.handler.writePacket(b.Bytes(), dst, c.src) if err != nil { diff --git a/proxy/wireguard/tun.go b/proxy/wireguard/tun.go index 2f9c1ab16..971ce89e0 100644 --- a/proxy/wireguard/tun.go +++ b/proxy/wireguard/tun.go @@ -345,19 +345,23 @@ type udpConn struct { } func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { - q, ok := <-c.queue - if !ok { - return nil, io.EOF + for { + q, ok := <-c.queue + if !ok { + return nil, io.EOF + } + + b := buf.New() + if _, err := b.Write(q.p); err != nil { + errors.LogErrorInner(context.Background(), err, "drop packet to ", q.dest, " with size ", len(q.p)) + b.Release() + continue + } + + b.UDP = q.dest + + return buf.MultiBuffer{b}, nil } - - b := buf.New() - if _, err := b.Write(q.p); err != nil { - return nil, err - } - - b.UDP = q.dest - - return buf.MultiBuffer{b}, nil } func (c *udpConn) Read(p []byte) (int, error) { @@ -376,7 +380,11 @@ func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { for i, b := range mb { dst := c.dst if b.UDP != nil { - dst = *b.UDP + if b.UDP.Address.Family().IsDomain() { + errors.LogError(context.Background(), "impossible domain packet ", b.UDP, " reply via original target ", dst) + } else { + dst = *b.UDP + } } err := c.writeFunc(b.Bytes(), dst, c.src) if err != nil { From d27b3e46e2df7d087c8a2acb0a5a9a41cdcf75b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:45:27 +0000 Subject: [PATCH 44/78] Bump golang.org/x/net from 0.55.0 to 0.56.0 (#6310) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0. - [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index b1a64c93e..e48eca14c 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,11 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.53.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/net v0.55.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.45.0 + golang.org/x/net v0.56.0 + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.46.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v1.0.1 @@ -51,10 +51,10 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e292923ef..59fe6c90d 100644 --- a/go.sum +++ b/go.sum @@ -98,22 +98,22 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -121,21 +121,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 862631172dd195679be14485c63ebb67b230615d Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Tue, 16 Jun 2026 23:24:39 +0800 Subject: [PATCH 45/78] WireGuard proxy: Refactor (#6287) And https://github.com/XTLS/Xray-core/pull/6303#issuecomment-4669158076 Fixes https://github.com/XTLS/Xray-core/issues/6257 --- app/proxyman/inbound/always.go | 30 +- app/proxyman/outbound/handler.go | 4 +- common/session/context.go | 10 + infra/conf/wireguard.go | 10 +- infra/conf/wireguard_test.go | 2 - proxy/hysteria/client.go | 15 +- proxy/hysteria/server.go | 18 +- proxy/wireguard/bind.go | 359 ++++++---------- proxy/wireguard/client.go | 601 ++++++++++++++++----------- proxy/wireguard/config.go | 53 --- proxy/wireguard/config.pb.go | 22 +- proxy/wireguard/config.proto | 4 +- proxy/wireguard/gvisortun/tun.go | 226 ---------- proxy/wireguard/netstack.go | 690 +++++++++++++++++++++++++++++++ proxy/wireguard/server.go | 312 +++++++++----- proxy/wireguard/server_test.go | 53 --- proxy/wireguard/tun.go | 198 +++------ proxy/wireguard/tun_default.go | 8 +- proxy/wireguard/tun_linux.go | 226 +++++----- proxy/wireguard/wireguard.go | 77 +--- 20 files changed, 1587 insertions(+), 1331 deletions(-) delete mode 100644 proxy/wireguard/gvisortun/tun.go create mode 100644 proxy/wireguard/netstack.go delete mode 100644 proxy/wireguard/server_test.go diff --git a/app/proxyman/inbound/always.go b/app/proxyman/inbound/always.go index f26a30c2e..462f894f5 100644 --- a/app/proxyman/inbound/always.go +++ b/app/proxyman/inbound/always.go @@ -57,16 +57,23 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig * if err != nil { return nil, err } - - // Set tag and sniffing config in context before creating proxy - // This allows proxies like TUN to access these settings - ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag}) - if receiverConfig.SniffingSettings != nil { - ctx = session.ContextWithContent(ctx, &session.Content{ - SniffingRequest: sniffingRequest, - }) + src := net.TCPDestination(net.AnyIP, 0) + if receiverConfig.Listen != nil { + src.Address = receiverConfig.Listen.AsAddress() } - rawProxy, err := common.CreateObject(ctx, proxyConfig) + if receiverConfig.PortList != nil && len(receiverConfig.PortList.Range) > 0 { + src.Port = net.Port(receiverConfig.PortList.Range[0].From) + } + mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings) + if err != nil { + return nil, errors.New("failed to parse stream config").Base(err).AtWarning() + } + + newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src}) + newCtx = session.ContextWithContent(newCtx, &session.Content{SniffingRequest: sniffingRequest}) + newCtx = session.ContextWithStreamSettings(newCtx, mss) + + rawProxy, err := common.CreateObject(newCtx, proxyConfig) if err != nil { return nil, err } @@ -92,11 +99,6 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig * address = net.AnyIP } - mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings) - if err != nil { - return nil, errors.New("failed to parse stream config").Base(err).AtWarning() - } - if receiverConfig.ReceiveOriginalDestination { if mss.SocketSettings == nil { mss.SocketSettings = &internet.SocketConfig{} diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 578b2ca16..2fe0fec55 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -108,7 +108,9 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou ctx = session.ContextWithFullHandler(ctx, h) - rawProxyHandler, err := common.CreateObject(ctx, proxyConfig) + newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings) + + rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig) if err != nil { return nil, err } diff --git a/common/session/context.go b/common/session/context.go index c28f20816..abe4219ca 100644 --- a/common/session/context.go +++ b/common/session/context.go @@ -26,6 +26,8 @@ const ( fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer + + streamSettingsKey ctx.SessionKey = 13 ) func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context { @@ -192,3 +194,11 @@ func MitmServerNameFromContext(ctx context.Context) string { } return "" } + +func ContextWithStreamSettings(ctx context.Context, streamSettings any) context.Context { + return context.WithValue(ctx, streamSettingsKey, streamSettings) +} + +func StreamSettingsFromContext(ctx context.Context) any { + return ctx.Value(streamSettingsKey) +} diff --git a/infra/conf/wireguard.go b/infra/conf/wireguard.go index 1ba61f96b..d985184f4 100644 --- a/infra/conf/wireguard.go +++ b/infra/conf/wireguard.go @@ -3,6 +3,7 @@ package conf import ( "encoding/base64" "encoding/hex" + "strconv" "strings" "github.com/xtls/xray-core/common/errors" @@ -37,8 +38,9 @@ func (c *WireGuardPeerConfig) Build() (proto.Message, error) { } config.Endpoint = c.Endpoint - // default 0 - config.KeepAlive = c.KeepAlive + if c.KeepAlive != 0 { + config.KeepAlive = strconv.FormatUint(uint64(c.KeepAlive), 10) + } if c.AllowedIPs == nil { config.AllowedIps = []string{"0.0.0.0/0", "::0/0"} } else { @@ -56,7 +58,6 @@ type WireGuardConfig struct { Address []string `json:"address"` Peers []*WireGuardPeerConfig `json:"peers"` MTU int32 `json:"mtu"` - NumWorkers int32 `json:"workers"` Reserved []byte `json:"reserved"` DomainStrategy string `json:"domainStrategy"` } @@ -93,9 +94,6 @@ func (c *WireGuardConfig) Build() (proto.Message, error) { } else { config.Mtu = c.MTU } - // these a fallback code exists in wireguard-go code, - // we don't need to process fallback manually - config.NumWorkers = c.NumWorkers if len(c.Reserved) != 0 && len(c.Reserved) != 3 { return nil, errors.New(`"reserved" should be empty or 3 bytes`) diff --git a/infra/conf/wireguard_test.go b/infra/conf/wireguard_test.go index c4c24c44a..2e1903db8 100644 --- a/infra/conf/wireguard_test.go +++ b/infra/conf/wireguard_test.go @@ -38,12 +38,10 @@ func TestWireGuardConfig(t *testing.T) { // also can read from hex form directly PublicKey: "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a", Endpoint: "127.0.0.1:1234", - KeepAlive: 0, AllowedIps: []string{"0.0.0.0/0", "::0/0"}, }, }, Mtu: 1300, - NumWorkers: 2, DomainStrategy: wireguard.DeviceConfig_FORCE_IP64, NoKernelTun: false, }, diff --git a/proxy/hysteria/client.go b/proxy/hysteria/client.go index 7d5730b6b..5b602c342 100644 --- a/proxy/hysteria/client.go +++ b/proxy/hysteria/client.go @@ -29,6 +29,13 @@ type Client struct { } func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + v := core.MustFromContext(ctx) + p := v.GetFeature(policy.ManagerType()).(policy.Manager) + + streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig) + if _, ok := streamSettings.ProtocolSettings.(*hysteria.Config); !ok { + return nil, errors.New("not hysteria transport") + } if config.Server == nil { return nil, errors.New(`no target server found`) } @@ -37,12 +44,10 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { return nil, errors.New("failed to get server spec").Base(err) } - v := core.MustFromContext(ctx) - client := &Client{ + return &Client{ server: server, - policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), - } - return client, nil + policyManager: p, + }, nil } func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index 815faca11..d7456dc30 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -16,6 +16,7 @@ import ( "github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/proxy/hysteria/account" "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/hysteria" "github.com/xtls/xray-core/transport/internet/stat" ) @@ -27,6 +28,14 @@ type Server struct { } func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + v := core.MustFromContext(ctx) + p := v.GetFeature(policy.ManagerType()).(policy.Manager) + + streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig) + if _, ok := streamSettings.ProtocolSettings.(*hysteria.Config); !ok { + return nil, errors.New("not hysteria transport") + } + validator := account.NewValidator() for _, user := range config.Users { u, err := user.ToMemoryUser() @@ -39,14 +48,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { } } - v := core.MustFromContext(ctx) - s := &Server{ + return &Server{ config: config, validator: validator, - policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), - } - - return s, nil + policyManager: p, + }, nil } func (s *Server) HysteriaInboundValidator() *account.Validator { diff --git a/proxy/wireguard/bind.go b/proxy/wireguard/bind.go index c5c0f8c8e..0e71e1539 100644 --- a/proxy/wireguard/bind.go +++ b/proxy/wireguard/bind.go @@ -2,265 +2,150 @@ package wireguard import ( "context" - gonet "net" + goerrors "errors" + "io" + "net" "net/netip" - "runtime" "strconv" + "sync" + "syscall" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - - "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/features/dns" - "github.com/xtls/xray-core/transport/internet" + "golang.zx2c4.com/wireguard/conn" ) -type netReadInfo struct { - buff *buf.Buffer - endpoint conn.Endpoint +type bind struct { + resolveFunc func(host string) (net.IP, error) + listenFunc func() (net.PacketConn, error) + downFunc func() error + reserved []byte + + net.PacketConn + closeCh chan struct{} + mu sync.Mutex } -// reduce duplicated code -type netBind struct { - dns dns.Client - dnsOption dns.IPOption +func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) { + b.mu.Lock() + defer b.mu.Unlock() - workers int - readQueue chan *netReadInfo - closedCh chan struct{} + if b.PacketConn != nil { + return nil, 0, conn.ErrBindAlreadyOpen + } + + c, err := b.listenFunc() + if err != nil { + return nil, 0, err + } + b.PacketConn = c + ch := make(chan struct{}) + b.closeCh = ch + + return []conn.ReceiveFunc{ + func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) { + for { + n, addr, err := c.ReadFrom(bufs[0]) + if err != nil { + if goerrors.Is(err, io.EOF) || goerrors.Is(err, io.ErrClosedPipe) || goerrors.Is(err, net.ErrClosed) { + select { + case <-ch: + default: + errors.LogErrorInner(context.Background(), err, "unexpected closed") + if b.downFunc != nil { + go func() { + common.Must(b.downFunc()) + }() + } + } + return 0, net.ErrClosed + } + errors.LogErrorInner(context.Background(), err, "bind recv err") + continue + } + if n > 3 { + bufs[0][1] = 0 + bufs[0][2] = 0 + bufs[0][3] = 0 + } + sizes[0] = n + eps[0] = &conn.StdNetEndpoint{AddrPort: addr.(*net.UDPAddr).AddrPort()} + return 1, nil + } + }, + }, uint16(c.LocalAddr().(*net.UDPAddr).Port), nil } -// SetMark implements conn.Bind -func (bind *netBind) SetMark(mark uint32) error { +func (b *bind) Close() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.PacketConn != nil { + close(b.closeCh) + _ = b.PacketConn.Close() + b.PacketConn = nil + } return nil } -// ParseEndpoint implements conn.Bind -func (n *netBind) ParseEndpoint(s string) (conn.Endpoint, error) { - ipStr, port, err := net.SplitHostPort(s) - if err != nil { - return nil, err - } - portNum, err := strconv.Atoi(port) - if err != nil { - return nil, err +func (b *bind) SetMark(mark uint32) error { + return nil +} + +func (b *bind) Send(bufs [][]byte, ep conn.Endpoint) (err error) { + b.mu.Lock() + c := b.PacketConn + b.mu.Unlock() + + if c == nil { + return syscall.EAFNOSUPPORT } - addr := net.ParseAddress(ipStr) - if addr.Family() == net.AddressFamilyDomain { - ips, _, err := n.dns.LookupIP(addr.Domain(), n.dnsOption) + for i := range bufs { + if len(bufs[i]) > 3 && len(b.reserved) == 3 { + bufs[i][1] = b.reserved[0] + bufs[i][2] = b.reserved[1] + bufs[i][3] = b.reserved[2] + } + _, err = c.WriteTo(bufs[i], net.UDPAddrFromAddrPort(ep.(*conn.StdNetEndpoint).AddrPort)) if err != nil { - return nil, err - } else if len(ips) == 0 { - return nil, dns.ErrEmptyResponse - } - addr = net.IPAddress(ips[0]) - } - - dst := net.Destination{ - Address: addr, - Port: net.Port(portNum), - Network: net.Network_UDP, - } - - return &netEndpoint{ - dst: dst, - }, nil -} - -// BatchSize implements conn.Bind -func (bind *netBind) BatchSize() int { - return 1 -} - -// Open implements conn.Bind -func (bind *netBind) Open(uport uint16) ([]conn.ReceiveFunc, uint16, error) { - bind.closedCh = make(chan struct{}) - errors.LogDebug(context.Background(), "bind opened") - - fun := func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) { - select { - case r := <-bind.readQueue: - sizes[0], eps[0] = copy(bufs[0], r.buff.Bytes()), r.endpoint - r.buff.Release() - return 1, nil - case <-bind.closedCh: - errors.LogDebug(context.Background(), "recv func closed") - return 0, gonet.ErrClosed + errors.LogErrorInner(context.Background(), err, "bind send err") + break } } - workers := bind.workers - if workers <= 0 { - workers = runtime.NumCPU() - } - if workers <= 0 { - workers = 1 - } - arr := make([]conn.ReceiveFunc, workers) - for i := 0; i < workers; i++ { - arr[i] = fun - } - - return arr, uint16(uport), nil -} - -// Close implements conn.Bind -func (bind *netBind) Close() error { - errors.LogDebug(context.Background(), "bind closed") - if bind.closedCh != nil { - close(bind.closedCh) - } - return nil -} - -type netBindClient struct { - netBind - - ctx context.Context - dialer internet.Dialer - reserved []byte -} - -func (bind *netBindClient) connectTo(endpoint *netEndpoint) error { - c, err := bind.dialer.Dial(bind.ctx, endpoint.dst) - if err != nil { - return err - } - endpoint.conn = c - - go func() { - for { - buff := buf.NewWithSize(device.MaxMessageSize) - n, err := buff.ReadFrom(c) - if err != nil { - buff.Release() - endpoint.conn = nil - c.Close() - return - } - - rawBytes := buff.Bytes() - if n > 3 { - rawBytes[1] = 0 - rawBytes[2] = 0 - rawBytes[3] = 0 - } - - select { - case bind.readQueue <- &netReadInfo{ - buff: buff, - endpoint: endpoint, - }: - case <-bind.closedCh: - buff.Release() - endpoint.conn = nil - c.Close() - return - } - } - }() - - return nil -} - -func (bind *netBindClient) Send(buff [][]byte, endpoint conn.Endpoint) error { - var err error - - nend, ok := endpoint.(*netEndpoint) - if !ok { - return conn.ErrWrongEndpointType - } - - if nend.conn == nil { - err = bind.connectTo(nend) - if err != nil { - return err - } - } - - for _, buff := range buff { - if len(buff) > 3 && len(bind.reserved) == 3 { - copy(buff[1:], bind.reserved) - } - if _, err = nend.conn.Write(buff); err != nil { - return err - } - } - return nil -} - -type netBindServer struct { - netBind -} - -func (bind *netBindServer) Send(buff [][]byte, endpoint conn.Endpoint) error { - var err error - - nend, ok := endpoint.(*netEndpoint) - if !ok { - return conn.ErrWrongEndpointType - } - - if nend.conn == nil { - errors.LogDebug(context.Background(), nend.dst.NetAddr(), " send on closed peer") - return errors.New("peer closed") - } - - for _, buff := range buff { - if _, err = nend.conn.Write(buff); err != nil { - return err - } - } - return err } -type netEndpoint struct { - dst net.Destination - conn net.Conn -} - -func (netEndpoint) ClearSrc() {} - -func (e netEndpoint) DstIP() netip.Addr { - return netip.Addr{} -} - -func (e netEndpoint) SrcIP() netip.Addr { - return netip.Addr{} -} - -func (e netEndpoint) DstToBytes() []byte { - var dat []byte - if e.dst.Address.Family().IsIPv4() { - dat = e.dst.Address.IP().To4()[:] - } else { - dat = e.dst.Address.IP().To16()[:] - } - dat = append(dat, byte(e.dst.Port), byte(e.dst.Port>>8)) - return dat -} - -func (e netEndpoint) DstToString() string { - return e.dst.NetAddr() -} - -func (e netEndpoint) SrcToString() string { - return "" -} - -func toNetIpAddr(addr net.Address) netip.Addr { - if addr.Family().IsIPv4() { - ip := addr.IP() - return netip.AddrFrom4([4]byte{ip[0], ip[1], ip[2], ip[3]}) - } else { - ip := addr.IP() - arr := [16]byte{} - for i := 0; i < 16; i++ { - arr[i] = ip[i] +func (b *bind) ParseEndpoint(s string) (conn.Endpoint, error) { + if b.resolveFunc == nil { + e, err := netip.ParseAddrPort(s) + if err != nil { + return nil, err } - return netip.AddrFrom16(arr) + return &conn.StdNetEndpoint{ + AddrPort: e, + }, nil } + host, sport, err := net.SplitHostPort(s) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(sport) + if err != nil { + return nil, err + } + if port < 0 || port > 65535 { + return nil, errors.New("invalid port " + sport) + } + ip, err := b.resolveFunc(host) + if err != nil { + return nil, err + } + addr, _ := netip.AddrFromSlice(ip) + return &conn.StdNetEndpoint{ + AddrPort: netip.AddrPortFrom(addr, uint16(port)), + }, nil +} + +func (b *bind) BatchSize() int { + return 1 } diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index 8a02d89f1..4491886ab 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -1,148 +1,135 @@ -/* - -Some of codes are copied from https://github.com/octeep/wireproxy, license below. - -Copyright (c) 2022 Wind T.F. Wong - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -*/ - package wireguard import ( "context" "fmt" + gonet "net" "net/netip" + reflect "reflect" "strings" "sync" + "golang.zx2c4.com/wireguard/tun" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" + "golang.zx2c4.com/wireguard/device" ) -// Handler is an outbound connection that silently swallow the entire payload. type Handler struct { conf *DeviceConfig - net Tunnel - bind *netBindClient policyManager policy.Manager dns dns.Client - // cached configuration - endpoints []netip.Addr - hasIPv4, hasIPv6 bool - wgLock sync.Mutex + + streamSettings *internet.MemoryStreamConfig + uplinkCounter stats.Counter + downlinkCounter stats.Counter + + tun tun.Device + tnet *Net + dev *device.Device + mu sync.Mutex } -// New creates a new wireguard handler. -func New(ctx context.Context, conf *DeviceConfig) (*Handler, error) { +func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { v := core.MustFromContext(ctx) + p := v.GetFeature(policy.ManagerType()).(policy.Manager) + d := v.GetFeature(dns.ClientType()).(dns.Client) - endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf) + streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig) + tag := session.FullHandlerFromContext(ctx).Tag() + var uplinkCounter stats.Counter + var downlinkCounter stats.Counter + if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "outbound>>>" + tag + ">>>traffic>>>uplink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + uplinkCounter = c + } + } + if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "outbound>>>" + tag + ">>>traffic>>>downlink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + downlinkCounter = c + } + } + + if len(conf.Peers) == 0 { + return nil, errors.New("empty peers") + } + for _, peer := range conf.Peers { + if peer.PublicKey == "" { + return nil, errors.New("peer without publickey") + } + if peer.Endpoint == "" { + return nil, errors.New("peer without endpoint") + } + } + + localAddresses := make([]netip.Addr, 0, len(conf.Endpoint)) + for _, localaddress := range conf.Endpoint { + addr, err := netip.ParseAddr(localaddress) + if err == nil { + localAddresses = append(localAddresses, addr) + continue + } + prefix, err := netip.ParsePrefix(localaddress) + if err == nil { + localAddresses = append(localAddresses, prefix.Addr()) + continue + } + return nil, err + } + + kernelTunSupported, err := KernelTunSupported() + if err != nil { + errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support") + } + var tun tun.Device + var tnet *Net + if !conf.NoKernelTun && kernelTunSupported { + errors.LogWarning(context.Background(), "Using kernel TUN") + tun, tnet, err = createKernelTun(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu)) + } else { + errors.LogWarning(context.Background(), "Using gVisor TUN") + tun, tnet, _, err = CreateNetTUN(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu), true) + } if err != nil { return nil, err } - d := v.GetFeature(dns.ClientType()).(dns.Client) return &Handler{ conf: conf, - policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + policyManager: p, dns: d, - endpoints: endpoints, - hasIPv4: hasIPv4, - hasIPv6: hasIPv6, + + streamSettings: streamSettings, + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, + + tun: tun, + tnet: tnet, }, nil } -func (h *Handler) Close() (err error) { - go func() { - h.wgLock.Lock() - defer h.wgLock.Unlock() - - if h.net != nil { - _ = h.net.Close() - h.net = nil - } - }() - - return nil -} - -func (h *Handler) processWireGuard(ctx context.Context, dialer internet.Dialer) (err error) { - h.wgLock.Lock() - defer h.wgLock.Unlock() - - if h.bind != nil && h.bind.dialer == dialer && h.net != nil { - return nil - } - - log.Record(&log.GeneralMessage{ - Severity: log.Severity_Info, - Content: "switching dialer", - }) - - if h.net != nil { - _ = h.net.Close() - h.net = nil - } - if h.bind != nil { - _ = h.bind.Close() - h.bind = nil - } - - // bind := conn.NewStdNetBind() // TODO: conn.Bind wrapper for dialer - h.bind = &netBindClient{ - netBind: netBind{ - dns: h.dns, - dnsOption: dns.IPOption{ - IPv4Enable: h.hasIPv4, - IPv6Enable: h.hasIPv6, - }, - workers: int(h.conf.NumWorkers), - readQueue: make(chan *netReadInfo), - }, - ctx: ctx, - dialer: dialer, - reserved: h.conf.Reserved, - } - defer func() { - if err != nil { - h.bind.Close() - h.bind = nil - } - }() - - h.net, err = h.makeVirtualTun() - if err != nil { - return errors.New("failed to create virtual tun interface").Base(err) - } - return nil -} - -// Process implements OutboundHandler.Dispatch(). +// Process implements proxy.Outbound.Process. func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { outbounds := session.OutboundsFromContext(ctx) ob := outbounds[len(outbounds)-1] @@ -152,40 +139,31 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte ob.Name = "wireguard" ob.CanSpliceCopy = 3 - if err := h.processWireGuard(ctx, dialer); err != nil { + if h.dev == nil { + if err := h.init(ctx); err != nil { + return err + } + } + + if err := h.dev.Up(); err != nil { return err } - // Destination of the inner request. - destination := ob.Target - command := protocol.RequestCommandTCP - if destination.Network == net.Network_UDP { - command = protocol.RequestCommandUDP + var addr netip.Addr + if ob.Target.Address.Family().IsDomain() { + ip, err := h.resolveRemote(ob.Target.Address.String()) + if err != nil { + return errors.New("failed to resolve domain").Base(err) + } + addr, _ = netip.AddrFromSlice(ip) + } else { + addr, _ = netip.AddrFromSlice(ob.Target.Address.IP()) } - // resolve dns - addr := destination.Address - if addr.Family().IsDomain() { - ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{ - IPv4Enable: h.hasIPv4 && h.conf.preferIP4(), - IPv6Enable: h.hasIPv6 && h.conf.preferIP6(), - }) - { // Resolve fallback - if (len(ips) == 0 || err != nil) && h.conf.hasFallback() { - ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{ - IPv4Enable: h.hasIPv4 && h.conf.fallbackIP4(), - IPv6Enable: h.hasIPv6 && h.conf.fallbackIP6(), - }) - } - } - if err != nil { - return errors.New("failed to lookup DNS").Base(err) - } else if len(ips) == 0 { - return dns.ErrEmptyResponse - } - addr = net.IPAddress(ips[dice.Roll(len(ips))]) + addrPort := netip.AddrPortFrom(addr, ob.Target.Port.Value()) + if !addrPort.IsValid() { + return errors.New("invalid target ", ob.Target) } - destination.Address = addr var newCtx context.Context var newCancel context.CancelFunc @@ -193,59 +171,64 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte newCtx, newCancel = context.WithCancel(context.Background()) } - p := h.policyManager.ForLevel(0) - + sessionPolicy := h.policyManager.ForLevel(0) ctx, cancel := context.WithCancel(ctx) timer := signal.CancelAfterInactivity(ctx, func() { cancel() if newCancel != nil { newCancel() } - }, p.Timeouts.ConnectionIdle) - addrPort := netip.AddrPortFrom(toNetIpAddr(addr), destination.Port.Value()) + }, sessionPolicy.Timeouts.ConnectionIdle) - var requestFunc func() error - var responseFunc func() error + if newCtx != nil { + ctx = newCtx + } - if command == protocol.RequestCommandTCP { - conn, err := h.net.DialContextTCPAddrPort(ctx, addrPort) + var reader buf.Reader + var writer buf.Writer + + switch ob.Target.Network { + case net.Network_TCP: + var conn net.Conn + var err error + if sessionPolicy.Timeouts.Handshake != 0 { + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, sessionPolicy.Timeouts.Handshake) + conn, err = h.tnet.DialContextTCPAddrPort(timeoutCtx, addrPort) + timeoutCancel() + } else { + conn, err = h.tnet.DialContextTCPAddrPort(ctx, addrPort) + } if err != nil { return errors.New("failed to create TCP connection").Base(err) } defer conn.Close() - - requestFunc = func() error { - defer timer.SetTimeout(p.Timeouts.DownlinkOnly) - return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) - } - responseFunc = func() error { - defer timer.SetTimeout(p.Timeouts.UplinkOnly) - return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) - } - } else if command == protocol.RequestCommandUDP { - conn, err := h.net.DialUDPAddrPort(netip.AddrPort{}, addrPort) + reader = buf.NewReader(conn) + writer = buf.NewWriter(conn) + case net.Network_UDP: + conn, err := h.tnet.DialUDPAddrPort(netip.AddrPort{}, addrPort) if err != nil { return errors.New("failed to create UDP connection").Base(err) } defer conn.Close() - - conn = &udpConnClient{ - Conn: conn, - dest: destination, - } - - requestFunc = func() error { - defer timer.SetTimeout(p.Timeouts.DownlinkOnly) - return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) - } - responseFunc = func() error { - defer timer.SetTimeout(p.Timeouts.UplinkOnly) - return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + c := &udpConnClient{ + PacketConn: conn.(*internet.PacketConnWrapper).PacketConn, + resolveFunc: h.resolveRemote, + dest: gonet.UDPAddrFromAddrPort(addrPort), } + reader = c + writer = c + default: + panic(ob.Target.Network) } - if newCtx != nil { - ctx = newCtx + requestFunc := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)) + } + + responseFunc := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) } responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer)) @@ -258,108 +241,191 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return nil } -// creates a tun interface on netstack given a configuration -func (h *Handler) makeVirtualTun() (Tunnel, error) { - t, err := h.conf.createTun()(h.endpoints, int(h.conf.Mtu), nil) +func (h *Handler) Close() (err error) { + h.mu.Lock() + defer h.mu.Unlock() + if h.dev != nil { + h.dev.Close() + h.dev = nil + h.tun = nil + } else if h.tun != nil { + h.tun.Close() + h.tun = nil + } + return nil +} + +func (h *Handler) init(ctx context.Context) error { + h.mu.Lock() + defer h.mu.Unlock() + if h.dev != nil { + return nil + } + resolveFunc := h.resolveLocal + listenFunc := func() (net.PacketConn, error) { + dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint) + if err != nil { + return nil, err + } + conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings) + if err != nil { + return nil, err + } + var pktConn net.PacketConn + switch c := conn.(type) { + case *internet.PacketConnWrapper: + pktConn = c.PacketConn + case *cnc.Connection: + pktConn = &internet.FakePacketConn{Conn: c} + default: + panic(reflect.TypeOf(c)) + } + if h.streamSettings.UdpmaskManager != nil { + newConn, err := h.streamSettings.UdpmaskManager.WrapPacketConnClient(pktConn) + if err != nil { + pktConn.Close() + return nil, errors.New("mask err").Base(err) + } + pktConn = newConn + } + if h.uplinkCounter != nil || h.downlinkCounter != nil { + pktConn = &PacketCounterConnection{ + PacketConn: pktConn, + ReadCounter: h.downlinkCounter, + WriteCounter: h.uplinkCounter, + } + } + return pktConn, nil + } + bind := &bind{} + logger := &device.Logger{ + Verbosef: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Debug, + Content: fmt.Sprintf(format, args...), + }) + }, + Errorf: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Error, + Content: fmt.Sprintf(format, args...), + }) + }, + } + dev := device.NewDevice(h.tun, bind, logger) + bind.resolveFunc = resolveFunc + bind.listenFunc = listenFunc + bind.downFunc = dev.Down + bind.reserved = h.conf.Reserved + var cfg strings.Builder + cfg.WriteString("private_key=" + h.conf.SecretKey + "\n") + for _, peer := range h.conf.Peers { + cfg.WriteString("public_key=" + peer.PublicKey + "\n") + if peer.PreSharedKey != "" { + cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n") + } + cfg.WriteString("endpoint=" + peer.Endpoint + "\n") + for _, ip := range peer.AllowedIps { + cfg.WriteString("allowed_ip=" + ip + "\n") + } + if peer.KeepAlive != "" { + cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n") + } + } + err := dev.IpcSet(cfg.String()) + if err != nil { + return err + } + err = dev.Up() + if err != nil { + return err + } + h.dev = dev + return nil +} + +func (h *Handler) resolveLocal(host string) (net.IP, error) { + return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) { + ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) + return ips, err + }) +} + +func (h *Handler) resolveRemote(host string) (net.IP, error) { + return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) { + addrs, err := h.tnet.LookupHost(host) + if err != nil { + return nil, err + } + ips := make([]net.IP, 0, len(addrs)) + for _, addr := range addrs { + ips = append(ips, net.ParseIP(addr)) + } + return ips, nil + }) +} + +func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) { + if ip := net.ParseIP(host); ip != nil { + return ip, nil + } + ips, err := lookupIP(host) if err != nil { return nil, err } - - h.bind.dnsOption.IPv4Enable = h.hasIPv4 - h.bind.dnsOption.IPv6Enable = h.hasIPv6 - - if err = t.BuildDevice(h.createIPCRequest(), h.bind); err != nil { - _ = t.Close() - return nil, err + if len(ips) == 0 { + return nil, dns.ErrEmptyResponse } - return t, nil -} - -// serialize the config into an IPC request -func (h *Handler) createIPCRequest() string { - var request strings.Builder - - request.WriteString(fmt.Sprintf("private_key=%s\n", h.conf.SecretKey)) - - if !h.conf.IsClient { - // placeholder, we'll handle actual port listening on Xray - request.WriteString("listen_port=1337\n") - } - - for _, peer := range h.conf.Peers { - if peer.PublicKey != "" { - request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey)) - } - - if peer.PreSharedKey != "" { - request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey)) - } - - address, port, err := net.SplitHostPort(peer.Endpoint) - if err != nil { - errors.LogError(h.bind.ctx, "failed to split endpoint ", peer.Endpoint, " into address and port") - } - addr := net.ParseAddress(address) - if addr.Family().IsDomain() { - dialerIp := h.bind.dialer.DestIpAddress() - if dialerIp != nil { - addr = net.ParseAddress(dialerIp.String()) - errors.LogInfo(h.bind.ctx, "createIPCRequest use dialer dest ip: ", addr) - } else { - ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{ - IPv4Enable: h.conf.preferIP4(), - IPv6Enable: h.conf.preferIP6(), - }) - { // Resolve fallback - if (len(ips) == 0 || err != nil) && h.conf.hasFallback() { - ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{ - IPv4Enable: h.conf.fallbackIP4(), - IPv6Enable: h.conf.fallbackIP6(), - }) - } - } - if err != nil { - errors.LogInfoInner(h.bind.ctx, err, "createIPCRequest failed to lookup DNS") - } else if len(ips) == 0 { - errors.LogInfo(h.bind.ctx, "createIPCRequest empty lookup DNS") - } else { - addr = net.IPAddress(ips[dice.Roll(len(ips))]) - } - } - } - - if peer.Endpoint != "" { - request.WriteString(fmt.Sprintf("endpoint=%s:%s\n", addr, port)) - } - - for _, ip := range peer.AllowedIps { - request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip)) - } - - if peer.KeepAlive != 0 { - request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive)) + var got4, got6 []net.IP + for _, ip := range ips { + if ip.To4() != nil { + got4 = append(got4, ip) + } else { + got6 = append(got6, ip) } } - - return request.String()[:request.Len()] + var got []net.IP + switch strategy { + case DeviceConfig_FORCE_IP: + got = ips + return ips[dice.Roll(len(ips))], nil + case DeviceConfig_FORCE_IP4: + got = got4 + case DeviceConfig_FORCE_IP6: + got = got6 + case DeviceConfig_FORCE_IP46: + got = got4 + if len(got) == 0 { + got = got6 + } + case DeviceConfig_FORCE_IP64: + got = got6 + if len(got) == 0 { + got = got4 + } + default: + panic(strategy) + } + if len(got) == 0 { + return nil, dns.ErrEmptyResponse + } + return got[dice.Roll(len(got))], nil } type udpConnClient struct { - net.Conn - dest net.Destination + net.PacketConn + resolveFunc func(host string) (net.IP, error) + dest *net.UDPAddr } func (c *udpConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) { b := buf.New() b.Resize(0, buf.Size) - n, addr, err := c.Conn.(net.PacketConn).ReadFrom(b.Bytes()) + n, addr, err := c.PacketConn.ReadFrom(b.Bytes()) if err != nil { b.Release() return nil, err } - if addr == nil { // should never hit - addr = c.dest.RawNetAddr() - } b.Resize(0, int32(n)) b.UDP = &net.Destination{ @@ -375,9 +441,22 @@ func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error { for i, b := range mb { dst := c.dest if b.UDP != nil { - dst = *b.UDP + if b.UDP.Address.Family().IsDomain() { + ip, err := c.resolveFunc(b.UDP.Address.String()) + if err != nil { + errors.LogErrorInner(context.Background(), err, "drop packet to ", b.UDP, " with size ", len(b.Bytes())) + b.Release() + continue + } + dst = &net.UDPAddr{ + IP: ip, + Port: int(b.UDP.Port), + } + } else { + dst = b.UDP.RawNetAddr().(*net.UDPAddr) + } } - _, err := c.Conn.(net.PacketConn).WriteTo(b.Bytes(), dst.RawNetAddr()) + _, err := c.PacketConn.WriteTo(b.Bytes(), dst) if err != nil { buf.ReleaseMulti(mb[i:]) return err @@ -386,3 +465,25 @@ func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error { } return nil } + +type PacketCounterConnection struct { + net.PacketConn + ReadCounter stats.Counter + WriteCounter stats.Counter +} + +func (c *PacketCounterConnection) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, addr, err = c.PacketConn.ReadFrom(p) + if err == nil && c.ReadCounter != nil { + c.ReadCounter.Add(int64(n)) + } + return +} + +func (c *PacketCounterConnection) WriteTo(p []byte, addr net.Addr) (n int, err error) { + n, err = c.PacketConn.WriteTo(p, addr) + if err == nil && c.WriteCounter != nil { + c.WriteCounter.Add(int64(n)) + } + return +} diff --git a/proxy/wireguard/config.go b/proxy/wireguard/config.go index cbaa670b7..0b280a906 100644 --- a/proxy/wireguard/config.go +++ b/proxy/wireguard/config.go @@ -1,54 +1 @@ package wireguard - -import ( - "context" - - "github.com/xtls/xray-core/common/errors" -) - -func (c *DeviceConfig) preferIP4() bool { - return c.DomainStrategy == DeviceConfig_FORCE_IP || - c.DomainStrategy == DeviceConfig_FORCE_IP4 || - c.DomainStrategy == DeviceConfig_FORCE_IP46 -} - -func (c *DeviceConfig) preferIP6() bool { - return c.DomainStrategy == DeviceConfig_FORCE_IP || - c.DomainStrategy == DeviceConfig_FORCE_IP6 || - c.DomainStrategy == DeviceConfig_FORCE_IP64 -} - -func (c *DeviceConfig) hasFallback() bool { - return c.DomainStrategy == DeviceConfig_FORCE_IP46 || c.DomainStrategy == DeviceConfig_FORCE_IP64 -} - -func (c *DeviceConfig) fallbackIP4() bool { - return c.DomainStrategy == DeviceConfig_FORCE_IP64 -} - -func (c *DeviceConfig) fallbackIP6() bool { - return c.DomainStrategy == DeviceConfig_FORCE_IP46 -} - -func (c *DeviceConfig) createTun() tunCreator { - if !c.IsClient { - // See tun_linux.go createKernelTun() - errors.LogWarning(context.Background(), "Using gVisor TUN. WG inbound doesn't support kernel TUN yet.") - return createGVisorTun - } - if c.NoKernelTun { - errors.LogWarning(context.Background(), "Using gVisor TUN. NoKernelTun is set to true.") - return createGVisorTun - } - kernelTunSupported, err := KernelTunSupported() - if err != nil { - errors.LogWarning(context.Background(), "Using gVisor TUN. Failed to check kernel TUN support:", err) - return createGVisorTun - } - if !kernelTunSupported { - errors.LogWarning(context.Background(), "Using gVisor TUN. Kernel TUN is not supported on your OS, or your permission is insufficient.") - return createGVisorTun - } - errors.LogWarning(context.Background(), "Using kernel TUN.") - return createKernelTun -} diff --git a/proxy/wireguard/config.pb.go b/proxy/wireguard/config.pb.go index 16134c8d6..5a5ab288b 100644 --- a/proxy/wireguard/config.pb.go +++ b/proxy/wireguard/config.pb.go @@ -81,7 +81,7 @@ type PeerConfig struct { PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` PreSharedKey string `protobuf:"bytes,2,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty"` Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` - KeepAlive uint32 `protobuf:"varint,4,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty"` + KeepAlive string `protobuf:"bytes,4,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty"` AllowedIps []string `protobuf:"bytes,5,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -138,11 +138,11 @@ func (x *PeerConfig) GetEndpoint() string { return "" } -func (x *PeerConfig) GetKeepAlive() uint32 { +func (x *PeerConfig) GetKeepAlive() string { if x != nil { return x.KeepAlive } - return 0 + return "" } func (x *PeerConfig) GetAllowedIps() []string { @@ -158,7 +158,6 @@ type DeviceConfig struct { Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"` Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"` Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"` - NumWorkers int32 `protobuf:"varint,5,opt,name=num_workers,json=numWorkers,proto3" json:"num_workers,omitempty"` Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"` DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"` IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"` @@ -225,13 +224,6 @@ func (x *DeviceConfig) GetMtu() int32 { return 0 } -func (x *DeviceConfig) GetNumWorkers() int32 { - if x != nil { - return x.NumWorkers - } - return 0 -} - func (x *DeviceConfig) GetReserved() []byte { if x != nil { return x.Reserved @@ -272,17 +264,15 @@ const file_proxy_wireguard_config_proto_rawDesc = "" + "\x0epre_shared_key\x18\x02 \x01(\tR\fpreSharedKey\x12\x1a\n" + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x1d\n" + "\n" + - "keep_alive\x18\x04 \x01(\rR\tkeepAlive\x12\x1f\n" + + "keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" + "\vallowed_ips\x18\x05 \x03(\tR\n" + - "allowedIps\"\xcb\x03\n" + + "allowedIps\"\xaa\x03\n" + "\fDeviceConfig\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" + "\bendpoint\x18\x02 \x03(\tR\bendpoint\x126\n" + "\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x12\x10\n" + - "\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1f\n" + - "\vnum_workers\x18\x05 \x01(\x05R\n" + - "numWorkers\x12\x1a\n" + + "\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1a\n" + "\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" + "\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" + "\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" + diff --git a/proxy/wireguard/config.proto b/proxy/wireguard/config.proto index aa05b822b..95291279b 100644 --- a/proxy/wireguard/config.proto +++ b/proxy/wireguard/config.proto @@ -10,7 +10,7 @@ message PeerConfig { string public_key = 1; string pre_shared_key = 2; string endpoint = 3; - uint32 keep_alive = 4; + string keep_alive = 4; repeated string allowed_ips = 5; } @@ -26,7 +26,7 @@ message DeviceConfig { repeated string endpoint = 2; repeated PeerConfig peers = 3; int32 mtu = 4; - int32 num_workers = 5; + bytes reserved = 6; DomainStrategy domain_strategy = 7; bool is_client = 8; diff --git a/proxy/wireguard/gvisortun/tun.go b/proxy/wireguard/gvisortun/tun.go deleted file mode 100644 index 379fad424..000000000 --- a/proxy/wireguard/gvisortun/tun.go +++ /dev/null @@ -1,226 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2017-2022 WireGuard LLC. All Rights Reserved. - */ - -package gvisortun - -import ( - "context" - "fmt" - "net/netip" - "os" - "sync" - "syscall" - - "golang.zx2c4.com/wireguard/tun" - "gvisor.dev/gvisor/pkg/buffer" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/pkg/tcpip/link/channel" - "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" - "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" - "gvisor.dev/gvisor/pkg/tcpip/stack" - "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" - "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" - "gvisor.dev/gvisor/pkg/tcpip/transport/udp" -) - -type netTun struct { - ep *channel.Endpoint - stack *stack.Stack - events chan tun.Event - notifyHandle *channel.NotificationHandle - incomingPacket chan *buffer.View - mtu int - hasV4, hasV6 bool - closeOnce sync.Once -} - -type Net netTun - -func CreateNetTUN(localAddresses []netip.Addr, mtu int, promiscuousMode bool) (tun.Device, *Net, *stack.Stack, error) { - opts := stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, - TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, - HandleLocal: !promiscuousMode, - } - dev := &netTun{ - ep: channel.New(1024, uint32(mtu), ""), - stack: stack.New(opts), - events: make(chan tun.Event, 10), - incomingPacket: make(chan *buffer.View), - mtu: mtu, - } - sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default - tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) - if tcpipErr != nil { - return nil, nil, dev.stack, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) - } - dev.notifyHandle = dev.ep.AddNotify(dev) - tcpipErr = dev.stack.CreateNIC(1, dev.ep) - if tcpipErr != nil { - return nil, nil, dev.stack, fmt.Errorf("CreateNIC: %v", tcpipErr) - } - for _, ip := range localAddresses { - var protoNumber tcpip.NetworkProtocolNumber - if ip.Is4() { - protoNumber = ipv4.ProtocolNumber - } else if ip.Is6() { - protoNumber = ipv6.ProtocolNumber - } - protoAddr := tcpip.ProtocolAddress{ - Protocol: protoNumber, - AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(), - } - tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}) - if tcpipErr != nil { - return nil, nil, dev.stack, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr) - } - if ip.Is4() { - dev.hasV4 = true - } else if ip.Is6() { - dev.hasV6 = true - } - } - if dev.hasV4 { - dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) - } - if dev.hasV6 { - dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1}) - } - if promiscuousMode { - // enable promiscuous mode to handle all packets processed by netstack - dev.stack.SetPromiscuousMode(1, true) - dev.stack.SetSpoofing(1, true) - } - - dev.events <- tun.EventUp - return dev, (*Net)(dev), dev.stack, nil -} - -// Name implements tun.Device -func (tun *netTun) Name() (string, error) { - return "go", nil -} - -// File implements tun.Device -func (tun *netTun) File() *os.File { - return nil -} - -// Events implements tun.Device -func (tun *netTun) Events() <-chan tun.Event { - return tun.events -} - -// Read implements tun.Device -func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { - view, ok := <-tun.incomingPacket - if !ok { - return 0, os.ErrClosed - } - - n, err := view.Read(buf[0][offset:]) - if err != nil { - return 0, err - } - sizes[0] = n - return 1, nil -} - -// Write implements tun.Device -func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { - for _, buf := range buf { - packet := buf[offset:] - if len(packet) == 0 { - continue - } - - pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) - switch packet[0] >> 4 { - case 4: - tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb) - case 6: - tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb) - default: - return 0, syscall.EAFNOSUPPORT - } - } - return len(buf), nil -} - -// WriteNotify implements channel.Notification -func (tun *netTun) WriteNotify() { - pkt := tun.ep.Read() - if pkt == nil { - return - } - - view := pkt.ToView() - pkt.DecRef() - - tun.incomingPacket <- view -} - -// Close implements tun.Device -func (tun *netTun) Close() error { - tun.closeOnce.Do(func() { - tun.stack.RemoveNIC(1) - tun.stack.Close() - tun.ep.RemoveNotify(tun.notifyHandle) - tun.ep.Close() - - close(tun.events) - - close(tun.incomingPacket) - }) - return nil -} - -// MTU implements tun.Device -func (tun *netTun) MTU() (int, error) { - return tun.mtu, nil -} - -// BatchSize implements tun.Device -func (tun *netTun) BatchSize() int { - return 1 -} - -func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) { - var protoNumber tcpip.NetworkProtocolNumber - if endpoint.Addr().Is4() { - protoNumber = ipv4.ProtocolNumber - } else { - protoNumber = ipv6.ProtocolNumber - } - return tcpip.FullAddress{ - NIC: 1, - Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()), - Port: endpoint.Port(), - }, protoNumber -} - -func (net *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) { - fa, pn := convertToFullAddr(addr) - return gonet.DialContextTCP(ctx, net.stack, fa, pn) -} - -func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, error) { - var lfa, rfa *tcpip.FullAddress - var pn tcpip.NetworkProtocolNumber - if laddr.IsValid() || laddr.Port() > 0 { - var addr tcpip.FullAddress - addr, pn = convertToFullAddr(laddr) - lfa = &addr - } - if raddr.IsValid() || raddr.Port() > 0 { - var addr tcpip.FullAddress - addr, pn = convertToFullAddr(raddr) - rfa = &addr - rfa = nil // do not ep connect - } - return gonet.DialUDP(net.stack, lfa, rfa, pn) -} diff --git a/proxy/wireguard/netstack.go b/proxy/wireguard/netstack.go new file mode 100644 index 000000000..22c42e3f9 --- /dev/null +++ b/proxy/wireguard/netstack.go @@ -0,0 +1,690 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package wireguard + +import ( + "context" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "net/netip" + "os" + "strings" + "syscall" + "time" + + "github.com/xtls/xray-core/transport/internet" + "golang.zx2c4.com/wireguard/tun" + + "golang.org/x/net/dns/dnsmessage" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +type netTun struct { + ep *channel.Endpoint + stack *stack.Stack + events chan tun.Event + notifyHandle *channel.NotificationHandle + incomingPacket chan *buffer.View + mtu int + dnsServers []netip.Addr + hasV4, hasV6 bool +} + +func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int, handleLocal bool) (tun.Device, *Net, *stack.Stack, error) { + opts := stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, + HandleLocal: handleLocal, + } + dev := &netTun{ + ep: channel.New(1024, uint32(mtu), ""), + stack: stack.New(opts), + events: make(chan tun.Event, 10), + incomingPacket: make(chan *buffer.View), + dnsServers: dnsServers, + mtu: mtu, + } + sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default + tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) + if tcpipErr != nil { + return nil, nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) + } + dev.notifyHandle = dev.ep.AddNotify(dev) + tcpipErr = dev.stack.CreateNIC(1, dev.ep) + if tcpipErr != nil { + return nil, nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr) + } + for _, ip := range localAddresses { + var protoNumber tcpip.NetworkProtocolNumber + if ip.Is4() { + protoNumber = ipv4.ProtocolNumber + } else if ip.Is6() { + protoNumber = ipv6.ProtocolNumber + } + protoAddr := tcpip.ProtocolAddress{ + Protocol: protoNumber, + AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(), + } + tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}) + if tcpipErr != nil { + return nil, nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr) + } + if ip.Is4() { + dev.hasV4 = true + } else if ip.Is6() { + dev.hasV6 = true + } + } + if dev.hasV4 { + dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) + } + if dev.hasV6 { + dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1}) + } + + tnet := &Net{ + DialContextTCPAddrPort: dev.DialContextTCPAddrPort, + DialUDPAddrPort: dev.DialUDPAddrPort, + dnsServers: dev.dnsServers, + hasV4: dev.hasV4, + hasV6: dev.hasV6, + } + + dev.events <- tun.EventUp + return dev, tnet, dev.stack, nil +} + +func (tun *netTun) Name() (string, error) { + return "go", nil +} + +func (tun *netTun) File() *os.File { + return nil +} + +func (tun *netTun) Events() <-chan tun.Event { + return tun.events +} + +func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { + view, ok := <-tun.incomingPacket + if !ok { + return 0, os.ErrClosed + } + + n, err := view.Read(buf[0][offset:]) + if err != nil { + return 0, err + } + sizes[0] = n + return 1, nil +} + +func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { + for _, buf := range buf { + packet := buf[offset:] + if len(packet) == 0 { + continue + } + + pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) + switch packet[0] >> 4 { + case 4: + tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb) + case 6: + tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb) + default: + return 0, syscall.EAFNOSUPPORT + } + } + return len(buf), nil +} + +func (tun *netTun) WriteNotify() { + pkt := tun.ep.Read() + if pkt == nil { + return + } + + view := pkt.ToView() + pkt.DecRef() + + tun.incomingPacket <- view +} + +func (tun *netTun) Close() error { + tun.stack.RemoveNIC(1) + tun.stack.Close() + tun.ep.RemoveNotify(tun.notifyHandle) + tun.ep.Close() + + if tun.events != nil { + close(tun.events) + } + + if tun.incomingPacket != nil { + close(tun.incomingPacket) + } + + return nil +} + +func (tun *netTun) MTU() (int, error) { + return tun.mtu, nil +} + +func (tun *netTun) BatchSize() int { + return 1 +} + +func (tun *netTun) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) { + fa, pn := convertToFullAddr(addr) + return gonet.DialContextTCP(ctx, tun.stack, fa, pn) +} + +func (tun *netTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { + var pn tcpip.NetworkProtocolNumber = ipv6.ProtocolNumber + if raddr.IsValid() || raddr.Port() > 0 { + _, pn = convertToFullAddr(raddr) + } + conn, err := gonet.DialUDP(tun.stack, nil, nil, pn) + if err != nil { + return nil, err + } + return &internet.PacketConnWrapper{ + PacketConn: conn, + Dest: net.UDPAddrFromAddrPort(raddr), + }, nil +} + +type Net struct { + DialContextTCPAddrPort func(ctx context.Context, addr netip.AddrPort) (net.Conn, error) + DialUDPAddrPort func(laddr, raddr netip.AddrPort) (net.Conn, error) + dnsServers []netip.Addr + hasV4, hasV6 bool +} + +func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) { + var protoNumber tcpip.NetworkProtocolNumber + if endpoint.Addr().Is4() { + protoNumber = ipv4.ProtocolNumber + } else { + protoNumber = ipv6.ProtocolNumber + } + return tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()), + Port: endpoint.Port(), + }, protoNumber +} + +var ( + errNoSuchHost = errors.New("no such host") + errLameReferral = errors.New("lame referral") + errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message") + errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message") + errServerMisbehaving = errors.New("server misbehaving") + errInvalidDNSResponse = errors.New("invalid DNS response") + errNoAnswerFromDNSServer = errors.New("no answer from DNS server") + errServerTemporarilyMisbehaving = errors.New("server misbehaving") + errCanceled = errors.New("operation was canceled") + errTimeout = errors.New("i/o timeout") +) + +func (net *Net) LookupHost(host string) (addrs []string, err error) { + return net.LookupContextHost(context.Background(), host) +} + +func isDomainName(s string) bool { + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + last := byte('.') + nonNumeric := false + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': + nonNumeric = true + partlen++ + case '0' <= c && c <= '9': + partlen++ + case c == '-': + if last == '.' { + return false + } + partlen++ + nonNumeric = true + case c == '.': + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + return nonNumeric +} + +func randU16() uint16 { + var b [2]byte + _, err := rand.Read(b[:]) + if err != nil { + panic(err) + } + return binary.LittleEndian.Uint16(b[:]) +} + +func newRequest(q dnsmessage.Question) (id uint16, udpReq, tcpReq []byte, err error) { + id = randU16() + b := dnsmessage.NewBuilder(make([]byte, 2, 514), dnsmessage.Header{ID: id, RecursionDesired: true}) + b.EnableCompression() + if err := b.StartQuestions(); err != nil { + return 0, nil, nil, err + } + if err := b.Question(q); err != nil { + return 0, nil, nil, err + } + tcpReq, err = b.Finish() + udpReq = tcpReq[2:] + l := len(tcpReq) - 2 + tcpReq[0] = byte(l >> 8) + tcpReq[1] = byte(l) + return id, udpReq, tcpReq, err +} + +func equalASCIIName(x, y dnsmessage.Name) bool { + if x.Length != y.Length { + return false + } + for i := 0; i < int(x.Length); i++ { + a := x.Data[i] + b := y.Data[i] + if 'A' <= a && a <= 'Z' { + a += 0x20 + } + if 'A' <= b && b <= 'Z' { + b += 0x20 + } + if a != b { + return false + } + } + return true +} + +func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage.Header, respQues dnsmessage.Question) bool { + if !respHdr.Response { + return false + } + if reqID != respHdr.ID { + return false + } + if reqQues.Type != respQues.Type || reqQues.Class != respQues.Class || !equalASCIIName(reqQues.Name, respQues.Name) { + return false + } + return true +} + +func dnsPacketRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) { + if _, err := c.Write(b); err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + b = make([]byte, 512) + for { + n, err := c.Read(b) + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + var p dnsmessage.Parser + h, err := p.Start(b[:n]) + if err != nil { + continue + } + q, err := p.Question() + if err != nil || !checkResponse(id, query, h, q) { + continue + } + return p, h, nil + } +} + +func dnsStreamRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []byte) (dnsmessage.Parser, dnsmessage.Header, error) { + if _, err := c.Write(b); err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + b = make([]byte, 1280) + if _, err := io.ReadFull(c, b[:2]); err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + l := int(b[0])<<8 | int(b[1]) + if l > len(b) { + b = make([]byte, l) + } + n, err := io.ReadFull(c, b[:l]) + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + var p dnsmessage.Parser + h, err := p.Start(b[:n]) + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage + } + q, err := p.Question() + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotUnmarshalDNSMessage + } + if !checkResponse(id, query, h, q) { + return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse + } + return p, h, nil +} + +func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) { + q.Class = dnsmessage.ClassINET + id, udpReq, tcpReq, err := newRequest(q) + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, errCannotMarshalDNSMessage + } + + for _, useUDP := range []bool{true, false} { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) + defer cancel() + + var c net.Conn + var err error + if useUDP { + c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, netip.AddrPortFrom(server, 53)) + } else { + c, err = tnet.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(server, 53)) + } + + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + if d, ok := ctx.Deadline(); ok && !d.IsZero() { + err := c.SetDeadline(d) + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + } + var p dnsmessage.Parser + var h dnsmessage.Header + if useUDP { + p, h, err = dnsPacketRoundTrip(c, id, q, udpReq) + } else { + p, h, err = dnsStreamRoundTrip(c, id, q, tcpReq) + } + c.Close() + if err != nil { + if err == context.Canceled { + err = errCanceled + } else if err == context.DeadlineExceeded { + err = errTimeout + } + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + if err := p.SkipQuestion(); err != dnsmessage.ErrSectionDone { + return dnsmessage.Parser{}, dnsmessage.Header{}, errInvalidDNSResponse + } + if h.Truncated { + continue + } + return p, h, nil + } + return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer +} + +func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error { + if h.RCode == dnsmessage.RCodeNameError { + return errNoSuchHost + } + _, err := p.AnswerHeader() + if err != nil && err != dnsmessage.ErrSectionDone { + return errCannotUnmarshalDNSMessage + } + if h.RCode == dnsmessage.RCodeSuccess && !h.Authoritative && !h.RecursionAvailable && err == dnsmessage.ErrSectionDone { + return errLameReferral + } + if h.RCode != dnsmessage.RCodeSuccess && h.RCode != dnsmessage.RCodeNameError { + if h.RCode == dnsmessage.RCodeServerFailure { + return errServerTemporarilyMisbehaving + } + return errServerMisbehaving + } + return nil +} + +func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error { + for { + h, err := p.AnswerHeader() + if err == dnsmessage.ErrSectionDone { + return errNoSuchHost + } + if err != nil { + return errCannotUnmarshalDNSMessage + } + if h.Type == qtype { + return nil + } + if err := p.SkipAnswer(); err != nil { + return errCannotUnmarshalDNSMessage + } + } +} + +func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) { + var lastErr error + + n, err := dnsmessage.NewName(name) + if err != nil { + return dnsmessage.Parser{}, "", errCannotMarshalDNSMessage + } + q := dnsmessage.Question{ + Name: n, + Type: qtype, + Class: dnsmessage.ClassINET, + } + + for i := 0; i < 2; i++ { + for _, server := range tnet.dnsServers { + p, h, err := tnet.exchange(ctx, server, q, time.Second*5) + if err != nil { + dnsErr := &net.DNSError{ + Err: err.Error(), + Name: name, + Server: server.String(), + } + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + dnsErr.IsTimeout = true + } + if _, ok := err.(*net.OpError); ok { + dnsErr.IsTemporary = true + } + lastErr = dnsErr + continue + } + + if err := checkHeader(&p, h); err != nil { + dnsErr := &net.DNSError{ + Err: err.Error(), + Name: name, + Server: server.String(), + } + if err == errServerTemporarilyMisbehaving { + dnsErr.IsTemporary = true + } + if err == errNoSuchHost { + dnsErr.IsNotFound = true + return p, server.String(), dnsErr + } + lastErr = dnsErr + continue + } + + err = skipToAnswer(&p, qtype) + if err == nil { + return p, server.String(), nil + } + lastErr = &net.DNSError{ + Err: err.Error(), + Name: name, + Server: server.String(), + } + if err == errNoSuchHost { + lastErr.(*net.DNSError).IsNotFound = true + return p, server.String(), lastErr + } + } + } + return dnsmessage.Parser{}, "", lastErr +} + +func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { + if host == "" || (!tnet.hasV6 && !tnet.hasV4) { + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + zlen := len(host) + if strings.IndexByte(host, ':') != -1 { + if zidx := strings.LastIndexByte(host, '%'); zidx != -1 { + zlen = zidx + } + } + if ip, err := netip.ParseAddr(host[:zlen]); err == nil { + return []string{ip.String()}, nil + } + + if !isDomainName(host) { + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + type result struct { + p dnsmessage.Parser + server string + error + } + var addrsV4, addrsV6 []netip.Addr + lanes := 0 + if tnet.hasV4 { + lanes++ + } + if tnet.hasV6 { + lanes++ + } + lane := make(chan result, lanes) + var lastErr error + if tnet.hasV4 { + go func() { + p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeA) + lane <- result{p, server, err} + }() + } + if tnet.hasV6 { + go func() { + p, server, err := tnet.tryOneName(ctx, host+".", dnsmessage.TypeAAAA) + lane <- result{p, server, err} + }() + } + for l := 0; l < lanes; l++ { + result := <-lane + if result.error != nil { + if lastErr == nil { + lastErr = result.error + } + continue + } + + loop: + for { + h, err := result.p.AnswerHeader() + if err != nil && err != dnsmessage.ErrSectionDone { + lastErr = &net.DNSError{ + Err: errCannotMarshalDNSMessage.Error(), + Name: host, + Server: result.server, + } + } + if err != nil { + break + } + switch h.Type { + case dnsmessage.TypeA: + a, err := result.p.AResource() + if err != nil { + lastErr = &net.DNSError{ + Err: errCannotMarshalDNSMessage.Error(), + Name: host, + Server: result.server, + } + break loop + } + addrsV4 = append(addrsV4, netip.AddrFrom4(a.A)) + + case dnsmessage.TypeAAAA: + aaaa, err := result.p.AAAAResource() + if err != nil { + lastErr = &net.DNSError{ + Err: errCannotMarshalDNSMessage.Error(), + Name: host, + Server: result.server, + } + break loop + } + addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA)) + + default: + if err := result.p.SkipAnswer(); err != nil { + lastErr = &net.DNSError{ + Err: errCannotMarshalDNSMessage.Error(), + Name: host, + Server: result.server, + } + break loop + } + continue + } + } + } + // We don't do RFC6724. Instead just put V6 addresses first if an IPv6 address is enabled + var addrs []netip.Addr + if tnet.hasV6 { + addrs = append(addrsV6, addrsV4...) + } else { + addrs = append(addrsV4, addrsV6...) + } + + if len(addrs) == 0 && lastErr != nil { + return nil, lastErr + } + saddrs := make([]string, 0, len(addrs)) + for _, ip := range addrs { + saddrs = append(saddrs, ip.String()) + } + return saddrs, nil +} diff --git a/proxy/wireguard/server.go b/proxy/wireguard/server.go index 876d749f7..2c634464c 100644 --- a/proxy/wireguard/server.go +++ b/proxy/wireguard/server.go @@ -2,6 +2,10 @@ package wireguard import ( "context" + "fmt" + "net/netip" + "strings" + "sync" "github.com/xtls/xray-core/common/buf" c "github.com/xtls/xray-core/common/ctx" @@ -10,162 +14,246 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/core" - "github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/stat" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" + "gvisor.dev/gvisor/pkg/tcpip/stack" ) -var nullDestination = net.TCPDestination(net.AnyIP, 0) - type Server struct { - bindServer *netBindServer - - info routingInfo + conf *DeviceConfig + ctx context.Context policyManager policy.Manager -} + dispatcher routing.Dispatcher -type routingInfo struct { - ctx context.Context - dispatcher routing.Dispatcher - inboundTag *session.Inbound - contentTag *session.Content + tag string + src net.Destination + sniffingRequest session.SniffingRequest + streamSettings *internet.MemoryStreamConfig + uplinkCounter stats.Counter + downlinkCounter stats.Counter + + tun tun.Device + stack *stack.Stack + dev *device.Device + mu sync.Mutex } func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) { v := core.MustFromContext(ctx) + p := v.GetFeature(policy.ManagerType()).(policy.Manager) + d := v.GetFeature(routing.DispatcherType()).(routing.Dispatcher) - endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf) + inbound := session.InboundFromContext(ctx) + content := session.ContentFromContext(ctx) + streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig) + tag := inbound.Tag + var uplinkCounter stats.Counter + var downlinkCounter stats.Counter + if len(tag) > 0 && p.ForSystem().Stats.InboundUplink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "inbound>>>" + tag + ">>>traffic>>>uplink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + uplinkCounter = c + } + } + if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "inbound>>>" + tag + ">>>traffic>>>downlink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + downlinkCounter = c + } + } + + if len(conf.Peers) == 0 { + return nil, errors.New("empty peers") + } + for _, peer := range conf.Peers { + if peer.PublicKey == "" { + return nil, errors.New("peer without publickey") + } + } + + localAddresses := make([]netip.Addr, 0, len(conf.Endpoint)) + for _, localaddress := range conf.Endpoint { + addr, err := netip.ParseAddr(localaddress) + if err == nil { + localAddresses = append(localAddresses, addr) + continue + } + prefix, err := netip.ParsePrefix(localaddress) + if err == nil { + localAddresses = append(localAddresses, prefix.Addr()) + continue + } + return nil, err + } + + tun, _, stack, err := CreateNetTUN(localAddresses, nil, int(conf.Mtu), false) if err != nil { return nil, err } - server := &Server{ - bindServer: &netBindServer{ - netBind: netBind{ - dns: v.GetFeature(dns.ClientType()).(dns.Client), - dnsOption: dns.IPOption{ - IPv4Enable: hasIPv4, - IPv6Enable: hasIPv6, - }, - workers: int(conf.NumWorkers), - readQueue: make(chan *netReadInfo), - }, - }, - policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), - } + return &Server{ + conf: conf, + ctx: core.ToBackgroundDetachedContext(ctx), + policyManager: p, + dispatcher: d, - tun, err := conf.createTun()(endpoints, int(conf.Mtu), server.forwardConnection) - if err != nil { - return nil, err - } + tag: inbound.Tag, + src: inbound.Source, + sniffingRequest: content.SniffingRequest, + streamSettings: streamSettings, + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, - if err = tun.BuildDevice(createIPCRequest(conf), server.bindServer); err != nil { - _ = tun.Close() - return nil, err - } - - return server, nil + tun: tun, + stack: stack, + }, nil } -// Network implements proxy.Inbound. +// Network implements proxy.Inbound.Network. func (*Server) Network() []net.Network { - return []net.Network{net.Network_UDP} + return []net.Network{} } -// Process implements proxy.Inbound. +// Process implements proxy.Inbound.Process. func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { - s.info = routingInfo{ - ctx: ctx, - dispatcher: dispatcher, - inboundTag: session.InboundFromContext(ctx), - contentTag: session.ContentFromContext(ctx), - } + return nil +} - ep, err := s.bindServer.ParseEndpoint(conn.RemoteAddr().String()) +// Close implements common.Closable.Close. +func (s *Server) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.dev != nil { + s.dev.Close() + s.dev = nil + s.tun = nil + } else if s.tun != nil { + s.tun.Close() + s.tun = nil + } + return nil +} + +// Start implements common.Runnable.Start. +func (s *Server) Start() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.dev != nil { + return nil + } + if s.src.Address.Family().IsDomain() { + return errors.New("address is domain") + } + listenFunc := func() (net.PacketConn, error) { + pktConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: s.src.Address.IP(), Port: int(s.src.Port)}, s.streamSettings.SocketSettings) + if err != nil { + return nil, err + } + if s.streamSettings.UdpmaskManager != nil { + newConn, err := s.streamSettings.UdpmaskManager.WrapPacketConnServer(pktConn) + if err != nil { + pktConn.Close() + return nil, errors.New("mask err").Base(err) + } + pktConn = newConn + } + if s.uplinkCounter != nil || s.downlinkCounter != nil { + pktConn = &PacketCounterConnection{ + PacketConn: pktConn, + ReadCounter: s.uplinkCounter, + WriteCounter: s.downlinkCounter, + } + } + return pktConn, nil + } + bind := &bind{ + listenFunc: listenFunc, + } + logger := &device.Logger{ + Verbosef: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Debug, + Content: fmt.Sprintf(format, args...), + }) + }, + Errorf: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Error, + Content: fmt.Sprintf(format, args...), + }) + }, + } + dev := device.NewDevice(s.tun, bind, logger) + var cfg strings.Builder + cfg.WriteString("private_key=" + s.conf.SecretKey + "\n") + for _, peer := range s.conf.Peers { + cfg.WriteString("public_key=" + peer.PublicKey + "\n") + if peer.PreSharedKey != "" { + cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n") + } + for _, ip := range peer.AllowedIps { + cfg.WriteString("allowed_ip=" + ip + "\n") + } + if peer.KeepAlive != "" { + cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n") + } + } + err := dev.IpcSet(cfg.String()) if err != nil { return err } - - nep := ep.(*netEndpoint) - nep.conn = conn - - reader := buf.NewPacketReader(conn) - for { - mb, err := reader.ReadMultiBuffer() - if err != nil { - nep.conn = nil - buf.ReleaseMulti(mb) - return err - } - - for i, b := range mb { - - rawBytes := b.Bytes() - if b.Len() > 3 { - rawBytes[1] = 0 - rawBytes[2] = 0 - rawBytes[3] = 0 - } - - select { - case s.bindServer.readQueue <- &netReadInfo{ - buff: b, - endpoint: nep, - }: - case <-s.bindServer.closedCh: - nep.conn = nil - buf.ReleaseMulti(mb[i:]) - return errors.New("bind closed") - } - } + err = dev.Up() + if err != nil { + return err } + s.dev = dev + createForwarder(s.stack, s.HandleConnection) + return nil } -func (s *Server) forwardConnection(dest net.Destination, conn net.Conn) { - if s.info.dispatcher == nil { - errors.LogError(s.info.ctx, "unexpected: dispatcher == nil") - return +func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) { + defer conn.Close() + ctx, cancel := context.WithCancel(s.ctx) + defer cancel() + ctx = c.ContextWithID(ctx, session.NewID()) + + source := net.DestinationFromAddr(conn.RemoteAddr()) + inbound := session.Inbound{ + Name: "wireguard", + Tag: s.tag, + CanSpliceCopy: 3, + Source: source, } - ctx, cancel := context.WithCancel(core.ToBackgroundDetachedContext(s.info.ctx)) - sid := session.NewID() - ctx = c.ContextWithID(ctx, sid) - inbound := session.Inbound{} // since promiscuousModeHandler mixed-up context, we shallow copy inbound (tag) and content (configs) - if s.info.inboundTag != nil { - inbound = *s.info.inboundTag - } - inbound.Name = "wireguard" - inbound.CanSpliceCopy = 3 - - // overwrite the source to use the tun address for each sub context. - // Since gvisor.ForwarderRequest doesn't provide any info to associate the sub-context with the Parent context - // Currently we have no way to link to the original source address - inbound.Source = net.DestinationFromAddr(conn.RemoteAddr()) ctx = session.ContextWithInbound(ctx, &inbound) - content := new(session.Content) - if s.info.contentTag != nil { - content.SniffingRequest = s.info.contentTag.SniffingRequest - } - ctx = session.ContextWithContent(ctx, content) + ctx = session.ContextWithContent(ctx, &session.Content{ + SniffingRequest: s.sniffingRequest, + }) ctx = session.SubContextFromMuxInbound(ctx) ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ - From: nullDestination, + From: inbound.Source, To: dest, Status: log.AccessAccepted, Reason: "", }) + errors.LogInfo(ctx, "processing from ", source, " to ", dest) - err := s.info.dispatcher.DispatchLink(ctx, dest, &transport.Link{ - Reader: buf.NewReader(conn), + link := &transport.Link{ + Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, Writer: buf.NewWriter(conn), - }) - if err != nil { - errors.LogInfoInner(ctx, err, "connection ends") } - - cancel() - conn.Close() + if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil { + errors.LogError(ctx, errors.New("connection closed").Base(err)) + } } diff --git a/proxy/wireguard/server_test.go b/proxy/wireguard/server_test.go deleted file mode 100644 index 1cb4697ce..000000000 --- a/proxy/wireguard/server_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package wireguard_test - -import ( - "context" - "runtime/debug" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/xtls/xray-core/core" - "github.com/xtls/xray-core/proxy/wireguard" -) - -// TestWireGuardServerInitializationError verifies that an error during TUN initialization -// (triggered by an empty SecretKey) in the WireGuard server does not cause a panic and returns an error instead. -func TestWireGuardServerInitializationError(t *testing.T) { - // Create a minimal core instance with default features - config := &core.Config{} - instance, err := core.New(config) - if err != nil { - t.Fatalf("Failed to create core instance: %v", err) - } - // Set the Xray instance in the context - ctx := context.WithValue(context.Background(), core.XrayKey(1), instance) - - // Define the server configuration with an empty SecretKey to trigger error - conf := &wireguard.DeviceConfig{ - IsClient: false, - Endpoint: []string{"10.0.0.1/32"}, - Mtu: 1420, - SecretKey: "", // Empty SecretKey to trigger error - Peers: []*wireguard.PeerConfig{ - { - PublicKey: "some_public_key", - AllowedIps: []string{"10.0.0.2/32"}, - }, - }, - } - - // Use defer to catch any panic and fail the test explicitly - defer func() { - if r := recover(); r != nil { - t.Errorf("TUN initialization panicked: %v", r) - debug.PrintStack() - } - }() - - // Attempt to initialize the WireGuard server - _, err = wireguard.NewServer(ctx, conf) - - // Check that an error is returned - assert.ErrorContains(t, err, "failed to set private_key: hex string does not fit the slice") -} diff --git a/proxy/wireguard/tun.go b/proxy/wireguard/tun.go index 971ce89e0..68bad3ac6 100644 --- a/proxy/wireguard/tun.go +++ b/proxy/wireguard/tun.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "net/netip" "runtime" "strconv" "strings" @@ -13,9 +12,7 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/proxy/wireguard/gvisortun" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" @@ -25,77 +22,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/waiter" - - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" ) -type tunCreator func(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error) - -type promiscuousModeHandler func(dest net.Destination, conn net.Conn) - -type Tunnel interface { - BuildDevice(ipc string, bind conn.Bind) error - DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) - DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) - Close() error -} - -type tunnel struct { - tun tun.Device - device *device.Device - rw sync.Mutex -} - -func (t *tunnel) BuildDevice(ipc string, bind conn.Bind) (err error) { - t.rw.Lock() - defer t.rw.Unlock() - - if t.device != nil { - return errors.New("device is already initialized") - } - - logger := &device.Logger{ - Verbosef: func(format string, args ...any) { - log.Record(&log.GeneralMessage{ - Severity: log.Severity_Debug, - Content: fmt.Sprintf(format, args...), - }) - }, - Errorf: func(format string, args ...any) { - log.Record(&log.GeneralMessage{ - Severity: log.Severity_Error, - Content: fmt.Sprintf(format, args...), - }) - }, - } - - t.device = device.NewDevice(t.tun, bind, logger) - if err = t.device.IpcSet(ipc); err != nil { - return err - } - if err = t.device.Up(); err != nil { - return err - } - return nil -} - -func (t *tunnel) Close() (err error) { - t.rw.Lock() - defer t.rw.Unlock() - - if t.device == nil { - return nil - } - - t.device.Close() - t.device = nil - err = t.tun.Close() - t.tun = nil - return nil -} - func CalculateInterfaceName(name string) (tunName string) { if runtime.GOOS == "darwin" { tunName = "utun" @@ -121,93 +49,61 @@ func CalculateInterfaceName(name string) (tunName string) { return } -var _ Tunnel = (*gvisorNet)(nil) +func createForwarder(gstack *stack.Stack, handler func(conn net.Conn, dest net.Destination)) { + gstack.SetPromiscuousMode(1, true) + gstack.SetSpoofing(1, true) -type gvisorNet struct { - tunnel - net *gvisortun.Net -} + tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) { + go func(r *tcp.ForwarderRequest) { + var wq waiter.Queue + id := r.ID() -func (g *gvisorNet) Close() error { - return g.tunnel.Close() -} - -func (g *gvisorNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) ( - net.Conn, error, -) { - return g.net.DialContextTCPAddrPort(ctx, addr) -} - -func (g *gvisorNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { - return g.net.DialUDPAddrPort(laddr, raddr) -} - -func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error) { - out := &gvisorNet{} - tun, n, gstack, err := gvisortun.CreateNetTUN(localAddresses, mtu, handler != nil) - if err != nil { - return nil, err - } - - if handler != nil { - // handler is only used for promiscuous mode - // capture all packets and send to handler - - tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) { - go func(r *tcp.ForwarderRequest) { - var wq waiter.Queue - id := r.ID() - - ep, err := r.CreateEndpoint(&wq) - if err != nil { - errors.LogError(context.Background(), err.String()) - r.Complete(true) - return - } - - options := ep.SocketOptions() - options.SetKeepAlive(false) - options.SetReuseAddress(true) - options.SetReusePort(true) - - handler(net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), gonet.NewTCPConn(&wq, ep)) - - ep.Close() - r.Complete(false) - }(r) - }) - gstack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) - - manager := &udpManager{ - stack: gstack, - handler: handler, - m: make(map[string]*udpConn), - } - - gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { - data := pkt.Clone().Data().AsRange().ToSlice() - // if len(data) == 0 { - // return false - // } - srcIP := net.IPAddress(id.RemoteAddress.AsSlice()) - dstIP := net.IPAddress(id.LocalAddress.AsSlice()) - if srcIP == nil || dstIP == nil { - panic(id) + ep, err := r.CreateEndpoint(&wq) + if err != nil { + errors.LogError(context.Background(), err.String()) + r.Complete(true) + return } - src := net.UDPDestination(srcIP, net.Port(id.RemotePort)) - dst := net.UDPDestination(dstIP, net.Port(id.LocalPort)) - manager.feed(src, dst, data) - return true - }) + + options := ep.SocketOptions() + options.SetKeepAlive(false) + options.SetReuseAddress(true) + options.SetReusePort(true) + + handler(gonet.NewTCPConn(&wq, ep), net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort))) + + ep.Close() + r.Complete(false) + }(r) + }) + gstack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + + manager := &udpManager{ + stack: gstack, + handler: handler, + m: make(map[string]*udpConn), } - out.tun, out.net = tun, n - return out, nil + gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + data := pkt.Clone().Data().AsRange().ToSlice() + // if len(data) == 0 { + // return false + // } + srcIP := net.IPAddress(id.RemoteAddress.AsSlice()) + dstIP := net.IPAddress(id.LocalAddress.AsSlice()) + if srcIP == nil || dstIP == nil { + panic(id) + } + src := net.UDPDestination(srcIP, net.Port(id.RemotePort)) + dst := net.UDPDestination(dstIP, net.Port(id.LocalPort)) + manager.feed(src, dst, data) + return true + }) } type udpManager struct { stack *stack.Stack - handler func(dest net.Destination, conn net.Conn) + handler func(conn net.Conn, dest net.Destination) m map[string]*udpConn mutex sync.RWMutex } @@ -246,7 +142,7 @@ func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte) m.mutex.Unlock() } m.m[src.NetAddr()] = uc - go m.handler(dst, uc) + go m.handler(uc, dst) } select { diff --git a/proxy/wireguard/tun_default.go b/proxy/wireguard/tun_default.go index 50a509444..edad5545d 100644 --- a/proxy/wireguard/tun_default.go +++ b/proxy/wireguard/tun_default.go @@ -1,14 +1,16 @@ -//go:build !linux || android +//go:build !linux package wireguard import ( "errors" "net/netip" + + "golang.zx2c4.com/wireguard/tun" ) -func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (t Tunnel, err error) { - return nil, errors.New("not implemented") +func createKernelTun([]netip.Addr, []netip.Addr, int) (tdev tun.Device, tnet *Net, err error) { + return nil, nil, errors.New("not implemented") } func KernelTunSupported() (bool, error) { diff --git a/proxy/wireguard/tun_linux.go b/proxy/wireguard/tun_linux.go index b8a742e11..eb0175476 100644 --- a/proxy/wireguard/tun_linux.go +++ b/proxy/wireguard/tun_linux.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build linux package wireguard @@ -20,17 +20,6 @@ import ( "golang.zx2c4.com/wireguard/tun" ) -type deviceNet struct { - tunnel - dialer *net.Dialer - lc *net.ListenConfig - - handle *netlink.Handle - linkAddrs []netlink.Addr - routes []*netlink.Route - rules []*netlink.Rule -} - var ( tableIndex int = 10230 mu sync.Mutex @@ -48,82 +37,18 @@ func allocateIPv6TableIndex() int { return currentIndex } -func newDeviceNet(interfaceName string) *deviceNet { - dialer := &net.Dialer{} - dialer.Control = func(network, address string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - if err := syscall.BindToDevice(int(fd), interfaceName); err != nil { - errors.LogInfoInner(context.Background(), err, "failed to bind to device") - } - }) - } - lc := &net.ListenConfig{} - lc.Control = func(network, address string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - if err := syscall.BindToDevice(int(fd), interfaceName); err != nil { - errors.LogInfoInner(context.Background(), err, "failed to bind to device") - } - }) - } - return &deviceNet{dialer: dialer, lc: lc} +type kernelTun struct { + tun.Device + + dialer *net.Dialer + lc *net.ListenConfig + handle *netlink.Handle + linkAddrs []netlink.Addr + routes []*netlink.Route + rules []*netlink.Rule } -func (d *deviceNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) ( - net.Conn, error, -) { - return d.dialer.DialContext(ctx, "tcp", addr.String()) -} - -func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { - var conn net.PacketConn - var err error - if raddr.Addr().Is4() { - conn, err = d.lc.ListenPacket(context.Background(), "udp", "0.0.0.0:0") - } else { - conn, err = d.lc.ListenPacket(context.Background(), "udp", "[::]:0") - } - if err != nil { - return nil, err - } - return &internet.PacketConnWrapper{ - PacketConn: conn, - Dest: &net.UDPAddr{ - IP: raddr.Addr().AsSlice(), - Port: int(raddr.Port()), - }, - }, nil -} - -func (d *deviceNet) Close() (err error) { - var errs []error - for _, rule := range d.rules { - if err = d.handle.RuleDel(rule); err != nil { - errs = append(errs, fmt.Errorf("failed to delete rule: %w", err)) - } - } - for _, route := range d.routes { - if err = d.handle.RouteDel(route); err != nil { - errs = append(errs, fmt.Errorf("failed to delete route: %w", err)) - } - } - if err = d.tunnel.Close(); err != nil { - errs = append(errs, fmt.Errorf("failed to close tunnel: %w", err)) - } - if d.handle != nil { - d.handle.Close() - d.handle = nil - } - if len(errs) == 0 { - return nil - } - return goerrors.Join(errs...) -} - -func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (t Tunnel, err error) { - if handler != nil { - return nil, errors.New("TODO: support promiscuous mode") - } - +func createKernelTun(localAddresses, dnsServers []netip.Addr, mtu int) (tdev tun.Device, tnet *Net, err error) { var v4, v6 *netip.Addr for _, prefixes := range localAddresses { if v4 == nil && prefixes.Is4() { @@ -150,22 +75,22 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo // system configs. if v4 != nil { if err = writeSysctlZero("/proc/sys/net/ipv4/conf/all/rp_filter"); err != nil { - return nil, fmt.Errorf("failed to disable ipv4 rp_filter for all: %w", err) + return nil, nil, fmt.Errorf("failed to disable ipv4 rp_filter for all: %w", err) } } if v6 != nil { if err = writeSysctlZero("/proc/sys/net/ipv6/conf/all/disable_ipv6"); err != nil { - return nil, fmt.Errorf("failed to enable ipv6: %w", err) + return nil, nil, fmt.Errorf("failed to enable ipv6: %w", err) } if err = writeSysctlZero("/proc/sys/net/ipv6/conf/all/rp_filter"); err != nil { - return nil, fmt.Errorf("failed to disable ipv6 rp_filter for all: %w", err) + return nil, nil, fmt.Errorf("failed to disable ipv6 rp_filter for all: %w", err) } } n := CalculateInterfaceName("wg") wgt, err := tun.CreateTUN(n, mtu) if err != nil { - return nil, err + return nil, nil, err } defer func() { if err != nil { @@ -177,12 +102,12 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo // the operation require root privilege on container require '--privileged' flag. if v4 != nil { if err = writeSysctlZero("/proc/sys/net/ipv4/conf/" + n + "/rp_filter"); err != nil { - return nil, fmt.Errorf("failed to disable ipv4 rp_filter for tunnel: %w", err) + return nil, nil, fmt.Errorf("failed to disable ipv4 rp_filter for tunnel: %w", err) } } if v6 != nil { if err = writeSysctlZero("/proc/sys/net/ipv6/conf/" + n + "/rp_filter"); err != nil { - return nil, fmt.Errorf("failed to disable ipv6 rp_filter for tunnel: %w", err) + return nil, nil, fmt.Errorf("failed to disable ipv6 rp_filter for tunnel: %w", err) } } @@ -196,25 +121,28 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo } ipv6TableIndex-- if ipv6TableIndex < 0 { - return nil, fmt.Errorf("failed to find available ipv6 table index") + return nil, nil, fmt.Errorf("failed to find available ipv6 table index") } } } - out := newDeviceNet(n) - out.handle, err = netlink.NewHandle() + t := &kernelTun{ + Device: wgt, + } + + t.handle, err = netlink.NewHandle() if err != nil { - return nil, err + return nil, nil, err } defer func() { if err != nil { - _ = out.Close() + t.Close() } }() l, err := netlink.LinkByName(n) if err != nil { - return nil, err + return nil, nil, err } if v4 != nil { @@ -224,7 +152,7 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo Mask: net.CIDRMask(v4.BitLen(), v4.BitLen()), }, } - out.linkAddrs = append(out.linkAddrs, addr) + t.linkAddrs = append(t.linkAddrs, addr) } if v6 != nil { addr := netlink.Addr{ @@ -233,7 +161,7 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo Mask: net.CIDRMask(v6.BitLen(), v6.BitLen()), }, } - out.linkAddrs = append(out.linkAddrs, addr) + t.linkAddrs = append(t.linkAddrs, addr) rt := &netlink.Route{ LinkIndex: l.Attrs().Index, @@ -243,40 +171,102 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo }, Table: ipv6TableIndex, } - out.routes = append(out.routes, rt) + t.routes = append(t.routes, rt) r := netlink.NewRule() r.Table, r.Family, r.Src = ipv6TableIndex, unix.AF_INET6, addr.IPNet - out.rules = append(out.rules, r) + t.rules = append(t.rules, r) r = netlink.NewRule() r.Table, r.Family, r.OifName = ipv6TableIndex, unix.AF_INET6, n - out.rules = append(out.rules, r) + t.rules = append(t.rules, r) } - for _, addr := range out.linkAddrs { - if err = out.handle.AddrAdd(l, &addr); err != nil { - return nil, fmt.Errorf("failed to add address %s to %s: %w", addr, n, err) + for _, addr := range t.linkAddrs { + if err = t.handle.AddrAdd(l, &addr); err != nil { + return nil, nil, fmt.Errorf("failed to add address %s to %s: %w", addr, n, err) } } - if err = out.handle.LinkSetMTU(l, mtu); err != nil { - return nil, err + if err = t.handle.LinkSetMTU(l, mtu); err != nil { + return nil, nil, err } - if err = out.handle.LinkSetUp(l); err != nil { - return nil, err + if err = t.handle.LinkSetUp(l); err != nil { + return nil, nil, err } - for _, route := range out.routes { - if err = out.handle.RouteAdd(route); err != nil { - return nil, fmt.Errorf("failed to add route %s: %w", route, err) + for _, route := range t.routes { + if err = t.handle.RouteAdd(route); err != nil { + return nil, nil, fmt.Errorf("failed to add route %s: %w", route, err) } } - for _, rule := range out.rules { - if err = out.handle.RuleAdd(rule); err != nil { - return nil, fmt.Errorf("failed to add rule %s: %w", rule, err) + for _, rule := range t.rules { + if err = t.handle.RuleAdd(rule); err != nil { + return nil, nil, fmt.Errorf("failed to add rule %s: %w", rule, err) } } - out.tun = wgt - return out, nil + + dialer := &net.Dialer{} + dialer.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := syscall.BindToDevice(int(fd), n); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to bind to device") + } + }) + } + lc := &net.ListenConfig{} + lc.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := syscall.BindToDevice(int(fd), n); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to bind to device") + } + }) + } + t.dialer = dialer + t.lc = lc + + tnet = &Net{ + DialContextTCPAddrPort: t.DialContextTCPAddrPort, + DialUDPAddrPort: t.DialUDPAddrPort, + dnsServers: dnsServers, + hasV4: v4 != nil, + hasV6: v6 != nil, + } + + return t, tnet, nil +} + +func (tun *kernelTun) Close() (err error) { + var errs []error + for _, rule := range tun.rules { + if err = tun.handle.RuleDel(rule); err != nil { + errs = append(errs, fmt.Errorf("failed to delete rule: %w", err)) + } + } + for _, route := range tun.routes { + if err = tun.handle.RouteDel(route); err != nil { + errs = append(errs, fmt.Errorf("failed to delete route: %w", err)) + } + } + if err = tun.Device.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close device: %w", err)) + } + tun.handle.Close() + errs = append(errs, tun.Device.Close()) + return goerrors.Join(errs...) +} + +func (tun *kernelTun) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) { + return tun.dialer.DialContext(ctx, "tcp", addr.String()) +} + +func (tun *kernelTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { + conn, err := tun.lc.ListenPacket(context.Background(), "udp", ":0") + if err != nil { + return nil, err + } + return &internet.PacketConnWrapper{ + PacketConn: conn, + Dest: net.UDPAddrFromAddrPort(raddr), + }, nil } func KernelTunSupported() (bool, error) { diff --git a/proxy/wireguard/wireguard.go b/proxy/wireguard/wireguard.go index 4f489114f..7e9dca029 100644 --- a/proxy/wireguard/wireguard.go +++ b/proxy/wireguard/wireguard.go @@ -2,10 +2,6 @@ package wireguard import ( "context" - "errors" - "fmt" - "net/netip" - "strings" "github.com/xtls/xray-core/common" ) @@ -14,80 +10,9 @@ func init() { common.Must(common.RegisterConfig((*DeviceConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { deviceConfig := config.(*DeviceConfig) if deviceConfig.IsClient { - return New(ctx, deviceConfig) + return NewClient(ctx, deviceConfig) } else { return NewServer(ctx, deviceConfig) } })) } - -// convert endpoint string to netip.Addr -func parseEndpoints(conf *DeviceConfig) ([]netip.Addr, bool, bool, error) { - var hasIPv4, hasIPv6 bool - - endpoints := make([]netip.Addr, len(conf.Endpoint)) - for i, str := range conf.Endpoint { - var addr netip.Addr - if strings.Contains(str, "/") { - prefix, err := netip.ParsePrefix(str) - if err != nil { - return nil, false, false, err - } - addr = prefix.Addr() - if prefix.Bits() != addr.BitLen() { - return nil, false, false, errors.New("interface address subnet should be /32 for IPv4 and /128 for IPv6") - } - } else { - var err error - addr, err = netip.ParseAddr(str) - if err != nil { - return nil, false, false, err - } - } - endpoints[i] = addr - - if addr.Is4() { - hasIPv4 = true - } else if addr.Is6() { - hasIPv6 = true - } - } - - return endpoints, hasIPv4, hasIPv6, nil -} - -// serialize the config into an IPC request -func createIPCRequest(conf *DeviceConfig) string { - var request strings.Builder - - request.WriteString(fmt.Sprintf("private_key=%s\n", conf.SecretKey)) - - if !conf.IsClient { - // placeholder, we'll handle actual port listening on Xray - request.WriteString("listen_port=1337\n") - } - - for _, peer := range conf.Peers { - if peer.PublicKey != "" { - request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey)) - } - - if peer.PreSharedKey != "" { - request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey)) - } - - if peer.Endpoint != "" { - request.WriteString(fmt.Sprintf("endpoint=%s\n", peer.Endpoint)) - } - - for _, ip := range peer.AllowedIps { - request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip)) - } - - if peer.KeepAlive != 0 { - request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive)) - } - } - - return request.String()[:request.Len()] -} From 829d54d7be3e5df9521c42948fe060d6c4660e19 Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Tue, 16 Jun 2026 22:49:35 +0100 Subject: [PATCH 46/78] Hysteria & XHTTP/3 clients: `udpHop` supports `dialerProxy` (#6320) https://github.com/XTLS/Xray-core/pull/6320#issuecomment-4725679616 Fixes https://github.com/XTLS/Xray-core/pull/6320#issuecomment-4699599655 --------- Co-authored-by: LjhAUMEM --- transport/internet/hysteria/dialer.go | 50 ++++++++++------------ transport/internet/hysteria/udphop/conn.go | 16 ++----- transport/internet/splithttp/dialer.go | 50 ++++++++++------------ 3 files changed, 49 insertions(+), 67 deletions(-) diff --git a/transport/internet/hysteria/dialer.go b/transport/internet/hysteria/dialer.go index 3cca93563..a928cac72 100644 --- a/transport/internet/hysteria/dialer.go +++ b/transport/internet/hysteria/dialer.go @@ -126,6 +126,8 @@ func (c *client) dial(ctx context.Context) error { switch c := conn.(type) { case *internet.PacketConnWrapper: pktConn = c.PacketConn + case *cnc.Connection: + pktConn = &internet.FakePacketConn{Conn: c} default: panic(reflect.TypeOf(c)) } @@ -135,36 +137,30 @@ func (c *client) dial(ctx context.Context) error { var pktConn net.PacketConn var udpAddr *net.UDPAddr + var index int + if len(quicParams.UdpHop.Ports) > 0 { - index := rand.Intn(len(quicParams.UdpHop.Ports)) + index = rand.Intn(len(quicParams.UdpHop.Ports)) c.dest.Port = net.Port(quicParams.UdpHop.Ports[index]) - conn, err := internet.DialSystem(ctx, c.dest, c.socketConfig) - if err != nil { - return errors.New("failed to dial to dest").Base(err) - } - switch c := conn.(type) { - case *internet.PacketConnWrapper: - pktConn = c.PacketConn - udpAddr = conn.RemoteAddr().(*net.UDPAddr) - default: - panic(reflect.TypeOf(c)) - } + } + + raw, err := internet.DialSystem(ctx, c.dest, c.socketConfig) + if err != nil { + return errors.New("failed to dial to dest").Base(err) + } + switch c := raw.(type) { + case *internet.PacketConnWrapper: + pktConn = c.PacketConn + udpAddr = raw.RemoteAddr().(*net.UDPAddr) + case *cnc.Connection: + pktConn = &internet.FakePacketConn{Conn: c} + udpAddr = &net.UDPAddr{IP: c.RemoteAddr().(*net.TCPAddr).IP, Port: c.RemoteAddr().(*net.TCPAddr).Port} + default: + panic(reflect.TypeOf(c)) + } + + if len(quicParams.UdpHop.Ports) > 0 { pktConn = udphop.NewUDPHopPacketConn(udphop.ToAddrs(udpAddr.IP, quicParams.UdpHop.Ports), time.Duration(quicParams.UdpHop.IntervalMin)*time.Second, time.Duration(quicParams.UdpHop.IntervalMax)*time.Second, udpHopDialer, pktConn, index) - } else { - conn, err := internet.DialSystem(ctx, c.dest, c.socketConfig) - if err != nil { - return errors.New("failed to dial to dest").Base(err) - } - switch c := conn.(type) { - case *internet.PacketConnWrapper: - pktConn = c.PacketConn - udpAddr = c.RemoteAddr().(*net.UDPAddr) - case *cnc.Connection: - pktConn = &internet.FakePacketConn{Conn: c} - udpAddr = &net.UDPAddr{IP: c.RemoteAddr().(*net.TCPAddr).IP, Port: c.RemoteAddr().(*net.TCPAddr).Port} - default: - panic(reflect.TypeOf(c)) - } } if c.udpmaskManager != nil { diff --git a/transport/internet/hysteria/udphop/conn.go b/transport/internet/hysteria/udphop/conn.go index 83a45a201..8ab8339d5 100644 --- a/transport/internet/hysteria/udphop/conn.go +++ b/transport/internet/hysteria/udphop/conn.go @@ -5,7 +5,6 @@ import ( "math/rand" "net" "sync" - "syscall" "time" "github.com/xtls/xray-core/transport/internet/finalmask" @@ -138,8 +137,8 @@ func (u *UdpHopPacketConn) hop() { if u.closed { return } - u.addrIndex = rand.Intn(len(u.Addrs)) - newConn, err := u.ListenUDPFunc(u.Addrs[u.addrIndex].(*net.UDPAddr)) + addrIndex := rand.Intn(len(u.Addrs)) + newConn, err := u.ListenUDPFunc(u.Addrs[addrIndex].(*net.UDPAddr)) if err != nil { return } @@ -147,6 +146,7 @@ func (u *UdpHopPacketConn) hop() { _ = u.prevConn.Close() } u.prevConn = u.currentConn + u.addrIndex = addrIndex u.currentConn = newConn if !u.deadline.IsZero() { _ = u.currentConn.SetDeadline(u.deadline) @@ -241,16 +241,6 @@ func (u *UdpHopPacketConn) SetWriteDeadline(t time.Time) error { return u.currentConn.SetWriteDeadline(t) } -func (u *UdpHopPacketConn) SyscallConn() (syscall.RawConn, error) { - u.connMutex.RLock() - defer u.connMutex.RUnlock() - sc, ok := u.currentConn.(syscall.Conn) - if !ok { - return nil, errors.New("not supported") - } - return sc.SyscallConn() -} - func ToAddrs(ip net.IP, ports []uint32) []net.Addr { var addrs []net.Addr for _, port := range ports { diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 5092a9dbf..0f1f8c59b 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -209,6 +209,8 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea switch c := conn.(type) { case *internet.PacketConnWrapper: pktConn = c.PacketConn + case *cnc.Connection: + pktConn = &internet.FakePacketConn{Conn: c} default: panic(reflect.TypeOf(c)) } @@ -218,36 +220,30 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea var pktConn net.PacketConn var udpAddr *net.UDPAddr + var index int + if len(quicParams.UdpHop.Ports) > 0 { - index := rand.Intn(len(quicParams.UdpHop.Ports)) + index = rand.Intn(len(quicParams.UdpHop.Ports)) dest.Port = net.Port(quicParams.UdpHop.Ports[index]) - conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) - if err != nil { - return nil, errors.New("failed to dial to dest").Base(err) - } - switch c := conn.(type) { - case *internet.PacketConnWrapper: - pktConn = c.PacketConn - udpAddr = conn.RemoteAddr().(*net.UDPAddr) - default: - panic(reflect.TypeOf(c)) - } + } + + raw, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to dial to dest").Base(err) + } + switch c := raw.(type) { + case *internet.PacketConnWrapper: + pktConn = c.PacketConn + udpAddr = raw.RemoteAddr().(*net.UDPAddr) + case *cnc.Connection: + pktConn = &internet.FakePacketConn{Conn: c} + udpAddr = &net.UDPAddr{IP: c.RemoteAddr().(*net.TCPAddr).IP, Port: c.RemoteAddr().(*net.TCPAddr).Port} + default: + panic(reflect.TypeOf(c)) + } + + if len(quicParams.UdpHop.Ports) > 0 { pktConn = udphop.NewUDPHopPacketConn(udphop.ToAddrs(udpAddr.IP, quicParams.UdpHop.Ports), time.Duration(quicParams.UdpHop.IntervalMin)*time.Second, time.Duration(quicParams.UdpHop.IntervalMax)*time.Second, udpHopDialer, pktConn, index) - } else { - conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) - if err != nil { - return nil, errors.New("failed to dial to dest").Base(err) - } - switch c := conn.(type) { - case *internet.PacketConnWrapper: - pktConn = c.PacketConn - udpAddr = c.RemoteAddr().(*net.UDPAddr) - case *cnc.Connection: - pktConn = &internet.FakePacketConn{Conn: c} - udpAddr = &net.UDPAddr{IP: c.RemoteAddr().(*net.TCPAddr).IP, Port: c.RemoteAddr().(*net.TCPAddr).Port} - default: - panic(reflect.TypeOf(c)) - } } if streamSettings.UdpmaskManager != nil { From ad2e4cb0e19f8d1ae3ae0856d943e1254ecfec84 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Wed, 17 Jun 2026 21:59:19 +0800 Subject: [PATCH 47/78] Finalmask: Fix unexpected order and UDP's buf issue (#6331) https://github.com/XTLS/Xray-docs-next/pull/866#issuecomment-4729859528 And https://github.com/XTLS/Xray-core/pull/6331#issuecomment-4730527410 Fixes https://github.com/XTLS/Xray-core/issues/6184#issuecomment-4725831023 --- infra/conf/transport_internet.go | 9 ++++++--- transport/internet/finalmask/finalmask.go | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 2c5650b0d..9b08f8c35 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1788,12 +1788,15 @@ func (c *MkcpLegacy) Build() (proto.Message, error) { } type Salamander struct { - Password string `json:"password"` - PacketSize *Int32Range `json:"packetSize"` + Password string `json:"password"` + PacketSize Int32Range `json:"packetSize"` } func (c *Salamander) Build() (proto.Message, error) { - if c.PacketSize != nil { + if c.PacketSize.To > 0 { + if c.PacketSize.From <= 0 || c.PacketSize.To > 2048 { + return nil, errors.New("gecko: invalid min/max packet size") + } return &salamander.GeckoConfig{ Password: c.Password, MinPacketSize: c.PacketSize.From, diff --git a/transport/internet/finalmask/finalmask.go b/transport/internet/finalmask/finalmask.go index d7697cfe1..4683082ed 100644 --- a/transport/internet/finalmask/finalmask.go +++ b/transport/internet/finalmask/finalmask.go @@ -3,6 +3,7 @@ package finalmask import ( "context" "net" + "slices" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -28,7 +29,7 @@ func NewUdpmaskManager(udpmasks []Udpmask) *UdpmaskManager { func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) { var sizes []int var conns []net.PacketConn - for i, mask := range m.udpmasks { + for i, mask := range slices.Backward(m.udpmasks) { if _, ok := mask.(headerConn); ok { conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1) if err != nil { @@ -61,7 +62,7 @@ func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketCon func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) { var sizes []int var conns []net.PacketConn - for i, mask := range m.udpmasks { + for i, mask := range slices.Backward(m.udpmasks) { if _, ok := mask.(headerConn); ok { conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1) if err != nil { @@ -124,7 +125,7 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) if err != nil { return n, addr, err } - b = b[:n] + buf := b[:n] sum := 0 for _, size := range c.sizes { @@ -132,24 +133,24 @@ func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) } if n < sum { - errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", len(b)) + errors.LogError(context.Background(), "[mask] drop packet from ", addr, " with size ", n) continue } for i := range c.conns { - n, _, err = c.conns[i].ReadFrom(b) + n, _, err = c.conns[i].ReadFrom(buf) if err != nil { - errors.LogErrorInner(context.Background(), err, "[mask] drop packet from ", addr, " with size ", len(b)) + errors.LogErrorInner(context.Background(), err, "[mask] drop packet from ", addr, " with size ", n) break } - b = b[c.sizes[i] : n+c.sizes[i]] + buf = buf[c.sizes[i] : n+c.sizes[i]] } if err != nil { continue } - return copy(p, b), addr, nil + return copy(p, buf), addr, nil } } @@ -212,7 +213,7 @@ func NewTcpmaskManager(tcpmasks []Tcpmask) *TcpmaskManager { func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) { var err error - for _, mask := range m.tcpmasks { + for _, mask := range slices.Backward(m.tcpmasks) { raw, err = mask.WrapConnClient(raw) if err != nil { return nil, err @@ -223,7 +224,7 @@ func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) { func (m *TcpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) { var err error - for _, mask := range m.tcpmasks { + for _, mask := range slices.Backward(m.tcpmasks) { raw, err = mask.WrapConnServer(raw) if err != nil { return nil, err From 641273848694934d6aec7992bb71162ec69f2a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 17 Jun 2026 22:27:26 +0800 Subject: [PATCH 48/78] Socks5 inbound: Fix issues in new UDP ASSOCIATE (#6325) https://github.com/XTLS/Xray-core/pull/6325#issuecomment-4724008713 Fixes https://github.com/XTLS/Xray-core/issues/6323 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- proxy/socks/protocol.go | 7 ++++--- proxy/socks/server.go | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index bf4f61a5b..5d4cfd9d9 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -209,11 +209,12 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co } responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) expectedRemote := &gonet.UDPAddr{} - if request.Address.IP().IsUnspecified() { + // UDP Associate should not specify a domain as source IP + if request.Address.Family().IsDomain() || request.Address.IP().IsUnspecified() { expectedRemote.IP = writer.RemoteAddr().(*net.TCPAddr).IP // unix? } else { - expectedRemote.IP = request.Address.IP() // panic? - expectedRemote.Port = int(request.Port) // 0 is allowed + expectedRemote.IP = request.Address.IP() + expectedRemote.Port = int(request.Port) // 0 is allowed } tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemote) } diff --git a/proxy/socks/server.go b/proxy/socks/server.go index 53049dfe2..e1d34a60b 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -4,6 +4,7 @@ import ( "context" goerrors "errors" "io" + "sync" "time" "github.com/xtls/xray-core/common" @@ -216,18 +217,27 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis defer udpServer.RemoveRay() inbound := session.InboundFromContext(ctx) - if inbound != nil && inbound.Source.IsValid() { - errors.LogInfo(ctx, "client UDP connection from ", inbound.Source) - } var dest *net.Destination reader := buf.NewPacketReader(conn) + var changeRemote sync.Once for { mpayload, err := reader.ReadMultiBuffer() if err != nil { return err } + changeRemote.Do(func() { + if inbound != nil { + newInbound := *inbound + // change source to real remote UDP address + newInbound.Source = net.DestinationFromAddr(conn.RemoteAddr()) + newInbound.Local = net.DestinationFromAddr(conn.LocalAddr()) + inbound = &newInbound + ctx = session.ContextWithInbound(ctx, inbound) + errors.LogInfo(ctx, "client UDP connection from ", inbound.Source) + } + }) for _, payload := range mpayload { request, err := DecodeUDPPacket(payload) From 711aea4e34b0a00073d2fea6577f28a5d7c9b5eb Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 19 Jun 2026 06:31:21 +0800 Subject: [PATCH 49/78] XHTTP & WS & HU & gRPC servers: Require `sockopt.trustedXForwardedFor` (#6309) https://github.com/XTLS/Xray-core/pull/6258#issuecomment-4663652131 Behavior: https://github.com/XTLS/Xray-core/pull/6258#issuecomment-4746598275 Replaces https://github.com/XTLS/Xray-core/pull/6159 --- common/protocol/http/headers.go | 36 ++++++++---- common/protocol/http/headers_test.go | 41 ++++++++++--- infra/conf/xray.go | 21 ------- transport/internet/grpc/dial.go | 4 +- transport/internet/grpc/encoding/hunkconn.go | 29 +--------- transport/internet/grpc/encoding/multiconn.go | 32 +--------- .../internet/grpc/encoding/remoteaddr.go | 58 +++++++++++++++++++ .../internet/grpc/encoding/remoteaddr_test.go | 53 +++++++++++++++++ transport/internet/grpc/hub.go | 16 +++-- .../internet/httpupgrade/httpupgrade_test.go | 3 + transport/internet/httpupgrade/hub.go | 20 ++----- transport/internet/splithttp/hub.go | 20 ++----- .../internet/splithttp/splithttp_test.go | 3 + transport/internet/websocket/hub.go | 20 ++----- transport/internet/websocket/ws_test.go | 3 + 15 files changed, 208 insertions(+), 151 deletions(-) create mode 100644 transport/internet/grpc/encoding/remoteaddr.go create mode 100644 transport/internet/grpc/encoding/remoteaddr_test.go diff --git a/common/protocol/http/headers.go b/common/protocol/http/headers.go index db3aa6705..61a07005c 100644 --- a/common/protocol/http/headers.go +++ b/common/protocol/http/headers.go @@ -1,25 +1,41 @@ package http import ( + "context" "net/http" "strconv" "strings" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" ) -// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it. -func ParseXForwardedFor(header http.Header) []net.Address { - xff := header.Get("X-Forwarded-For") - if xff == "" { - return nil +// ApplyTrustedXForwardedFor returns remoteAddr overridden by X-Forwarded-For only when a configured trusted header is present. +func ApplyTrustedXForwardedFor(header http.Header, trusted []string, remoteAddr net.Addr) net.Addr { + value := header.Get("X-Forwarded-For") + if value == "" { + return remoteAddr } - list := strings.Split(xff, ",") - addrs := make([]net.Address, 0, len(list)) - for _, proxy := range list { - addrs = append(addrs, net.ParseAddress(proxy)) + for _, t := range trusted { + if len(header.Values(t)) > 0 { + if idx := strings.IndexByte(value, ','); idx >= 0 { + value = value[:idx] + } + if addr := net.ParseAddress(value); addr.Family().IsIP() { + return &net.TCPAddr{ + IP: addr.IP(), + Port: 0, + } + } + return remoteAddr + } } - return addrs + if len(trusted) == 0 { + errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`) + } else { + errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value) + } + return remoteAddr } // RemoveHopByHopHeaders removes hop by hop headers in http header list. diff --git a/common/protocol/http/headers_test.go b/common/protocol/http/headers_test.go index 80e243aeb..bad395d0c 100644 --- a/common/protocol/http/headers_test.go +++ b/common/protocol/http/headers_test.go @@ -2,23 +2,48 @@ package http_test import ( "bufio" + gonet "net" "net/http" "strings" "testing" - "github.com/google/go-cmp/cmp" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/net" . "github.com/xtls/xray-core/common/protocol/http" ) -func TestParseXForwardedFor(t *testing.T) { - header := http.Header{} - header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103") - addrs := ParseXForwardedFor(header) - if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" { - t.Error(r) - } +func TestApplyTrustedXForwardedFor(t *testing.T) { + remoteAddr := &gonet.TCPAddr{IP: gonet.ParseIP("127.0.0.1"), Port: 12345} + + t.Run("ignore X-Forwarded-For without trusted header", func(t *testing.T) { + header := http.Header{} + header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103") + + if addr := ApplyTrustedXForwardedFor(header, nil, remoteAddr); addr != remoteAddr { + t.Fatalf("unexpected remote address: %v", addr) + } + }) + + t.Run("trust X-Forwarded-For", func(t *testing.T) { + header := http.Header{} + header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103") + header.Add("X-Trusted-CDN", "") + + addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr) + if addr.String() != "129.78.138.66:0" { + t.Fatalf("unexpected remote address: %v", addr) + } + }) + + t.Run("ignore non-IP X-Forwarded-For", func(t *testing.T) { + header := http.Header{} + header.Add("X-Forwarded-For", "example.com") + header.Add("X-Trusted-CDN", "") + + if addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr); addr != remoteAddr { + t.Fatalf("unexpected remote address: %v", addr) + } + }) } func TestHopByHopHeadersRemoving(t *testing.T) { diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 3e6e43710..c8ff16279 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -173,27 +173,6 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { return nil, err } receiverSettings.StreamSettings = ss - // TODO: Actually implement this breaking change - protocol := ss.GetEffectiveProtocol() - if (protocol == "websocket" || protocol == "httpupgrade" || protocol == "splithttp") && - (c.StreamSetting.SocketSettings == nil || len(c.StreamSetting.SocketSettings.TrustedXForwardedFor) == 0) { - errors.LogWarning( - context.Background(), - `====== SECURITY WARNING ======`, - "\n", - `inbound "`, c.Tag, `" using `, protocol, ` has not configured "sockopt.trustedXForwardedFor".`, - "\n", - `THIS IS VERY INSECURE!!!`, - "\n", - `For compatibility, Xray still allows this for now and still trusts X-Forwarded-For implicitly.`, - "\n", - `Please configure "sockopt.trustedXForwardedFor" immediately.`, - "\n", - `In future versions, this option must be explicitly set.`, - "\n", - `====== SECURITY WARNING ======`, - ) - } if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil || len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) { errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`) diff --git a/transport/internet/grpc/dial.go b/transport/internet/grpc/dial.go index 9ffbbfd1d..4883f30ab 100644 --- a/transport/internet/grpc/dial.go +++ b/transport/internet/grpc/dial.go @@ -62,7 +62,7 @@ func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *interne if err != nil { return nil, errors.New("Cannot dial gRPC").Base(err) } - return encoding.NewMultiHunkConn(grpcService, nil), nil + return encoding.NewMultiHunkConn(grpcService, nil, nil), nil } errors.LogDebug(ctx, "using gRPC tun mode service name: `"+grpcSettings.getServiceName()+"` stream name: `"+grpcSettings.getTunStreamName()+"`") @@ -71,7 +71,7 @@ func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *interne return nil, errors.New("Cannot dial gRPC").Base(err) } - return encoding.NewHunkConn(grpcService, nil), nil + return encoding.NewHunkConn(grpcService, nil, nil), nil } func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (*grpc.ClientConn, error) { diff --git a/transport/internet/grpc/encoding/hunkconn.go b/transport/internet/grpc/encoding/hunkconn.go index f1155de8a..92769065e 100644 --- a/transport/internet/grpc/encoding/hunkconn.go +++ b/transport/internet/grpc/encoding/hunkconn.go @@ -9,8 +9,6 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/signal/done" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" ) type HunkConn interface { @@ -38,31 +36,8 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter return &HunkReaderWriter{hc, cancel, done.New(), nil, 0} } -func NewHunkConn(hc HunkConn, cancel context.CancelFunc) net.Conn { - var rAddr net.Addr - pr, ok := peer.FromContext(hc.Context()) - if ok { - rAddr = pr.Addr - } else { - rAddr = &net.TCPAddr{ - IP: []byte{0, 0, 0, 0}, - Port: 0, - } - } - - md, ok := metadata.FromIncomingContext(hc.Context()) - if ok { - header := md.Get("x-real-ip") - if len(header) > 0 { - realip := net.ParseAddress(header[0]) - if realip.Family().IsIP() { - rAddr = &net.TCPAddr{ - IP: realip.IP(), - Port: 0, - } - } - } - } +func NewHunkConn(hc HunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn { + rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor) wrc := NewHunkReadWriter(hc, cancel) return cnc.NewConnection( cnc.ConnectionInput(wrc), diff --git a/transport/internet/grpc/encoding/multiconn.go b/transport/internet/grpc/encoding/multiconn.go index fa95cba8c..f27a1c35e 100644 --- a/transport/internet/grpc/encoding/multiconn.go +++ b/transport/internet/grpc/encoding/multiconn.go @@ -3,15 +3,12 @@ package encoding import ( "context" "io" - "net" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/signal/done" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" ) type MultiHunkConn interface { @@ -34,31 +31,8 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH return &MultiHunkReaderWriter{hc, cancel, done.New(), nil} } -func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc) net.Conn { - var rAddr net.Addr - pr, ok := peer.FromContext(hc.Context()) - if ok { - rAddr = pr.Addr - } else { - rAddr = &net.TCPAddr{ - IP: []byte{0, 0, 0, 0}, - Port: 0, - } - } - - md, ok := metadata.FromIncomingContext(hc.Context()) - if ok { - header := md.Get("x-real-ip") - if len(header) > 0 { - realip := xnet.ParseAddress(header[0]) - if realip.Family().IsIP() { - rAddr = &net.TCPAddr{ - IP: realip.IP(), - Port: 0, - } - } - } - } +func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn { + rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor) wrc := NewMultiHunkReadWriter(hc, cancel) return cnc.NewConnection( cnc.ConnectionInputMulti(wrc), diff --git a/transport/internet/grpc/encoding/remoteaddr.go b/transport/internet/grpc/encoding/remoteaddr.go new file mode 100644 index 000000000..a28d91933 --- /dev/null +++ b/transport/internet/grpc/encoding/remoteaddr.go @@ -0,0 +1,58 @@ +package encoding + +import ( + "context" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" +) + +func remoteAddrFromContext(ctx context.Context, trusted []string) net.Addr { + var remoteAddr net.Addr + if pr, ok := peer.FromContext(ctx); ok { + remoteAddr = pr.Addr + } else { + remoteAddr = &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return remoteAddr + } + + if forwardedAddr := parseTrustedXForwardedFor(md, trusted, remoteAddr); forwardedAddr != nil && forwardedAddr.Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddr.IP(), + Port: 0, + } + } + return remoteAddr +} + +func parseTrustedXForwardedFor(md metadata.MD, trusted []string, remoteAddr net.Addr) net.Address { + values := md.Get("X-Forwarded-For") + if len(values) == 0 || values[0] == "" { + return nil + } + value := values[0] + for _, t := range trusted { + if len(md.Get(t)) > 0 { + if idx := strings.IndexByte(value, ','); idx >= 0 { + value = value[:idx] + } + return net.ParseAddress(value) + } + } + if len(trusted) == 0 { + errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`) + } else { + errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value) + } + return nil +} diff --git a/transport/internet/grpc/encoding/remoteaddr_test.go b/transport/internet/grpc/encoding/remoteaddr_test.go new file mode 100644 index 000000000..81e40aa96 --- /dev/null +++ b/transport/internet/grpc/encoding/remoteaddr_test.go @@ -0,0 +1,53 @@ +package encoding + +import ( + "context" + "net" + "testing" + + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" +) + +func TestRemoteAddrFromContext(t *testing.T) { + tests := []struct { + name string + metadata metadata.MD + trustedXForwardedFor []string + expectedRemoteAddress string + }{ + { + name: "trust X-Forwarded-For when configured", + metadata: metadata.Pairs("X-Forwarded-For", "2.2.2.2, 3.3.3.3"), + trustedXForwardedFor: []string{"X-Forwarded-For"}, + expectedRemoteAddress: "2.2.2.2:0", + }, + { + name: "trust X-Forwarded-For with trusted marker", + metadata: metadata.Pairs("X-Forwarded-For", "4.4.4.4", "X-Trusted-CDN", "1"), + trustedXForwardedFor: []string{"X-Trusted-CDN"}, + expectedRemoteAddress: "4.4.4.4:0", + }, + { + name: "ignore X-Forwarded-For without trusted marker", + metadata: metadata.Pairs("X-Forwarded-For", "5.5.5.5"), + trustedXForwardedFor: []string{"X-Trusted-CDN"}, + expectedRemoteAddress: "127.0.0.1:12345", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := peer.NewContext(metadata.NewIncomingContext(context.Background(), test.metadata), &peer.Peer{ + Addr: &net.TCPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 12345, + }, + }) + remoteAddr := remoteAddrFromContext(ctx, test.trustedXForwardedFor) + if remoteAddr.String() != test.expectedRemoteAddress { + t.Fatalf("unexpected remote address: %s", remoteAddr.String()) + } + }) + } +} diff --git a/transport/internet/grpc/hub.go b/transport/internet/grpc/hub.go index 34f8f1c0e..91bd2ab60 100644 --- a/transport/internet/grpc/hub.go +++ b/transport/internet/grpc/hub.go @@ -19,24 +19,25 @@ import ( type Listener struct { encoding.UnimplementedGRPCServiceServer - ctx context.Context - handler internet.ConnHandler - local net.Addr - config *Config + ctx context.Context + handler internet.ConnHandler + local net.Addr + config *Config + trustedXForwardedFor []string s *grpc.Server } func (l Listener) Tun(server encoding.GRPCService_TunServer) error { tunCtx, cancel := context.WithCancel(l.ctx) - l.handler(encoding.NewHunkConn(server, cancel)) + l.handler(encoding.NewHunkConn(server, cancel, l.trustedXForwardedFor)) <-tunCtx.Done() return nil } func (l Listener) TunMulti(server encoding.GRPCService_TunMultiServer) error { tunCtx, cancel := context.WithCancel(l.ctx) - l.handler(encoding.NewMultiHunkConn(server, cancel)) + l.handler(encoding.NewMultiHunkConn(server, cancel, l.trustedXForwardedFor)) <-tunCtx.Done() return nil } @@ -74,6 +75,9 @@ func Listen(ctx context.Context, address net.Address, port net.Port, settings *i } listener.ctx = ctx + if settings.SocketSettings != nil { + listener.trustedXForwardedFor = settings.SocketSettings.TrustedXForwardedFor + } config := tls.ConfigFromStreamSettings(settings) diff --git a/transport/internet/httpupgrade/httpupgrade_test.go b/transport/internet/httpupgrade/httpupgrade_test.go index c0310b957..5b420ed6e 100644 --- a/transport/internet/httpupgrade/httpupgrade_test.go +++ b/transport/internet/httpupgrade/httpupgrade_test.go @@ -138,6 +138,9 @@ func TestDialWithRemoteAddr(t *testing.T) { ProtocolSettings: &Config{ Path: "httpupgrade", }, + SocketSettings: &internet.SocketConfig{ + TrustedXForwardedFor: []string{"X-Forwarded-For"}, + }, }, func(conn stat.Connection) { go func(c stat.Connection) { defer c.Close() diff --git a/transport/internet/httpupgrade/hub.go b/transport/internet/httpupgrade/hub.go index 8e70ad08a..cb9a79d9a 100644 --- a/transport/internet/httpupgrade/hub.go +++ b/transport/internet/httpupgrade/hub.go @@ -80,24 +80,12 @@ func (s *server) upgrade(conn net.Conn) (stat.Connection, error) { return nil, err } - var forwardedAddrs []net.Address - if s.socketSettings != nil && len(s.socketSettings.TrustedXForwardedFor) > 0 { - for _, key := range s.socketSettings.TrustedXForwardedFor { - if len(req.Header.Values(key)) > 0 { - forwardedAddrs = http_proto.ParseXForwardedFor(req.Header) - break - } - } - } else { - forwardedAddrs = http_proto.ParseXForwardedFor(req.Header) - } remoteAddr := conn.RemoteAddr() - if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { - remoteAddr = &net.TCPAddr{ - IP: forwardedAddrs[0].IP(), - Port: int(0), - } + var trustedXFF []string + if s.socketSettings != nil { + trustedXFF = s.socketSettings.TrustedXForwardedFor } + remoteAddr = http_proto.ApplyTrustedXForwardedFor(req.Header, trustedXFF, remoteAddr) return stat.Connection(newConnection(conn, remoteAddr)), nil } diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index 535549bf4..a28866cf9 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -155,17 +155,6 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req return } - var forwardedAddrs []net.Address - if h.socketSettings != nil && len(h.socketSettings.TrustedXForwardedFor) > 0 { - for _, key := range h.socketSettings.TrustedXForwardedFor { - if len(request.Header.Values(key)) > 0 { - forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) - break - } - } - } else { - forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) - } var remoteAddr net.Addr var err error remoteAddr, err = net.ResolveTCPAddr("tcp", request.RemoteAddr) @@ -181,12 +170,11 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req Port: remoteAddr.(*net.TCPAddr).Port, } } - if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { - remoteAddr = &net.TCPAddr{ - IP: forwardedAddrs[0].IP(), - Port: 0, - } + var trustedXFF []string + if h.socketSettings != nil { + trustedXFF = h.socketSettings.TrustedXForwardedFor } + remoteAddr = http_proto.ApplyTrustedXForwardedFor(request.Header, trustedXFF, remoteAddr) var currentSession *httpSession if sessionId != "" { diff --git a/transport/internet/splithttp/splithttp_test.go b/transport/internet/splithttp/splithttp_test.go index ab02b6193..62ca858f3 100644 --- a/transport/internet/splithttp/splithttp_test.go +++ b/transport/internet/splithttp/splithttp_test.go @@ -88,6 +88,9 @@ func TestDialWithRemoteAddr(t *testing.T) { ProtocolSettings: &Config{ Path: "sh", }, + SocketSettings: &internet.SocketConfig{ + TrustedXForwardedFor: []string{"X-Forwarded-For"}, + }, }, func(conn stat.Connection) { go func(c stat.Connection) { defer c.Close() diff --git a/transport/internet/websocket/hub.go b/transport/internet/websocket/hub.go index 73799174c..42dba880f 100644 --- a/transport/internet/websocket/hub.go +++ b/transport/internet/websocket/hub.go @@ -65,24 +65,12 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req return } - var forwardedAddrs []net.Address - if h.socketSettings != nil && len(h.socketSettings.TrustedXForwardedFor) > 0 { - for _, key := range h.socketSettings.TrustedXForwardedFor { - if len(request.Header.Values(key)) > 0 { - forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) - break - } - } - } else { - forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) - } remoteAddr := conn.RemoteAddr() - if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { - remoteAddr = &net.TCPAddr{ - IP: forwardedAddrs[0].IP(), - Port: int(0), - } + var trustedXFF []string + if h.socketSettings != nil { + trustedXFF = h.socketSettings.TrustedXForwardedFor } + remoteAddr = http_proto.ApplyTrustedXForwardedFor(request.Header, trustedXFF, remoteAddr) h.ln.addConn(NewConnection(conn, remoteAddr, extraReader, h.ln.config.HeartbeatPeriod)) } diff --git a/transport/internet/websocket/ws_test.go b/transport/internet/websocket/ws_test.go index aab28af60..08cd27517 100644 --- a/transport/internet/websocket/ws_test.go +++ b/transport/internet/websocket/ws_test.go @@ -79,6 +79,9 @@ func TestDialWithRemoteAddr(t *testing.T) { ProtocolSettings: &Config{ Path: "ws", }, + SocketSettings: &internet.SocketConfig{ + TrustedXForwardedFor: []string{"X-Forwarded-For"}, + }, }, func(conn stat.Connection) { go func(c stat.Connection) { defer c.Close() From 986c512e0f7bc40069e84de903ae2d5a32e324b4 Mon Sep 17 00:00:00 2001 From: bytecategory Date: Fri, 19 Jun 2026 06:55:18 +0800 Subject: [PATCH 50/78] XHTTP client: Avoid panic when `host` is invalid (#6316) Fixes https://github.com/XTLS/Xray-core/issues/6315 --- transport/internet/splithttp/client.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/transport/internet/splithttp/client.go b/transport/internet/splithttp/client.go index 47f6560a9..a736d01d7 100644 --- a/transport/internet/splithttp/client.go +++ b/transport/internet/splithttp/client.go @@ -59,7 +59,11 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio if body != nil { method = c.transportConfig.GetNormalizedUplinkHTTPMethod() // stream-up/one } - req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) + req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) + if err != nil { + errors.LogInfoInner(ctx, err, "failed to create HTTP request for "+url) + return nil, nil, nil, err + } c.transportConfig.FillStreamRequest(req, sessionId, "") wrc = &WaitReadCloser{Wait: make(chan struct{})} From c815c2f2df61dd37fe8c5cec2037e3b93c27266c Mon Sep 17 00:00:00 2001 From: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com> Date: Fri, 19 Jun 2026 07:17:01 +0800 Subject: [PATCH 51/78] Loopback outbound: Add `sniffing` (#6326) Example: https://github.com/XTLS/Xray-core/pull/6326#issue-4659701786 --- infra/conf/loopback.go | 14 ++++++++++++-- proxy/loopback/config.pb.go | 32 ++++++++++++++++++++++---------- proxy/loopback/config.proto | 3 +++ proxy/loopback/loopback.go | 19 ++++++++++++++----- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/infra/conf/loopback.go b/infra/conf/loopback.go index 87d349cee..ef30ddc3d 100644 --- a/infra/conf/loopback.go +++ b/infra/conf/loopback.go @@ -1,14 +1,24 @@ package conf import ( + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/proxy/loopback" "google.golang.org/protobuf/proto" ) type LoopbackConfig struct { - InboundTag string `json:"inboundTag"` + InboundTag string `json:"inboundTag"` + Sniffing *SniffingConfig `json:"sniffing"` } func (l LoopbackConfig) Build() (proto.Message, error) { - return &loopback.Config{InboundTag: l.InboundTag}, nil + c := &loopback.Config{InboundTag: l.InboundTag} + if l.Sniffing != nil { + sc, err := l.Sniffing.Build() + if err != nil { + return nil, errors.New("failed to build sniffing config").Base(err) + } + c.Sniffing = sc + } + return c, nil } diff --git a/proxy/loopback/config.pb.go b/proxy/loopback/config.pb.go index ee2728843..2f0619601 100644 --- a/proxy/loopback/config.pb.go +++ b/proxy/loopback/config.pb.go @@ -7,6 +7,7 @@ package loopback import ( + proxyman "github.com/xtls/xray-core/app/proxyman" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -22,8 +23,9 @@ const ( ) type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - InboundTag string `protobuf:"bytes,1,opt,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + InboundTag string `protobuf:"bytes,1,opt,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"` + Sniffing *proxyman.SniffingConfig `protobuf:"bytes,2,opt,name=sniffing,proto3" json:"sniffing,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -65,14 +67,22 @@ func (x *Config) GetInboundTag() string { return "" } +func (x *Config) GetSniffing() *proxyman.SniffingConfig { + if x != nil { + return x.Sniffing + } + return nil +} + var File_proxy_loopback_config_proto protoreflect.FileDescriptor const file_proxy_loopback_config_proto_rawDesc = "" + "\n" + - "\x1bproxy/loopback/config.proto\x12\x13xray.proxy.loopback\")\n" + + "\x1bproxy/loopback/config.proto\x12\x13xray.proxy.loopback\x1a\x19app/proxyman/config.proto\"h\n" + "\x06Config\x12\x1f\n" + "\vinbound_tag\x18\x01 \x01(\tR\n" + - "inboundTagB[\n" + + "inboundTag\x12=\n" + + "\bsniffing\x18\x02 \x01(\v2!.xray.app.proxyman.SniffingConfigR\bsniffingB[\n" + "\x17com.xray.proxy.loopbackP\x01Z(github.com/xtls/xray-core/proxy/loopback\xaa\x02\x13Xray.Proxy.Loopbackb\x06proto3" var ( @@ -89,14 +99,16 @@ func file_proxy_loopback_config_proto_rawDescGZIP() []byte { var file_proxy_loopback_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_proxy_loopback_config_proto_goTypes = []any{ - (*Config)(nil), // 0: xray.proxy.loopback.Config + (*Config)(nil), // 0: xray.proxy.loopback.Config + (*proxyman.SniffingConfig)(nil), // 1: xray.app.proxyman.SniffingConfig } var file_proxy_loopback_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 1, // 0: xray.proxy.loopback.Config.sniffing:type_name -> xray.app.proxyman.SniffingConfig + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_proxy_loopback_config_proto_init() } diff --git a/proxy/loopback/config.proto b/proxy/loopback/config.proto index 7b8fe9016..8543dda6a 100644 --- a/proxy/loopback/config.proto +++ b/proxy/loopback/config.proto @@ -6,6 +6,9 @@ option go_package = "github.com/xtls/xray-core/proxy/loopback"; option java_package = "com.xray.proxy.loopback"; option java_multiple_files = true; +import "app/proxyman/config.proto"; + message Config { string inbound_tag = 1; + xray.app.proxyman.SniffingConfig sniffing = 2; } diff --git a/proxy/loopback/loopback.go b/proxy/loopback/loopback.go index cb2b7e9ba..185496474 100644 --- a/proxy/loopback/loopback.go +++ b/proxy/loopback/loopback.go @@ -3,6 +3,7 @@ package loopback import ( "context" + proxyman "github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/session" @@ -13,7 +14,8 @@ import ( ) type Loopback struct { - config *Config + inboundTag string + sniffingRequest session.SniffingRequest dispatcherInstance routing.Dispatcher } @@ -29,6 +31,7 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet errors.LogInfo(ctx, "opening connection to ", destination) content := new(session.Content) content.SkipDNSResolve = true + content.SniffingRequest = l.sniffingRequest ctx = session.ContextWithContent(ctx, content) inbound := &session.Inbound{} @@ -37,20 +40,26 @@ func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet // get a shallow copy to avoid modifying the inbound tag in upstream context *inbound = *originInbound } - inbound.Tag = l.config.InboundTag + inbound.Tag = l.inboundTag ctx = session.ContextWithInbound(ctx, inbound) err := l.dispatcherInstance.DispatchLink(ctx, destination, link) if err != nil { - errors.New(ctx, "failed to process loopback connection").Base(err) - return err + return errors.New(ctx, "failed to process loopback connection").Base(err) } return nil } func (l *Loopback) init(config *Config, dispatcherInstance routing.Dispatcher) error { l.dispatcherInstance = dispatcherInstance - l.config = config + l.inboundTag = config.InboundTag + if config.Sniffing.GetEnabled() { + request, err := proxyman.BuildSniffingRequest(config.Sniffing) + if err != nil { + return errors.New("failed to build loopback sniffing request").Base(err).AtError() + } + l.sniffingRequest = request + } return nil } From 1e036ce1c5076f695d149a672ca578ebf907f882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Fri, 19 Jun 2026 07:55:11 +0800 Subject: [PATCH 52/78] XHTTP/3 client: Actively close underlying QUIC & UDP (#6332) Fixes https://github.com/XTLS/Xray-core/issues/6328#issuecomment-4730379021 --- transport/internet/splithttp/client.go | 10 +++++++++ transport/internet/splithttp/dialer.go | 9 ++++---- transport/internet/splithttp/mux.go | 26 +++++++++++++++++++++--- transport/internet/splithttp/mux_test.go | 4 ++-- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/transport/internet/splithttp/client.go b/transport/internet/splithttp/client.go index a736d01d7..0e3a73f2e 100644 --- a/transport/internet/splithttp/client.go +++ b/transport/internet/splithttp/client.go @@ -9,6 +9,7 @@ import ( "net/http/httptrace" "sync" + "github.com/apernet/quic-go/http3" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -176,6 +177,15 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio return nil } +// HTTP/1.1 and HTTP/2 will close itself, we only handle HTTP/3 here +func (c *DefaultDialerClient) Close() error { + transport := c.client.Transport + if h3Transport, ok := transport.(*http3.Transport); ok { + h3Transport.Close() + } + return nil +} + type WaitReadCloser struct { Wait chan struct{} io.ReadCloser diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 0f1f8c59b..817d93552 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -259,6 +259,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea if err != nil { return nil, err } + context.AfterFunc(conn.Context(), func() { pktConn.Close() }) switch quicParams.Congestion { case "reno": @@ -425,10 +426,10 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me } if xmuxClient != nil { - xmuxClient.OpenUsage.Add(1) + xmuxClient.AddRunning() } if xmuxClient2 != nil && xmuxClient2 != xmuxClient { - xmuxClient2.OpenUsage.Add(1) + xmuxClient2.AddRunning() } var closed atomic.Int32 @@ -440,10 +441,10 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me return } if xmuxClient != nil { - xmuxClient.OpenUsage.Add(-1) + xmuxClient.DoneRunning() } if xmuxClient2 != nil && xmuxClient2 != xmuxClient { - xmuxClient2.OpenUsage.Add(-1) + xmuxClient2.DoneRunning() } }, } diff --git a/transport/internet/splithttp/mux.go b/transport/internet/splithttp/mux.go index 093ddd127..4897ace45 100644 --- a/transport/internet/splithttp/mux.go +++ b/transport/internet/splithttp/mux.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" ) @@ -17,10 +18,27 @@ type XmuxConn interface { type XmuxClient struct { XmuxConn XmuxConn - OpenUsage atomic.Int32 + Running atomic.Int32 leftUsage int32 LeftRequests atomic.Int32 UnreusableAt time.Time + NotUsed atomic.Bool +} + +func (c *XmuxClient) AddRunning() { + c.Running.Add(1) +} + +func (c *XmuxClient) DoneRunning() { + c.Running.Add(-1) + c.maybeClose() +} + +// close the XmuxConn if it is not used and has no running requests +func (c *XmuxClient) maybeClose() { + if c.NotUsed.Load() && c.Running.Load() <= 0 { + common.Close(c.XmuxConn) + } } type XmuxManager struct { @@ -68,10 +86,12 @@ func (m *XmuxManager) GetXmuxClient(ctx context.Context) *XmuxClient { // when l xmuxClient.LeftRequests.Load() <= 0 || (xmuxClient.UnreusableAt != time.Time{} && time.Now().After(xmuxClient.UnreusableAt)) { errors.LogDebug(ctx, "XMUX: removing xmuxClient, IsClosed() = ", xmuxClient.XmuxConn.IsClosed(), - ", OpenUsage = ", xmuxClient.OpenUsage.Load(), + ", Running = ", xmuxClient.Running.Load(), ", leftUsage = ", xmuxClient.leftUsage, ", LeftRequests = ", xmuxClient.LeftRequests.Load(), ", UnreusableAt = ", xmuxClient.UnreusableAt) + xmuxClient.NotUsed.Store(true) + xmuxClient.maybeClose() m.xmuxClients = append(m.xmuxClients[:i], m.xmuxClients[i+1:]...) } else { i++ @@ -91,7 +111,7 @@ func (m *XmuxManager) GetXmuxClient(ctx context.Context) *XmuxClient { // when l xmuxClients := make([]*XmuxClient, 0) if m.concurrency > 0 { for _, xmuxClient := range m.xmuxClients { - if xmuxClient.OpenUsage.Load() < m.concurrency { + if xmuxClient.Running.Load() < m.concurrency { xmuxClients = append(xmuxClients, xmuxClient) } } diff --git a/transport/internet/splithttp/mux_test.go b/transport/internet/splithttp/mux_test.go index 835d07f0c..2f5b3520e 100644 --- a/transport/internet/splithttp/mux_test.go +++ b/transport/internet/splithttp/mux_test.go @@ -63,7 +63,7 @@ func TestMaxConcurrency(t *testing.T) { xmuxClients := make(map[interface{}]struct{}) for i := 0; i < 64; i++ { xmuxClient := xmuxManager.GetXmuxClient(context.Background()) - xmuxClient.OpenUsage.Add(1) + xmuxClient.AddRunning() xmuxClients[xmuxClient] = struct{}{} } @@ -82,7 +82,7 @@ func TestDefault(t *testing.T) { xmuxClients := make(map[interface{}]struct{}) for i := 0; i < 64; i++ { xmuxClient := xmuxManager.GetXmuxClient(context.Background()) - xmuxClient.OpenUsage.Add(1) + xmuxClient.AddRunning() xmuxClients[xmuxClient] = struct{}{} } From 8734774e4a6000a7e2a68ad4e25d60475253e84e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:23:41 +0000 Subject: [PATCH 53/78] Bump actions/checkout from 6 to 7 (#6344) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/release-win7.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 38fff2bef..a540f36a7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -65,7 +65,7 @@ jobs: echo "LATEST=$LATEST" >>${GITHUB_ENV} - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up QEMU uses: docker/setup-qemu-action@v4 diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index d6bce6076..73553e10d 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -83,7 +83,7 @@ jobs: CGO_ENABLED: 0 steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Show workflow information run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ff09d52c..b6bf7cd5f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,7 +170,7 @@ jobs: CGO_ENABLED: 0 steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up NDK if: matrix.goos == 'android' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccc2fab60..62460115e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Check Proto Version Header run: | head -n 4 core/config.pb.go > ref.txt @@ -59,7 +59,7 @@ jobs: contents: read steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v6 with: @@ -83,7 +83,7 @@ jobs: os: [windows-latest, ubuntu-latest, macos-latest] steps: - name: Checkout codebase - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v6 with: From be8009c62509322682299bfbe969a62cee03f4d5 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:02:27 +0800 Subject: [PATCH 54/78] Geodata: Cleanup unneeded matchers & `domain:` ignore case (#6342) Completes https://github.com/XTLS/Xray-core/pull/6139 --- common/geodata/domain_matcher.go | 2 +- common/geodata/domain_registry.go | 22 +++++++++++++++------- common/geodata/ip_registry.go | 30 +++++++++++++++++++----------- common/geodata/rule_parser.go | 4 ++-- common/utils/weak_cache.go | 14 ++++++++++++++ 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/common/geodata/domain_matcher.go b/common/geodata/domain_matcher.go index 2c231d98c..a4d3564e6 100644 --- a/common/geodata/domain_matcher.go +++ b/common/geodata/domain_matcher.go @@ -220,7 +220,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) { case Domain_Regex: return strmatcher.Regex.New(d.Value) case Domain_Domain: - return strmatcher.Domain.New(d.Value) + return strmatcher.Domain.New(strings.ToLower(d.Value)) case Domain_Full: return strmatcher.Full.New(strings.ToLower(d.Value)) default: diff --git a/common/geodata/domain_registry.go b/common/geodata/domain_registry.go index 7b6c65272..1bcc333a8 100644 --- a/common/geodata/domain_registry.go +++ b/common/geodata/domain_registry.go @@ -6,12 +6,14 @@ import ( "sync/atomic" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/uuid" ) type DomainRegistry struct { mu sync.Mutex factory DomainMatcherFactory - matchers []*DynamicDomainMatcher + matchers *utils.WeakCacheMap[uuid.UUID, DynamicDomainMatcher] } func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) { @@ -24,7 +26,7 @@ func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, } d := NewDynamicDomainMatcher(rules, m) - r.matchers = append(r.matchers, d) + r.matchers.Store(uuid.New(), d) return d, nil } @@ -32,15 +34,20 @@ func (r *DomainRegistry) Reload() error { r.mu.Lock() defer r.mu.Unlock() - errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)") + var matchers []*DynamicDomainMatcher + r.matchers.Range(func(_ uuid.UUID, matcher *DynamicDomainMatcher) bool { + matchers = append(matchers, matcher) + return true + }) + errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(matchers), " domain matcher(s)") factory := newDomainMatcherFactory() type reloadEntry struct { dynamic *DynamicDomainMatcher matcher DomainMatcher } - reloaded := make([]reloadEntry, len(r.matchers)) - for i, d := range r.matchers { + reloaded := make([]reloadEntry, len(matchers)) + for i, d := range matchers { m, err := factory.BuildMatcher(d.rules) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i) @@ -52,13 +59,14 @@ func (r *DomainRegistry) Reload() error { entry.dynamic.Reload(entry.matcher) } r.factory = factory - errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)") + errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(matchers), " domain matcher(s)") return nil } func newDomainRegistry() *DomainRegistry { return &DomainRegistry{ - factory: newDomainMatcherFactory(), + factory: newDomainMatcherFactory(), + matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicDomainMatcher](), } } diff --git a/common/geodata/ip_registry.go b/common/geodata/ip_registry.go index 21fbdf9ec..a02adf6d6 100644 --- a/common/geodata/ip_registry.go +++ b/common/geodata/ip_registry.go @@ -7,25 +7,27 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/uuid" ) type IPRegistry struct { - mu sync.Mutex - ipsetFactory *IPSetFactory - matchers []*DynamicIPMatcher + mu sync.Mutex + factory *IPSetFactory + matchers *utils.WeakCacheMap[uuid.UUID, DynamicIPMatcher] } func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) { r.mu.Lock() defer r.mu.Unlock() - m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules) + m, err := buildOptimizedIPMatcher(r.factory, rules) if err != nil { return nil, err } d := NewDynamicIPMatcher(rules, m) - r.matchers = append(r.matchers, d) + r.matchers.Store(uuid.New(), d) return d, nil } @@ -33,15 +35,20 @@ func (r *IPRegistry) Reload() error { r.mu.Lock() defer r.mu.Unlock() - errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)") + var matchers []*DynamicIPMatcher + r.matchers.Range(func(_ uuid.UUID, matcher *DynamicIPMatcher) bool { + matchers = append(matchers, matcher) + return true + }) + errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(matchers), " IP matcher(s)") factory := newIPSetFactory() type reloadEntry struct { dynamic *DynamicIPMatcher matcher IPMatcher } - reloaded := make([]reloadEntry, len(r.matchers)) - for i, d := range r.matchers { + reloaded := make([]reloadEntry, len(matchers)) + for i, d := range matchers { m, err := buildOptimizedIPMatcher(factory, d.rules) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i) @@ -52,14 +59,15 @@ func (r *IPRegistry) Reload() error { for _, entry := range reloaded { entry.dynamic.Reload(entry.matcher) } - r.ipsetFactory = factory - errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)") + r.factory = factory + errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(matchers), " IP matcher(s)") return nil } func newIPRegistry() *IPRegistry { return &IPRegistry{ - ipsetFactory: newIPSetFactory(), + factory: newIPSetFactory(), + matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicIPMatcher](), } } diff --git a/common/geodata/rule_parser.go b/common/geodata/rule_parser.go index c05fed31e..16b80b76f 100644 --- a/common/geodata/rule_parser.go +++ b/common/geodata/rule_parser.go @@ -138,7 +138,7 @@ func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) { } prefix := 0 - for _, ext := range [...]string{"ext:", "ext-domain:"} { + for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} { if strings.HasPrefix(r, ext) { prefix = len(ext) break @@ -167,7 +167,7 @@ func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, e } prefix := 0 - for _, ext := range [...]string{"ext:", "ext-domain:"} { + for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} { if strings.HasPrefix(r, ext) { prefix = len(ext) break diff --git a/common/utils/weak_cache.go b/common/utils/weak_cache.go index a9aaacca6..48cc8ce07 100644 --- a/common/utils/weak_cache.go +++ b/common/utils/weak_cache.go @@ -1,6 +1,7 @@ package utils import ( + "maps" "runtime" "sync" "weak" @@ -43,3 +44,16 @@ func (c *WeakCacheMap[K, V]) Store(key K, value *V) { } }, struct{}{}) } + +func (c *WeakCacheMap[K, V]) Range(f func(K, *V) bool) { + c.mu.Lock() + snapshot := maps.Clone(c.m) + c.mu.Unlock() + for k, v := range snapshot { + if value := v.Value(); value != nil { + if !f(k, value) { + break + } + } + } +} From 5aefcb41fbe0c2a7c59ac77a281ef88cfcd8220d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:05:47 +0000 Subject: [PATCH 55/78] Bump github.com/pion/stun/v3 from 3.1.5 to 3.1.6 (#6357) Bumps [github.com/pion/stun/v3](https://github.com/pion/stun) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pion/stun/releases) - [Commits](https://github.com/pion/stun/compare/v3.1.5...v3.1.6) --- updated-dependencies: - dependency-name: github.com/pion/stun/v3 dependency-version: 3.1.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e48eca14c..6a637f35e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 - github.com/pion/stun/v3 v3.1.5 + github.com/pion/stun/v3 v3.1.6 github.com/pires/go-proxyproto v0.12.0 github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/robfig/cron/v3 v3.0.1 diff --git a/go.sum b/go.sum index 59fe6c90d..6325e2b9e 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= -github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= +github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= From 567500c4af132f8bbe213c0098a5a9445b6465ed Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:57:23 +0330 Subject: [PATCH 56/78] Fragment finalmask: Add `lengths` and `delays` (#6334) Usage: https://github.com/XTLS/Xray-core/pull/6334#issue-4685556394 Behavior: https://github.com/XTLS/Xray-core/pull/6334#issuecomment-4751547750 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- infra/conf/transport_internet.go | 37 +++++--- .../internet/finalmask/fragment/config.pb.go | 85 ++++++++++--------- .../internet/finalmask/fragment/config.proto | 8 +- transport/internet/finalmask/fragment/conn.go | 50 +++++++++-- 4 files changed, 116 insertions(+), 64 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 9b08f8c35..feb593b4b 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1409,10 +1409,12 @@ func (c *HeaderCustomTCP) Build() (proto.Message, error) { } type FragmentMask struct { - Packets string `json:"packets"` - Length Int32Range `json:"length"` - Delay Int32Range `json:"delay"` - MaxSplit Int32Range `json:"maxSplit"` + Packets string `json:"packets"` + Length Int32Range `json:"length"` + Delay Int32Range `json:"delay"` + Lengths []Int32Range `json:"lengths"` + Delays []Int32Range `json:"delays"` + MaxSplit Int32Range `json:"maxSplit"` } func (c *FragmentMask) Build() (proto.Message, error) { @@ -1437,14 +1439,29 @@ func (c *FragmentMask) Build() (proto.Message, error) { } } - config.LengthMin = int64(c.Length.From) - config.LengthMax = int64(c.Length.To) - if config.LengthMin == 0 { - return nil, errors.New("LengthMin can't be 0") + if len(c.Lengths) > 0 { + for _, r := range c.Lengths { + config.LengthsMin = append(config.LengthsMin, int64(r.From)) + config.LengthsMax = append(config.LengthsMax, int64(r.To)) + } + } else { + config.LengthsMin = append(config.LengthsMin, int64(c.Length.From)) + config.LengthsMax = append(config.LengthsMax, int64(c.Length.To)) } - config.DelayMin = int64(c.Delay.From) - config.DelayMax = int64(c.Delay.To) + if config.LengthsMin[len(config.LengthsMin)-1] == 0 { + return nil, errors.New("last lengths entry min can't be 0") + } + + if len(c.Delays) > 0 { + for _, r := range c.Delays { + config.DelaysMin = append(config.DelaysMin, int64(r.From)) + config.DelaysMax = append(config.DelaysMax, int64(r.To)) + } + } else { + config.DelaysMin = append(config.DelaysMin, int64(c.Delay.From)) + config.DelaysMax = append(config.DelaysMax, int64(c.Delay.To)) + } config.MaxSplitMin = int64(c.MaxSplit.From) config.MaxSplitMax = int64(c.MaxSplit.To) diff --git a/transport/internet/finalmask/fragment/config.pb.go b/transport/internet/finalmask/fragment/config.pb.go index c8660f5ec..3659c7ac3 100644 --- a/transport/internet/finalmask/fragment/config.pb.go +++ b/transport/internet/finalmask/fragment/config.pb.go @@ -25,12 +25,12 @@ type Config struct { state protoimpl.MessageState `protogen:"open.v1"` PacketsFrom int64 `protobuf:"varint,1,opt,name=packets_from,json=packetsFrom,proto3" json:"packets_from,omitempty"` PacketsTo int64 `protobuf:"varint,2,opt,name=packets_to,json=packetsTo,proto3" json:"packets_to,omitempty"` - LengthMin int64 `protobuf:"varint,3,opt,name=length_min,json=lengthMin,proto3" json:"length_min,omitempty"` - LengthMax int64 `protobuf:"varint,4,opt,name=length_max,json=lengthMax,proto3" json:"length_max,omitempty"` - DelayMin int64 `protobuf:"varint,5,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` - DelayMax int64 `protobuf:"varint,6,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` MaxSplitMin int64 `protobuf:"varint,7,opt,name=max_split_min,json=maxSplitMin,proto3" json:"max_split_min,omitempty"` MaxSplitMax int64 `protobuf:"varint,8,opt,name=max_split_max,json=maxSplitMax,proto3" json:"max_split_max,omitempty"` + LengthsMin []int64 `protobuf:"varint,9,rep,packed,name=lengths_min,json=lengthsMin,proto3" json:"lengths_min,omitempty"` + LengthsMax []int64 `protobuf:"varint,10,rep,packed,name=lengths_max,json=lengthsMax,proto3" json:"lengths_max,omitempty"` + DelaysMin []int64 `protobuf:"varint,11,rep,packed,name=delays_min,json=delaysMin,proto3" json:"delays_min,omitempty"` + DelaysMax []int64 `protobuf:"varint,12,rep,packed,name=delays_max,json=delaysMax,proto3" json:"delays_max,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -79,34 +79,6 @@ func (x *Config) GetPacketsTo() int64 { return 0 } -func (x *Config) GetLengthMin() int64 { - if x != nil { - return x.LengthMin - } - return 0 -} - -func (x *Config) GetLengthMax() int64 { - if x != nil { - return x.LengthMax - } - return 0 -} - -func (x *Config) GetDelayMin() int64 { - if x != nil { - return x.DelayMin - } - return 0 -} - -func (x *Config) GetDelayMax() int64 { - if x != nil { - return x.DelayMax - } - return 0 -} - func (x *Config) GetMaxSplitMin() int64 { if x != nil { return x.MaxSplitMin @@ -121,23 +93,54 @@ func (x *Config) GetMaxSplitMax() int64 { return 0 } +func (x *Config) GetLengthsMin() []int64 { + if x != nil { + return x.LengthsMin + } + return nil +} + +func (x *Config) GetLengthsMax() []int64 { + if x != nil { + return x.LengthsMax + } + return nil +} + +func (x *Config) GetDelaysMin() []int64 { + if x != nil { + return x.DelaysMin + } + return nil +} + +func (x *Config) GetDelaysMax() []int64 { + if x != nil { + return x.DelaysMax + } + return nil +} + var File_transport_internet_finalmask_fragment_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_fragment_config_proto_rawDesc = "" + "\n" + - "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x8a\x02\n" + + "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x92\x02\n" + "\x06Config\x12!\n" + "\fpackets_from\x18\x01 \x01(\x03R\vpacketsFrom\x12\x1d\n" + "\n" + - "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\x1d\n" + - "\n" + - "length_min\x18\x03 \x01(\x03R\tlengthMin\x12\x1d\n" + - "\n" + - "length_max\x18\x04 \x01(\x03R\tlengthMax\x12\x1b\n" + - "\tdelay_min\x18\x05 \x01(\x03R\bdelayMin\x12\x1b\n" + - "\tdelay_max\x18\x06 \x01(\x03R\bdelayMax\x12\"\n" + + "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\"\n" + "\rmax_split_min\x18\a \x01(\x03R\vmaxSplitMin\x12\"\n" + - "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMaxB\xa0\x01\n" + + "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMax\x12\x1f\n" + + "\vlengths_min\x18\t \x03(\x03R\n" + + "lengthsMin\x12\x1f\n" + + "\vlengths_max\x18\n" + + " \x03(\x03R\n" + + "lengthsMax\x12\x1d\n" + + "\n" + + "delays_min\x18\v \x03(\x03R\tdelaysMin\x12\x1d\n" + + "\n" + + "delays_max\x18\f \x03(\x03R\tdelaysMaxB\xa0\x01\n" + ".com.xray.transport.internet.finalmask.fragmentP\x01Z?github.com/xtls/xray-core/transport/internet/finalmask/fragment\xaa\x02*Xray.Transport.Internet.Finalmask.Fragmentb\x06proto3" var ( diff --git a/transport/internet/finalmask/fragment/config.proto b/transport/internet/finalmask/fragment/config.proto index 62aaf1864..c83a962a1 100644 --- a/transport/internet/finalmask/fragment/config.proto +++ b/transport/internet/finalmask/fragment/config.proto @@ -9,10 +9,10 @@ option java_multiple_files = true; message Config { int64 packets_from = 1; int64 packets_to = 2; - int64 length_min = 3; - int64 length_max = 4; - int64 delay_min = 5; - int64 delay_max = 6; int64 max_split_min = 7; int64 max_split_max = 8; + repeated int64 lengths_min = 9; + repeated int64 lengths_max = 10; + repeated int64 delays_min = 11; + repeated int64 delays_max = 12; } \ No newline at end of file diff --git a/transport/internet/finalmask/fragment/conn.go b/transport/internet/finalmask/fragment/conn.go index 91822ace3..3c65a8bd5 100644 --- a/transport/internet/finalmask/fragment/conn.go +++ b/transport/internet/finalmask/fragment/conn.go @@ -43,6 +43,29 @@ func (c *fragmentConn) Splice() bool { return true } +// lengthForSegment returns the length range (min, max) for the given segment index (0-based). +// Clamps to the last entry when the index exceeds the list length. +func (c *fragmentConn) lengthForSegment(segIdx int) (int64, int64) { + if segIdx >= len(c.config.LengthsMin) { + segIdx = len(c.config.LengthsMin) - 1 + } + return c.config.LengthsMin[segIdx], c.config.LengthsMax[segIdx] +} + +// delayForSegment returns the delay range (min, max) for the given segment index (0-based). +// Clamps to the last entry when the index exceeds the list length. +func (c *fragmentConn) delayForSegment(segIdx int) (int64, int64) { + if segIdx >= len(c.config.DelaysMin) { + segIdx = len(c.config.DelaysMin) - 1 + } + return c.config.DelaysMin[segIdx], c.config.DelaysMax[segIdx] +} + +// mergeTlsHelloSegments returns true only when delays has exactly one zero entry. +func (c *fragmentConn) mergeTlsHelloSegments() bool { + return len(c.config.DelaysMax) == 1 && c.config.DelaysMax[0] == 0 +} + func (c *fragmentConn) Write(p []byte) (n int, err error) { c.count++ @@ -57,12 +80,13 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { data := p[5:recordLen] buff := make([]byte, 2048) var hello []byte + mergeHello := c.mergeTlsHelloSegments() maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) var splitNum int64 for from := 0; ; { - to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) - splitNum++ - if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) { + lengthMin, lengthMax := c.lengthForSegment(int(splitNum)) + to := from + int(crypto.RandBetween(lengthMin, lengthMax)) + if to > len(data) || (maxSplit > 0 && splitNum+1 >= maxSplit) { to = len(data) } l := to - from @@ -74,15 +98,19 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { from = to buff[3] = byte(l >> 8) buff[4] = byte(l) - if c.config.DelayMax == 0 { + if mergeHello { hello = append(hello, buff[:5+l]...) } else { + delayMin, delayMax := c.delayForSegment(int(splitNum)) _, err := c.Conn.Write(buff[:5+l]) - time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + if delayMax > 0 { + time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond) + } if err != nil { return 0, err } } + splitNum++ if from == len(data) { if len(hello) > 0 { _, err := c.Conn.Write(hello) @@ -107,9 +135,9 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) var splitNum int64 for from := 0; ; { - to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) - splitNum++ - if to > len(p) || (maxSplit > 0 && splitNum >= maxSplit) { + lengthMin, lengthMax := c.lengthForSegment(int(splitNum)) + to := from + int(crypto.RandBetween(lengthMin, lengthMax)) + if to > len(p) || (maxSplit > 0 && splitNum+1 >= maxSplit) { to = len(p) } n, err := c.Conn.Write(p[from:to]) @@ -117,7 +145,11 @@ func (c *fragmentConn) Write(p []byte) (n int, err error) { if err != nil { return from, err } - time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + delayMin, delayMax := c.delayForSegment(int(splitNum)) + if delayMax > 0 { + time.Sleep(time.Duration(crypto.RandBetween(delayMin, delayMax)) * time.Millisecond) + } + splitNum++ if from >= len(p) { return from, nil } From 9cd9382e3d7da0bddbbd12edb109bf80848b0da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=96=D0=BE=D1=80=D0=B0=20=D0=97=D0=BC=D0=B5=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8=D0=BD?= <48821354+Katze-942@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:29:23 +0400 Subject: [PATCH 57/78] TUN inbound: Support env `XRAY_TUN_FD` on Linux as well (#6338) https://github.com/XTLS/Xray-core/pull/6338#issuecomment-4770945163 --- proxy/tun/tun_linux.go | 76 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go index ef3f35054..3e5a30bf3 100644 --- a/proxy/tun/tun_linux.go +++ b/proxy/tun/tun_linux.go @@ -4,8 +4,11 @@ package tun import ( "net" + "strconv" "github.com/vishvananda/netlink" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -18,6 +21,7 @@ type LinuxTun struct { tunFd int tunLink netlink.Link options *Config + ownsTun bool } // LinuxTun implements Tun @@ -25,12 +29,24 @@ var _ Tun = (*LinuxTun)(nil) // NewTun builds new tun interface handler (linux specific) func NewTun(options *Config) (Tun, error) { - tunFd, err := open(options.Name) + tunFd, tunLink, fdProvided, err := openFromEnv(options.Name) + if err != nil { + return nil, err + } + if fdProvided { + return &LinuxTun{ + tunFd: tunFd, + tunLink: tunLink, + options: options, + }, nil + } + + tunFd, err = open(options.Name) if err != nil { return nil, err } - tunLink, err := setup(options.Name, int(options.MTU)) + tunLink, err = setup(options.Name, int(options.MTU)) if err != nil { _ = unix.Close(tunFd) return nil, err @@ -40,11 +56,59 @@ func NewTun(options *Config) (Tun, error) { tunFd: tunFd, tunLink: tunLink, options: options, + ownsTun: true, } return linuxTun, nil } +func openFromEnv(expectedName string) (int, netlink.Link, bool, error) { + fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" }) + if fdStr == "" { + return -1, nil, false, nil + } + + fd, err := strconv.Atoi(fdStr) + if err != nil { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey).Base(err) + } + if fd < 3 { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor must be >= 3") + } + + ifr, err := unix.NewIfreq("") + if err != nil { + return -1, nil, true, err + } + if err = unix.IoctlIfreq(fd, unix.TUNGETIFF, ifr); err != nil { + return -1, nil, true, err + } + + flags := ifr.Uint16() + if flags&unix.IFF_TUN == 0 { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor is not a TUN device") + } + if flags&unix.IFF_NO_PI == 0 { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device must use IFF_NO_PI") + } + + actualName := ifr.Name() + if expectedName != "" && actualName != expectedName { + return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device name ", actualName, " does not match configured name ", expectedName) + } + + tunLink, err := netlink.LinkByName(actualName) + if err != nil { + return -1, nil, true, err + } + + if err = unix.SetNonblock(fd, true); err != nil { + return -1, nil, true, err + } + + return fd, tunLink, true, nil +} + // open the file that implements tun interface in the OS func open(name string) (int, error) { fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0) @@ -93,6 +157,10 @@ func setup(name string, MTU int) (netlink.Link, error) { // Start is called by handler to bring tun interface to life func (t *LinuxTun) Start() error { + if !t.ownsTun { + return nil + } + err := netlink.LinkSetUp(t.tunLink) if err != nil { return err @@ -103,7 +171,9 @@ func (t *LinuxTun) Start() error { // Close is called to shut down the tun interface func (t *LinuxTun) Close() error { - _ = netlink.LinkSetDown(t.tunLink) + if t.ownsTun { + _ = netlink.LinkSetDown(t.tunLink) + } _ = unix.Close(t.tunFd) return nil From 583bb4a63f726745199b596fb1fab86861731e16 Mon Sep 17 00:00:00 2001 From: Omoeba <38597972+Omoeba@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:51:47 -0700 Subject: [PATCH 58/78] XHTTP server: Fix `scStreamUpServerSecs` when `xPaddingObfsMode` is true (#6343) https://github.com/XTLS/Xray-core/pull/6343#issuecomment-4771666966 --- transport/internet/splithttp/hub.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index a28866cf9..8416591a2 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -146,6 +146,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req writer.WriteHeader(http.StatusBadRequest) return } + obfsPaddingAccepted := h.config.XPaddingObfsMode && paddingValue != "" sessionId, seqStr := h.config.ExtractMetaFromRequest(request, h.path) @@ -215,8 +216,8 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(http.StatusOK) scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs() - referrer := request.Header.Get("Referer") - if referrer != "" && scStreamUpServerSecs.To > 0 { + hasLegacyRefererCompatMarker := request.Header.Get("Referer") != "" + if (hasLegacyRefererCompatMarker || obfsPaddingAccepted) && scStreamUpServerSecs.To > 0 { go func() { for { _, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.config.GetNormalizedXPaddingBytes().rand()))) From b99c3e56574fb0317608c49dd1dd9af816db7a9e Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:55:10 +0000 Subject: [PATCH 59/78] Xray-core v26.6.22 Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668 Project X Channel: https://t.me/projectXtls Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633 Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1 VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067 VLESS NFT: https://opensea.io/collection/vless XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113 REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2 --- core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/core.go b/core/core.go index bb66da8ec..78d7d2383 100644 --- a/core/core.go +++ b/core/core.go @@ -20,7 +20,7 @@ import ( var ( Version_x byte = 26 Version_y byte = 6 - Version_z byte = 1 + Version_z byte = 22 ) var ( From fab4bcc1edf8c031913da030cdb482fa6b00dbe2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:34:07 +0000 Subject: [PATCH 60/78] Bump github.com/cloudflare/circl from 1.6.3 to 1.6.4 (#6362) Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.3 to 1.6.4. - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.3...v1.6.4) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6a637f35e..88932cf08 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26 require ( github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 - github.com/cloudflare/circl v1.6.3 + github.com/cloudflare/circl v1.6.4 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index 6325e2b9e..5e2820856 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkd github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From e7e9254630bd0557363a13dc8e5dfe3a9754a3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 23 Jun 2026 19:37:21 +0800 Subject: [PATCH 61/78] TUN inbound: Avoid panic on nil RemoteAddr due to quickly closed connection (#6365) Fixes https://github.com/XTLS/Xray-core/issues/6364#issuecomment-4778262075 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- proxy/tun/handler.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index d552eca53..732468f76 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -143,7 +143,14 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { defer cancel() ctx = c.ContextWithID(ctx, session.NewID()) - source := net.DestinationFromAddr(conn.RemoteAddr()) + // if the connection is already closed, conn.RemoteAddr() will be nil + // due to gvisor weird behavior + remote := conn.RemoteAddr() + if remote == nil { + errors.LogInfo(t.ctx, "dropped quickly closed connection") + return + } + source := net.DestinationFromAddr(remote) inbound := session.Inbound{ Name: "tun", Tag: t.tag, From ac04c445bd09541cc9c35a120ee01d8a177a4d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 23 Jun 2026 19:48:17 +0800 Subject: [PATCH 62/78] DNS: Fix unexpected TTL clamp (#6363) Fixes https://github.com/XTLS/Xray-core/issues/6359 --- app/dns/dnscommon.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go index d16316b88..5ffae51be 100644 --- a/app/dns/dnscommon.go +++ b/app/dns/dnscommon.go @@ -198,9 +198,14 @@ func parseResponse(payload []byte) (*IPRecord, error) { ipRecord := &IPRecord{ ReqID: h.ID, RCode: h.RCode, - Expire: now.Add(time.Second * dns_feature.DefaultTTL), RawHeader: &h, } + defer func() { + // set to default TTL if no valid TTL is found + if ipRecord.Expire.IsZero() { + ipRecord.Expire = now.Add(time.Second * dns_feature.DefaultTTL) + } + }() L: for { @@ -217,7 +222,7 @@ L: ttl = 1 } expire := now.Add(time.Duration(ttl) * time.Second) - if ipRecord.Expire.After(expire) { + if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) { ipRecord.Expire = expire } From f9eb1597adb6b95b1139f76697f7039862d40859 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:48:21 +0000 Subject: [PATCH 63/78] Bump actions/cache from 5 to 6 (#6368) Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-win7.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- .github/workflows/scheduled-assets-update.yml | 8 ++++---- .github/workflows/test.yml | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 73553e10d..f7f997bb6 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -14,13 +14,13 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- - name: Restore Wintun Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-wintun- @@ -119,13 +119,13 @@ jobs: # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- - name: Restore Wintun Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-wintun- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6bf7cd5f..c459a8837 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,13 +14,13 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- - name: Restore Wintun Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-wintun- @@ -225,14 +225,14 @@ jobs: fi - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- - name: Restore Wintun Cache if: matrix.goos == 'windows' - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-wintun- diff --git a/.github/workflows/scheduled-assets-update.yml b/.github/workflows/scheduled-assets-update.yml index 3e20fc433..cb92ceadc 100644 --- a/.github/workflows/scheduled-assets-update.yml +++ b/.github/workflows/scheduled-assets-update.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- @@ -59,7 +59,7 @@ jobs: done - name: Save Geodat Cache - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 if: ${{ steps.update.outputs.unhit }} with: path: resources @@ -73,7 +73,7 @@ jobs: ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51 steps: - name: Restore Wintun Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-wintun- @@ -129,7 +129,7 @@ jobs: fi - name: Save Wintun Cache - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 if: ${{ steps.update.outputs.unhit }} with: path: resources diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62460115e..d7b875d91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- @@ -90,7 +90,7 @@ jobs: go-version-file: go.mod check-latest: true - name: Restore Geodat Cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: path: resources key: xray-geodat- From 7e7e820763dbe257ae6f48929e6476505fcfe5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 24 Jun 2026 18:49:05 +0800 Subject: [PATCH 64/78] Geodata: Apply uTLS Chrome fingerprint when downloading (#6371) Closes https://github.com/XTLS/Xray-core/issues/6369 --- app/geodata/download.go | 128 ++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 43 deletions(-) diff --git a/app/geodata/download.go b/app/geodata/download.go index cc1498a0f..e24e3e4fd 100644 --- a/app/geodata/download.go +++ b/app/geodata/download.go @@ -2,6 +2,7 @@ package geodata import ( "context" + "crypto/tls" go_errors "errors" "io" "net/http" @@ -9,6 +10,7 @@ import ( "path/filepath" "time" + utls "github.com/refraction-networking/utls" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/platform/filesystem" @@ -16,6 +18,7 @@ import ( "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/transport/internet/tagged" + "golang.org/x/net/http2" ) const idleTimeout = 30 * time.Second @@ -26,8 +29,9 @@ type stage struct { } type downloader struct { - ctx context.Context - client *http.Client + ctx context.Context + httpClient *http.Client + httpsClient *http.Client } type idleConn struct { @@ -53,52 +57,84 @@ func (c *idleConn) Write(b []byte) (int, error) { func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader { return &downloader{ - ctx: ctx, - client: newClient(ctx, dispatcher, outbound), + ctx: ctx, + httpClient: newClient(ctx, dispatcher, outbound, false), + httpsClient: newClient(ctx, dispatcher, outbound, true), } } -func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client { - return &http.Client{ - Transport: &http.Transport{ - Proxy: nil, - DisableKeepAlives: true, - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - var conn net.Conn - err := task.Run(ctx, func() error { - if tagged.Dialer == nil { - return errors.New("tagged dialer is not initialized") - } - dest, err := net.ParseDestination(network + ":" + address) - if err != nil { - return errors.New("cannot understand address").Base(err) - } - c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound) - if err != nil { - return errors.New("cannot dial remote address ", dest).Base(err) - } - conn = c - return nil - }) - if err != nil { - return nil, errors.New("cannot finish connection").Base(err) - } - return &idleConn{ - Conn: conn, - }, nil - }, - TLSHandshakeTimeout: idleTimeout, - ResponseHeaderTimeout: idleTimeout, - }, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - if req.URL.Scheme != "https" { - return errors.New("redirected to non-https URL: ", req.URL.String()) +func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client { + dial := func(ctx context.Context, network, address string) (net.Conn, error) { + var conn net.Conn + err := task.Run(ctx, func() error { + if tagged.Dialer == nil { + return errors.New("tagged dialer is not initialized") } - if len(via) >= 10 { - return errors.New("stopped after 10 redirects") + dest, err := net.ParseDestination(network + ":" + address) + if err != nil { + return errors.New("cannot understand address").Base(err) } + c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound) + if err != nil { + return errors.New("cannot dial remote address ", dest).Base(err) + } + conn = c return nil - }, + }) + if err != nil { + return nil, errors.New("cannot finish connection").Base(err) + } + return &idleConn{ + Conn: conn, + }, nil + } + if isHTTPS { + return &http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) { + conn, err := dial(ctx, network, address) + if err != nil { + return nil, err + } + host, _, _ := net.SplitHostPort(address) + tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto) + handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout) + defer cancel() + if err := tlsConn.HandshakeContext(handshakeCtx); err != nil { + conn.Close() + return nil, err + } + return tlsConn, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if req.URL.Scheme != "https" { + return errors.New("redirected to non-https URL: ", req.URL.String()) + } + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + return nil + }, + } + } else { + return &http.Client{ + Transport: &http.Transport{ + Proxy: nil, + DisableKeepAlives: true, + DialContext: dial, + ResponseHeaderTimeout: idleTimeout, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if req.URL.Scheme != "https" { + return errors.New("redirected to non-https URL: ", req.URL.String()) + } + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + return nil + }, + } } } @@ -160,7 +196,13 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error { } utils.TryDefaultHeadersWith(req.Header, "nav") - resp, err := d.client.Do(req) + var client *http.Client + if req.URL.Scheme == "https" { + client = d.httpsClient + } else { + client = d.httpClient + } + resp, err := client.Do(req) if err != nil { return err } From 241aa38ac00c3c396efb7c70eee4eb3a80efb19c Mon Sep 17 00:00:00 2001 From: Jasper344612 <282825138+Jasper344612@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:06:00 +0800 Subject: [PATCH 65/78] TUN inbound: Support `autoSystemRoutingTable` and `autoOutboundsInterface` on macOS and Linux as well (#6366) https://github.com/XTLS/Xray-core/pull/6366#issuecomment-4788510365 --- proxy/tun/config.go | 88 +------------ proxy/tun/tun_android.go | 14 ++ proxy/tun/tun_darwin.go | 274 ++++++++++++++++++++++++++++++++++++++- proxy/tun/tun_default.go | 14 ++ proxy/tun/tun_freebsd.go | 14 ++ proxy/tun/tun_linux.go | 155 +++++++++++++++++++++- proxy/tun/tun_windows.go | 76 +++++++++++ 7 files changed, 550 insertions(+), 85 deletions(-) diff --git a/proxy/tun/config.go b/proxy/tun/config.go index cdeeeffae..6a9dfff96 100644 --- a/proxy/tun/config.go +++ b/proxy/tun/config.go @@ -3,8 +3,6 @@ package tun import ( "context" "net" - "sort" - "strings" "sync" "github.com/xtls/xray-core/common/errors" @@ -31,95 +29,23 @@ func (updater *InterfaceUpdater) Update() { updater.Lock() defer updater.Unlock() - if updater.iface != nil { - iface, err := net.InterfaceByIndex(updater.iface.Index) - if err == nil && iface.Name == updater.iface.Name { - return - } - } - - updater.iface = nil - - interfaces, err := net.Interfaces() + got, err := findOutboundInterface(updater.tunIndex, updater.fixedName) if err != nil { errors.LogInfoInner(context.Background(), err, "[tun] failed to update interface") + updater.iface = nil return } - var got *net.Interface - if updater.fixedName != "" { - for _, iface := range interfaces { - if iface.Index == updater.tunIndex { - continue - } - if iface.Name == updater.fixedName { - got = &iface - break - } - } - } else { - var ifs []struct { - index int - score int - } - for i, iface := range interfaces { - if iface.Index == updater.tunIndex { - continue - } - if strings.Contains(iface.Name, "vEthernet") { - continue - } - if iface.Flags&net.FlagUp == 0 { - continue - } - if iface.Flags&net.FlagLoopback != 0 { - continue - } - addrs, err := iface.Addrs() - if err != nil || len(addrs) == 0 { - continue - } - ifs = append(ifs, struct { - index int - score int - }{i, score(&iface, addrs)}) - } - sort.Slice(ifs, func(i, j int) bool { - if ifs[i].score != ifs[j].score { - return ifs[i].score > ifs[j].score - } - - return interfaces[ifs[i].index].Name < interfaces[ifs[j].index].Name - }) - if len(ifs) > 0 { - iface := interfaces[ifs[0].index] - got = &iface - } - } - if got == nil { errors.LogInfo(context.Background(), "[tun] failed to update interface > got == nil") + updater.iface = nil + return + } + + if updater.iface != nil && updater.iface.Index == got.Index && updater.iface.Name == got.Name { return } updater.iface = got errors.LogInfo(context.Background(), "[tun] update interface ", got.Name, " ", got.Index) } - -func score(iface *net.Interface, addrs []net.Addr) int { - score := 0 - - name := strings.ToLower(iface.Name) - if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") { - score += 2 - } - - for _, addr := range addrs { - if strings.HasPrefix(addr.String(), "192.168.") { - score += 1 - break - } - } - - return score -} diff --git a/proxy/tun/tun_android.go b/proxy/tun/tun_android.go index 16dd51612..f287bb1ea 100644 --- a/proxy/tun/tun_android.go +++ b/proxy/tun/tun_android.go @@ -81,3 +81,17 @@ func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) { func setinterface(network, address string, fd uintptr, iface *net.Interface) error { return unix.BindToDevice(int(fd), iface.Name) } + +func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { + if fixedName == "" { + return nil, errors.New("automatic outbound interface selection is not supported on this platform") + } + iface, err := net.InterfaceByName(fixedName) + if err != nil { + return nil, err + } + if iface.Index == tunIndex { + return nil, errors.New("outbound interface cannot be the TUN interface") + } + return iface, nil +} diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index 81eaece0e..fdaafd3e1 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -3,16 +3,20 @@ package tun import ( + "context" "errors" "fmt" "net" "net/netip" "os" "strconv" + "sync" "unsafe" "github.com/xtls/xray-core/common/buf" + xerrors "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/platform" + "golang.org/x/net/route" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" @@ -42,6 +46,10 @@ type DarwinTun struct { options *Config tunFd int ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) + + routeMonitor *os.File + routeMonitorOnce sync.Once + systemRoutes []netip.Prefix } var ( @@ -92,15 +100,53 @@ func NewTun(options *Config) (Tun, error) { } func (t *DarwinTun) Start() error { + if !t.ownsFd { + return nil + } + + if err := t.setSystemRoutes(); err != nil { + return err + } + + if updater != nil { + fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) + if err != nil { + _ = t.unsetSystemRoutes() + return err + } + t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor") + go t.monitorRouteChanges() + } return nil } func (t *DarwinTun) Close() error { + t.routeMonitorOnce.Do(func() { + if t.routeMonitor != nil { + _ = t.routeMonitor.Close() + } + }) + routeErr := t.unsetSystemRoutes() if t.ownsFd { - return t.tunFile.Close() + return xerrors.Combine(routeErr, t.tunFile.Close()) } // iOS: don't close the fd, it's owned by NetworkExtension - return nil + return routeErr +} + +func (t *DarwinTun) monitorRouteChanges() { + buffer := make([]byte, 64*1024) + for { + if _, err := t.routeMonitor.Read(buffer); err != nil { + if !errors.Is(err, os.ErrClosed) { + xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes") + } + return + } + if updater != nil { + updater.Update() + } + } } func (t *DarwinTun) Name() (string, error) { @@ -388,3 +434,227 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err return errors.Join(err1, err2) } + +func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { + if fixedName != "" { + iface, err := net.InterfaceByName(fixedName) + if err != nil { + return nil, err + } + if iface.Index == tunIndex { + return nil, errors.New("outbound interface cannot be the TUN interface") + } + return iface, nil + } + + rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0) + if err != nil { + return nil, err + } + messages, err := route.ParseRIB(route.RIBTypeRoute, rib) + if err != nil { + return nil, err + } + + var ipv6Index int + for _, message := range messages { + routeMessage, ok := message.(*route.RouteMessage) + if !ok || routeMessage.Index == tunIndex { + continue + } + if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 { + continue + } + + family, ok := defaultRouteFamily(routeMessage) + if !ok { + continue + } + if family == unix.AF_INET { + return usableDarwinInterface(routeMessage.Index) + } + if family == unix.AF_INET6 && ipv6Index == 0 { + ipv6Index = routeMessage.Index + } + } + + if ipv6Index != 0 { + return usableDarwinInterface(ipv6Index) + } + return nil, errors.New("default route not found") +} + +func defaultRouteFamily(message *route.RouteMessage) (int, bool) { + if len(message.Addrs) <= unix.RTAX_NETMASK { + return 0, false + } + + switch destination := message.Addrs[unix.RTAX_DST].(type) { + case *route.Inet4Addr: + mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr) + if !ok || destination.IP != netip.IPv4Unspecified().As4() { + return 0, false + } + ones, bits := net.IPMask(mask.IP[:]).Size() + return unix.AF_INET, ones == 0 && bits == 32 + case *route.Inet6Addr: + mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr) + if !ok || destination.IP != netip.IPv6Unspecified().As16() { + return 0, false + } + ones, bits := net.IPMask(mask.IP[:]).Size() + return unix.AF_INET6, ones == 0 && bits == 128 + default: + return 0, false + } +} + +func usableDarwinInterface(index int) (*net.Interface, error) { + iface, err := net.InterfaceByIndex(index) + if err != nil { + return nil, err + } + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + return nil, errors.New("default route interface is not usable") + } + return iface, nil +} + +func (t *DarwinTun) setSystemRoutes() error { + routes, err := buildDarwinSystemRoutes(t.options.AutoSystemRoutingTable) + if err != nil { + return err + } + if len(routes) == 0 { + return nil + } + + tunIndex, err := t.Index() + if err != nil { + return err + } + for _, destination := range routes { + if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination); err != nil { + _ = t.unsetSystemRoutes() + return xerrors.New("failed to add system route ", destination).Base(err) + } + t.systemRoutes = append(t.systemRoutes, destination) + } + return nil +} + +func (t *DarwinTun) unsetSystemRoutes() error { + var errs []error + tunIndex, indexErr := t.Index() + if indexErr != nil && len(t.systemRoutes) > 0 { + errs = append(errs, indexErr) + } + for i := len(t.systemRoutes) - 1; i >= 0; i-- { + destination := t.systemRoutes[i] + if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination); err != nil && !errors.Is(err, unix.ESRCH) { + errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err)) + } + } + t.systemRoutes = nil + return xerrors.Combine(errs...) +} + +func buildDarwinSystemRoutes(configured []string) ([]netip.Prefix, error) { + routes := make([]netip.Prefix, 0, len(configured)) + seen := make(map[netip.Prefix]struct{}) + + appendRoute := func(prefix netip.Prefix) { + prefix = prefix.Masked() + if _, found := seen[prefix]; found { + return + } + seen[prefix] = struct{}{} + routes = append(routes, prefix) + } + + for _, value := range configured { + prefix, err := netip.ParsePrefix(value) + if err != nil { + return nil, xerrors.New("invalid system route ", value).Base(err) + } + prefix = prefix.Masked() + if prefix.Bits() == 0 { + for _, protected := range darwinProtectedDefaultRoutes(prefix.Addr().Is4()) { + appendRoute(protected) + } + continue + } + appendRoute(prefix) + } + + return routes, nil +} + +func darwinProtectedDefaultRoutes(ipv4 bool) []netip.Prefix { + routes := make([]netip.Prefix, 0, 8) + for i := 0; i < 8; i++ { + if ipv4 { + var address [4]byte + address[0] = 1 << i + routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i)) + } else { + var address [16]byte + address[0] = 1 << i + routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i)) + } + } + return routes +} + +func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix) error { + message := route.RouteMessage{ + Type: messageType, + Version: unix.RTM_VERSION, + Flags: unix.RTF_STATIC | unix.RTF_GATEWAY, + Seq: 1, + } + if messageType == unix.RTM_ADD { + message.Flags |= unix.RTF_UP + } + + if destination.Addr().Is4() { + gatewayPrefix := netip.MustParsePrefix(gateway) + message.Addrs = []route.Addr{ + unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()}, + unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())}, + unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gatewayPrefix.Addr().As4()}, + } + } else { + message.Flags &^= unix.RTF_GATEWAY + message.Index = interfaceIndex + message.Addrs = []route.Addr{ + unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()}, + unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())}, + unix.RTAX_GATEWAY: &route.LinkAddr{Index: interfaceIndex}, + } + } + + request, err := message.Marshal() + if err != nil { + return err + } + fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) + if err != nil { + return err + } + defer unix.Close(fd) + _, err = unix.Write(fd, request) + return err +} + +func prefixMask4(bits int) [4]byte { + var mask [4]byte + copy(mask[:], net.CIDRMask(bits, 32)) + return mask +} + +func prefixMask6(bits int) [16]byte { + var mask [16]byte + copy(mask[:], net.CIDRMask(bits, 128)) + return mask +} diff --git a/proxy/tun/tun_default.go b/proxy/tun/tun_default.go index 3ab6c9610..e26b7cf92 100644 --- a/proxy/tun/tun_default.go +++ b/proxy/tun/tun_default.go @@ -42,3 +42,17 @@ func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) { func setinterface(string, string, uintptr, *net.Interface) error { return errors.New("Tun is not supported on your platform") } + +func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { + if fixedName == "" { + return nil, errors.New("automatic outbound interface selection is not supported on this platform") + } + iface, err := net.InterfaceByName(fixedName) + if err != nil { + return nil, err + } + if iface.Index == tunIndex { + return nil, errors.New("outbound interface cannot be the TUN interface") + } + return iface, nil +} diff --git a/proxy/tun/tun_freebsd.go b/proxy/tun/tun_freebsd.go index 07469ca2b..655f5ed2e 100644 --- a/proxy/tun/tun_freebsd.go +++ b/proxy/tun/tun_freebsd.go @@ -147,3 +147,17 @@ func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) { func setinterface(network, address string, fd uintptr, iface *net.Interface) error { return nil } + +func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { + if fixedName == "" { + return nil, errors.New("automatic outbound interface selection is not supported on this platform") + } + iface, err := net.InterfaceByName(fixedName) + if err != nil { + return nil, err + } + if iface.Index == tunIndex { + return nil, errors.New("outbound interface cannot be the TUN interface") + } + return iface, nil +} diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go index 3e5a30bf3..b8eff0174 100644 --- a/proxy/tun/tun_linux.go +++ b/proxy/tun/tun_linux.go @@ -3,8 +3,11 @@ package tun import ( + "context" "net" + "net/netip" "strconv" + "sync" "github.com/vishvananda/netlink" "github.com/xtls/xray-core/common/errors" @@ -22,6 +25,10 @@ type LinuxTun struct { tunLink netlink.Link options *Config ownsTun bool + + systemRoutes []netlink.Route + routeMonitorStop chan struct{} + routeMonitorOnce sync.Once } // LinuxTun implements Tun @@ -161,16 +168,32 @@ func (t *LinuxTun) Start() error { return nil } - err := netlink.LinkSetUp(t.tunLink) - if err != nil { + if err := netlink.LinkSetUp(t.tunLink); err != nil { return err } + if err := t.setSystemRoutes(); err != nil { + return err + } + + if updater != nil { + t.routeMonitorStop = make(chan struct{}) + go t.monitorRouteChanges() + } + return nil } // Close is called to shut down the tun interface func (t *LinuxTun) Close() error { + t.routeMonitorOnce.Do(func() { + if t.routeMonitorStop != nil { + close(t.routeMonitorStop) + } + }) + + _ = t.unsetSystemRoutes() + if t.ownsTun { _ = netlink.LinkSetDown(t.tunLink) } @@ -199,3 +222,131 @@ func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) { func setinterface(network, address string, fd uintptr, iface *net.Interface) error { return unix.BindToDevice(int(fd), iface.Name) } + +func (t *LinuxTun) setSystemRoutes() error { + if len(t.options.AutoSystemRoutingTable) == 0 { + return nil + } + tunIndex := t.tunLink.Attrs().Index + for _, cidr := range t.options.AutoSystemRoutingTable { + prefix, err := netip.ParsePrefix(cidr) + if err != nil { + return errors.New("invalid system route ", cidr).Base(err) + } + prefix = prefix.Masked() + _, ipNet, _ := net.ParseCIDR(prefix.String()) + route := netlink.Route{ + LinkIndex: tunIndex, + Dst: ipNet, + Priority: 1, + } + if err := netlink.RouteAdd(&route); err != nil { + _ = t.unsetSystemRoutes() + return errors.New("failed to add system route ", cidr).Base(err) + } + t.systemRoutes = append(t.systemRoutes, route) + } + return nil +} + +func (t *LinuxTun) unsetSystemRoutes() error { + var errs []error + for i := len(t.systemRoutes) - 1; i >= 0; i-- { + route := t.systemRoutes[i] + if err := netlink.RouteDel(&route); err != nil { + errs = append(errs, errors.New("failed to delete system route").Base(err)) + } + } + t.systemRoutes = nil + return errors.Combine(errs...) +} + +func (t *LinuxTun) monitorRouteChanges() { + routeCh := make(chan netlink.RouteUpdate) + if err := netlink.RouteSubscribe(routeCh, t.routeMonitorStop); err != nil { + errors.LogInfoInner(context.Background(), err, "[tun] failed to subscribe route changes") + return + } + + linkCh := make(chan netlink.LinkUpdate) + if err := netlink.LinkSubscribe(linkCh, t.routeMonitorStop); err != nil { + errors.LogInfoInner(context.Background(), err, "[tun] failed to subscribe link changes") + return + } + + for { + select { + case _, ok := <-routeCh: + if !ok { + return + } + if updater != nil { + updater.Update() + } + case _, ok := <-linkCh: + if !ok { + return + } + if updater != nil { + updater.Update() + } + case <-t.routeMonitorStop: + return + } + } +} + +func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { + if fixedName != "" { + iface, err := net.InterfaceByName(fixedName) + if err != nil { + return nil, err + } + if iface.Index == tunIndex { + return nil, errors.New("outbound interface cannot be the TUN interface") + } + return iface, nil + } + + probeIPs := []net.IP{ + net.ParseIP("8.8.8.8"), + net.ParseIP("2001:4860:4860::8888"), + } + + for _, ip := range probeIPs { + routes, err := netlink.RouteGet(ip) + if err != nil || len(routes) == 0 { + continue + } + route := routes[0] + if route.LinkIndex == tunIndex { + continue + } + + link, err := netlink.LinkByIndex(route.LinkIndex) + if err != nil { + continue + } + attrs := link.Attrs() + + if attrs.Flags&net.FlagUp == 0 { + continue + } + operState := attrs.OperState + if operState != netlink.OperUp && operState != netlink.OperUnknown { + continue + } + + if route.Src == nil || route.Src.IsLoopback() || route.Src.IsLinkLocalUnicast() { + continue + } + + iface, err := net.InterfaceByIndex(route.LinkIndex) + if err != nil { + continue + } + return iface, nil + } + + return nil, errors.New("no usable outbound interface found") +} diff --git a/proxy/tun/tun_windows.go b/proxy/tun/tun_windows.go index 3072b6124..daa44c8ae 100644 --- a/proxy/tun/tun_windows.go +++ b/proxy/tun/tun_windows.go @@ -8,6 +8,8 @@ import ( go_errors "errors" "net" "net/netip" + "sort" + "strings" "sync" "unsafe" @@ -307,3 +309,77 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err return errors.Combine(err1, err2, err3, err4) } + +func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + if fixedName != "" { + for _, iface := range interfaces { + if iface.Index != tunIndex && iface.Name == fixedName { + return &iface, nil + } + } + return nil, nil + } + + var candidates []struct { + index int + score int + } + for i, iface := range interfaces { + if iface.Index == tunIndex { + continue + } + if strings.Contains(iface.Name, "vEthernet") { + continue + } + if iface.Flags&net.FlagUp == 0 { + continue + } + if iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil || len(addrs) == 0 { + continue + } + candidates = append(candidates, struct { + index int + score int + }{i, scoreWindowsInterface(&iface, addrs)}) + } + + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].score != candidates[j].score { + return candidates[i].score > candidates[j].score + } + return interfaces[candidates[i].index].Name < interfaces[candidates[j].index].Name + }) + if len(candidates) == 0 { + return nil, nil + } + + iface := interfaces[candidates[0].index] + return &iface, nil +} + +func scoreWindowsInterface(iface *net.Interface, addrs []net.Addr) int { + score := 0 + + name := strings.ToLower(iface.Name) + if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") { + score += 2 + } + + for _, addr := range addrs { + if strings.HasPrefix(addr.String(), "192.168.") { + score++ + break + } + } + + return score +} From dda2b10c9d18f2329d65f4a2effc09fed6ac9cd1 Mon Sep 17 00:00:00 2001 From: yiguodev <147401898+yiguodev@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:00:22 +0800 Subject: [PATCH 66/78] TUN inbound: Add traffic counters; Metrics: Rely on instance (#6349) https://github.com/XTLS/Xray-core/pull/6349#issuecomment-4775121300 --- app/metrics/metrics.go | 210 ++++++++++++++++++++++++++---------- app/metrics/metrics_test.go | 161 +++++++++++++++++++++++++++ proxy/tun/handler.go | 28 +++++ proxy/tun/handler_test.go | 133 +++++++++++++++++++++++ 4 files changed, 473 insertions(+), 59 deletions(-) create mode 100644 app/metrics/metrics_test.go create mode 100644 proxy/tun/handler_test.go diff --git a/app/metrics/metrics.go b/app/metrics/metrics.go index a2ae2f30c..6d1cff721 100644 --- a/app/metrics/metrics.go +++ b/app/metrics/metrics.go @@ -2,15 +2,18 @@ package metrics import ( "context" + "encoding/json" + stderrors "errors" "expvar" + stdnet "net" "net/http" - _ "net/http/pprof" + "net/http/pprof" "strings" "github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/net" + xnet "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/signal/done" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/extension" @@ -21,15 +24,17 @@ import ( type MetricsHandler struct { ohm outbound.Manager statsManager feature_stats.Manager - observatory extension.Observatory + ctx context.Context tag string listen string - tcpListener net.Listener + tcpListener xnet.Listener + listener *OutboundListener } // NewMetricsHandler creates a new MetricsHandler based on the given config. func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) { c := &MetricsHandler{ + ctx: ctx, tag: config.Tag, listen: config.Listen, } @@ -37,46 +42,6 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er c.statsManager = sm c.ohm = om })) - expvar.Publish("stats", expvar.Func(func() interface{} { - resp := map[string]map[string]map[string]int64{ - "inbound": {}, - "outbound": {}, - "user": {}, - } - c.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool { - nameSplit := strings.Split(name, ">>>") - typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3] - if item, found := resp[typeName][tagOrUser]; found { - item[direction] = counter.Value() - } else { - resp[typeName][tagOrUser] = map[string]int64{ - direction: counter.Value(), - } - } - return true - }) - return resp - })) - expvar.Publish("observatory", expvar.Func(func() interface{} { - if c.observatory == nil { - common.Must(core.RequireFeatures(ctx, func(observatory extension.Observatory) error { - c.observatory = observatory - return nil - })) - if c.observatory == nil { - return nil - } - } - resp := map[string]*observatory.OutboundStatus{} - if o, err := c.observatory.GetObservation(context.Background()); err != nil { - return err - } else { - for _, x := range o.(*observatory.ObservationResult).GetStatus() { - resp[x.OutboundTag] = x - } - } - return resp - })) return c, nil } @@ -85,45 +50,172 @@ func (p *MetricsHandler) Type() interface{} { } func (p *MetricsHandler) Start() error { + handler := p.httpHandler() + // direct listen a port if listen is set if p.listen != "" { - TCPlistener, err := net.Listen("tcp", p.listen) + TCPlistener, err := xnet.Listen("tcp", p.listen) if err != nil { return err } p.tcpListener = TCPlistener errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen) - go func() { - if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil { - errors.LogErrorInner(context.Background(), err, "failed to start metrics server") - } - }() + go p.serve(TCPlistener, handler) + } + + if p.tag == "" { + if p.tcpListener == nil { + return errors.New("metrics must have a tag or listen address") + } + return nil } listener := &OutboundListener{ - buffer: make(chan net.Conn, 4), + buffer: make(chan xnet.Conn, 4), done: done.New(), } + p.listener = listener - go func() { - if err := http.Serve(listener, http.DefaultServeMux); err != nil { - errors.LogErrorInner(context.Background(), err, "failed to start metrics server") - } - }() + go p.serve(listener, handler) if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil { errors.LogInfo(context.Background(), "failed to remove existing handler") } - return p.ohm.AddHandler(context.Background(), &Outbound{ + if err := p.ohm.AddHandler(context.Background(), &Outbound{ tag: p.tag, listener: listener, - }) + }); err != nil { + if closeErr := p.Close(); closeErr != nil { + errors.LogErrorInner(context.Background(), closeErr, "failed to close metrics server after start failure") + } + return err + } + + return nil } func (p *MetricsHandler) Close() error { - return nil + var errs []error + if p.tcpListener != nil { + errs = append(errs, p.tcpListener.Close()) + p.tcpListener = nil + } + if p.listener != nil { + errs = append(errs, p.listener.Close()) + p.listener = nil + } + if p.ohm != nil && p.tag != "" { + if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil { + errors.LogInfo(context.Background(), "failed to remove metrics handler") + } + } + return errors.Combine(errs...) +} + +func (p *MetricsHandler) serve(listener xnet.Listener, handler http.Handler) { + if err := http.Serve(listener, handler); err != nil && !isClosedListenerError(err) { + errors.LogErrorInner(context.Background(), err, "failed to start metrics server") + } +} + +func isClosedListenerError(err error) bool { + if err == nil { + return true + } + if stderrors.Is(err, stdnet.ErrClosed) || stderrors.Is(err, http.ErrServerClosed) { + return true + } + errText := err.Error() + return strings.Contains(errText, "listen closed") || + strings.Contains(errText, "use of closed network connection") +} + +func (p *MetricsHandler) httpHandler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/debug/vars", p.handleDebugVars) + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + return mux +} + +func (p *MetricsHandler) handleDebugVars(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + vars := map[string]json.RawMessage{} + expvar.Do(func(kv expvar.KeyValue) { + value := json.RawMessage(kv.Value.String()) + if !json.Valid(value) { + value = json.RawMessage("null") + } + vars[kv.Key] = value + }) + vars["stats"] = marshalJSON(p.stats()) + vars["observatory"] = marshalJSON(p.observatoryStatus()) + + payload, err := json.Marshal(vars) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(payload) +} + +func marshalJSON(value interface{}) json.RawMessage { + data, err := json.Marshal(value) + if err != nil { + return json.RawMessage("null") + } + return data +} + +func (p *MetricsHandler) stats() map[string]map[string]map[string]int64 { + resp := map[string]map[string]map[string]int64{ + "inbound": {}, + "outbound": {}, + "user": {}, + } + p.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool { + nameSplit := strings.Split(name, ">>>") + if len(nameSplit) < 4 { + return true + } + typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3] + items, found := resp[typeName] + if !found { + items = map[string]map[string]int64{} + resp[typeName] = items + } + if item, found := items[tagOrUser]; found { + item[direction] = counter.Value() + } else { + items[tagOrUser] = map[string]int64{ + direction: counter.Value(), + } + } + return true + }) + return resp +} + +func (p *MetricsHandler) observatoryStatus() interface{} { + feature := core.MustFromContext(p.ctx).GetFeature(extension.ObservatoryType()) + if feature == nil { + return nil + } + observatoryFeature := feature.(extension.Observatory) + resp := map[string]*observatory.OutboundStatus{} + if o, err := observatoryFeature.GetObservation(context.Background()); err != nil { + return err + } else { + for _, x := range o.(*observatory.ObservationResult).GetStatus() { + resp[x.OutboundTag] = x + } + } + return resp } func init() { diff --git a/app/metrics/metrics_test.go b/app/metrics/metrics_test.go new file mode 100644 index 000000000..a493887f8 --- /dev/null +++ b/app/metrics/metrics_test.go @@ -0,0 +1,161 @@ +package metrics + +import ( + "context" + "encoding/json" + stdnet "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/proxyman" + _ "github.com/xtls/xray-core/app/proxyman/inbound" + _ "github.com/xtls/xray-core/app/proxyman/outbound" + appstats "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + feature_outbound "github.com/xtls/xray-core/features/outbound" +) + +func TestMetricsCanRestartInSameProcess(t *testing.T) { + for i := 0; i < 2; i++ { + server := startMetricsTestServer(t) + readMetricsVars(t, server) + readMetricsPprof(t, server) + if err := server.Close(); err != nil { + t.Fatalf("failed to close metrics server: %v", err) + } + } +} + +func TestMetricsCanRunMultipleInstancesInSameProcess(t *testing.T) { + server1 := startMetricsTestServer(t) + t.Cleanup(func() { + _ = server1.Close() + }) + server2 := startMetricsTestServer(t) + t.Cleanup(func() { + _ = server2.Close() + }) + + readMetricsVars(t, server1) + readMetricsVars(t, server2) +} + +func TestMetricsListenOnlyWithoutTagDoesNotRegisterOutbound(t *testing.T) { + listen := pickMetricsListenAddress(t) + server := startMetricsTestServerWithMetricsConfig(t, &Config{ + Listen: listen, + }) + t.Cleanup(func() { + _ = server.Close() + }) + + response, err := http.Get("http://" + listen + "/debug/vars") + if err != nil { + t.Fatalf("failed to read listen-only metrics: %v", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("unexpected listen-only metrics status: %d", response.StatusCode) + } + + outboundManager := server.GetFeature(feature_outbound.ManagerType()).(feature_outbound.Manager) + if handlers := outboundManager.ListHandlers(context.Background()); len(handlers) != 0 { + t.Fatalf("listen-only metrics registered outbound handlers: got %d, want 0", len(handlers)) + } +} + +func startMetricsTestServer(t *testing.T) *core.Instance { + return startMetricsTestServerWithMetricsConfig(t, &Config{ + Tag: "metrics_out", + }) +} + +func startMetricsTestServerWithMetricsConfig(t *testing.T, metricsConfig *Config) *core.Instance { + t.Helper() + + server, err := core.New(metricsTestConfig(metricsConfig)) + if err != nil { + t.Fatalf("failed to create metrics server: %v", err) + } + if err := server.Start(); err != nil { + _ = server.Close() + t.Fatalf("failed to start metrics server: %v", err) + } + return server +} + +func metricsTestConfig(metricsConfig *Config) *core.Config { + return &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&appstats.Config{}), + serial.ToTypedMessage(metricsConfig), + }, + } +} + +func pickMetricsListenAddress(t *testing.T) string { + t.Helper() + + listener, err := stdnet.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to pick metrics listen address: %v", err) + } + defer listener.Close() + return listener.Addr().String() +} + +func readMetricsVars(t *testing.T, server *core.Instance) { + t.Helper() + + recorder := httptest.NewRecorder() + metricsHandler(t, server).httpHandler().ServeHTTP( + recorder, + httptest.NewRequest(http.MethodGet, "/debug/vars", nil), + ) + + if recorder.Code != http.StatusOK { + t.Fatalf("unexpected metrics vars status: %d", recorder.Code) + } + + var payload map[string]interface{} + if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { + t.Fatalf("failed to decode metrics vars: %v", err) + } + if _, found := payload["stats"]; !found { + t.Fatal("metrics vars missing stats") + } + if _, found := payload["observatory"]; !found { + t.Fatal("metrics vars missing observatory") + } +} + +func readMetricsPprof(t *testing.T, server *core.Instance) { + t.Helper() + + recorder := httptest.NewRecorder() + metricsHandler(t, server).httpHandler().ServeHTTP( + recorder, + httptest.NewRequest(http.MethodGet, "/debug/pprof/goroutine?debug=1", nil), + ) + + if recorder.Code != http.StatusOK { + t.Fatalf("unexpected metrics pprof status: %d", recorder.Code) + } +} + +func metricsHandler(t *testing.T, server *core.Instance) *MetricsHandler { + t.Helper() + + feature := server.GetFeature((*MetricsHandler)(nil)) + handler, ok := feature.(*MetricsHandler) + if !ok || handler == nil { + t.Fatal("metrics handler not registered") + } + return handler +} diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 732468f76..5bf8f231f 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -17,6 +17,7 @@ import ( "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/stat" @@ -32,6 +33,8 @@ type Handler struct { dispatcher routing.Dispatcher tag string sniffingRequest session.SniffingRequest + uplinkCounter stats.Counter + downlinkCounter stats.Counter } // ConnectionHandler interface with the only method that stack is going to push new connections to @@ -59,6 +62,23 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin t.policyManager = pm t.dispatcher = dispatcher + if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink { + statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager) + name := "inbound>>>" + t.tag + ">>>traffic>>>uplink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + t.uplinkCounter = c + } + } + if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink { + statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager) + name := "inbound>>>" + t.tag + ">>>traffic>>>downlink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + t.downlinkCounter = c + } + } + return nil } @@ -151,6 +171,14 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { return } source := net.DestinationFromAddr(remote) + if t.uplinkCounter != nil || t.downlinkCounter != nil { + conn = &stat.CounterConnection{ + Connection: conn, + ReadCounter: t.uplinkCounter, + WriteCounter: t.downlinkCounter, + } + } + inbound := session.Inbound{ Name: "tun", Tag: t.tag, diff --git a/proxy/tun/handler_test.go b/proxy/tun/handler_test.go new file mode 100644 index 000000000..25e9117ab --- /dev/null +++ b/proxy/tun/handler_test.go @@ -0,0 +1,133 @@ +package tun + +import ( + "bytes" + "context" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/xtls/xray-core/common/buf" + xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" +) + +type testCounter struct { + value int64 +} + +func (c *testCounter) Value() int64 { + return atomic.LoadInt64(&c.value) +} + +func (c *testCounter) Set(value int64) int64 { + return atomic.SwapInt64(&c.value, value) +} + +func (c *testCounter) Add(value int64) int64 { + return atomic.AddInt64(&c.value, value) - value +} + +type testConn struct { + reader *bytes.Reader + writer bytes.Buffer +} + +func newTestConn(input []byte) *testConn { + return &testConn{reader: bytes.NewReader(input)} +} + +func (c *testConn) Read(payload []byte) (int, error) { + return c.reader.Read(payload) +} + +func (c *testConn) Write(payload []byte) (int, error) { + return c.writer.Write(payload) +} + +func (c *testConn) Close() error { + return nil +} + +func (c *testConn) LocalAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 1080} +} + +func (c *testConn) RemoteAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv4(10, 0, 0, 2), Port: 12345} +} + +func (c *testConn) SetDeadline(time.Time) error { + return nil +} + +func (c *testConn) SetReadDeadline(time.Time) error { + return nil +} + +func (c *testConn) SetWriteDeadline(time.Time) error { + return nil +} + +type testDispatcher struct { + writePayload []byte + readBytes int32 +} + +func (d *testDispatcher) Type() interface{} { + return routing.DispatcherType() +} + +func (d *testDispatcher) Start() error { + return nil +} + +func (d *testDispatcher) Close() error { + return nil +} + +func (d *testDispatcher) Dispatch(context.Context, xnet.Destination) (*transport.Link, error) { + return nil, nil +} + +func (d *testDispatcher) DispatchLink(ctx context.Context, dest xnet.Destination, link *transport.Link) error { + mb, err := link.Reader.ReadMultiBuffer() + if err != nil { + return err + } + atomic.StoreInt32(&d.readBytes, mb.Len()) + buf.ReleaseMulti(mb) + + return link.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(d.writePayload)}) +} + +func TestHandlerCountsTunConnectionTraffic(t *testing.T) { + uplinkCounter := new(testCounter) + downlinkCounter := new(testCounter) + dispatcher := &testDispatcher{writePayload: []byte("downlink")} + conn := newTestConn([]byte("uplink")) + + handler := &Handler{ + ctx: context.Background(), + config: &Config{}, + dispatcher: dispatcher, + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, + } + handler.HandleConnection(conn, xnet.TCPDestination(xnet.LocalHostIP, 443)) + + if got := uplinkCounter.Value(); got != int64(len("uplink")) { + t.Fatalf("unexpected uplink counter: got %d, want %d", got, len("uplink")) + } + if got := downlinkCounter.Value(); got != int64(len("downlink")) { + t.Fatalf("unexpected downlink counter: got %d, want %d", got, len("downlink")) + } + if got := int(atomic.LoadInt32(&dispatcher.readBytes)); got != len("uplink") { + t.Fatalf("dispatcher read unexpected bytes: got %d, want %d", got, len("uplink")) + } + if got := conn.writer.String(); got != "downlink" { + t.Fatalf("connection write mismatch: got %q, want %q", got, "downlink") + } +} From b12bc504c8c2879a98e4aadb1c3f14d11f86cd0d Mon Sep 17 00:00:00 2001 From: kokoro <89308518+kokoromagic@users.noreply.github.com> Date: Thu, 25 Jun 2026 04:06:24 +0700 Subject: [PATCH 67/78] README.md: Add Magic_V2Ray to Magisk in Installation (#6355) https://github.com/XTLS/Xray-core/pull/6355#issuecomment-4793603321 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index daf82368e..d52ca9bb3 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ - [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless) - [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU) - Magisk + - [Magic_V2Ray](https://github.com/vincentng295/Magic_V2Ray) - [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk) - Homebrew - `brew install xray` From f496437b84d4f05e07243da11fe380b302a82145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BF=B7=E9=80=94=E7=9A=84=E7=8C=AB?= Date: Sat, 27 Jun 2026 18:40:58 +0800 Subject: [PATCH 68/78] XHTTP server: Refactor upload_queue.go (#6372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/XTLS/Xray-core/pull/6372#issuecomment-4801395378 --------- Co-authored-by: 风扇滑翔翼 Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- transport/internet/splithttp/upload_queue.go | 93 ++++++++------------ 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/transport/internet/splithttp/upload_queue.go b/transport/internet/splithttp/upload_queue.go index 69b9a9725..8f4a0764b 100644 --- a/transport/internet/splithttp/upload_queue.go +++ b/transport/internet/splithttp/upload_queue.go @@ -6,27 +6,25 @@ package splithttp import ( "container/heap" "io" - "runtime" - "sync" + "sync/atomic" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/signal/done" ) type Packet struct { - Reader io.ReadCloser + Reader *httpServerConn Payload []byte Seq uint64 } type uploadQueue struct { - reader io.ReadCloser - nomore bool - pushedPackets chan Packet - writeCloseMutex sync.Mutex - heap uploadHeap - nextSeq uint64 - closed bool - maxPackets int + reader atomic.Pointer[httpServerConn] + pushedPackets chan Packet + heap uploadHeap + nextSeq uint64 + maxPackets int + closed *done.Instance } func NewUploadQueue(maxPackets int) *uploadQueue { @@ -34,73 +32,53 @@ func NewUploadQueue(maxPackets int) *uploadQueue { pushedPackets: make(chan Packet, maxPackets), heap: uploadHeap{}, nextSeq: 0, - closed: false, + closed: done.New(), maxPackets: maxPackets, } } func (h *uploadQueue) Push(p Packet) error { - h.writeCloseMutex.Lock() - defer h.writeCloseMutex.Unlock() - - if h.closed { - return errors.New("packet queue closed") - } - if h.nomore { + if h.reader.Load() != nil || (p.Reader != nil && !h.reader.CompareAndSwap(nil, p.Reader)) { return errors.New("h.reader already exists") } - if p.Reader != nil { - h.nomore = true + select { + case h.pushedPackets <- p: // no panic + if h.closed.Done() { + return errors.New("packet queue closed") + } + return nil + case <-h.closed.Wait(): + return errors.New("packet queue closed") } - h.pushedPackets <- p - return nil } func (h *uploadQueue) Close() error { - h.writeCloseMutex.Lock() - defer h.writeCloseMutex.Unlock() - - if !h.closed { - h.closed = true - runtime.Gosched() // hope Read() gets the packet - f: - for { - select { - case p := <-h.pushedPackets: - if p.Reader != nil { - h.reader = p.Reader - } - default: - break f - } - } - close(h.pushedPackets) - } - if h.reader != nil { - return h.reader.Close() + h.closed.Close() + if reader := h.reader.Load(); reader != nil { + return reader.Close() } return nil } func (h *uploadQueue) Read(b []byte) (int, error) { - if h.reader != nil { - return h.reader.Read(b) + if reader := h.reader.Load(); reader != nil { + return reader.Read(b) } - if h.closed { + if h.closed.Done() { return 0, io.EOF } if len(h.heap) == 0 { - packet, more := <-h.pushedPackets - if !more { + select { + case p := <-h.pushedPackets: + if p.Reader != nil { + return p.Reader.Read(b) + } + heap.Push(&h.heap, p) + case <-h.closed.Wait(): return 0, io.EOF } - if packet.Reader != nil { - h.reader = packet.Reader - return h.reader.Read(b) - } - heap.Push(&h.heap, packet) } for len(h.heap) > 0 { @@ -131,11 +109,12 @@ func (h *uploadQueue) Read(b []byte) (int, error) { return 0, errors.New("packet queue is too large") } heap.Push(&h.heap, packet) - packet2, more := <-h.pushedPackets - if !more { + select { + case p := <-h.pushedPackets: + heap.Push(&h.heap, p) + case <-h.closed.Wait(): return 0, io.EOF } - heap.Push(&h.heap, packet2) } } From 345c76f9a8ec438e24bbbc5a246bd0bfeab480b1 Mon Sep 17 00:00:00 2001 From: bitwiresys <46227999+bitwiresys@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:41:22 +0300 Subject: [PATCH 69/78] WireGuard inbound: Support dynamic peer management (#6360) https://github.com/XTLS/Xray-core/pull/6360#issuecomment-4780311547 Closes https://github.com/XTLS/Xray-core/issues/6314 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: LjhAUMEM --- infra/conf/hysteria.go | 5 +- infra/conf/transport_internet.go | 1 - infra/conf/wireguard.go | 30 ++- infra/conf/wireguard_test.go | 50 ----- proxy/hysteria/config.pb.go | 17 +- proxy/hysteria/config.proto | 3 +- proxy/wireguard/config.go | 59 ++++++ proxy/wireguard/config.pb.go | 30 ++- proxy/wireguard/config.proto | 3 + proxy/wireguard/server.go | 172 ++++++++++++++++-- .../internet/finalmask/sudoku/sudoku_test.go | 3 - transport/internet/hysteria/config.pb.go | 15 +- transport/internet/hysteria/config.proto | 2 +- transport/internet/splithttp/hub.go | 4 +- 14 files changed, 280 insertions(+), 114 deletions(-) delete mode 100644 infra/conf/wireguard_test.go diff --git a/infra/conf/hysteria.go b/infra/conf/hysteria.go index 744338d79..0c9fc3c71 100644 --- a/infra/conf/hysteria.go +++ b/infra/conf/hysteria.go @@ -22,7 +22,6 @@ func (c *HysteriaClientConfig) Build() (proto.Message, error) { } config := &hysteria.ClientConfig{} - config.Version = c.Version config.Server = &protocol.ServerEndpoint{ Address: c.Address.Build(), Port: uint32(c.Port), @@ -44,6 +43,10 @@ type HysteriaServerConfig struct { } func (c *HysteriaServerConfig) Build() (proto.Message, error) { + if c.Version != 2 { + return nil, errors.New("version != 2") + } + config := new(hysteria.ServerConfig) if c.Clients != nil { diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index feb593b4b..5a8d4f895 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -571,7 +571,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { } config := &hysteria.Config{} - config.Version = c.Version config.Auth = c.Auth config.UdpIdleTimeout = c.UdpIdleTimeout config.MasqType = c.Masquerade.Type diff --git a/infra/conf/wireguard.go b/infra/conf/wireguard.go index d985184f4..8131be28a 100644 --- a/infra/conf/wireguard.go +++ b/infra/conf/wireguard.go @@ -7,6 +7,9 @@ import ( "strings" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/proxy/wireguard" "google.golang.org/protobuf/proto" ) @@ -17,9 +20,12 @@ type WireGuardPeerConfig struct { Endpoint string `json:"endpoint"` KeepAlive uint32 `json:"keepAlive"` AllowedIPs []string `json:"allowedIPs,omitempty"` + + Level uint32 `json:"level"` + Email string `json:"email"` } -func (c *WireGuardPeerConfig) Build() (proto.Message, error) { +func (c *WireGuardPeerConfig) Build() (*wireguard.PeerConfig, error) { var err error config := new(wireguard.PeerConfig) @@ -78,14 +84,32 @@ func (c *WireGuardConfig) Build() (proto.Message, error) { config.Endpoint = c.Address } - if c.Peers != nil { + if c.IsClient { config.Peers = make([]*wireguard.PeerConfig, len(c.Peers)) for i, p := range c.Peers { msg, err := p.Build() if err != nil { return nil, err } - config.Peers[i] = msg.(*wireguard.PeerConfig) + config.Peers[i] = msg + } + } else { + config.Users = make([]*protocol.User, len(c.Peers)) + processUser := func(idx int) error { + p := c.Peers[idx] + m, err := p.Build() + if err != nil { + return err + } + config.Users[idx] = &protocol.User{ + Email: p.Email, + Level: p.Level, + Account: serial.ToTypedMessage(m), + } + return nil + } + if err := task.ParallelForN(len(c.Peers), processUser); err != nil { + return nil, err } } diff --git a/infra/conf/wireguard_test.go b/infra/conf/wireguard_test.go deleted file mode 100644 index 2e1903db8..000000000 --- a/infra/conf/wireguard_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package conf_test - -import ( - "testing" - - . "github.com/xtls/xray-core/infra/conf" - "github.com/xtls/xray-core/proxy/wireguard" -) - -func TestWireGuardConfig(t *testing.T) { - creator := func() Buildable { - return new(WireGuardConfig) - } - - runMultiTestCase(t, []TestCase{ - { - Input: `{ - "secretKey": "uJv5tZMDltsiYEn+kUwb0Ll/CXWhMkaSCWWhfPEZM3A=", - "address": ["10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"], - "peers": [ - { - "publicKey": "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a", - "endpoint": "127.0.0.1:1234" - } - ], - "mtu": 1300, - "workers": 2, - "domainStrategy": "ForceIPv6v4", - "noKernelTun": false - }`, - Parser: loadJSON(creator), - Output: &wireguard.DeviceConfig{ - // key converted into hex form - SecretKey: "b89bf9b5930396db226049fe914c1bd0b97f0975a13246920965a17cf1193370", - Endpoint: []string{"10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"}, - Peers: []*wireguard.PeerConfig{ - { - // also can read from hex form directly - PublicKey: "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a", - Endpoint: "127.0.0.1:1234", - AllowedIps: []string{"0.0.0.0/0", "::0/0"}, - }, - }, - Mtu: 1300, - DomainStrategy: wireguard.DeviceConfig_FORCE_IP64, - NoKernelTun: false, - }, - }, - }) -} diff --git a/proxy/hysteria/config.pb.go b/proxy/hysteria/config.pb.go index 5764b78ca..5228330ae 100644 --- a/proxy/hysteria/config.pb.go +++ b/proxy/hysteria/config.pb.go @@ -24,8 +24,7 @@ const ( type ClientConfig struct { state protoimpl.MessageState `protogen:"open.v1"` - Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - Server *protocol.ServerEndpoint `protobuf:"bytes,2,opt,name=server,proto3" json:"server,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -60,13 +59,6 @@ func (*ClientConfig) Descriptor() ([]byte, []int) { return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{0} } -func (x *ClientConfig) GetVersion() int32 { - if x != nil { - return x.Version - } - return 0 -} - func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { if x != nil { return x.Server @@ -122,10 +114,9 @@ var File_proxy_hysteria_config_proto protoreflect.FileDescriptor const file_proxy_hysteria_config_proto_rawDesc = "" + "\n" + - "\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"f\n" + - "\fClientConfig\x12\x18\n" + - "\aversion\x18\x01 \x01(\x05R\aversion\x12<\n" + - "\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" + + "\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"L\n" + + "\fClientConfig\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" + "\fServerConfig\x120\n" + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05usersB[\n" + "\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3" diff --git a/proxy/hysteria/config.proto b/proxy/hysteria/config.proto index 03fdf4e62..6788f7a60 100644 --- a/proxy/hysteria/config.proto +++ b/proxy/hysteria/config.proto @@ -10,8 +10,7 @@ import "common/protocol/server_spec.proto"; import "common/protocol/user.proto"; message ClientConfig { - int32 version = 1; - xray.common.protocol.ServerEndpoint server = 2; + xray.common.protocol.ServerEndpoint server = 1; } message ServerConfig { diff --git a/proxy/wireguard/config.go b/proxy/wireguard/config.go index 0b280a906..d7881dd31 100644 --- a/proxy/wireguard/config.go +++ b/proxy/wireguard/config.go @@ -1 +1,60 @@ package wireguard + +import ( + "encoding/hex" + "net/netip" + + "github.com/xtls/xray-core/common/protocol" + "google.golang.org/protobuf/proto" +) + +func (p *PeerConfig) AsAccount() (protocol.Account, error) { + pub, err := ParseKey(p.PublicKey) + if err != nil { + return nil, err + } + + allowedIPs := make([]netip.Prefix, 0, len(p.AllowedIps)) + for i := range p.AllowedIps { + p, err := netip.ParsePrefix(p.AllowedIps[i]) + if err != nil { + return nil, err + } + allowedIPs = append(allowedIPs, p) + } + + return &MemoryAccount{ + Pub: *pub, + AllowedIPs: allowedIPs, + PreSharedKey: p.PreSharedKey, + KeepAlive: p.KeepAlive, + }, nil +} + +type MemoryAccount struct { + Pub [32]byte + AllowedIPs []netip.Prefix + PreSharedKey string + KeepAlive string +} + +func (a *MemoryAccount) Equals(other protocol.Account) bool { + if b, ok := other.(*MemoryAccount); ok { + return a.Pub == b.Pub + } + return false +} + +func (a *MemoryAccount) ToProto() proto.Message { + allowedIPs := make([]string, 0, len(a.AllowedIPs)) + for i := range a.AllowedIPs { + allowedIPs = append(allowedIPs, a.AllowedIPs[i].String()) + } + + return &PeerConfig{ + PublicKey: hex.EncodeToString(a.Pub[:]), + AllowedIps: allowedIPs, + PreSharedKey: a.PreSharedKey, + KeepAlive: a.KeepAlive, + } +} diff --git a/proxy/wireguard/config.pb.go b/proxy/wireguard/config.pb.go index 5a5ab288b..59db6564f 100644 --- a/proxy/wireguard/config.pb.go +++ b/proxy/wireguard/config.pb.go @@ -7,6 +7,7 @@ package wireguard import ( + protocol "github.com/xtls/xray-core/common/protocol" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -157,6 +158,7 @@ type DeviceConfig struct { SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"` Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"` + Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"` Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"` Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"` DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"` @@ -217,6 +219,13 @@ func (x *DeviceConfig) GetPeers() []*PeerConfig { return nil } +func (x *DeviceConfig) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + func (x *DeviceConfig) GetMtu() int32 { if x != nil { return x.Mtu @@ -256,7 +265,7 @@ var File_proxy_wireguard_config_proto protoreflect.FileDescriptor const file_proxy_wireguard_config_proto_rawDesc = "" + "\n" + - "\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\"\xad\x01\n" + + "\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\x1a\x1acommon/protocol/user.proto\"\xad\x01\n" + "\n" + "PeerConfig\x12\x1d\n" + "\n" + @@ -266,12 +275,13 @@ const file_proxy_wireguard_config_proto_rawDesc = "" + "\n" + "keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" + "\vallowed_ips\x18\x05 \x03(\tR\n" + - "allowedIps\"\xaa\x03\n" + + "allowedIps\"\xdc\x03\n" + "\fDeviceConfig\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" + "\bendpoint\x18\x02 \x03(\tR\bendpoint\x126\n" + - "\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x12\x10\n" + + "\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x120\n" + + "\x05users\x18\x05 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x12\x10\n" + "\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1a\n" + "\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" + "\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" + @@ -305,15 +315,17 @@ var file_proxy_wireguard_config_proto_goTypes = []any{ (DeviceConfig_DomainStrategy)(0), // 0: xray.proxy.wireguard.DeviceConfig.DomainStrategy (*PeerConfig)(nil), // 1: xray.proxy.wireguard.PeerConfig (*DeviceConfig)(nil), // 2: xray.proxy.wireguard.DeviceConfig + (*protocol.User)(nil), // 3: xray.common.protocol.User } var file_proxy_wireguard_config_proto_depIdxs = []int32{ 1, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig - 0, // 1: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 3, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User + 0, // 2: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_proxy_wireguard_config_proto_init() } diff --git a/proxy/wireguard/config.proto b/proxy/wireguard/config.proto index 95291279b..171d5b21a 100644 --- a/proxy/wireguard/config.proto +++ b/proxy/wireguard/config.proto @@ -6,6 +6,8 @@ option go_package = "github.com/xtls/xray-core/proxy/wireguard"; option java_package = "com.xray.proxy.wireguard"; option java_multiple_files = true; +import "common/protocol/user.proto"; + message PeerConfig { string public_key = 1; string pre_shared_key = 2; @@ -25,6 +27,7 @@ message DeviceConfig { string secret_key = 1; repeated string endpoint = 2; repeated PeerConfig peers = 3; + repeated xray.common.protocol.User users = 5; int32 mtu = 4; bytes reserved = 6; diff --git a/proxy/wireguard/server.go b/proxy/wireguard/server.go index 2c634464c..e26479ad6 100644 --- a/proxy/wireguard/server.go +++ b/proxy/wireguard/server.go @@ -2,16 +2,20 @@ package wireguard import ( "context" + "encoding/hex" "fmt" "net/netip" + "reflect" "strings" "sync" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" c "github.com/xtls/xray-core/common/ctx" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/policy" @@ -20,6 +24,7 @@ import ( "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/stat" + "golang.org/x/crypto/curve25519" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -42,6 +47,9 @@ type Server struct { stack *stack.Stack dev *device.Device mu sync.Mutex + + pub [32]byte + users *sync.Map } func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) { @@ -72,15 +80,6 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) { } } - if len(conf.Peers) == 0 { - return nil, errors.New("empty peers") - } - for _, peer := range conf.Peers { - if peer.PublicKey == "" { - return nil, errors.New("peer without publickey") - } - } - localAddresses := make([]netip.Addr, 0, len(conf.Endpoint)) for _, localaddress := range conf.Endpoint { addr, err := netip.ParseAddr(localaddress) @@ -101,6 +100,19 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) { return nil, err } + pri := common.Must2(ParseKey(conf.SecretKey)) + var pub [32]byte + curve25519.ScalarBaseMult(&pub, pri) + + users := &sync.Map{} + for _, u := range conf.Users { + user, err := u.ToMemoryUser() + if err != nil { + return nil, err + } + users.Store(user.Account.(*MemoryAccount).Pub, user) + } + return &Server{ conf: conf, ctx: core.ToBackgroundDetachedContext(ctx), @@ -116,9 +128,100 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) { tun: tun, stack: stack, + + pub: pub, + users: users, }, nil } +func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.dev == nil { + return errors.New("too early") + } + peer := user.Account.(*MemoryAccount) + if peer.Pub == s.pub { + return errors.New("invalid public key") + } + var sb strings.Builder + sb.WriteString("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\n") + sb.WriteString("replace_allowed_ips=true\n") + for i := range peer.AllowedIPs { + sb.WriteString("allowed_ip=" + peer.AllowedIPs[i].String() + "\n") + } + if peer.PreSharedKey != "" { + sb.WriteString("preshared_key=" + peer.PreSharedKey + "\n") + } + if peer.KeepAlive != "" { + sb.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n") + } + err := s.dev.IpcSet(sb.String()) + if err != nil { + return err + } + s.users.Store(peer.Pub, user) + return nil +} + +func (s *Server) RemoveUser(ctx context.Context, email string) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.dev == nil { + return errors.New("too early") + } + if user := s.GetUser(ctx, email); user != nil { + peer := user.Account.(*MemoryAccount) + err := s.dev.IpcSet("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\nremove=true\n") + if err != nil { + return err + } + s.users.Delete(peer.Pub) + } + return nil +} + +func (s *Server) GetUser(ctx context.Context, email string) (user *protocol.MemoryUser) { + s.users.Range(func(key, value any) bool { + if value.(*protocol.MemoryUser).Email == email { + user = value.(*protocol.MemoryUser) + return false + } + return true + }) + return +} + +func (s *Server) GetUserByAddr(ctx context.Context, addr netip.Addr) (user *protocol.MemoryUser) { + s.users.Range(func(key, value any) bool { + peer := value.(*protocol.MemoryUser).Account.(*MemoryAccount) + for i := range peer.AllowedIPs { + if peer.AllowedIPs[i].Contains(addr) { + user = value.(*protocol.MemoryUser) + return false + } + } + return true + }) + return +} + +func (s *Server) GetUsers(ctx context.Context) (users []*protocol.MemoryUser) { + s.users.Range(func(key, value interface{}) bool { + users = append(users, value.(*protocol.MemoryUser)) + return true + }) + return +} + +func (s *Server) GetUsersCount(context.Context) (count int64) { + s.users.Range(func(key, value interface{}) bool { + count++ + return true + }) + return +} + // Network implements proxy.Inbound.Network. func (*Server) Network() []net.Network { return []net.Network{} @@ -196,18 +299,20 @@ func (s *Server) Start() error { dev := device.NewDevice(s.tun, bind, logger) var cfg strings.Builder cfg.WriteString("private_key=" + s.conf.SecretKey + "\n") - for _, peer := range s.conf.Peers { - cfg.WriteString("public_key=" + peer.PublicKey + "\n") + s.users.Range(func(key, value any) bool { + peer := value.(*protocol.MemoryUser).Account.(*MemoryAccount) + cfg.WriteString("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\n") + for i := range peer.AllowedIPs { + cfg.WriteString("allowed_ip=" + peer.AllowedIPs[i].String() + "\n") + } if peer.PreSharedKey != "" { cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n") } - for _, ip := range peer.AllowedIps { - cfg.WriteString("allowed_ip=" + ip + "\n") - } if peer.KeepAlive != "" { cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n") } - } + return true + }) err := dev.IpcSet(cfg.String()) if err != nil { return err @@ -227,12 +332,36 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) { defer cancel() ctx = c.ContextWithID(ctx, session.NewID()) - source := net.DestinationFromAddr(conn.RemoteAddr()) + remote := conn.RemoteAddr() + if remote == nil { + errors.LogError(context.Background(), "nil remote") + return + } + + var addr netip.Addr + switch v := remote.(type) { + case *net.TCPAddr: + addr, _ = netip.AddrFromSlice(v.IP) + case *net.UDPAddr: + addr, _ = netip.AddrFromSlice(v.IP) + default: + errors.LogError(context.Background(), "invalid addr type ", reflect.TypeOf(v)) + return + } + + user := s.GetUserByAddr(context.TODO(), addr) + if user == nil { + errors.LogError(context.Background(), "nil user form ", remote, " to ", dest) + return + } + + source := net.DestinationFromAddr(remote) inbound := session.Inbound{ Name: "wireguard", Tag: s.tag, CanSpliceCopy: 3, Source: source, + User: user, } ctx = session.ContextWithInbound(ctx, &inbound) @@ -257,3 +386,14 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) { errors.LogError(ctx, errors.New("connection closed").Base(err)) } } + +func ParseKey(str string) (*[32]byte, error) { + slice, err := hex.DecodeString(str) + if err != nil { + return nil, err + } + if len(slice) != 32 { + return nil, errors.New("len(slice) != 32") + } + return (*[32]byte)(slice), nil +} diff --git a/transport/internet/finalmask/sudoku/sudoku_test.go b/transport/internet/finalmask/sudoku/sudoku_test.go index 1a01de923..2c03a21f0 100644 --- a/transport/internet/finalmask/sudoku/sudoku_test.go +++ b/transport/internet/finalmask/sudoku/sudoku_test.go @@ -372,7 +372,6 @@ func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize in { ProtocolName: "hysteria", Settings: serial.ToTypedMessage(&hytransport.Config{ - Version: 2, Auth: auth, UdpIdleTimeout: 60, }), @@ -421,7 +420,6 @@ func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize in Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&hyproxy.ClientConfig{ - Version: 2, Server: &protocol.ServerEndpoint{ Address: xnet.NewIPOrDomain(xnet.LocalHostIP), Port: uint32(relayPort), @@ -437,7 +435,6 @@ func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize in { ProtocolName: "hysteria", Settings: serial.ToTypedMessage(&hytransport.Config{ - Version: 2, Auth: auth, UdpIdleTimeout: 60, }), diff --git a/transport/internet/hysteria/config.pb.go b/transport/internet/hysteria/config.pb.go index 7a0b02ca8..2a2cf1a2f 100644 --- a/transport/internet/hysteria/config.pb.go +++ b/transport/internet/hysteria/config.pb.go @@ -23,7 +23,6 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` - Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` Auth string `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"` UdpIdleTimeout int64 `protobuf:"varint,3,opt,name=udp_idle_timeout,json=udpIdleTimeout,proto3" json:"udp_idle_timeout,omitempty"` MasqType string `protobuf:"bytes,4,opt,name=masq_type,json=masqType,proto3" json:"masq_type,omitempty"` @@ -68,13 +67,6 @@ func (*Config) Descriptor() ([]byte, []int) { return file_transport_internet_hysteria_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetVersion() int32 { - if x != nil { - return x.Version - } - return 0 -} - func (x *Config) GetAuth() string { if x != nil { return x.Auth @@ -149,9 +141,8 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor const file_transport_internet_hysteria_config_proto_rawDesc = "" + "\n" + - "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xa3\x04\n" + - "\x06Config\x12\x18\n" + - "\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" + + "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\x8f\x04\n" + + "\x06Config\x12\x12\n" + "\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" + "\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" + "\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" + @@ -166,7 +157,7 @@ const file_transport_internet_hysteria_config_proto_rawDesc = "" + "\x17masq_string_status_code\x18\v \x01(\x05R\x14masqStringStatusCode\x1aD\n" + "\x16MasqStringHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x82\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02B\x82\x01\n" + "$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3" var ( diff --git a/transport/internet/hysteria/config.proto b/transport/internet/hysteria/config.proto index 3d039da8c..8eaac87c1 100644 --- a/transport/internet/hysteria/config.proto +++ b/transport/internet/hysteria/config.proto @@ -7,7 +7,7 @@ option java_package = "com.xray.transport.internet.hysteria"; option java_multiple_files = true; message Config { - int32 version = 1; + reserved 1; string auth = 2; int64 udp_idle_timeout = 3; diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index 8416591a2..557f8a54d 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -584,9 +584,7 @@ func (ln *Listener) Addr() net.Addr { // Close implements net.Listener.Close(). func (ln *Listener) Close() error { if ln.h3server != nil { - if err := ln.h3server.Close(); err != nil { - return err - } + return ln.h3server.Close() } else if ln.listener != nil { return ln.listener.Close() } From 452b719504ef93b6c7a2777e4eaabfffff913209 Mon Sep 17 00:00:00 2001 From: seally <70375705+nasralbek@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:04:58 +0500 Subject: [PATCH 70/78] Hysteria inbound: Support `routing`'s `vlessRoute` as well (#6375) https://github.com/XTLS/Xray-core/pull/6375#issuecomment-4795522284 --------- Co-authored-by: LjhAUMEM --- proxy/hysteria/account/config.go | 129 ++++++++++++----------------- proxy/hysteria/server.go | 17 ++-- transport/internet/hysteria/hub.go | 2 +- 3 files changed, 62 insertions(+), 86 deletions(-) diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index 63ecaf652..299104585 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -3,25 +3,32 @@ package account import ( "sync" - "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" "google.golang.org/protobuf/proto" ) func (a *Account) AsAccount() (protocol.Account, error) { + var VR net.Port + if id, err := uuid.ParseString(a.Auth); err == nil { + VR = net.PortFromBytes(id[6:8]) + } return &MemoryAccount{ Auth: a.Auth, + VR: VR, }, nil } type MemoryAccount struct { Auth string + VR net.Port } -func (a *MemoryAccount) Equals(another protocol.Account) bool { - if account, ok := another.(*MemoryAccount); ok { - return a.Auth == account.Auth +func (a *MemoryAccount) Equals(other protocol.Account) bool { + if b, ok := other.(*MemoryAccount); ok { + return a.Auth == b.Auth } return false } @@ -33,97 +40,63 @@ func (a *MemoryAccount) ToProto() proto.Message { } type Validator struct { - emails map[string]struct{} - users map[string]*protocol.MemoryUser - - mutex sync.Mutex + users sync.Map } func NewValidator() *Validator { - return &Validator{ - emails: make(map[string]struct{}), - users: make(map[string]*protocol.MemoryUser), - } + return &Validator{} } -func (v *Validator) Add(u *protocol.MemoryUser) error { - v.mutex.Lock() - defer v.mutex.Unlock() - - if u.Email != "" { - if _, ok := v.emails[u.Email]; ok { - return errors.New("User ", u.Email, " already exists.") - } - v.emails[u.Email] = struct{}{} - } - v.users[u.Account.(*MemoryAccount).Auth] = u - +func (v *Validator) Add(user *protocol.MemoryUser) error { + v.users.Store(user.Account.(*MemoryAccount).Auth, user) return nil } -func (v *Validator) Del(email string) error { - if email == "" { - return errors.New("Email must not be empty.") +func (v *Validator) DelByEmail(email string) error { + if user := v.GetByEmail(email); user != nil { + v.users.Delete(user.Account.(*MemoryAccount).Auth) } - - v.mutex.Lock() - defer v.mutex.Unlock() - - if _, ok := v.emails[email]; !ok { - return errors.New("User ", email, " not found.") - } - delete(v.emails, email) - for key, user := range v.users { - if user.Email == email { - delete(v.users, key) - break - } - } - return nil } func (v *Validator) Get(auth string) *protocol.MemoryUser { - v.mutex.Lock() - defer v.mutex.Unlock() - - return v.users[auth] -} - -func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { - if email == "" { - return nil + if value, ok := v.users.Load(auth); ok { + return value.(*protocol.MemoryUser) } - - v.mutex.Lock() - defer v.mutex.Unlock() - - if _, ok := v.emails[email]; ok { - for _, user := range v.users { - if user.Email == email { - return user - } - } - } - return nil } -func (v *Validator) GetAll() []*protocol.MemoryUser { - v.mutex.Lock() - defer v.mutex.Unlock() - - users := make([]*protocol.MemoryUser, 0, len(v.users)) - for _, user := range v.users { - users = append(users, user) - } - - return users +func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) { + v.users.Range(func(key, value any) bool { + if value.(*protocol.MemoryUser).Email == email { + user = value.(*protocol.MemoryUser) + return false + } + return true + }) + return } -func (v *Validator) GetCount() int64 { - v.mutex.Lock() - defer v.mutex.Unlock() - - return int64(len(v.users)) +func (v *Validator) GetAll() (users []*protocol.MemoryUser) { + v.users.Range(func(key, value any) bool { + users = append(users, value.(*protocol.MemoryUser)) + return true + }) + return +} + +func (v *Validator) GetCount() (count int64) { + v.users.Range(func(key, value any) bool { + count++ + return true + }) + return +} + +func (v *Validator) NotEmpty() (not_empty bool) { + v.users.Range(func(key, value any) bool { + not_empty = true + return false + }) + return } diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index d7456dc30..1e54b654b 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -59,12 +59,12 @@ func (s *Server) HysteriaInboundValidator() *account.Validator { return s.validator } -func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error { - return s.validator.Add(u) +func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error { + return s.validator.Add(user) } -func (s *Server) RemoveUser(ctx context.Context, e string) error { - return s.validator.Del(e) +func (s *Server) RemoveUser(ctx context.Context, email string) error { + return s.validator.DelByEmail(email) } func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser { @@ -91,9 +91,12 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con iConn := stat.TryUnwrapStatsConn(conn) - type User interface{ User() *protocol.MemoryUser } - if v, ok := iConn.(User); ok && v.User() != nil { - inbound.User = v.User() + if v, ok := iConn.(interface{ User() *protocol.MemoryUser }); ok { + user := v.User() + if user != nil { + inbound.User = user + inbound.VlessRoute = user.Account.(*account.MemoryAccount).VR + } } if _, ok := iConn.(*hysteria.InterConn); ok { diff --git a/transport/internet/hysteria/hub.go b/transport/internet/hysteria/hub.go index d20313b5d..59bcd7d42 100644 --- a/transport/internet/hysteria/hub.go +++ b/transport/internet/hysteria/hub.go @@ -58,7 +58,7 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { var user *protocol.MemoryUser var ok bool - if h.validator != nil && h.validator.GetCount() > 0 { + if h.validator != nil && h.validator.NotEmpty() { user = h.validator.Get(auth) } else if h.config.Auth != "" { ok = auth == h.config.Auth From 18b85adb4e288f49a7894351c6e0f2428c0beef6 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:41:39 +0000 Subject: [PATCH 71/78] XHTTP client: Change default `maxConnections` to 6 for anti-RKN "xmux": { "maxConcurrency": 0, "maxConnections": "6", "cMaxReuseTimes": 0, "hMaxRequestTimes": "600-900", "hMaxReusableSecs": "1800-3000", "hKeepAlivePeriod": 0 } Replaces https://github.com/XTLS/Xray-core/commit/9cc7907234a8297a87a0ff77fc40db373b74a0f2 and https://github.com/XTLS/Xray-core/commit/4ce65fc74c4c50919b10b3faff9725f75bba5d73 Closes https://github.com/XTLS/Xray-core/issues/6376#issuecomment-4817033592 --- infra/conf/transport_internet.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 5a8d4f895..a6052ff9f 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -404,8 +404,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") } if c.Xmux == (XmuxConfig{}) { - c.Xmux.MaxConcurrency.From = 1 - c.Xmux.MaxConcurrency.To = 1 + c.Xmux.MaxConnections.From = 6 + c.Xmux.MaxConnections.To = 6 c.Xmux.HMaxRequestTimes.From = 600 c.Xmux.HMaxRequestTimes.To = 900 c.Xmux.HMaxReusableSecs.From = 1800 From 45cf2898ab12e97a55dd8f1f3d78d903340bdc9e Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:18:03 +0000 Subject: [PATCH 72/78] Xray-core v26.6.27 Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668 Project X Channel: https://t.me/projectXtls Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633 Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1 VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067 VLESS NFT: https://opensea.io/collection/vless XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113 REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2 --- core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/core.go b/core/core.go index 78d7d2383..3cc9f7802 100644 --- a/core/core.go +++ b/core/core.go @@ -20,7 +20,7 @@ import ( var ( Version_x byte = 26 Version_y byte = 6 - Version_z byte = 22 + Version_z byte = 27 ) var ( From 2b828b7bc2cd1c91635ea0e9e7ed287e87589ae1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:45:47 +0000 Subject: [PATCH 73/78] Bump google.golang.org/grpc from 1.81.1 to 1.82.0 (#6415) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.82.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 88932cf08..ed304240e 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v1.0.1 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.0 google.golang.org/protobuf v1.36.11 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h12.io/socks v1.0.3 @@ -55,7 +55,7 @@ require ( golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.45.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5e2820856..00a62bfdf 100644 --- a/go.sum +++ b/go.sum @@ -147,10 +147,10 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From dfdbcf86ccbd46ee582c75081933e4230b82e70a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:45:51 +0000 Subject: [PATCH 74/78] Bump github.com/klauspost/cpuid/v2 from 2.3.0 to 2.4.0 (#6417) Bumps [github.com/klauspost/cpuid/v2](https://github.com/klauspost/cpuid) from 2.3.0 to 2.4.0. - [Release notes](https://github.com/klauspost/cpuid/releases) - [Commits](https://github.com/klauspost/cpuid/compare/v2.3.0...v2.4.0) --- updated-dependencies: - dependency-name: github.com/klauspost/cpuid/v2 dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ed304240e..1f62f3063 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 github.com/gorilla/websocket v1.5.3 - github.com/klauspost/cpuid/v2 v2.3.0 + github.com/klauspost/cpuid/v2 v2.4.0 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 github.com/pion/stun/v3 v3.1.6 diff --git a/go.sum b/go.sum index 00a62bfdf..064bfa2e8 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From 695e68ef9ef61b68dff8ec18d762bb127bfa778f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:53:15 +0000 Subject: [PATCH 75/78] Bump github.com/pires/go-proxyproto from 0.12.0 to 0.14.0 (#6420) Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.12.0 to 0.14.0. - [Release notes](https://github.com/pires/go-proxyproto/releases) - [Commits](https://github.com/pires/go-proxyproto/compare/v0.12.0...v0.14.0) --- updated-dependencies: - dependency-name: github.com/pires/go-proxyproto dependency-version: 0.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f62f3063..1ddc88fdc 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 github.com/pion/stun/v3 v3.1.6 - github.com/pires/go-proxyproto v0.12.0 + github.com/pires/go-proxyproto v0.14.0 github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/robfig/cron/v3 v3.0.1 github.com/sagernet/sing v0.5.1 diff --git a/go.sum b/go.sum index 064bfa2e8..92ccc6bb3 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= -github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= -github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= +github.com/pires/go-proxyproto v0.14.0 h1:2vIGIfVG8eVRsKF0xukEoeT5RWhDXxBU0uv6smLOKdI= +github.com/pires/go-proxyproto v0.14.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= From 3dc8bf3d8b5d2249ef07f793ab544cafacd18e3c Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Sat, 4 Jul 2026 09:28:21 +0800 Subject: [PATCH 76/78] Hysteria inbound: Fix dynamic UUID not accepted (#6395) Fixes https://github.com/XTLS/Xray-core/pull/6375#issuecomment-4825923934 --- go.mod | 1 + proxy/hysteria/account/config.go | 64 ++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 1ddc88fdc..d5af7f407 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/klauspost/cpuid/v2 v2.4.0 github.com/miekg/dns v1.1.72 diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index 299104585..401dfd0b1 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -5,14 +5,14 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" - "github.com/xtls/xray-core/common/uuid" + "github.com/google/uuid" "google.golang.org/protobuf/proto" ) func (a *Account) AsAccount() (protocol.Account, error) { var VR net.Port - if id, err := uuid.ParseString(a.Auth); err == nil { + if id, err := uuid.Parse(a.Auth); err == nil { VR = net.PortFromBytes(id[6:8]) } return &MemoryAccount{ @@ -41,29 +41,71 @@ func (a *MemoryAccount) ToProto() proto.Message { type Validator struct { users sync.Map + ids sync.Map + mu sync.Mutex } func NewValidator() *Validator { return &Validator{} } -func (v *Validator) Add(user *protocol.MemoryUser) error { +func (v *Validator) Add(user *protocol.MemoryUser) (err error) { + v.mu.Lock() v.users.Store(user.Account.(*MemoryAccount).Auth, user) - return nil + if id, err := uuid.Parse(user.Account.(*MemoryAccount).Auth); err == nil { + id[6] = 0 + id[7] = 0 + v.ids.Store(id, user) + } + v.mu.Unlock() + return } -func (v *Validator) DelByEmail(email string) error { +func (v *Validator) DelByEmail(email string) (err error) { + v.mu.Lock() if user := v.GetByEmail(email); user != nil { - v.users.Delete(user.Account.(*MemoryAccount).Auth) + auth := user.Account.(*MemoryAccount).Auth + v.users.Delete(auth) + if id, err := uuid.Parse(auth); err == nil { + id[6] = 0 + id[7] = 0 + v.ids.Delete(id) + } } - return nil + v.mu.Unlock() + return } -func (v *Validator) Get(auth string) *protocol.MemoryUser { - if value, ok := v.users.Load(auth); ok { - return value.(*protocol.MemoryUser) +func (v *Validator) Get(auth string) (user *protocol.MemoryUser) { + if id, err := uuid.Parse(auth); err == nil { + if user = v.GetByID(id); user != nil { + VR := net.PortFromBytes(id[6:8]) + if user.Account.(*MemoryAccount).VR != VR { + user = &protocol.MemoryUser{ + Email: user.Email, + Level: user.Level, + Account: &MemoryAccount{ + Auth: auth, + VR: VR, + }, + } + } + } + return } - return nil + if value, ok := v.users.Load(auth); ok { + user = value.(*protocol.MemoryUser) + } + return +} + +func (v *Validator) GetByID(id uuid.UUID) (user *protocol.MemoryUser) { + id[6] = 0 + id[7] = 0 + if value, ok := v.ids.Load(id); ok { + user = value.(*protocol.MemoryUser) + } + return } func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) { From 3263ae925506afbe905c13467e57f37b17f5a895 Mon Sep 17 00:00:00 2001 From: Jasper344612 <282825138+Jasper344612@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:38:25 +0800 Subject: [PATCH 77/78] TUN inbound: Fix `autoOutboundsInterface` on Linux (#6413) Fixes https://github.com/XTLS/Xray-core/issues/6412 --- proxy/tun/tun_linux.go | 70 +++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go index b8eff0174..110809549 100644 --- a/proxy/tun/tun_linux.go +++ b/proxy/tun/tun_linux.go @@ -308,36 +308,37 @@ func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, erro return iface, nil } - probeIPs := []net.IP{ - net.ParseIP("8.8.8.8"), - net.ParseIP("2001:4860:4860::8888"), + for _, family := range []int{ + netlink.FAMILY_V4, + netlink.FAMILY_V6, + } { + iface, err := findDefaultInterface(family, tunIndex) + if err == nil { + return iface, nil + } } - for _, ip := range probeIPs { - routes, err := netlink.RouteGet(ip) - if err != nil || len(routes) == 0 { - continue - } - route := routes[0] - if route.LinkIndex == tunIndex { - continue + return nil, errors.New("no usable outbound interface found") +} + +func findDefaultInterface(family int, tunIndex int) (*net.Interface, error) { + routes, err := netlink.RouteList(nil, family) + if err != nil { + return nil, err + } + + var selected *net.Interface + selectedMetric := -1 + + for _, route := range routes { + if route.Dst != nil { + ones, _ := route.Dst.Mask.Size() + if ones != 0 { + continue + } } - link, err := netlink.LinkByIndex(route.LinkIndex) - if err != nil { - continue - } - attrs := link.Attrs() - - if attrs.Flags&net.FlagUp == 0 { - continue - } - operState := attrs.OperState - if operState != netlink.OperUp && operState != netlink.OperUnknown { - continue - } - - if route.Src == nil || route.Src.IsLoopback() || route.Src.IsLinkLocalUnicast() { + if route.LinkIndex == 0 || route.LinkIndex == tunIndex { continue } @@ -345,8 +346,21 @@ func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, erro if err != nil { continue } - return iface, nil + + if iface.Flags&net.FlagUp == 0 || + iface.Flags&net.FlagLoopback != 0 { + continue + } + + if selected == nil || route.Priority < selectedMetric { + selected = iface + selectedMetric = route.Priority + } } - return nil, errors.New("no usable outbound interface found") + if selected == nil { + return nil, errors.New("physical default route not found") + } + + return selected, nil } From 65f6f0a43b814b2980740e119aa96f1adcb82e9a Mon Sep 17 00:00:00 2001 From: yiguodev <147401898+yiguodev@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:25:15 +0800 Subject: [PATCH 78/78] TUN inbound: Refine `gateway` and `autoSystemRoutingTable` on Linux (#6398) https://github.com/XTLS/Xray-core/pull/6398#issuecomment-4880261591 --- proxy/tun/README.md | 10 ++++----- proxy/tun/tun_linux.go | 46 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/proxy/tun/README.md b/proxy/tun/README.md index 2b9e0a428..8d4421ccf 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -14,11 +14,11 @@ Plainly enabling it in the config probably will result nothing, or lock your rou ## DETAILS -Current implementation does not contain options to configure network level addresses, routing or rules. -Enabling the feature will result only tun interface up, and that's it. \ -This is explicit decision, significantly simplifying implementation, and allowing any number of custom configurations, consumers could come up with. Network interface is OS level entity, and OS is what should manage it. \ -Working configuration, is tun enabled in Xray config with specific name (e.g. xray0), and OS level configuration to manage "xray0" interface, applying routing and rules on interface up. -This way consistency of system level routing and rules is ensured from single place of responsibility - the OS itself. \ +By default, enabling the feature will only bring the tun interface up. \ +When configured explicitly, Windows and Linux can also apply interface addresses from `gateway` and on-link routes from `autoSystemRoutingTable`. +Linux does not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \ +For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears. +This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \ Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README. Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \ diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go index 110809549..b2c5d35a1 100644 --- a/proxy/tun/tun_linux.go +++ b/proxy/tun/tun_linux.go @@ -26,9 +26,10 @@ type LinuxTun struct { options *Config ownsTun bool - systemRoutes []netlink.Route - routeMonitorStop chan struct{} - routeMonitorOnce sync.Once + interfaceAddresses []netlink.Addr + systemRoutes []netlink.Route + routeMonitorStop chan struct{} + routeMonitorOnce sync.Once } // LinuxTun implements Tun @@ -172,7 +173,14 @@ func (t *LinuxTun) Start() error { return err } + if err := t.setInterfaceAddresses(); err != nil { + _ = netlink.LinkSetDown(t.tunLink) + return err + } + if err := t.setSystemRoutes(); err != nil { + _ = t.unsetInterfaceAddresses() + _ = netlink.LinkSetDown(t.tunLink) return err } @@ -193,6 +201,7 @@ func (t *LinuxTun) Close() error { }) _ = t.unsetSystemRoutes() + _ = t.unsetInterfaceAddresses() if t.ownsTun { _ = netlink.LinkSetDown(t.tunLink) @@ -223,6 +232,37 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err return unix.BindToDevice(int(fd), iface.Name) } +func (t *LinuxTun) setInterfaceAddresses() error { + if len(t.options.Gateway) == 0 { + return nil + } + for _, address := range t.options.Gateway { + addr, err := netlink.ParseAddr(address) + if err != nil { + _ = t.unsetInterfaceAddresses() + return errors.New("invalid interface address ", address).Base(err) + } + if err := netlink.AddrAdd(t.tunLink, addr); err != nil { + _ = t.unsetInterfaceAddresses() + return errors.New("failed to add interface address ", address).Base(err) + } + t.interfaceAddresses = append(t.interfaceAddresses, *addr) + } + return nil +} + +func (t *LinuxTun) unsetInterfaceAddresses() error { + var errs []error + for i := len(t.interfaceAddresses) - 1; i >= 0; i-- { + address := t.interfaceAddresses[i] + if err := netlink.AddrDel(t.tunLink, &address); err != nil { + errs = append(errs, errors.New("failed to delete interface address ", address.String()).Base(err)) + } + } + t.interfaceAddresses = nil + return errors.Combine(errs...) +} + func (t *LinuxTun) setSystemRoutes() error { if len(t.options.AutoSystemRoutingTable) == 0 { return nil