Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Compile script. It is recommended to always use this script to compile libXray.

depends on git and go.

By default, the build script does not clone [Xray-core](https://github.com/XTLS/Xray-core). It uses Go modules and pins Xray-core to release tag `v26.7.11` through its pseudo-version.
By default, the build script does not clone [Xray-core](https://github.com/XTLS/Xray-core). It uses Go modules and pins Xray-core to release tag `v26.7.28` through its pseudo-version.
Pass the optional `local` argument to use an existing local checkout at `../Xray-core` through a Go module `replace`.

### Usage
Expand Down Expand Up @@ -161,6 +161,14 @@ Design notes:
5. `convertShareLinksToXrayJson` validates each parsed outbound with the current
Xray-core config builder. Invalid outbounds are omitted, and the method fails
if none remain. Validation does not create or start an Xray instance.
6. Xray-core keeps its system dialer DNS client and outbound manager in
process-wide state. Creating another Xray instance through `ping`,
`pingBatch`, `testXray`, or the exported Go APIs while `runXray` or
`runXrayFromJson` is active may replace that state and affect the running
instance. Closing the temporary instance does not restore the previous
state. libXray does not serialize, isolate, or restore concurrent instances;
callers that require overlapping instances must place them in separate
processes.

Supported methods:

Expand All @@ -170,6 +178,7 @@ convertShareLinksToXrayJson
convertXrayJsonToShareLinks
countGeoData
ping
pingBatch
testXray
runXray
runXrayFromJson
Expand Down Expand Up @@ -282,6 +291,44 @@ Some tools used to parse shared links.

Latency testing.

### pingBatch

Tests multiple outbound configurations concurrently in one temporary Xray
instance. Each config file is parsed only for its `outbounds`; all other root
fields are ignored. The target outbound is selected by `outboundTag`, then by
the `proxy` tag, and finally by the first outbound.

```json
{
"apiVersion": 1,
"method": "pingBatch",
"payload": {
"configs": [
{
"configPath": "/path/to/node-1.json"
},
{
"configPath": "/path/to/full-config.json",
"outboundTag": "media"
}
],
"timeout": 5,
"url": "https://cp.cloudflare.com/"
}
}
```

Each request accepts at most five configurations and tests all accepted
configurations concurrently. Requests containing more than five configurations
fail before any configuration is tested.

The top-level response succeeds when the batch itself was accepted. Each item
has its own result; `delay` is `10000` for an error and `11000` for a timeout.
The result array has the same length and order as the input config array.
Outbound dependencies referenced by
`streamSettings.sockopt.dialerProxy` or `proxySettings.tag` are included
automatically.

### metrics

Refer to the following configuration:
Expand Down
8 changes: 4 additions & 4 deletions android_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ type ProcessFinder interface {
}

func RegisterDialerController(controller DialerController) {
c.RegisterDialerController(func(fd uintptr) {
controller.ProtectFd(int(fd))
c.RegisterDialerController(func(fd uintptr) bool {
return controller.ProtectFd(int(fd))
})
}

func RegisterListenerController(controller DialerController) {
c.RegisterListenerController(func(fd uintptr) {
controller.ProtectFd(int(fd))
c.RegisterListenerController(func(fd uintptr) bool {
return controller.ProtectFd(int(fd))
})
}

Expand Down
4 changes: 2 additions & 2 deletions build/app/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

LIBXRAY_MOD_NAME = "github.com/xtls/libxray"
XRAY_CORE_MOD_NAME = "github.com/xtls/xray-core"
# Go modules resolve the Xray-core v26.7.11 release tag through this version.
DEFAULT_XRAY_CORE_VERSION = "v1.260327.1-0.20260711155151-50231eaff98c"
# Go modules resolve the Xray-core v26.7.28 release tag through this version.
DEFAULT_XRAY_CORE_VERSION = "v1.260327.1-0.20260728075948-5ca6f4b7d4dc"
LOCAL_XRAY_CORE_DIR_NAME = "Xray-core"


Expand Down
19 changes: 19 additions & 0 deletions controller/controller.go
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
package controller

import (
"errors"
"syscall"
)

var errProtectSocket = errors.New("protect Android VPN socket failed")

func protectSocket(conn syscall.RawConn, controller func(fd uintptr) bool) error {
var protectErr error
if err := conn.Control(func(fd uintptr) {
if !controller(fd) {
protectErr = errProtectSocket
}
}); err != nil {
return err
}
return protectErr
}
8 changes: 4 additions & 4 deletions controller/controller_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import (

// RegisterDialerController -> callback before connection begins.
// Depends on xray:api:beta
func RegisterDialerController(controller func(fd uintptr)) {
func RegisterDialerController(controller func(fd uintptr) bool) {
xinternet.RegisterDialerController(func(network, address string, conn syscall.RawConn) error {
return conn.Control(controller)
return protectSocket(conn, controller)
})
}

// RegisterListenerController -> callback before listener begins.
// Depends on xray:api:beta
func RegisterListenerController(controller func(fd uintptr)) {
func RegisterListenerController(controller func(fd uintptr) bool) {
xinternet.RegisterListenerController(func(network, address string, conn syscall.RawConn) error {
return conn.Control(controller)
return protectSocket(conn, controller)
})
}

Expand Down
56 changes: 56 additions & 0 deletions controller/controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package controller

import (
"errors"
"testing"
)

type rawConnStub struct {
fd uintptr
controlErr error
}

func (conn rawConnStub) Control(callback func(fd uintptr)) error {
if conn.controlErr != nil {
return conn.controlErr
}
callback(conn.fd)
return nil
}

func (rawConnStub) Read(func(fd uintptr) (done bool)) error {
return nil
}

func (rawConnStub) Write(func(fd uintptr) (done bool)) error {
return nil
}

func TestProtectSocketPropagatesControllerFailure(t *testing.T) {
err := protectSocket(rawConnStub{fd: 42}, func(fd uintptr) bool {
return fd != 42
})
if !errors.Is(err, errProtectSocket) {
t.Fatalf("protectSocket() error = %v, want %v", err, errProtectSocket)
}
}

func TestProtectSocketPropagatesRawConnFailure(t *testing.T) {
controlErr := errors.New("control failed")
err := protectSocket(
rawConnStub{controlErr: controlErr},
func(uintptr) bool { return true },
)
if !errors.Is(err, controlErr) {
t.Fatalf("protectSocket() error = %v, want %v", err, controlErr)
}
}

func TestProtectSocketSucceeds(t *testing.T) {
err := protectSocket(rawConnStub{fd: 42}, func(fd uintptr) bool {
return fd == 42
})
if err != nil {
t.Fatalf("protectSocket() error = %v", err)
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.26.3

require (
github.com/stretchr/testify v1.11.1
github.com/xtls/xray-core v1.260327.1-0.20260711155151-50231eaff98c
github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)
Expand Down Expand Up @@ -53,7 +53,7 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.0 // indirect
google.golang.org/grpc v1.82.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ 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/xtls/xray-core v1.260327.1-0.20260711155151-50231eaff98c h1:SbB1ez0bqZllbzaVj0PC+Vje3dRA8m/7jW1ussjDSgM=
github.com/xtls/xray-core v1.260327.1-0.20260711155151-50231eaff98c/go.mod h1:Jts8yHqPCpvsdL5CW5xMd8H9d2fkg1cILeBNqEwRXNw=
github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc h1:fkOkmgHWbF2Q8MdV9VxrsyxRz4OndcrUXUkh1ANBTg0=
github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc/go.mod h1:wukQoBGnQ6GaLTGuKwv8rCTgf80QxPj+6iznDZHQEWo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
Expand All @@ -98,8 +98,6 @@ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJ
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mobile v0.0.0-20260709172247-6129f5bee9d5 h1:Mn1OzFmF0ZKX/ZayHz/UdnWHufPp1wlD9lZ5U8LRDFY=
golang.org/x/mobile v0.0.0-20260709172247-6129f5bee9d5/go.mod h1:YX+n47s+53POxN3dx9cIGxG3hGUm/lD64hvrRJFbcSA=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
Expand All @@ -114,10 +112,12 @@ golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
Expand All @@ -128,8 +128,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-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/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/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=
Expand Down
36 changes: 36 additions & 0 deletions invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func Invoke(requestJSON string) string {
return invokeCountGeoData(request.Payload)
case LibXrayMethodPing:
return invokePing(request.Payload)
case LibXrayMethodPingBatch:
return invokePingBatch(request.Payload)
case LibXrayMethodTestXray:
return invokeTestXray(request.Payload)
case LibXrayMethodRunXray:
Expand Down Expand Up @@ -178,6 +180,40 @@ func invokePing(payload json.RawMessage) string {
return encodeInvokeResponse(&PingResponse{Delay: delay}, nil)
}

func invokePingBatch(payload json.RawMessage) string {
request, err := decodePayload[PingBatchRequest](payload)
if err != nil {
return encodeInvokeResponse(nil, err)
}

configs := make([]xray.PingBatchItem, len(request.Configs))
for i, config := range request.Configs {
configs[i] = xray.PingBatchItem{
ConfigPath: config.ConfigPath,
OutboundTag: config.OutboundTag,
}
}

results, err := xray.PingBatch(
configs,
request.Timeout,
request.URL,
)
if err != nil {
return encodeInvokeResponse(nil, err)
}

responseResults := make([]PingBatchItemResponse, len(results))
for i, result := range results {
responseResults[i] = PingBatchItemResponse{
Success: result.Success,
Delay: result.Delay,
Error: result.Error,
}
}
return encodeInvokeResponse(&PingBatchResponse{Results: responseResults}, nil)
}

func invokeTestXray(payload json.RawMessage) string {
request, err := decodePayload[RunXrayRequest](payload)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions invoke_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
LibXrayMethodConvertXrayJsonToShareLinks LibXrayMethod = "convertXrayJsonToShareLinks"
LibXrayMethodCountGeoData LibXrayMethod = "countGeoData"
LibXrayMethodPing LibXrayMethod = "ping"
LibXrayMethodPingBatch LibXrayMethod = "pingBatch"
LibXrayMethodTestXray LibXrayMethod = "testXray"
LibXrayMethodRunXray LibXrayMethod = "runXray"
LibXrayMethodRunXrayFromJson LibXrayMethod = "runXrayFromJson"
Expand Down Expand Up @@ -62,6 +63,27 @@ type PingResponse struct {
Delay int64 `json:"delay,omitempty"`
}

type PingBatchRequest struct {
Configs []PingBatchItemRequest `json:"configs,omitempty"`
Timeout int `json:"timeout,omitempty"`
URL string `json:"url,omitempty"`
}

type PingBatchItemRequest struct {
ConfigPath string `json:"configPath,omitempty"`
OutboundTag string `json:"outboundTag,omitempty"`
}

type PingBatchResponse struct {
Results []PingBatchItemResponse `json:"results,omitempty"`
}

type PingBatchItemResponse struct {
Success bool `json:"success"`
Delay int64 `json:"delay,omitempty"`
Error string `json:"error,omitempty"`
}

type RunXrayRequest struct {
ConfigPath string `json:"configPath,omitempty"`
}
Expand Down
Loading