From 43c7b52900759c5e0fbe8f10668faf2922b115a0 Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 29 Jun 2026 13:31:38 +0800 Subject: [PATCH 1/7] Remove desktop bin wrapper --- .github/workflows/build.yml | 2 - README.md | 1 + build/app/build.py | 24 ----------- build/app/linux.py | 2 - build/app/windows.py | 3 -- desktop_bin/main.go | 29 ------------- desktop_bin/route.go | 31 -------------- desktop_bin/route_linux.go | 84 ------------------------------------- desktop_bin/route_other.go | 7 ---- desktop_bin/run.go | 46 -------------------- readme/README.zh_CN.md | 1 + 11 files changed, 2 insertions(+), 228 deletions(-) delete mode 100644 desktop_bin/main.go delete mode 100644 desktop_bin/route.go delete mode 100644 desktop_bin/route_linux.go delete mode 100644 desktop_bin/route_other.go delete mode 100644 desktop_bin/run.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad70695b..f03278fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -178,13 +178,11 @@ jobs: mkdir -p "dist/${{ matrix.artifact }}" cp linux_so/libXray.so "dist/${{ matrix.artifact }}/" cp linux_so/libXray.h "dist/${{ matrix.artifact }}/" - cp bin/xray "dist/${{ matrix.artifact }}/" ;; windows) mkdir -p "dist/${{ matrix.artifact }}" cp windows_dll/libXray.dll "dist/${{ matrix.artifact }}/" cp windows_dll/libXray.h "dist/${{ matrix.artifact }}/" - cp bin/xray.exe "dist/${{ matrix.artifact }}/" ;; android) mkdir -p "dist/${{ matrix.artifact }}" diff --git a/README.md b/README.md index b36bf604..f06f5296 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ 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 tag `v26.6.27` (recorded by Go as the matching pseudo-version). Pass the optional `local` argument to use an existing local checkout at `../Xray-core` through a Go module `replace`. +Linux and Windows builds only produce the libXray shared library. Applications that need a standalone Xray executable should use official Xray-core release binaries. ### Usage diff --git a/build/app/build.py b/build/app/build.py index 67b3122b..e7b3166f 100644 --- a/build/app/build.py +++ b/build/app/build.py @@ -4,7 +4,6 @@ import subprocess from app.cmd import ( - create_dir_if_not_exists, delete_file_if_exists, delete_dir_if_exists, ) @@ -22,7 +21,6 @@ class Builder(object): def __init__(self, build_dir: str, use_local_xray_core: bool = False): self.build_dir = build_dir self.lib_dir = os.path.abspath(os.path.join(self.build_dir, "..")) - self.bin_file = "xray" self.use_local_xray_core = use_local_xray_core self.xray_core_replace_path = f"../{LOCAL_XRAY_CORE_DIR_NAME}" self.xray_core_dir = os.path.abspath( @@ -167,28 +165,6 @@ def reset_package_name(self, file_name: str): with open(file_path, "w") as f: f.writelines(new_lines) - def build_desktop_bin(self): - bin_dir = os.path.join(self.lib_dir, "bin") - create_dir_if_not_exists(bin_dir) - output_file = os.path.join(bin_dir, self.bin_file) - run_env = os.environ.copy() - run_env["CGO_ENABLED"] = "0" - - cmd = [ - "go", - "build", - "-trimpath", - "-ldflags", - "-s -w", - f"-o={output_file}", - "./desktop_bin", - ] - os.chdir(self.lib_dir) - print(cmd) - ret = subprocess.run(cmd, env=run_env) - if ret.returncode != 0: - raise Exception(f"build_desktop_bin failed") - def revert_go_env(self): os.chdir(self.lib_dir) ret = subprocess.run( diff --git a/build/app/linux.py b/build/app/linux.py index 5b5b8415..d0d69ee5 100644 --- a/build/app/linux.py +++ b/build/app/linux.py @@ -22,8 +22,6 @@ def build(self): self.before_build() self.build_linux() self.after_build() - - self.build_desktop_bin() self.revert_go_env() def build_linux(self): diff --git a/build/app/windows.py b/build/app/windows.py index e13ee204..e715d8cf 100644 --- a/build/app/windows.py +++ b/build/app/windows.py @@ -13,7 +13,6 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.dll" self.lib_header_file = "libXray.h" - self.bin_file = "xray.exe" def before_build(self): super().before_build() @@ -24,8 +23,6 @@ def build(self): self.build_windows() self.after_build() - - self.build_desktop_bin() self.revert_go_env() def build_windows(self): diff --git a/desktop_bin/main.go b/desktop_bin/main.go deleted file mode 100644 index 3edf2732..00000000 --- a/desktop_bin/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "flag" - "os" - "os/signal" - "runtime" - "runtime/debug" - "syscall" -) - -func main() { - configPath := flag.String("configPath", "config.json", "Path of config.json") - flag.Parse() - err := runXray(*configPath) - if err != nil { - os.Exit(1) - } - defer stopXray() - // Explicitly triggering GC to remove garbage from config loading. - runtime.GC() - debug.FreeOSMemory() - - { - osSignals := make(chan os.Signal, 1) - signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) - <-osSignals - } -} diff --git a/desktop_bin/route.go b/desktop_bin/route.go deleted file mode 100644 index 972b4ea1..00000000 --- a/desktop_bin/route.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -const ( - routeInitRetryAttempts = 3 - routeInitRetryInterval = 2 * time.Second - defaultTunIPv4Address = "198.18.0.1/15" - defaultTunIPv4Gateway = "198.18.0.2" - defaultIPv4Route = "0.0.0.0/0" - defaultTunIPv6Address = "fc00::1/64" - defaultTunIPv6Gateway = "fc00::2" - defaultIPv6Route = "::/0" -) - -func retryRouteInitStep(step string, fn func() error) error { - var err error - for i := 1; i <= routeInitRetryAttempts; i++ { - err = fn() - if err == nil { - return nil - } - if i < routeInitRetryAttempts { - time.Sleep(routeInitRetryInterval) - } - } - return fmt.Errorf("%s failed after %d attempts: %w", step, routeInitRetryAttempts, err) -} diff --git a/desktop_bin/route_linux.go b/desktop_bin/route_linux.go deleted file mode 100644 index 0af20ee6..00000000 --- a/desktop_bin/route_linux.go +++ /dev/null @@ -1,84 +0,0 @@ -//go:build linux - -package main - -import ( - "fmt" - "net" - - "github.com/vishvananda/netlink" -) - -// sudo ip addr add 198.18.0.1/15 dev tun0 -// sudo ip route add default via 198.18.0.2 dev tun0 metric 20 -// sudo ip -6 addr add fc00::1/64 dev tun0 -// sudo ip -6 route add default via fc00::2 dev tun0 metric 20 -func initIpRoute(tunName string, tunPriority int, enableIPv6 bool) error { - var link netlink.Link - err := retryRouteInitStep("find tun device "+tunName, func() error { - var err error - link, err = netlink.LinkByName(tunName) - return err - }) - if err != nil { - return err - } - - err = netlink.LinkSetUp(link) - if err != nil { - return fmt.Errorf("set %s up failed: %w", tunName, err) - } - - err = addAddress(link, defaultTunIPv4Address) - if err != nil { - return err - } - - err = addRoute(link.Attrs().Index, defaultIPv4Route, defaultTunIPv4Gateway, netlink.FAMILY_V4, tunPriority) - if err != nil { - return err - } - - if enableIPv6 { - err = addAddress(link, defaultTunIPv6Address) - if err != nil { - return err - } - - err = addRoute(link.Attrs().Index, defaultIPv6Route, defaultTunIPv6Gateway, netlink.FAMILY_V6, tunPriority) - if err != nil { - return err - } - } - - return nil -} - -func addAddress(link netlink.Link, address string) error { - addr, err := netlink.ParseAddr(address) - if err != nil { - return fmt.Errorf("invalid address %q: %w", address, err) - } - err = netlink.AddrAdd(link, addr) - if err != nil { - return fmt.Errorf("add address %q failed: %w", address, err) - } - return nil -} - -func addRoute(index int, cidr string, gateway string, family int, priority int) error { - _, defaultDst, err := net.ParseCIDR(cidr) - if err != nil { - return err - } - gw := net.ParseIP(gateway) - if gw == nil { - return fmt.Errorf("invalid gateway %q", gateway) - } - route := netlink.Route{Dst: defaultDst, Gw: gw, LinkIndex: index, Family: family, Priority: priority} - err = netlink.RouteAdd(&route) - if err != nil { - return err - } - return nil -} diff --git a/desktop_bin/route_other.go b/desktop_bin/route_other.go deleted file mode 100644 index 8e94505d..00000000 --- a/desktop_bin/route_other.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux - -package main - -func initIpRoute(_ string, _ int, _ bool) error { - return nil -} diff --git a/desktop_bin/run.go b/desktop_bin/run.go deleted file mode 100644 index d8446d8b..00000000 --- a/desktop_bin/run.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - - "github.com/xtls/libxray/xray" -) - -type runXrayConfig struct { - // tun - TunName string `json:"tunName,omitempty"` - TunPriority int `json:"tunPriority,omitempty"` - EnableIPv6 bool `json:"enableIPv6,omitempty"` - // xray - DatDir string `json:"datDir,omitempty"` - ConfigPath string `json:"configPath,omitempty"` - // metrics - MetricsPort string `json:"metricsPort,omitempty"` -} - -func runXray(configPath string) error { - configBytes, err := os.ReadFile(configPath) - if err != nil { - return err - } - var config runXrayConfig - err = json.Unmarshal(configBytes, &config) - if err != nil { - return err - } - - err = xray.RunXray(config.DatDir, config.ConfigPath) - if err != nil { - return err - } - err = initIpRoute(config.TunName, config.TunPriority, config.EnableIPv6) - if err != nil { - return err - } - return nil -} - -func stopXray() { - xray.StopXray() -} diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index cb462a7f..1722981d 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -18,6 +18,7 @@ 默认情况下,编译脚本不会 clone [Xray-core](https://github.com/XTLS/Xray-core),而是通过 Go modules 将 Xray-core 固定到 tag `v26.6.27`(Go 会记录为对应的 pseudo-version)。 传入可选参数 `local` 时,会通过 Go module `replace` 改用已有的本地仓库 `../Xray-core`。 +Linux 和 Windows 构建只输出 libXray 动态库。需要独立 Xray 可执行文件的应用应使用 Xray-core 官方发布的二进制。 ### 使用方式 From 03b755d784f919235a1c18e01f1b8fba847dedbf Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 29 Jun 2026 13:37:48 +0800 Subject: [PATCH 2/7] Restrict build workflow release to tag pushes --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f03278fb..189d19b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -206,6 +206,7 @@ jobs: if-no-files-found: error release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: build runs-on: ubuntu-24.04 permissions: @@ -230,6 +231,7 @@ jobs: with: name: libXray ${{ github.ref_name }} tag_name: ${{ github.ref_name }} + prerelease: true files: dist/*.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6030d69548e842352ca0236a12c57a636f1b7378 Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 29 Jun 2026 15:30:19 +0800 Subject: [PATCH 3/7] Restore desktop core wrapper --- build/app/build.py | 23 +++++++++++++++++++++++ build/app/linux.py | 2 ++ build/app/windows.py | 3 ++- desktop_bin/main.go | 29 +++++++++++++++++++++++++++++ desktop_bin/run.go | 38 ++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 desktop_bin/main.go create mode 100644 desktop_bin/run.go diff --git a/build/app/build.py b/build/app/build.py index e7b3166f..be2c62bb 100644 --- a/build/app/build.py +++ b/build/app/build.py @@ -4,6 +4,7 @@ import subprocess from app.cmd import ( + create_dir_if_not_exists, delete_file_if_exists, delete_dir_if_exists, ) @@ -142,6 +143,28 @@ def before_build(self): def build(self): pass + def build_desktop_bin(self, bin_file: str): + bin_dir = os.path.join(self.lib_dir, "bin") + create_dir_if_not_exists(bin_dir) + output_file = os.path.join(bin_dir, bin_file) + run_env = os.environ.copy() + run_env["CGO_ENABLED"] = "0" + + cmd = [ + "go", + "build", + "-trimpath", + "-ldflags", + "-s -w", + f"-o={output_file}", + "./desktop_bin", + ] + os.chdir(self.lib_dir) + print(cmd) + ret = subprocess.run(cmd, env=run_env) + if ret.returncode != 0: + raise Exception("build_desktop_bin failed") + def after_build(self): pass diff --git a/build/app/linux.py b/build/app/linux.py index d0d69ee5..181c1279 100644 --- a/build/app/linux.py +++ b/build/app/linux.py @@ -13,6 +13,7 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.so" self.lib_header_file = "libXray.h" + self.bin_file = "xray" def before_build(self): super().before_build() @@ -22,6 +23,7 @@ def build(self): self.before_build() self.build_linux() self.after_build() + self.build_desktop_bin(self.bin_file) self.revert_go_env() def build_linux(self): diff --git a/build/app/windows.py b/build/app/windows.py index e715d8cf..9cb70a45 100644 --- a/build/app/windows.py +++ b/build/app/windows.py @@ -13,6 +13,7 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.dll" self.lib_header_file = "libXray.h" + self.bin_file = "xray.exe" def before_build(self): super().before_build() @@ -21,8 +22,8 @@ def before_build(self): def build(self): self.before_build() self.build_windows() - self.after_build() + self.build_desktop_bin(self.bin_file) self.revert_go_env() def build_windows(self): diff --git a/desktop_bin/main.go b/desktop_bin/main.go new file mode 100644 index 00000000..dc4a1996 --- /dev/null +++ b/desktop_bin/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "runtime" + "runtime/debug" + "syscall" +) + +func main() { + configPath := flag.String("configPath", "config.json", "Path of core config json") + flag.Parse() + + if err := runXray(*configPath); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer stopXray() + + runtime.GC() + debug.FreeOSMemory() + + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) + <-osSignals +} diff --git a/desktop_bin/run.go b/desktop_bin/run.go new file mode 100644 index 00000000..5ef43636 --- /dev/null +++ b/desktop_bin/run.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/xtls/libxray/xray" +) + +type runXrayConfig struct { + DatDir string `json:"datDir,omitempty"` + ConfigPath string `json:"configPath,omitempty"` +} + +func runXray(configPath string) error { + configBytes, err := os.ReadFile(configPath) + if err != nil { + return err + } + + var config runXrayConfig + if err := json.Unmarshal(configBytes, &config); err != nil { + return err + } + if config.DatDir == "" { + return fmt.Errorf("datDir is required") + } + if config.ConfigPath == "" { + return fmt.Errorf("configPath is required") + } + + return xray.RunXray(config.DatDir, config.ConfigPath) +} + +func stopXray() { + _ = xray.StopXray() +} diff --git a/go.mod b/go.mod index 81f14323..df915815 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.26.3 require ( github.com/stretchr/testify v1.11.1 - github.com/vishvananda/netlink v1.3.1 github.com/xtls/xray-core v1.260327.1-0.20260627131803-45cf2898ab12 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 @@ -34,6 +33,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagernet/sing v0.5.1 // indirect github.com/sagernet/sing-shadowsocks v0.2.7 // indirect + github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect From c9d39c8bd0505490083237a1914fdfe68a4fd22a Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 29 Jun 2026 17:35:03 +0800 Subject: [PATCH 4/7] Remove desktop wrapper build --- build/app/build.py | 23 ----------------------- build/app/linux.py | 2 -- build/app/windows.py | 2 -- desktop_bin/main.go | 29 ----------------------------- desktop_bin/run.go | 38 -------------------------------------- 5 files changed, 94 deletions(-) delete mode 100644 desktop_bin/main.go delete mode 100644 desktop_bin/run.go diff --git a/build/app/build.py b/build/app/build.py index be2c62bb..e7b3166f 100644 --- a/build/app/build.py +++ b/build/app/build.py @@ -4,7 +4,6 @@ import subprocess from app.cmd import ( - create_dir_if_not_exists, delete_file_if_exists, delete_dir_if_exists, ) @@ -143,28 +142,6 @@ def before_build(self): def build(self): pass - def build_desktop_bin(self, bin_file: str): - bin_dir = os.path.join(self.lib_dir, "bin") - create_dir_if_not_exists(bin_dir) - output_file = os.path.join(bin_dir, bin_file) - run_env = os.environ.copy() - run_env["CGO_ENABLED"] = "0" - - cmd = [ - "go", - "build", - "-trimpath", - "-ldflags", - "-s -w", - f"-o={output_file}", - "./desktop_bin", - ] - os.chdir(self.lib_dir) - print(cmd) - ret = subprocess.run(cmd, env=run_env) - if ret.returncode != 0: - raise Exception("build_desktop_bin failed") - def after_build(self): pass diff --git a/build/app/linux.py b/build/app/linux.py index 181c1279..d0d69ee5 100644 --- a/build/app/linux.py +++ b/build/app/linux.py @@ -13,7 +13,6 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.so" self.lib_header_file = "libXray.h" - self.bin_file = "xray" def before_build(self): super().before_build() @@ -23,7 +22,6 @@ def build(self): self.before_build() self.build_linux() self.after_build() - self.build_desktop_bin(self.bin_file) self.revert_go_env() def build_linux(self): diff --git a/build/app/windows.py b/build/app/windows.py index 9cb70a45..7b0353ae 100644 --- a/build/app/windows.py +++ b/build/app/windows.py @@ -13,7 +13,6 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.dll" self.lib_header_file = "libXray.h" - self.bin_file = "xray.exe" def before_build(self): super().before_build() @@ -23,7 +22,6 @@ def build(self): self.before_build() self.build_windows() self.after_build() - self.build_desktop_bin(self.bin_file) self.revert_go_env() def build_windows(self): diff --git a/desktop_bin/main.go b/desktop_bin/main.go deleted file mode 100644 index dc4a1996..00000000 --- a/desktop_bin/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "os/signal" - "runtime" - "runtime/debug" - "syscall" -) - -func main() { - configPath := flag.String("configPath", "config.json", "Path of core config json") - flag.Parse() - - if err := runXray(*configPath); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - defer stopXray() - - runtime.GC() - debug.FreeOSMemory() - - osSignals := make(chan os.Signal, 1) - signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) - <-osSignals -} diff --git a/desktop_bin/run.go b/desktop_bin/run.go deleted file mode 100644 index 5ef43636..00000000 --- a/desktop_bin/run.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/xtls/libxray/xray" -) - -type runXrayConfig struct { - DatDir string `json:"datDir,omitempty"` - ConfigPath string `json:"configPath,omitempty"` -} - -func runXray(configPath string) error { - configBytes, err := os.ReadFile(configPath) - if err != nil { - return err - } - - var config runXrayConfig - if err := json.Unmarshal(configBytes, &config); err != nil { - return err - } - if config.DatDir == "" { - return fmt.Errorf("datDir is required") - } - if config.ConfigPath == "" { - return fmt.Errorf("configPath is required") - } - - return xray.RunXray(config.DatDir, config.ConfigPath) -} - -func stopXray() { - _ = xray.StopXray() -} From 85c34017171c3c92f2e17c4f0d0fe52b1ebd6d7a Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 30 Jun 2026 15:13:48 +0800 Subject: [PATCH 5/7] Refactor libXray Invoke API --- README.md | 107 ++++++++++--- build/template/main.gotemplate | 70 +-------- download_geo/main.go | 43 ++++-- geo/read.go | 134 ---------------- invoke.go | 183 ++++++++++++++++++++++ invoke_model.go | 80 ++++++++++ invoke_test.go | 270 +++++++++++++++++++++++++++++++++ nodep/model.go | 28 ---- nodep_wrapper.go | 50 ------ readme/README.zh_CN.md | 107 ++++++++++--- xray/ping.go | 4 +- xray/validation.go | 4 +- xray/xray.go | 22 +-- xray_wrapper.go | 188 ----------------------- xray_wrapper_test.go | 236 ---------------------------- 15 files changed, 743 insertions(+), 783 deletions(-) delete mode 100644 geo/read.go create mode 100644 invoke.go create mode 100644 invoke_model.go create mode 100644 invoke_test.go delete mode 100644 nodep/model.go delete mode 100644 nodep_wrapper.go delete mode 100644 xray_wrapper.go delete mode 100644 xray_wrapper_test.go diff --git a/README.md b/README.md index f06f5296..0cfadd07 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,98 @@ depend on gcc and g++. ### Windows -depend on MinGW. +depend on LLVM MinGW. -you can use winget to install [LLVM MinGW](https://github.com/mstorsjo/llvm-mingw) or [WinLibs](https://github.com/brechtsanders/winlibs_mingw) . +you can use winget to install [LLVM MinGW](https://github.com/mstorsjo/llvm-mingw). ```shell winget install MartinStorsjo.LLVM-MinGW.UCRT -winget install BrechtSanders.WinLibs.POSIX.UCRT +``` + +## API + +libXray exposes a single structured entrypoint: + +```go +func Invoke(requestJSON string) string +``` + +The C export is: + +```c +char* CGoInvoke(char* requestJSON); +``` + +The request is a JSON object: + +```json +{ + "apiVersion": 1, + "method": "runXray", + "env": { + "xray.location.config": "/path/to/config.json", + "xray.location.asset": "/path/to/dat", + "xray.location.cert": "/path/to/dat", + "xray.tun.fd": "123" + }, + "payload": { + "configPath": "/path/to/config.json" + } +} +``` + +The response is a JSON object: + +```json +{ + "success": true, + "data": {}, + "error": "" +} +``` + +`env` is optional and only supports Xray-core environment variables that are +explicitly modeled by libXray: + +| JSON key | Meaning | +| --- | --- | +| `xray.location.config` | Xray config file location | +| `xray.location.confdir` | Xray config directory location | +| `xray.location.asset` | Directory containing `geosite.dat`, `geoip.dat`, and custom GeoData files | +| `xray.location.cert` | Certificate directory used by Xray-core | +| `xray.buf.readv` | Xray-core readv buffer switch | +| `xray.buf.splice` | Xray-core splice buffer switch | +| `xray.vmess.padding` | VMess padding switch | +| `xray.cone.disabled` | Cone behavior switch | +| `xray.json.strict` | Strict JSON parsing switch | +| `xray.ray.buffer.size` | Ray buffer size | +| `xray.browser.dialer` | Browser dialer address | +| `xray.xudp.show` | XUDP log display switch | +| `xray.xudp.basekey` | XUDP base key | +| `xray.tun.fd` | TUN file descriptor for Android, iOS, and macOS packet tunnel integrations | + +Design notes: + +1. `env` is modeled as fixed fields in Go and Dart. It is not a free-form map. +2. Unknown `env` keys are ignored and are not written to the process environment. +3. `env` only sets modeled, non-empty fields. Missing fields are not unset. +4. libXray does not restore previous environment values after a method returns. Callers must pass the required env fields on every request that depends on them. This avoids concurrent calls restoring stale values over newer values. +5. `SetTunFd` has been removed. Pass `xray.tun.fd` in the `env` object of the `runXray` request. + +Supported methods: + +```text +getFreePorts +convertShareLinksToXrayJson +convertXrayJsonToShareLinks +countGeoData +ping +testXray +runXray +runXrayFromJson +stopXray +xrayVersion +getXrayState ``` ## controller @@ -121,10 +206,6 @@ Used to solve the socket protect problem on Android. Read geo files and count the categories and rules. -### read - -Read the Xray Json configuration and extract the geo file name used. - ## main Download geosite.dat and geoip.dat and count them. @@ -143,10 +224,6 @@ Write data to a file. Speed ​​test the Xray configuration. -### model - -The response body of the wrapper interface. - ### port Get free ports. @@ -225,14 +302,6 @@ Verify the Xray configuration. Start and stop Xray instances. -## nodep_wrapper - -export nodep. - -### xray_wrapper - -export xray. - # Credits [Project X](https://github.com/XTLS/Xray-core) diff --git a/build/template/main.gotemplate b/build/template/main.gotemplate index 1a930735..216320e5 100644 --- a/build/template/main.gotemplate +++ b/build/template/main.gotemplate @@ -4,70 +4,8 @@ import "C" func main() {} -//export CGoSetTunFd -func CGoSetTunFd(fd C.int) { - SetTunFd(int32(fd)) -} - -//export CGoRunXrayFromJSON -func CGoRunXrayFromJSON(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(RunXrayFromJSON(text)) -} - -//export CGoGetFreePorts -func CGoGetFreePorts(count int) *C.char { - return C.CString(GetFreePorts(count)) -} - -//export CGoConvertShareLinksToXrayJson -func CGoConvertShareLinksToXrayJson(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(ConvertShareLinksToXrayJson(text)) -} - -//export CGOConvertXrayJsonToShareLinks -func CGOConvertXrayJsonToShareLinks(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(ConvertXrayJsonToShareLinks(text)) -} - -//export CGoCountGeoData -func CGoCountGeoData(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(CountGeoData(text)) -} - -//export CGoReadGeoFiles -func CGoReadGeoFiles(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(ReadGeoFiles(text)) -} - -//export CGoPing -func CGoPing(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(Ping(text)) -} - -//export CGoTestXray -func CGoTestXray(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(TestXray(text)) -} - -//export CGoRunXray -func CGoRunXray(base64Text *C.char) *C.char { - text := C.GoString(base64Text) - return C.CString(RunXray(text)) -} - -//export CGoStopXray -func CGoStopXray() *C.char { - return C.CString(StopXray()) -} - -//export CGoXrayVersion -func CGoXrayVersion() *C.char { - return C.CString(XrayVersion()) +//export CGoInvoke +func CGoInvoke(requestJSON *C.char) *C.char { + text := C.GoString(requestJSON) + return C.CString(Invoke(text)) } diff --git a/download_geo/main.go b/download_geo/main.go index 54f109e5..b202c126 100644 --- a/download_geo/main.go +++ b/download_geo/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/base64" "encoding/json" "fmt" "io" @@ -15,6 +14,11 @@ import ( "github.com/xtls/libxray/nodep" ) +type invokeResponseEnvelope struct { + Success bool `json:"success"` + Err string `json:"error,omitempty"` +} + func ensureDir(dir string) error { if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.Mkdir(dir, os.ModePerm); err != nil { @@ -52,27 +56,34 @@ func saveTimestamp(datDir string, fileName string) error { return nodep.WriteText(tsText, tsPath) } -func parseCallResponse(text string) (nodep.CallResponse[string], error) { - var response nodep.CallResponse[string] - decoded, err := base64.StdEncoding.DecodeString(text) - if err != nil { - return response, err - } - err = json.Unmarshal(decoded, &response) +func parseInvokeResponse(text string) (invokeResponseEnvelope, error) { + var response invokeResponseEnvelope + err := json.Unmarshal([]byte(text), &response) return response, err } func makeLoadGeoDataRequest(datDir string, name string, geoType string) (string, error) { - var request libXray.CountGeoDataRequest - request.DatDir = datDir - request.Name = name - request.GeoType = geoType - + payload, err := json.Marshal(&libXray.CountGeoDataRequest{ + Name: name, + GeoType: geoType, + }) + if err != nil { + return "", err + } + request := libXray.LibXrayInvokeRequest{ + APIVersion: 1, + Method: libXray.LibXrayMethodCountGeoData, + Env: &libXray.LibXrayEnvJson{ + AssetLocation: datDir, + CertLocation: datDir, + }, + Payload: payload, + } data, err := json.Marshal(&request) if err != nil { return "", err } - return base64.StdEncoding.EncodeToString(data), nil + return string(data), nil } func downloadDat(url string, datDir string, fileName string, geoType string) { @@ -89,8 +100,8 @@ func downloadDat(url string, datDir string, fileName string, geoType string) { fmt.Println(err) os.Exit(1) } - res := libXray.CountGeoData(geoReq) - resp, err := parseCallResponse(res) + res := libXray.Invoke(geoReq) + resp, err := parseInvokeResponse(res) if err != nil || !resp.Success { fmt.Println("Failed to load geosite:", url, res, resp.Err) os.Exit(1) diff --git a/geo/read.go b/geo/read.go deleted file mode 100644 index 4a31aaf5..00000000 --- a/geo/read.go +++ /dev/null @@ -1,134 +0,0 @@ -package geo - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/xtls/xray-core/infra/conf" -) - -// Read all geo files in config file. -// configPath means where xray config file is. -func ReadGeoFiles(xrayBytes []byte) ([]string, []string) { - domain, ip := loadXrayConfig(xrayBytes) - domainCodes := filterAndStrip(domain, "geosite") - domainFiles := []string{} - for key := range domainCodes { - domainFiles = append(domainFiles, key) - } - - ipCodes := filterAndStrip(ip, "geoip") - ipFiles := []string{} - for key := range ipCodes { - ipFiles = append(ipFiles, key) - } - - return domainFiles, ipFiles -} - -func loadXrayConfig(configBytes []byte) ([]string, []string) { - domain := []string{} - ip := []string{} - - var xray *conf.Config - err := json.Unmarshal(configBytes, &xray) - if err != nil { - return domain, ip - } - - routingDomain, routingIP := filterRouting(xray) - domain = append(domain, routingDomain...) - ip = append(ip, routingIP...) - - dnsDomain, dnsIP := filterDns(xray) - domain = append(domain, dnsDomain...) - ip = append(ip, dnsIP...) - - return domain, ip -} - -func filterRouting(xray *conf.Config) ([]string, []string) { - domain := []string{} - ip := []string{} - - routing := xray.RouterConfig - if routing == nil { - return domain, ip - } - rules := routing.RuleList - if len(rules) == 0 { - return domain, ip - } - // parse rules - // we only care about domain and ip - type RawRule struct { - Domain *conf.StringList `json:"domain"` - IP *conf.StringList `json:"ip"` - } - - for _, rule := range rules { - var rawRule RawRule - err := json.Unmarshal(rule, &rawRule) - if err != nil { - continue - } - if rawRule.Domain != nil { - domain = append(domain, *rawRule.Domain...) - } - if rawRule.IP != nil { - ip = append(ip, *rawRule.IP...) - } - } - return domain, ip -} - -func filterDns(xray *conf.Config) ([]string, []string) { - domain := []string{} - ip := []string{} - - dns := xray.DNSConfig - if dns == nil { - return domain, ip - } - servers := dns.Servers - if len(servers) == 0 { - return domain, ip - } - - for _, server := range servers { - if len(server.Domains) > 0 { - domain = append(domain, server.Domains...) - } - if len(server.ExpectIPs) > 0 { - ip = append(ip, server.ExpectIPs...) - } - } - return domain, ip -} - -func filterAndStrip(rules []string, retain string) map[string][]string { - m := make(map[string][]string) - retainPrefix := fmt.Sprintf("%s:", retain) - retainFile := fmt.Sprintf("%s.dat", retain) - for _, rule := range rules { - if strings.HasPrefix(rule, retainPrefix) { - values := strings.SplitN(rule, ":", 2) - appendMap(m, retainFile, values[1]) - } else if strings.HasPrefix(rule, "ext:") { - values := strings.SplitN(rule, ":", 3) - appendMap(m, values[1], values[2]) - } - } - return m -} - -func appendMap(m map[string][]string, key string, value string) { - v, ok := m[key] - if ok { - v = append(v, value) - } else { - v = []string{value} - } - m[key] = v -} diff --git a/invoke.go b/invoke.go new file mode 100644 index 00000000..6d21a6b0 --- /dev/null +++ b/invoke.go @@ -0,0 +1,183 @@ +package libXray + +import ( + "encoding/json" + "errors" + "os" + + "github.com/xtls/libxray/geo" + "github.com/xtls/libxray/nodep" + "github.com/xtls/libxray/share" + "github.com/xtls/libxray/xray" + "github.com/xtls/xray-core/common/platform" +) + +type invokeResponse struct { + Success bool `json:"success"` + Data any `json:"data"` + Err string `json:"error"` +} + +func Invoke(requestJSON string) string { + var request LibXrayInvokeRequest + if err := json.Unmarshal([]byte(requestJSON), &request); err != nil { + return encodeInvokeResponse(nil, err) + } + applyEnv(request.Env) + + switch request.Method { + case LibXrayMethodGetFreePorts: + return invokeGetFreePorts(request.Payload) + case LibXrayMethodConvertShareLinksToXrayJson: + return invokeConvertShareLinksToXrayJson(request.Payload) + case LibXrayMethodConvertXrayJsonToShareLinks: + return invokeConvertXrayJsonToShareLinks(request.Payload) + case LibXrayMethodCountGeoData: + return invokeCountGeoData(request.Payload) + case LibXrayMethodPing: + return invokePing(request.Payload) + case LibXrayMethodTestXray: + return invokeTestXray(request.Payload) + case LibXrayMethodRunXray: + return invokeRunXray(request.Payload) + case LibXrayMethodRunXrayFromJson: + return invokeRunXrayFromJSON(request.Payload) + case LibXrayMethodStopXray: + return encodeInvokeResponse("", xray.StopXray()) + case LibXrayMethodXrayVersion: + return encodeInvokeResponse(xray.XrayVersion(), nil) + case LibXrayMethodGetXrayState: + return encodeInvokeResponse(xray.GetXrayState(), nil) + default: + return encodeInvokeResponse(nil, errors.New("unknown method")) + } +} + +func applyEnv(env *LibXrayEnvJson) { + if env == nil { + return + } + setEnvIfNotEmpty(platform.ConfigLocation, env.ConfigLocation) + setEnvIfNotEmpty(platform.ConfdirLocation, env.ConfdirLocation) + setEnvIfNotEmpty(platform.AssetLocation, env.AssetLocation) + setEnvIfNotEmpty(platform.CertLocation, env.CertLocation) + setEnvIfNotEmpty(platform.UseReadV, env.UseReadV) + setEnvIfNotEmpty(platform.UseFreedomSplice, env.UseFreedomSplice) + setEnvIfNotEmpty(platform.UseVmessPadding, env.UseVmessPadding) + setEnvIfNotEmpty(platform.UseCone, env.UseCone) + setEnvIfNotEmpty(platform.UseStrictJSON, env.UseStrictJSON) + setEnvIfNotEmpty(platform.BufferSize, env.BufferSize) + setEnvIfNotEmpty(platform.BrowserDialerAddress, env.BrowserDialerAddress) + setEnvIfNotEmpty(platform.XUDPLog, env.XUDPLog) + setEnvIfNotEmpty(platform.XUDPBaseKey, env.XUDPBaseKey) + setEnvIfNotEmpty(platform.TunFdKey, env.TunFd) +} + +func setEnvIfNotEmpty(key string, value string) { + if value != "" { + _ = os.Setenv(key, value) + } +} + +func decodePayload[T any](payload json.RawMessage) (T, error) { + var request T + if len(payload) == 0 { + return request, nil + } + err := json.Unmarshal(payload, &request) + return request, err +} + +func encodeInvokeResponse(data any, err error) string { + response := invokeResponse{Data: data} + if err != nil { + response.Success = false + response.Err = err.Error() + } else { + response.Success = true + } + raw, err := json.Marshal(&response) + if err != nil { + return `{"success":false,"error":"failed to encode response"}` + } + return string(raw) +} + +func invokeGetFreePorts(payload json.RawMessage) string { + request, err := decodePayload[GetFreePortsRequest](payload) + if err != nil { + return encodeInvokeResponse(nil, err) + } + ports, err := nodep.GetFreePorts(request.Count) + if err != nil { + return encodeInvokeResponse(nil, err) + } + return encodeInvokeResponse(&getFreePortsResponse{Ports: ports}, nil) +} + +func invokeConvertShareLinksToXrayJson(payload json.RawMessage) string { + request, err := decodePayload[ConvertShareLinksToXrayJsonRequest](payload) + if err != nil { + return encodeInvokeResponse(nil, err) + } + xrayJson, err := share.ConvertShareLinksToXrayJson(request.Text) + return encodeInvokeResponse(xrayJson, err) +} + +func invokeConvertXrayJsonToShareLinks(payload json.RawMessage) string { + request, err := decodePayload[ConvertXrayJsonToShareLinksRequest](payload) + if err != nil { + return encodeInvokeResponse("", err) + } + links, err := share.ConvertXrayJsonToShareLinks([]byte(request.XrayJson)) + return encodeInvokeResponse(links, err) +} + +func invokeCountGeoData(payload json.RawMessage) string { + request, err := decodePayload[CountGeoDataRequest](payload) + if err != nil { + return encodeInvokeResponse("", err) + } + datDir := platform.NewEnvFlag(platform.AssetLocation).GetValue(func() string { return "" }) + if datDir == "" { + return encodeInvokeResponse("", errors.New("missing xray.location.asset")) + } + err = geo.CountGeoData(datDir, request.Name, request.GeoType) + return encodeInvokeResponse("", err) +} + +func invokePing(payload json.RawMessage) string { + request, err := decodePayload[PingRequest](payload) + if err != nil { + return encodeInvokeResponse(nodep.PingDelayError, err) + } + delay, err := xray.Ping(request.ConfigPath, request.Timeout, request.URL, request.Proxy) + return encodeInvokeResponse(delay, err) +} + +func invokeTestXray(payload json.RawMessage) string { + request, err := decodePayload[RunXrayRequest](payload) + if err != nil { + return encodeInvokeResponse("", err) + } + err = xray.TestXray(request.ConfigPath) + return encodeInvokeResponse("", err) +} + +func invokeRunXray(payload json.RawMessage) string { + request, err := decodePayload[RunXrayRequest](payload) + if err != nil { + return encodeInvokeResponse("", err) + } + err = xray.RunXray(request.ConfigPath) + return encodeInvokeResponse("", err) +} + +func invokeRunXrayFromJSON(payload json.RawMessage) string { + request, err := decodePayload[RunXrayFromJSONRequest](payload) + if err != nil { + return encodeInvokeResponse("", err) + } + err = xray.RunXrayFromJSON(request.ConfigJSON) + return encodeInvokeResponse("", err) +} diff --git a/invoke_model.go b/invoke_model.go new file mode 100644 index 00000000..f5fd90fd --- /dev/null +++ b/invoke_model.go @@ -0,0 +1,80 @@ +// libXray is an Xray wrapper focusing on improving the experience of Xray-core mobile development. +package libXray + +import "encoding/json" + +type LibXrayMethod string + +const ( + LibXrayMethodGetFreePorts LibXrayMethod = "getFreePorts" + LibXrayMethodConvertShareLinksToXrayJson LibXrayMethod = "convertShareLinksToXrayJson" + LibXrayMethodConvertXrayJsonToShareLinks LibXrayMethod = "convertXrayJsonToShareLinks" + LibXrayMethodCountGeoData LibXrayMethod = "countGeoData" + LibXrayMethodPing LibXrayMethod = "ping" + LibXrayMethodTestXray LibXrayMethod = "testXray" + LibXrayMethodRunXray LibXrayMethod = "runXray" + LibXrayMethodRunXrayFromJson LibXrayMethod = "runXrayFromJson" + LibXrayMethodStopXray LibXrayMethod = "stopXray" + LibXrayMethodXrayVersion LibXrayMethod = "xrayVersion" + LibXrayMethodGetXrayState LibXrayMethod = "getXrayState" +) + +type LibXrayInvokeRequest struct { + APIVersion int `json:"apiVersion,omitempty"` + Method LibXrayMethod `json:"method,omitempty"` + Env *LibXrayEnvJson `json:"env,omitempty"` + Payload json.RawMessage `json:"payload,omitempty"` +} + +type LibXrayEnvJson struct { + ConfigLocation string `json:"xray.location.config,omitempty"` + ConfdirLocation string `json:"xray.location.confdir,omitempty"` + AssetLocation string `json:"xray.location.asset,omitempty"` + CertLocation string `json:"xray.location.cert,omitempty"` + UseReadV string `json:"xray.buf.readv,omitempty"` + UseFreedomSplice string `json:"xray.buf.splice,omitempty"` + UseVmessPadding string `json:"xray.vmess.padding,omitempty"` + UseCone string `json:"xray.cone.disabled,omitempty"` + UseStrictJSON string `json:"xray.json.strict,omitempty"` + BufferSize string `json:"xray.ray.buffer.size,omitempty"` + BrowserDialerAddress string `json:"xray.browser.dialer,omitempty"` + XUDPLog string `json:"xray.xudp.show,omitempty"` + XUDPBaseKey string `json:"xray.xudp.basekey,omitempty"` + TunFd string `json:"xray.tun.fd,omitempty"` +} + +type GetFreePortsRequest struct { + Count int `json:"count,omitempty"` +} + +type getFreePortsResponse struct { + Ports []int `json:"ports,omitempty"` +} + +type ConvertShareLinksToXrayJsonRequest struct { + Text string `json:"text,omitempty"` +} + +type ConvertXrayJsonToShareLinksRequest struct { + XrayJson string `json:"xrayJson,omitempty"` +} + +type CountGeoDataRequest struct { + Name string `json:"name,omitempty"` + GeoType string `json:"geoType,omitempty"` +} + +type PingRequest struct { + ConfigPath string `json:"configPath,omitempty"` + Timeout int `json:"timeout,omitempty"` + URL string `json:"url,omitempty"` + Proxy string `json:"proxy,omitempty"` +} + +type RunXrayRequest struct { + ConfigPath string `json:"configPath,omitempty"` +} + +type RunXrayFromJSONRequest struct { + ConfigJSON string `json:"configJSON,omitempty"` +} diff --git a/invoke_test.go b/invoke_test.go new file mode 100644 index 00000000..b928ec49 --- /dev/null +++ b/invoke_test.go @@ -0,0 +1,270 @@ +package libXray + +import ( + "encoding/base64" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/xtls/xray-core/common/platform" +) + +type testResponse struct { + Success bool `json:"success"` + Data json.RawMessage `json:"data,omitempty"` + Err string `json:"error,omitempty"` +} + +func invokeForTest(t *testing.T, method LibXrayMethod, env *LibXrayEnvJson, payload any) testResponse { + t.Helper() + rawPayload, err := json.Marshal(payload) + if err != nil { + t.Fatal(err) + } + rawRequest, err := json.Marshal(&LibXrayInvokeRequest{ + APIVersion: 1, + Method: method, + Env: env, + Payload: rawPayload, + }) + if err != nil { + t.Fatal(err) + } + var response testResponse + if err := json.Unmarshal([]byte(Invoke(string(rawRequest))), &response); err != nil { + t.Fatal(err) + } + return response +} + +func writeConfigToFile(t *testing.T, config any, path string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + t.Fatal(err) + } + file, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer file.Close() + if err := json.NewEncoder(file).Encode(config); err != nil { + t.Fatal(err) + } +} + +func testVmessPayloadBase64() string { + const vmessJSON = `{"add":"127.0.0.1","port":1080,"id":"00000000-0000-0000-0000-000000000000","aid":0,"scy":"auto"}` + return base64.StdEncoding.EncodeToString([]byte(vmessJSON)) +} + +func testXrayConfig(t *testing.T) any { + t.Helper() + decoded, err := base64.StdEncoding.DecodeString(testVmessPayloadBase64()) + if err != nil { + t.Fatal(err) + } + var vmessConfig struct { + Add string `json:"add"` + Port int `json:"port"` + ID string `json:"id"` + AID int `json:"aid"` + Scy string `json:"scy"` + } + if err := json.Unmarshal(decoded, &vmessConfig); err != nil { + t.Fatal(err) + } + return struct { + Log any `json:"log"` + Inbounds []any `json:"inbounds"` + Outbounds []any `json:"outbounds"` + }{ + Log: struct { + Loglevel string `json:"loglevel"` + }{Loglevel: "debug"}, + Inbounds: []any{ + struct { + Port int `json:"port"` + Protocol string `json:"protocol"` + Settings any `json:"settings"` + }{ + Port: 1080, + Protocol: "socks", + Settings: struct { + Auth string `json:"auth"` + }{Auth: "noauth"}, + }, + }, + Outbounds: []any{ + struct { + Protocol string `json:"protocol"` + Settings any `json:"settings"` + }{ + Protocol: "vmess", + Settings: struct { + Vnext []any `json:"vnext"` + }{ + Vnext: []any{ + struct { + Address string `json:"address"` + Port int `json:"port"` + Users []any `json:"users"` + }{ + Address: vmessConfig.Add, + Port: vmessConfig.Port, + Users: []any{ + struct { + ID string `json:"id"` + AlterID int `json:"alterId"` + Security string `json:"security"` + }{ + ID: vmessConfig.ID, + AlterID: vmessConfig.AID, + Security: vmessConfig.Scy, + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestInvokeTestXray(t *testing.T) { + projectRoot, _ := filepath.Abs(".") + configPath := filepath.Join(projectRoot, "config", "xray_config_test.json") + writeConfigToFile(t, testXrayConfig(t), configPath) + + response := invokeForTest( + t, + LibXrayMethodTestXray, + &LibXrayEnvJson{AssetLocation: filepath.Join(projectRoot, "dat"), CertLocation: filepath.Join(projectRoot, "dat")}, + RunXrayRequest{ConfigPath: configPath}, + ) + if !response.Success { + t.Fatalf("TestXray failed: %s", response.Err) + } +} + +func TestInvokeRunXray(t *testing.T) { + projectRoot, _ := filepath.Abs(".") + configPath := filepath.Join(projectRoot, "config", "xray_config_run.json") + writeConfigToFile(t, testXrayConfig(t), configPath) + + response := invokeForTest( + t, + LibXrayMethodRunXray, + &LibXrayEnvJson{AssetLocation: filepath.Join(projectRoot, "dat"), CertLocation: filepath.Join(projectRoot, "dat")}, + RunXrayRequest{ConfigPath: configPath}, + ) + defer xrayStopForTest(t) + if !response.Success { + t.Fatalf("RunXray failed: %s", response.Err) + } +} + +func TestInvokeXrayVersion(t *testing.T) { + response := invokeForTest(t, LibXrayMethodXrayVersion, nil, nil) + if !response.Success { + t.Fatalf("XrayVersion failed: %s", response.Err) + } + var version string + if err := json.Unmarshal(response.Data, &version); err != nil { + t.Fatal(err) + } + if version == "" { + t.Fatal("Xray version should not be empty") + } +} + +func TestInvokeEnvOnlySetsProvidedFields(t *testing.T) { + t.Setenv(platform.AssetLocation, "initial-asset") + t.Setenv(platform.CertLocation, "initial-cert") + response := invokeForTest( + t, + LibXrayMethodXrayVersion, + &LibXrayEnvJson{AssetLocation: "updated-asset"}, + nil, + ) + if !response.Success { + t.Fatalf("XrayVersion failed: %s", response.Err) + } + if got := os.Getenv(platform.AssetLocation); got != "updated-asset" { + t.Fatalf("asset env = %q", got) + } + if got := os.Getenv(platform.CertLocation); got != "initial-cert" { + t.Fatalf("cert env = %q", got) + } +} + +func TestInvokeSetsAllSupportedEnvFields(t *testing.T) { + tests := []struct { + name string + key string + value string + }{ + {"config", platform.ConfigLocation, "config-value"}, + {"confdir", platform.ConfdirLocation, "confdir-value"}, + {"asset", platform.AssetLocation, "asset-value"}, + {"cert", platform.CertLocation, "cert-value"}, + {"readv", platform.UseReadV, "readv-value"}, + {"splice", platform.UseFreedomSplice, "splice-value"}, + {"vmess padding", platform.UseVmessPadding, "vmess-padding-value"}, + {"cone", platform.UseCone, "cone-value"}, + {"strict json", platform.UseStrictJSON, "strict-json-value"}, + {"buffer size", platform.BufferSize, "buffer-size-value"}, + {"browser dialer", platform.BrowserDialerAddress, "browser-dialer-value"}, + {"xudp log", platform.XUDPLog, "xudp-log-value"}, + {"xudp base key", platform.XUDPBaseKey, "xudp-base-key-value"}, + {"tun fd", platform.TunFdKey, "123"}, + } + for _, tt := range tests { + t.Setenv(tt.key, "") + t.Run(tt.name, func(t *testing.T) { + requestJSON := `{"apiVersion":1,"method":"xrayVersion","env":{"` + tt.key + `":"` + tt.value + `"}}` + var response testResponse + if err := json.Unmarshal([]byte(Invoke(requestJSON)), &response); err != nil { + t.Fatal(err) + } + if !response.Success { + t.Fatalf("XrayVersion failed: %s", response.Err) + } + if got := os.Getenv(tt.key); got != tt.value { + t.Fatalf("%s = %q", tt.key, got) + } + }) + } +} + +func TestInvokeUnknownMethod(t *testing.T) { + response := invokeForTest(t, LibXrayMethod("unknown"), nil, nil) + if response.Success { + t.Fatal("unknown method should fail") + } +} + +func TestInvokeIgnoresUnknownEnvField(t *testing.T) { + const key = "XRAY_LIBXRAY_UNKNOWN_ENV_TEST" + _ = os.Unsetenv(key) + t.Cleanup(func() { _ = os.Unsetenv(key) }) + requestJSON := `{"apiVersion":1,"method":"xrayVersion","env":{"` + key + `":"/tmp"}}` + var response testResponse + if err := json.Unmarshal([]byte(Invoke(requestJSON)), &response); err != nil { + t.Fatal(err) + } + if !response.Success { + t.Fatalf("unknown env field should be ignored: %s", response.Err) + } + if _, found := os.LookupEnv(key); found { + t.Fatal("unknown env field should not be set") + } +} + +func xrayStopForTest(t *testing.T) { + t.Helper() + response := invokeForTest(t, LibXrayMethodStopXray, nil, nil) + if !response.Success { + t.Fatalf("StopXray failed: %s", response.Err) + } +} diff --git a/nodep/model.go b/nodep/model.go deleted file mode 100644 index 84688b82..00000000 --- a/nodep/model.go +++ /dev/null @@ -1,28 +0,0 @@ -package nodep - -import ( - "encoding/base64" - "encoding/json" -) - -type CallResponse[T any] struct { - Success bool `json:"success"` - Data T `json:"data,omitempty"` - Err string `json:"error,omitempty"` -} - -func (response CallResponse[T]) EncodeToBase64(data T, err error) string { - response.Data = data - if err != nil { - response.Success = false - response.Err = err.Error() - } else { - response.Success = true - } - - jsonData, err := json.Marshal(&response) - if err != nil { - return "" - } - return base64.StdEncoding.EncodeToString(jsonData) -} diff --git a/nodep_wrapper.go b/nodep_wrapper.go deleted file mode 100644 index 2cd63b49..00000000 --- a/nodep_wrapper.go +++ /dev/null @@ -1,50 +0,0 @@ -package libXray - -import ( - "encoding/base64" - - "github.com/xtls/libxray/nodep" - "github.com/xtls/libxray/share" - "github.com/xtls/xray-core/infra/conf" -) - -type getFreePortsResponse struct { - Ports []int `json:"ports,omitempty"` -} - -// Wrapper of nodep.GetFreePorts -// count means how many ports you need. -func GetFreePorts(count int) string { - var response nodep.CallResponse[*getFreePortsResponse] - ports, err := nodep.GetFreePorts(count) - if err != nil { - return response.EncodeToBase64(nil, err) - } - var res getFreePortsResponse - res.Ports = ports - return response.EncodeToBase64(&res, nil) -} - -// Convert share text to XrayJson -// support XrayJson, v2rayN plain text, v2rayN base64 text, Clash.Meta yaml -func ConvertShareLinksToXrayJson(base64Text string) string { - var response nodep.CallResponse[*conf.Config] - links, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64(nil, err) - } - xrayJson, err := share.ConvertShareLinksToXrayJson(string(links)) - return response.EncodeToBase64(xrayJson, err) -} - -// Convert XrayJson to share links. -// VMess will generate VMessAEAD link. -func ConvertXrayJsonToShareLinks(base64Text string) string { - var response nodep.CallResponse[string] - xray, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64("", err) - } - links, err := share.ConvertXrayJsonToShareLinks(xray) - return response.EncodeToBase64(links, err) -} diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index 1722981d..5adda226 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -70,13 +70,97 @@ python3 build/main.py windows local ### Windows -依赖 MinGW 。 +依赖 LLVM MinGW 。 -你可使用 winget 安装 [LLVM MinGW](https://github.com/mstorsjo/llvm-mingw) 或 [WinLibs](https://github.com/brechtsanders/winlibs_mingw) 。 +你可使用 winget 安装 [LLVM MinGW](https://github.com/mstorsjo/llvm-mingw)。 ```shell winget install MartinStorsjo.LLVM-MinGW.UCRT -winget install BrechtSanders.WinLibs.POSIX.UCRT +``` + +## API + +libXray 只暴露一个结构化入口: + +```go +func Invoke(requestJSON string) string +``` + +C 导出为: + +```c +char* CGoInvoke(char* requestJSON); +``` + +请求是 JSON 对象: + +```json +{ + "apiVersion": 1, + "method": "runXray", + "env": { + "xray.location.config": "/path/to/config.json", + "xray.location.asset": "/path/to/dat", + "xray.location.cert": "/path/to/dat", + "xray.tun.fd": "123" + }, + "payload": { + "configPath": "/path/to/config.json" + } +} +``` + +响应是 JSON 对象: + +```json +{ + "success": true, + "data": {}, + "error": "" +} +``` + +`env` 是可选字段,只支持 libXray 显式建模的 Xray-core 环境变量: + +| JSON key | 含义 | +| --- | --- | +| `xray.location.config` | Xray 配置文件路径 | +| `xray.location.confdir` | Xray 配置目录路径 | +| `xray.location.asset` | 保存 `geosite.dat`、`geoip.dat` 和自定义 GeoData 的目录 | +| `xray.location.cert` | Xray-core 使用的证书目录 | +| `xray.buf.readv` | Xray-core readv buffer 开关 | +| `xray.buf.splice` | Xray-core splice buffer 开关 | +| `xray.vmess.padding` | VMess padding 开关 | +| `xray.cone.disabled` | Cone 行为开关 | +| `xray.json.strict` | 严格 JSON 解析开关 | +| `xray.ray.buffer.size` | Ray buffer size | +| `xray.browser.dialer` | Browser dialer 地址 | +| `xray.xudp.show` | XUDP 日志显示开关 | +| `xray.xudp.basekey` | XUDP base key | +| `xray.tun.fd` | Android、iOS、macOS Packet Tunnel 使用的 TUN 文件描述符 | + +设计决定: + +1. `env` 在 Go 和 Dart 侧都是固定字段 model,不是自由 map。 +2. 未知 `env` key 会被忽略,不会写入进程环境变量。 +3. `env` 只设置已建模的非空字段,缺失字段不会 unset。 +4. libXray 不会在 method 结束后 restore 旧环境变量。调用方必须在每次依赖环境变量的请求中显式传入对应字段。这样可以避免并发调用时,一个请求恢复旧值覆盖另一个请求的新值。 +5. `SetTunFd` 已删除。请在 `runXray` 请求的 `env` 对象中传入 `xray.tun.fd`。 + +支持的 method: + +```text +getFreePorts +convertShareLinksToXrayJson +convertXrayJsonToShareLinks +countGeoData +ping +testXray +runXray +runXrayFromJson +stopXray +xrayVersion +getXrayState ``` ## controller @@ -89,10 +173,6 @@ winget install BrechtSanders.WinLibs.POSIX.UCRT 读取 geo 文件,并对分类和规则进行计数。 -### read - -读取 Xray Json 配置,提取使用到的 geo 文件名。 - ## main 下载 geosite.dat 和 geoip.dat,并进行计数。 @@ -111,10 +191,6 @@ winget install BrechtSanders.WinLibs.POSIX.UCRT 对 Xray 配置进行测速。 -### model - -包装接口的响应体。 - ### port 获取空闲端口。 @@ -194,15 +270,6 @@ http://localhost:49227/debug/vars 启动和停止 Xray 实例。 - -## nodep_wrapper - -导出 nodep 。 - -### xray_wrapper - -导出 xray 。 - # 致谢 [Project X](https://github.com/XTLS/Xray-core) diff --git a/xray/ping.go b/xray/ping.go index ff03b331..0aaaae07 100644 --- a/xray/ping.go +++ b/xray/ping.go @@ -5,13 +5,11 @@ import ( ) // Ping Xray config and find the delay and country code of its outbound. -// datDir means the dir which geosite.dat and geoip.dat are in. // configPath means the config.json file path. // timeout means how long the http request will be cancelled if no response, in units of seconds. // url means the website we use to test speed. "https://www.google.com" is a good choice for most cases. // proxy means the local http/socks5 proxy, like "socks5://[::1]:1080". -func Ping(datDir string, configPath string, timeout int, url string, proxy string) (int64, error) { - InitEnv(datDir) +func Ping(configPath string, timeout int, url string, proxy string) (int64, error) { server, err := StartXray(configPath) if err != nil { return nodep.PingDelayError, err diff --git a/xray/validation.go b/xray/validation.go index e6e3f21e..f7e2c1e1 100644 --- a/xray/validation.go +++ b/xray/validation.go @@ -1,10 +1,8 @@ package xray // Test Xray Config. -// datDir means the dir which geosite.dat and geoip.dat are in. // configPath means the config.json file path. -func TestXray(datDir string, configPath string) error { - InitEnv(datDir) +func TestXray(configPath string) error { server, err := StartXray(configPath) if err != nil { return err diff --git a/xray/xray.go b/xray/xray.go index 03a949a8..3dcea0c9 100644 --- a/xray/xray.go +++ b/xray/xray.go @@ -1,13 +1,10 @@ package xray import ( - "os" "runtime/debug" - "strconv" "github.com/xtls/libxray/memory" "github.com/xtls/xray-core/common/cmdarg" - "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/core" _ "github.com/xtls/xray-core/main/distro/all" ) @@ -44,22 +41,9 @@ func StartXrayFromJSON(configJSON string) (*core.Instance, error) { return server, nil } -// SetTunFd sets the TUN file descriptor. -// Call this BEFORE RunXray/RunXrayFromJSON. -func SetTunFd(fd int32) { - os.Setenv(platform.TunFdKey, strconv.Itoa(int(fd))) -} - -func InitEnv(datDir string) { - os.Setenv(platform.AssetLocation, datDir) - os.Setenv(platform.CertLocation, datDir) -} - // Run Xray instance. -// datDir means the dir which geosite.dat and geoip.dat are in. // configPath means the config.json file path. -func RunXray(datDir, configPath string) (err error) { - InitEnv(datDir) +func RunXray(configPath string) (err error) { memory.InitForceFree() coreServer, err = StartXray(configPath) if err != nil { @@ -75,10 +59,8 @@ func RunXray(datDir, configPath string) (err error) { } // Run Xray instance with JSON configuration string. -// datDir means the dir which geosite.dat and geoip.dat are in. // configJSON means the JSON configuration string. -func RunXrayFromJSON(datDir, configJSON string) (err error) { - InitEnv(datDir) +func RunXrayFromJSON(configJSON string) (err error) { memory.InitForceFree() coreServer, err = StartXrayFromJSON(configJSON) if err != nil { diff --git a/xray_wrapper.go b/xray_wrapper.go deleted file mode 100644 index 43011e8b..00000000 --- a/xray_wrapper.go +++ /dev/null @@ -1,188 +0,0 @@ -// libXray is an Xray wrapper focusing on improving the experience of Xray-core mobile development. -package libXray - -import ( - "encoding/base64" - "encoding/json" - - "github.com/xtls/libxray/geo" - "github.com/xtls/libxray/nodep" - "github.com/xtls/libxray/xray" -) - -// SetTunFd sets the TUN file descriptor. -// Call this BEFORE RunXray/RunXrayFromJSON. -func SetTunFd(fd int32) { - xray.SetTunFd(fd) -} - -type CountGeoDataRequest struct { - DatDir string `json:"datDir,omitempty"` - Name string `json:"name,omitempty"` - GeoType string `json:"geoType,omitempty"` -} - -// Read geo data and write all codes to text file. -func CountGeoData(base64Text string) string { - var response nodep.CallResponse[string] - req, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64("", err) - } - var request CountGeoDataRequest - err = json.Unmarshal(req, &request) - if err != nil { - return response.EncodeToBase64("", err) - } - err = geo.CountGeoData(request.DatDir, request.Name, request.GeoType) - return response.EncodeToBase64("", err) -} - -type readGeoFilesResponse struct { - Domain []string `json:"domain,omitempty"` - IP []string `json:"ip,omitempty"` -} - -// thin geo data -func ReadGeoFiles(base64Text string) string { - var response nodep.CallResponse[*readGeoFilesResponse] - xray, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64(nil, err) - } - domain, ip := geo.ReadGeoFiles(xray) - var resp readGeoFilesResponse - resp.Domain = domain - resp.IP = ip - return response.EncodeToBase64(&resp, nil) -} - -type pingRequest struct { - DatDir string `json:"datDir,omitempty"` - ConfigPath string `json:"configPath,omitempty"` - Timeout int `json:"timeout,omitempty"` - Url string `json:"url,omitempty"` - Proxy string `json:"proxy,omitempty"` -} - -// Ping Xray config and get the delay of its outbound. -func Ping(base64Text string) string { - var response nodep.CallResponse[int64] - req, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64(nodep.PingDelayError, err) - } - var request pingRequest - err = json.Unmarshal(req, &request) - if err != nil { - return response.EncodeToBase64(nodep.PingDelayError, err) - } - delay, err := xray.Ping(request.DatDir, request.ConfigPath, request.Timeout, request.Url, request.Proxy) - return response.EncodeToBase64(delay, err) -} - -// Test Xray Config. -func TestXray(base64Text string) string { - var response nodep.CallResponse[string] - req, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64("", err) - } - var request RunXrayRequest - err = json.Unmarshal(req, &request) - if err != nil { - return response.EncodeToBase64("", err) - } - err = xray.TestXray(request.DatDir, request.ConfigPath) - return response.EncodeToBase64("", err) -} - -type RunXrayRequest struct { - DatDir string `json:"datDir,omitempty"` - ConfigPath string `json:"configPath,omitempty"` -} - -type RunXrayFromJSONRequest struct { - DatDir string `json:"datDir,omitempty"` - ConfigJSON string `json:"configJSON,omitempty"` -} - -// Create Xray Run Request -func NewXrayRunRequest(datDir, configPath string) (string, error) { - request := RunXrayRequest{ - DatDir: datDir, - ConfigPath: configPath, - } - requestBytes, err := json.Marshal(&request) - if err != nil { - return "", err - } - - // Encode the JSON bytes to a base64 string - return base64.StdEncoding.EncodeToString(requestBytes), nil -} - -// Create Xray Run From JSON Request -func NewXrayRunFromJSONRequest(datDir, configJSON string) (string, error) { - request := RunXrayFromJSONRequest{ - DatDir: datDir, - ConfigJSON: configJSON, - } - requestBytes, err := json.Marshal(&request) - if err != nil { - return "", err - } - - // Encode the JSON bytes to a base64 string - return base64.StdEncoding.EncodeToString(requestBytes), nil -} - -// Run Xray instance. -func RunXray(base64Text string) string { - var response nodep.CallResponse[string] - req, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64("", err) - } - var request RunXrayRequest - err = json.Unmarshal(req, &request) - if err != nil { - return response.EncodeToBase64("", err) - } - err = xray.RunXray(request.DatDir, request.ConfigPath) - return response.EncodeToBase64("", err) -} - -// Run Xray instance with JSON configuration. -func RunXrayFromJSON(base64Text string) string { - var response nodep.CallResponse[string] - req, err := base64.StdEncoding.DecodeString(base64Text) - if err != nil { - return response.EncodeToBase64("", err) - } - var request RunXrayFromJSONRequest - err = json.Unmarshal(req, &request) - if err != nil { - return response.EncodeToBase64("", err) - } - err = xray.RunXrayFromJSON(request.DatDir, request.ConfigJSON) - return response.EncodeToBase64("", err) -} - -// Get Xray State -func GetXrayState() bool { - return xray.GetXrayState() -} - -// Stop Xray instance. -func StopXray() string { - var response nodep.CallResponse[string] - err := xray.StopXray() - return response.EncodeToBase64("", err) -} - -// Xray's version -func XrayVersion() string { - var response nodep.CallResponse[string] - return response.EncodeToBase64(xray.XrayVersion(), nil) -} diff --git a/xray_wrapper_test.go b/xray_wrapper_test.go deleted file mode 100644 index ce7e932d..00000000 --- a/xray_wrapper_test.go +++ /dev/null @@ -1,236 +0,0 @@ -package libXray - -import ( - "encoding/base64" - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -// writeConfigToFile writes the configuration to a file -func writeConfigToFile(config map[string]interface{}, path string) error { - // Ensure the directory exists - dir := filepath.Dir(path) - if _, err := os.Stat(dir); os.IsNotExist(err) { - err := os.MkdirAll(dir, os.ModePerm) - if err != nil { - return err - } - } - - // Create the configuration file - file, err := os.Create(path) - if err != nil { - return err - } - defer file.Close() - - // Write the configuration to the file - encoder := json.NewEncoder(file) - return encoder.Encode(config) -} - -// decodeVmessConfig decodes and unmarshals the VMess configuration from a base64 string -func decodeVmessConfig(vmess string) (map[string]interface{}, error) { - // Decode the VMess configuration from base64 - decodedVmess, err := base64.StdEncoding.DecodeString(vmess) - if err != nil { - return nil, err - } - - // Unmarshal the decoded string into a map - var vmessConfig map[string]interface{} - err = json.Unmarshal(decodedVmess, &vmessConfig) - if err != nil { - return nil, err - } - - return vmessConfig, nil -} - -// createXrayConfig creates an Xray configuration map based on the VMess configuration -func createXrayConfig(vmessConfig map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "log": map[string]interface{}{ - "loglevel": "debug", - }, - "inbounds": []map[string]interface{}{ - { - "port": 1080, - "protocol": "socks", - "settings": map[string]interface{}{ - "auth": "noauth", - }, - }, - }, - "outbounds": []map[string]interface{}{ - { - "protocol": "vmess", - "settings": map[string]interface{}{ - "vnext": []map[string]interface{}{ - { - "address": vmessConfig["add"], - "port": vmessConfig["port"], - "users": []map[string]interface{}{ - { - "id": vmessConfig["id"], - "alterId": vmessConfig["aid"], - "security": vmessConfig["scy"], - }, - }, - }, - }, - }, - }, - }, - } -} - -// base64EncodeRequest encodes a request struct into a base64 string -func base64EncodeRequest(request interface{}) (string, error) { - // Marshal the request struct into JSON bytes - requestBytes, err := json.Marshal(request) - if err != nil { - return "", err - } - - // Encode the JSON bytes to a base64 string - return base64.StdEncoding.EncodeToString(requestBytes), nil -} - -// testVmessPayloadBase64 is standard base64 of a minimal VMess JSON (add, port, id, aid, scy) for tests. -func testVmessPayloadBase64() string { - const vmessJSON = `{"add":"127.0.0.1","port":1080,"id":"00000000-0000-0000-0000-000000000000","aid":0,"scy":"auto"}` - return base64.StdEncoding.EncodeToString([]byte(vmessJSON)) -} - -// handleTestResponse decodes and checks the response from Xray -func handleTestResponse(response string, t *testing.T) { - // Decode the base64 response - decoded, err := base64.StdEncoding.DecodeString(response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - // Parse the decoded response into a map - var result map[string]interface{} - if err := json.Unmarshal(decoded, &result); err != nil { - t.Fatalf("Failed to parse response JSON: %v", err) - } - - // Check if the "success" field is true - if success, ok := result["success"].(bool); !ok || !success { - t.Fatalf("TestXray failed: %v", response) - } - - t.Log("TestXray passed successfully", string(decoded)) -} - -// TestRunXrayWithVmess tests running Xray with VMess configuration -func TestRunXrayWithVmess(t *testing.T) { - vmess := testVmessPayloadBase64() - - // Decode and parse the VMess configuration - vmessConfig, err := decodeVmessConfig(vmess) - if err != nil { - t.Fatalf("Failed to decode VMess: %v", err) - } - - // Create an Xray configuration from the VMess configuration - xrayConfig := createXrayConfig(vmessConfig) - - // Prepare the path for the configuration file - projectRoot, _ := filepath.Abs(".") - configPath := filepath.Join(projectRoot, "config", "xray_config_test.json") - - // Write the Xray configuration to a file - err = writeConfigToFile(xrayConfig, configPath) - if err != nil { - t.Fatalf("Failed to write Xray config: %v", err) - } - - // Create a request for testing Xray - datDir := filepath.Join(projectRoot, "dat") - request := RunXrayRequest{ - DatDir: datDir, - ConfigPath: configPath, - } - - // Encode the request to base64 - base64Request, err := base64EncodeRequest(request) - if err != nil { - t.Fatalf("Failed to encode request: %v", err) - } - - // Call TestXray with the base64-encoded request - response := TestXray(base64Request) - - // Handle and check the response - handleTestResponse(response, t) -} - -// TestRunXray tests running Xray with a VMess configuration for real-world usage -func TestRunXray(t *testing.T) { - vmess := testVmessPayloadBase64() - - // Decode and parse the VMess configuration - vmessConfig, err := decodeVmessConfig(vmess) - if err != nil { - t.Fatalf("Failed to decode VMess: %v", err) - } - - // Create an Xray configuration from the VMess configuration - xrayConfig := createXrayConfig(vmessConfig) - - // Prepare the path for the configuration file - projectRoot, _ := filepath.Abs(".") - configPath := filepath.Join(projectRoot, "config", "xray_config_run.json") - - // Write the Xray configuration to a file - err = writeConfigToFile(xrayConfig, configPath) - if err != nil { - t.Fatalf("Failed to write Xray config: %v", err) - } - - // Create a request for running Xray - datDir := filepath.Join(projectRoot, "dat") - runRequest := RunXrayRequest{ - DatDir: datDir, - ConfigPath: configPath, // Set max memory limit for Xray - } - - // Encode the request to base64 - base64Request, err := base64EncodeRequest(runRequest) - if err != nil { - t.Fatalf("Failed to encode request: %v", err) - } - - // Call RunXray with the base64-encoded request - response := RunXray(base64Request) - - // Handle and check the response - handleTestResponse(response, t) -} - -// TestXrayVersion tests the XrayVersion function. -func TestXrayVersion(t *testing.T) { - // Call the XrayVersion function - version := XrayVersion() - - // Decode the base64 response to get the version string - decodedVersion, err := base64.StdEncoding.DecodeString(version) - assert.NoError(t, err, "Failed to decode the version string") - - // Check that the decoded version is not empty - assert.NotEmpty(t, decodedVersion, "Xray version should not be empty") - - // Log the decoded version for visibility - t.Log("Xray Version: ", string(decodedVersion)) - - // Optionally, you could assert against the expected version if known - // expectedVersion := "some_expected_version_string" - // assert.Equal(t, expectedVersion, string(decodedVersion), "Xray version mismatch") -} From b898fa4eecc0e2b79b09ca72293545aa5ef61517 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 30 Jun 2026 23:21:35 +0800 Subject: [PATCH 6/7] Simplify libXray build workflow --- .github/workflows/build.yml | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 189d19b4..85493c55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,12 +39,7 @@ jobs: artifact: windows-x64 - runner: macos-26 target: apple - apple_tool: go artifact: apple-cgo - - runner: macos-26 - target: apple - apple_tool: gomobile - artifact: apple-gomobile runs-on: ${{ matrix.runner }} timeout-minutes: 60 @@ -115,32 +110,6 @@ jobs: ndk-version: ${{ env.NDK_VERSION }} add-to-path: true - - name: Install gomobile (Android) - if: matrix.target == 'android' - run: | - go install golang.org/x/mobile/cmd/gomobile@latest - gomobile init - - # ========================= - # Windows toolchain - # ========================= - - name: Setup MSYS2 (Windows GCC) - if: matrix.target == 'windows' - uses: msys2/setup-msys2@v2 - with: - install: >- - mingw-w64-x86_64-gcc - mingw-w64-x86_64-toolchain - - # ========================= - # Apple toolchain - # ========================= - - name: Setup gomobile (Apple) - if: matrix.target == 'apple' && matrix.apple_tool == 'gomobile' - run: | - go install golang.org/x/mobile/cmd/gomobile@latest - gomobile init - # ========================= # Build steps # ========================= @@ -163,7 +132,7 @@ jobs: - name: Build Apple (macOS/iOS) if: matrix.target == 'apple' run: | - python3 build/main.py apple ${{ matrix.apple_tool }} + python3 build/main.py apple go # ========================= # Collect artifacts From 37bffb59dd62a10dd73595ab739d234b5b58bd43 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 30 Jun 2026 23:40:47 +0800 Subject: [PATCH 7/7] Stabilize invoke response shape --- invoke.go | 37 +++++++++++++++++++++++++---------- invoke_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/invoke.go b/invoke.go index 6d21a6b0..5e40d56b 100644 --- a/invoke.go +++ b/invoke.go @@ -23,6 +23,9 @@ func Invoke(requestJSON string) string { if err := json.Unmarshal([]byte(requestJSON), &request); err != nil { return encodeInvokeResponse(nil, err) } + if err := validateAPIVersion(request.APIVersion); err != nil { + return encodeInvokeResponse(nil, err) + } applyEnv(request.Env) switch request.Method { @@ -43,7 +46,7 @@ func Invoke(requestJSON string) string { case LibXrayMethodRunXrayFromJson: return invokeRunXrayFromJSON(request.Payload) case LibXrayMethodStopXray: - return encodeInvokeResponse("", xray.StopXray()) + return encodeInvokeNoDataResponse(xray.StopXray()) case LibXrayMethodXrayVersion: return encodeInvokeResponse(xray.XrayVersion(), nil) case LibXrayMethodGetXrayState: @@ -53,6 +56,13 @@ func Invoke(requestJSON string) string { } } +func validateAPIVersion(version int) error { + if version == 0 || version == 1 { + return nil + } + return errors.New("unsupported apiVersion") +} + func applyEnv(env *LibXrayEnvJson) { if env == nil { return @@ -103,6 +113,13 @@ func encodeInvokeResponse(data any, err error) string { return string(raw) } +func encodeInvokeNoDataResponse(err error) string { + if err != nil { + return encodeInvokeResponse(nil, err) + } + return encodeInvokeResponse(struct{}{}, nil) +} + func invokeGetFreePorts(payload json.RawMessage) string { request, err := decodePayload[GetFreePortsRequest](payload) if err != nil { @@ -136,14 +153,14 @@ func invokeConvertXrayJsonToShareLinks(payload json.RawMessage) string { func invokeCountGeoData(payload json.RawMessage) string { request, err := decodePayload[CountGeoDataRequest](payload) if err != nil { - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } datDir := platform.NewEnvFlag(platform.AssetLocation).GetValue(func() string { return "" }) if datDir == "" { - return encodeInvokeResponse("", errors.New("missing xray.location.asset")) + return encodeInvokeNoDataResponse(errors.New("missing xray.location.asset")) } err = geo.CountGeoData(datDir, request.Name, request.GeoType) - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } func invokePing(payload json.RawMessage) string { @@ -158,26 +175,26 @@ func invokePing(payload json.RawMessage) string { func invokeTestXray(payload json.RawMessage) string { request, err := decodePayload[RunXrayRequest](payload) if err != nil { - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } err = xray.TestXray(request.ConfigPath) - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } func invokeRunXray(payload json.RawMessage) string { request, err := decodePayload[RunXrayRequest](payload) if err != nil { - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } err = xray.RunXray(request.ConfigPath) - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } func invokeRunXrayFromJSON(payload json.RawMessage) string { request, err := decodePayload[RunXrayFromJSONRequest](payload) if err != nil { - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } err = xray.RunXrayFromJSON(request.ConfigJSON) - return encodeInvokeResponse("", err) + return encodeInvokeNoDataResponse(err) } diff --git a/invoke_test.go b/invoke_test.go index b928ec49..10229b54 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -38,6 +38,22 @@ func invokeForTest(t *testing.T, method LibXrayMethod, env *LibXrayEnvJson, payl return response } +func invokeRawForTest(t *testing.T, requestJSON string) testResponse { + t.Helper() + var response testResponse + if err := json.Unmarshal([]byte(Invoke(requestJSON)), &response); err != nil { + t.Fatal(err) + } + return response +} + +func requireNoDataObject(t *testing.T, response testResponse) { + t.Helper() + if got := string(response.Data); got != "{}" { + t.Fatalf("data = %s, want {}", got) + } +} + func writeConfigToFile(t *testing.T, config any, path string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { @@ -145,6 +161,7 @@ func TestInvokeTestXray(t *testing.T) { if !response.Success { t.Fatalf("TestXray failed: %s", response.Err) } + requireNoDataObject(t, response) } func TestInvokeRunXray(t *testing.T) { @@ -162,6 +179,7 @@ func TestInvokeRunXray(t *testing.T) { if !response.Success { t.Fatalf("RunXray failed: %s", response.Err) } + requireNoDataObject(t, response) } func TestInvokeXrayVersion(t *testing.T) { @@ -244,6 +262,41 @@ func TestInvokeUnknownMethod(t *testing.T) { } } +func TestInvokeAPIVersion(t *testing.T) { + response := invokeRawForTest(t, `{"method":"xrayVersion"}`) + if !response.Success { + t.Fatalf("omitted apiVersion should default to v1: %s", response.Err) + } + + t.Setenv(platform.AssetLocation, "initial-asset") + response = invokeRawForTest(t, `{"apiVersion":2,"method":"xrayVersion","env":{"xray.location.asset":"updated-asset"}}`) + if response.Success { + t.Fatal("unsupported apiVersion should fail") + } + if got := string(response.Data); got != "null" { + t.Fatalf("data = %s, want null", got) + } + if got := os.Getenv(platform.AssetLocation); got != "initial-asset" { + t.Fatalf("asset env = %q", got) + } +} + +func TestInvokeNoDataResponseShape(t *testing.T) { + response := invokeForTest(t, LibXrayMethodStopXray, nil, nil) + if !response.Success { + t.Fatalf("StopXray failed: %s", response.Err) + } + requireNoDataObject(t, response) + + response = invokeRawForTest(t, `{"apiVersion":1,"method":"runXray","payload":"invalid"}`) + if response.Success { + t.Fatal("invalid runXray payload should fail") + } + if got := string(response.Data); got != "null" { + t.Fatalf("data = %s, want null", got) + } +} + func TestInvokeIgnoresUnknownEnvField(t *testing.T) { const key = "XRAY_LIBXRAY_UNKNOWN_ENV_TEST" _ = os.Unsetenv(key)