Skip to content

Repository files navigation

libXray

简体中文

This is a wrapper around Xray-core to improve the client development experience.

Note

  1. This repository has few maintainers. If you do not report a bug or initiate a PR, your issue will be ignored.
  2. This repository does not guarantee API stability, you need to adapt it yourself.
  3. This repository is only compatible with the latest release of Xray-core.

Versioning

Releases use CalVer in the form v<YY>.<M>.<D> (e.g. v26.3.27 = 2026-03-27). Because Go modules require any module with major version >= 2 to encode the major in its import path, every CalVer release is mirrored onto a Go-friendly SemVer tag on the same commit:

CalVer tag Go-import tag
v26.3.27 v1.260327.0

Go consumers should pin against the SemVer mirror:

go get github.com/xtls/libxray@v1.260327.0

The mirror tag is created automatically by .github/workflows/release-go-mirror.yml on every CalVer push. Existing CalVer tags can be backfilled with scripts/backfill-semver-tags.sh.

Features

build

Compile script. It is recommended to always use this script to compile libXray. We will not answer questions caused by using other compilation methods.

depends on git and go.

By default, the build script does not clone Xray-core. It uses Go modules and pins Xray-core to release tag v26.7.28 through its pseudo-version. Pass the optional local argument to use an existing local checkout at ../Xray-core through a Go module replace.

Usage

# Android (min Android API level is 21)
python3 build/main.py android
python3 build/main.py android local

# Apple (gomobile or go)
python3 build/main.py apple gomobile
python3 build/main.py apple go
python3 build/main.py apple gomobile local
python3 build/main.py apple go local

# Linux
python3 build/main.py linux
python3 build/main.py linux local

# Windows
python3 build/main.py windows
python3 build/main.py windows local

Builds restore go.mod and go.sum on success or failure. Gomobile builds resolve latest by default; set LIBXRAY_GOMOBILE_VERSION to select a Go module version. Both gomobile and gobind use that resolved version.

Linux and Windows builds also produce bin/xray or bin/xray.exe. This session Core protects Go DNS lookups from the VPN route and accepts only:

xray run -dns <IP:port> -interface <name> -config <xray.json> [-runtime <runtime.json>]

All three options are required. -dns must be an IP endpoint, and -config points directly to the Xray JSON configuration. Optional -runtime reads the host metadata object described under "Managed runtime accounting", without a wrapping runtime key; it does not replace -config.

Warning

Use only one Go runtime per process. Go does not support loading multiple independently built Go runtimes into one process. Every native libXray artifact embeds a Go runtime, whether it is produced through cgo or gomobile. Do not load libXray together with another independently built Go, cgo, or gomobile library in the same executable or process. Doing so can fail during build, link, or load, or crash during runtime initialization before application code runs. If one process needs Go packages from several libraries, include those packages in the same Go build or gomobile bind invocation and produce one native artifact so they share a runtime. Merely repackaging or merging independently built frameworks, archives, AARs, shared libraries, or DLLs is not sufficient. Separate OS processes may each load one Go runtime, so apply this rule independently to each process. See Go #18976, golang/go#15956, and libXray #116.

Android

use gomobile .

iOS && macOS

1. use gomobile

Need "iOS Simulator Runtime".

This is the best choice for general scenarios. The cross-platform single-runtime restriction above still applies when linking other Go-based libraries.

Supports iOS, iOSSimulator, macOS, macCatalyst.

But it is not possible to set the minimum macOS version, which will cause some warnings when compiling. And it does not support tvOS.

2. use cgo

Need "iOS Simulator Runtime" and "tvOS Simulator Runtime".

Support more compilation options, output c header files.

This works well when you use ffi for integration. For example, integration with swift, kotlin, dart.

Support iOS, iOSSimulator, macOS, tvOS.

The product LibXray.xcframework contains module.modulemap. When using Swift, import it as module LibXray.

Linux

depend on gcc and g++.

Windows

Depends on gcc and g++ in PATH.

Native amd64 and arm64 builds are supported. The release workflow builds each architecture on its matching GitHub-hosted Windows runner.

API

libXray exposes a single structured entrypoint:

func Invoke(requestJSON string) string

The C export is:

char* CGoInvoke(char* requestJSON);
void CGoFree(char* value);

CGoInvoke allocates its response. The caller must release every non-null response with CGoFree; do not use a platform allocator directly.

The request is a JSON object:

{
  "apiVersion": 3,
  "method": "runXray",
  "payload": {
    "xrayJson": "{\"outbounds\":[...]}"
  }
}

The response is a JSON object:

{
  "success": true,
  "data": {},
  "error": ""
}

Design notes:

  1. Invoke accepts only apiVersion: 3; the API version remains fixed at 3. Contract changes require synchronized consumers and documentation within that version. Xray configurations are passed as UTF-8 JSON text in xrayJson; libXray does not read configuration file paths.
  2. A top-level env field is ignored and has no effect. Xray-core runtime environment options belong in the root env object of the Xray config.
  3. SetTunFd has been removed. When the fd is only known at runtime, write xray.tun.fd into the Xray config root env object before calling runXray.
  4. countGeoData is not backed by an Xray config, so its datDir is passed in the method payload.
  5. The complete UTF-8 encoded Invoke request and response JSON envelopes are limited to 16 MiB. If either limit is exceeded, Invoke returns a failure response with success: false, data: null, and a size-limit error.
  6. convertShareLinksToXrayJson validates each parsed outbound with the current Xray-core config builder. Invalid outbounds are omitted, and the method fails if none remain. Validation does not create or start an Xray instance. Xray JSON input is treated as a node source: only its root outbounds are retained, and all other root fields are ignored. The response contains only fields supported by libXray share links; unsupported and generated empty fields are omitted. Opaque XHTTP extra and FinalMask mask settings JSON remain unchanged. Every successful response returns the projected config together with usableCount and failedCount. Its optional age.secretKey decrypts official age ASCII armor in memory before the existing parser runs. Plaintext input remains unchanged.
  7. Xray-core keeps its system dialer DNS client and outbound manager in process-wide state. pingBatch, testXray, and their exported Go entrypoints take the managed lifecycle lock and reject an active runXray instance before loading/building config. A batch holds the lock through all workers and temporary-core close. This also serializes these operations with one another. Instances created outside the managed APIs are not detected or restored; callers requiring overlap with them must still use separate processes.

Supported methods:

getFreePorts
convertShareLinksToXrayJson
convertXrayJsonToShareLinks
generateAgeKeyPair
countGeoData
pingBatch
testXray
runXray
stopXray
xrayVersion
getXrayState

controller

Socket protect

Used to solve the socket protect problem on Android.

DNS resolver

Android may expose a loopback DNS server to Go's resolver while a VPN is active. Call SetDNS before runXray to make Go use the DNS server selected by the VPN configuration and protect the DNS socket from the VPN tunnel. The server must be an IP endpoint with a port, such as 8.8.8.8:53 or [2001:4860:4860::8888]:53.

Call ResetDNS after Xray has stopped. These APIs are available only in the Android artifact and change the process-wide Go resolver.

LibXray.setDNS(controller, "8.8.8.8:53");
LibXray.invoke(runXrayRequest);

// Later, when stopping the core:
LibXray.invoke(stopXrayRequest);
LibXray.resetDNS();

Process finder (per-app routing)

ConnectivityManager.getConnectionOwnerUid() is API 30+. On older Android libXray falls back to parsing /proc/net/{tcp,udp}{,6} in pure Go.

Usage (Java/Kotlin):

ProcessFinder finder = new ProcessFinder() {
    @Override
    public long findProcessByConnection(String network, String srcIP, long srcPort,
                                         String destIP, long destPort) {
        return -1; // return UID or -1
    }
};
LibXray.registerProcessFinder(finder, Build.VERSION.SDK_INT);

geo

count

Read geo files and count the categories and rules.

main

Download geosite.dat and geoip.dat and count them.

memory

Only executed on iOS, GC is initiated once a second. This can alleviate memory pressure on iOS.

nodep

file

Write data to a file.

measure

Speed ​​test the Xray configuration.

port

Get free ports.

share

libXray stores outbound names in tag. sendThrough keeps its native Xray meaning as the local bind address.

clash_meta

Parse Clash.Meta configuration.

generate_share

convert Xray Json to VMessAEAD/VLESS sharing protocol.

parse_share

convert VMessAEAD/VLESS sharing protocol to Xray Json.

convert VMessQRCode to Xray Json.

Parsing result

convertShareLinksToXrayJson has one response shape. Its payload contains text and optional age. Every successful conversion returns data: {"config":{"outbounds":[...]},"usableCount":2,"failedCount":1}.

Counts describe this input only, not added/changed nodes. Each root JSON outbounds element or YAML proxies element is one candidate. In detected share-link lists, each URI-like row is one candidate; blank lines, comments and text headers are ignored. Base64 and age wrappers use the inner format's candidates. Malformed individual elements are skipped without discarding other valid elements. usableCount equals the final projected, buildable outbound count; parse, build and unsupported-projection failures count toward failedCount. No per-node hash comparison or deduplication is performed.

A recognized container with zero usable nodes returns success: false with structured counts and config: {"outbounds":[]}. An unrecognized format, malformed whole document, invalid container or decryption failure returns data: null; counts are not guessed. Error text never includes rejected candidates or decrypted subscription text. Callers must not import/replace a subscription when no usable nodes remain.

age-encrypted subscriptions

convertShareLinksToXrayJson accepts an optional native age secret key. Only X25519 (AGE-SECRET-KEY-1...) and ML-KEM-768 + X25519 hybrid (AGE-SECRET-KEY-PQ-1...) identities are accepted. Recognized age armor is decrypted in memory and limited to 16 MiB of plaintext.

{
  "apiVersion": 3,
  "method": "convertShareLinksToXrayJson",
  "payload": {
    "text": "-----BEGIN AGE ENCRYPTED FILE-----\n...",
    "age": {
      "secretKey": "AGE-SECRET-KEY-1..."
    }
  }
}

Generate a new keypair with keyType set to x25519 or hybrid. An omitted keyType defaults to x25519. The hybrid option matches Mihomo age keygen-pq and produces an AGE-SECRET-KEY-PQ-1... identity with an age1pq1... recipient.

{
  "apiVersion": 3,
  "method": "generateAgeKeyPair",
  "payload": {
    "keyType": "x25519"
  }
}

The response contains both secretKey and publicKey. The integrating application must persist the pair and send only publicKey as X-Age-Public-Key. libXray does not perform the subscription HTTP request, persist keys, or add headers. Applications must never send the secret key over HTTP or write decrypted subscription text to disk.

vmess

convert VMessQRCode to Xray Json.

xray_json

Some tools used to parse shared links.

xray

pingBatch

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

{
  "apiVersion": 3,
  "method": "pingBatch",
  "payload": {
    "configs": [
      {
        "xrayJson": "{\"outbounds\":[...]}"
      },
      {
        "xrayJson": "{\"outbounds\":[...]}",
        "outboundTag": "media"
      }
    ],
    "timeout": 5,
    "url": "https://cp.cloudflare.com/",
    "locationUrl": "https://ip-check-perf.radar.cloudflare.com/"
  }
}

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

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

locationUrl is optional and must be an absolute HTTP(S) URL. When omitted, no location request is made and no location fields are returned. When supplied, each prepared item sends its latency HEAD and then its location GET using the same client forced through that item's selected outbound and dependencies. Each request has the configured timeout (so an item may take up to twice it). Location time is not included in delay, and the two results are independent: success, delay and error describe latency only; a location failure does not invalidate a successful latency result, and GET is still attempted after a latency failure.

A successful GET adds the unmodified response body as the locationJson string. The App owns JSON parsing and provider-specific field handling. The provider must return HTTP 200 and at most 64 KiB; transport or body-read failures instead add locationError. Errors do not echo the URL, credentials or response body. Invalid outbound configs retain their ordinary per-item failure and do not perform either request.

testXray

Loads and builds the complete configuration from the supplied JSON text. The payload contains only xrayJson; success returns data: {}:

{
  "apiVersion": 3,
  "method": "testXray",
  "payload": {
    "xrayJson": "{\"outbounds\":[...]}"
  }
}

The Go entrypoint TestXray uses core.LoadConfig without constructing or starting an Xray instance or runtime handlers. It validates configuration structure, including TUN/WireGuard definitions, without creating devices, listeners, log files, or background connections. The builder can still read local GeoData/certificates and apply the root env to the current process. Geodata asset declarations validate HTTPS URLs and existing local files; their downloader/cron does not run during validation.

A successful check establishes that the configuration builds. It does not prove that runtime resources are available, that an instance can start, or that the network is reachable. Callers must handle actual startup failures.

runXray

Starts the managed Xray instance from the supplied JSON text. Use stopXray to stop that instance. runXrayFromJson is no longer a separate method.

Managed runtime accounting

runXray.payload.runtime is optional API v3 host metadata. Omitting it preserves the original lifecycle and writes no runtime snapshots. Hosts opt in with this object (also the complete content of the desktop -runtime file):

{
  "statePath": "/private/app/run/runtime.json",
  "inboundTag": "tunIn",
  "listen": "127.0.0.1:49228",
  "token": "538fc3253a3e433491bc2d653fc74214"
}

The host supplies an existing private directory and an absolute statePath. inboundTag must be nonempty and at most 256 bytes. Metadata stays separate from Xray JSON, so user configuration cannot override it. The named inbound must exist, with uplink/downlink system statistics and a statistics manager enabled. listen and token may both be omitted to save snapshots without HTTP. When enabled, listen must be 127.0.0.1:<port> with port 1–65535, and the host must generate a fresh random 32-character lowercase hex token. Keep it private; do not reuse the example token. Invalid metadata, an occupied HTTP port, or an initial save failure rejects startup; any constructed core and statistics listener are closed.

The saved file contains only the current session's raw inbound counter values:

{
  "version": 1,
  "session": {
    "id": "2a7e2e49b947a802d8b39af4fbc48f52",
    "startedAtMs": 1788300000000,
    "endedAtMs": 0,
    "uplink": 120,
    "downlink": 800
  },
  "available": true,
  "sampledAtMs": 1788300030000,
  "savedAtMs": 1788300030000,
  "error": ""
}

Timestamps are Unix milliseconds. Each new start generates a random 32-character lowercase hex session ID, even when replaying identical metadata. endedAtMs: 0 means no final stop was saved; it is not proof that the VPN is running. The host saves an initial snapshot, samples/saves every 30 seconds, and attempts a final sample/save before closing the core on stopXray.

Sampling reads the named inbound's Value(), never resets it and never adds outbound/node counters. Repeated samples do not accumulate bytes. A nonnegative counter rollback is recorded as the smaller raw value, not a synthetic delta. Missing or negative counters set available: false and error: "counters_unavailable", retaining the last valid nonnegative values. Idle valid counters report available zero. There are no application-wide totals, reset generations, or VPN control HTTP methods. resetRuntime is not an Invoke method. Applications may read existing Xray metrics for live rates; their own totals/reset policy stays outside libXray.

Starting a new session atomically replaces the previous runtime.json; libXray does not archive or merge earlier sessions. Traffic not read by the App before replacement is intentionally lost. Each session starts from zero and receives a new ID.

Snapshot files use a mode-0600 same-directory temporary file, sync, and atomic replacement (Windows uses MoveFileEx with replace-existing and write-through). The private parent directory/Windows ACL remains the host's responsibility. Failed saves leave the previous complete disk snapshot for later retry; a final save error is returned but never prevents core shutdown. An error after rename can have an uncertain persistence outcome, so consumers must re-read saved snapshots through HTTP when available. This is reference data, not billing: crashes, forced termination, or replacement before the App reads the file can lose traffic, with no strict loss bound.

A nonblocking OS lock on statePath + ".lock" is held until core close, preventing another process from writing the current session. Hosts must use one consistent canonical path and leave the lock file in place. App code reads snapshots through HTTP instead of opening the host's files, so macOS System Extension files can remain root-owned. This does not provide graceful final settlement when Windows forcibly terminates a job.

Snapshot HTTP

The optional statistics listener starts with the managed session and closes on stop, including when the final save fails. It uses a separate loopback port from Xray's native metrics; it provides no VPN start/stop/configuration methods. Every request requires Authorization: Bearer <token>. Responses use Cache-Control: no-store; CORS is not enabled.

  • GET /runtime returns the current saved snapshot directly.

Requests read the host's saved atomic snapshot without sampling, resetting counters, or updating the save time. Use native metrics for live rates. A missing, corrupt, or non-regular snapshot returns service unavailable. Requests have bounded read/write timeouts. While stopped, HTTP is unavailable; libXray never owns App totals or clear/reset policy.

metrics

Refer to the following configuration:

{
  "metrics" : {
    "listen": "127.0.0.1:49227"
  },
  "policy" : {
    "system" : {
      "statsInboundDownlink" : true,
      "statsInboundUplink" : true,
      "statsOutboundDownlink" : true,
      "statsOutboundUplink" : true
    }
  },
  "stats" : {}
}

The metrics server exposes the Xray runtime counters through HTTP. For example, when listen is 127.0.0.1:49227, read:

http://localhost:49227/debug/vars

Metrics only needs the listen field in this wrapper. Query /debug/vars directly with an HTTP client instead of going through libXray.

validation

Verify the Xray configuration.

xray

Start and stop Xray instances.

Credits

Project X

VMessPing

FreePort

MetaCubeX age (BSD 3-Clause)

License

This repository is based on the MIT License.

About

Xray-core wrapper, support all platforms.

Resources

Stars

223 stars

Watchers

10 watching

Forks

Releases

Packages

Used by

Contributors

Languages