Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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. Use `Invoke.env["xray.tun.fd"]` when the fd is
only known at runtime.
5. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in
Comment thread
yiguodev marked this conversation as resolved.
Outdated
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
29 changes: 29 additions & 0 deletions desktop_bin/main.go
Original file line number Diff line number Diff line change
@@ -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 libXray invoke request json")
flag.Parse()

if err := runXray(*configPath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Comment thread
yiguodev marked this conversation as resolved.
Outdated
defer stopXray()

runtime.GC()
debug.FreeOSMemory()

osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
<-osSignals
}
54 changes: 54 additions & 0 deletions desktop_bin/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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() {
requestBytes, err := json.Marshal(&libXray.LibXrayInvokeRequest{
APIVersion: 1,
Method: libXray.LibXrayMethodStopXray,
})
if err != nil {
return
}
_ = libXray.Invoke(string(requestBytes))
}
Comment thread
yiguodev marked this conversation as resolved.
Outdated
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
Loading