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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,13 @@ 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 }}"
Expand Down
41 changes: 14 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ 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.
Linux and Windows builds also produce a small desktop wrapper executable:
`bin/xray` or `bin/xray.exe`. The wrapper accepts `-configPath` pointing to a
`LibXrayInvokeRequest` JSON file whose method is `runXray`.

### Usage

Expand Down Expand Up @@ -131,7 +133,6 @@ The request is a JSON object:
"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"
Expand All @@ -152,33 +153,19 @@ The response is a JSON object:
}
```

`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.
1. `Invoke.env` supports only fixed Xray-core runtime environment keys:
`xray.location.asset`, `xray.location.cert`, and `xray.tun.fd`.
2. Non-empty `env` fields are set on the process before the method runs.
Missing fields are not unset and old values are not restored.
3. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are
load-stage process environment variables and cannot be supplied through
`Invoke.env`.
4. `SetTunFd` has been removed. Set the `"xray.tun.fd"` key in the request
`env` object when the fd is only known at runtime.
5. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in
the method payload.

Supported methods:

Expand Down
23 changes: 23 additions & 0 deletions build/app/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import subprocess

from app.cmd import (
create_dir_if_not_exists,
delete_file_if_exists,
delete_dir_if_exists,
)
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions build/app/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions build/app/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -22,6 +23,7 @@ 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):
Expand Down
38 changes: 38 additions & 0 deletions desktop_bin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"flag"
"fmt"
"os"
"os/signal"
"runtime"
"runtime/debug"
"syscall"
)

func main() {
configPath := flag.String("configPath", "", "Path of LibXrayInvokeRequest JSON")
flag.Parse()
if *configPath == "" {
flag.Usage()
fmt.Fprintln(os.Stderr, "missing -configPath")
os.Exit(1)
}

if err := runXray(*configPath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer func() {
if err := stopXray(); err != nil {
fmt.Fprintf(os.Stderr, "stopXray failed: %v\n", err)
}
}()

runtime.GC()
debug.FreeOSMemory()

osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
<-osSignals
}
65 changes: 65 additions & 0 deletions desktop_bin/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"os"

libXray "github.com/xtls/libxray"
)

type invokeResponse struct {
Success bool `json:"success"`
Err string `json:"error"`
}

func runXray(configPath string) error {
requestBytes, err := os.ReadFile(configPath)
if err != nil {
return err
}

var request libXray.LibXrayInvokeRequest
if err := json.Unmarshal(requestBytes, &request); err != nil {
return err
}
if request.Method != libXray.LibXrayMethodRunXray {
return fmt.Errorf("unsupported method %q: desktop_bin only supports runXray", request.Method)
}

responseText := libXray.Invoke(string(requestBytes))
var response invokeResponse
if err := json.Unmarshal([]byte(responseText), &response); err != nil {
return err
}
if !response.Success {
if response.Err == "" {
response.Err = "runXray failed"
}
return errors.New(response.Err)
}
return nil
}

func stopXray() error {
requestBytes, err := json.Marshal(&libXray.LibXrayInvokeRequest{
APIVersion: 1,
Method: libXray.LibXrayMethodStopXray,
})
if err != nil {
return err
}
responseText := libXray.Invoke(string(requestBytes))
var response invokeResponse
if err := json.Unmarshal([]byte(responseText), &response); err != nil {
return err
}
if !response.Success {
if response.Err == "" {
response.Err = "stopXray failed"
}
return errors.New(response.Err)
}
return nil
}
7 changes: 2 additions & 5 deletions download_geo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,15 @@ func makeLoadGeoDataRequest(datDir string, name string, geoType string) (string,
payload, err := json.Marshal(&libXray.CountGeoDataRequest{
Name: name,
GeoType: geoType,
DatDir: datDir,
})
if err != nil {
return "", err
}
request := libXray.LibXrayInvokeRequest{
APIVersion: 1,
Method: libXray.LibXrayMethodCountGeoData,
Env: &libXray.LibXrayEnvJson{
AssetLocation: datDir,
CertLocation: datDir,
},
Payload: payload,
Payload: payload,
}
data, err := json.Marshal(&request)
if err != nil {
Expand Down
40 changes: 16 additions & 24 deletions invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,27 @@ 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
}
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)
if value == "" {
return
}
_ = os.Setenv(key, value)
}

func validateAPIVersion(version int) error {
if version == 0 || version == 1 {
return nil
}
return errors.New("unsupported apiVersion")
}

func decodePayload[T any](payload json.RawMessage) (T, error) {
Expand Down Expand Up @@ -158,11 +148,10 @@ func invokeCountGeoData(payload json.RawMessage) string {
if err != nil {
return encodeInvokeNoDataResponse(err)
}
datDir := platform.NewEnvFlag(platform.AssetLocation).GetValue(func() string { return "" })
if datDir == "" {
return encodeInvokeNoDataResponse(errors.New("missing xray.location.asset"))
if request.DatDir == "" {
return encodeInvokeNoDataResponse(errors.New("missing datDir"))
}
err = geo.CountGeoData(datDir, request.Name, request.GeoType)
err = geo.CountGeoData(request.DatDir, request.Name, request.GeoType)
return encodeInvokeNoDataResponse(err)
}

Expand All @@ -173,6 +162,9 @@ func invokePing(payload json.RawMessage) string {
}
delay, err := xray.Ping(request.ConfigPath, request.Timeout, request.URL, request.Proxy)
if err != nil {
if delay == nodep.PingDelayError || delay == nodep.PingDelayTimeout {
return encodeInvokeResponse(&PingResponse{Delay: delay}, err)
}
return encodeInvokeResponse(nil, err)
}
return encodeInvokeResponse(&PingResponse{Delay: delay}, nil)
Expand Down
18 changes: 4 additions & 14 deletions invoke_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,9 @@ type LibXrayInvokeRequest struct {
}

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"`
AssetLocation string `json:"xray.location.asset,omitempty"`
CertLocation string `json:"xray.location.cert,omitempty"`
TunFd string `json:"xray.tun.fd,omitempty"`
}

type GetFreePortsRequest struct {
Expand All @@ -66,6 +55,7 @@ type ConvertXrayJsonToShareLinksResponse struct {
type CountGeoDataRequest struct {
Name string `json:"name,omitempty"`
GeoType string `json:"geoType,omitempty"`
DatDir string `json:"datDir,omitempty"`
}

type PingRequest struct {
Expand Down
Loading