This is a wrapper around Xray-core to improve the client development experience.
- This repository has few maintainers. If you do not report a bug or initiate a PR, your issue will be ignored.
- This repository does not guarantee API stability, you need to adapt it yourself.
- This repository is only compatible with the latest release of Xray-core.
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.0The 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.
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.
# 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.
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.
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.
depend on gcc and g++.
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.
libXray exposes a single structured entrypoint:
func Invoke(requestJSON string) stringThe 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:
- 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 inxrayJson; libXray does not read configuration file paths. - A top-level
envfield is ignored and has no effect. Xray-core runtime environment options belong in the rootenvobject of the Xray config. SetTunFdhas been removed. When the fd is only known at runtime, writexray.tun.fdinto the Xray config rootenvobject before callingrunXray.countGeoDatais not backed by an Xray config, so itsdatDiris passed in the method payload.- 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. convertShareLinksToXrayJsonvalidates 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 rootoutboundsare 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 XHTTPextraand FinalMask masksettingsJSON remain unchanged. Every successful response returns the projected config together withusableCountandfailedCount. Its optionalage.secretKeydecrypts official age ASCII armor in memory before the existing parser runs. Plaintext input remains unchanged.- 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 activerunXrayinstance 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
Used to solve the socket protect problem on Android.
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();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);Read geo files and count the categories and rules.
Download geosite.dat and geoip.dat and count them.
Only executed on iOS, GC is initiated once a second. This can alleviate memory pressure on iOS.
Write data to a file.
Speed test the Xray configuration.
Get free ports.
libXray stores outbound names in tag. sendThrough keeps its native Xray
meaning as the local bind address.
Parse Clash.Meta configuration.
convert Xray Json to VMessAEAD/VLESS sharing protocol.
convert VMessAEAD/VLESS sharing protocol to Xray Json.
convert VMessQRCode to Xray Json.
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.
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.
convert VMessQRCode to Xray Json.
Some tools used to parse shared links.
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.
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.
Starts the managed Xray instance from the supplied JSON text. Use stopXray
to stop that instance. runXrayFromJson is no longer a separate method.
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.
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 /runtimereturns 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.
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.
Verify the Xray configuration.
Start and stop Xray instances.
MetaCubeX age (BSD 3-Clause)
This repository is based on the MIT License.