# One-command Xray Clients and Servers

> One-command VLESS, REALITY, and Vision client/server setup, stable server credentials, and verified proxy connectivity.

---

LLMS index: [llms.txt](/llms.txt)

---

> **Decision date:** 2026-10-05<br>
> **Status:** Released in [v1.9.0](/release/pig-1.9.0/).<br>
> **Current reference:** [`pig build`](/build/), [operations safety](/design/ops-cli-safety/), and [release history](/release/)<br>
> **Scope:** One-command Xray client and server setup; Linux clients and servers, plus a macOS client. Shared Nginx ingress remains a separate deployment boundary.

## Decision {#decision}

Extend `pig build proxy` with explicit `client` and `server` subcommands. Use VLESS over RAW,
REALITY, and `xtls-rprx-vision` for the new workflow. First server setup generates matching credentials;
repeated setup preserves the existing client connection contract and offers the same standard URI.
The client takes a URI or explicit command-line parameters,
installs Xray, configures the local listener, starts its service, and verifies the connection.
A URI can be pasted directly or read from a file or stdin.

Retain the existing command path and `x` alias. The zero-argument command installs or verifies
Xray. The historical `id@host:port [local]` form retains its VMess meaning and default port during
a compatibility period, through the existing V2Ray workflow.

## Context {#context}

The [previous implementation](https://github.com/pgsty/pig/blob/74cb1281860fae659f2aa4fec2898af34a0c2519/cli/build/proxy.go)
installs `vray`, writes an HTTP-to-VMess configuration to `/etc/v2ray.json`, grants the `v2ray`
group access, writes shell aliases, restarts `v2ray`, and checks an HTTPS request. Existing tests
cover argument parsing, package-manager selection, and credential redaction.

The inspected reference pair uses Xray 26.3.27. The running macOS client exposes HTTP and SOCKS
on one loopback port, 8888, and uses VLESS/RAW/REALITY/Vision with a Chrome fingerprint, disabled
multiplexing, and ML-DSA-65 verification. Both HTTP and SOCKS requests succeeded during the review.
The client UUID, short ID, and SNI match the server's corresponding fields.

The reference Linux server listens on loopback port 9443. Nginx owns public port 443 and selects
the backend by SNI, forwarding a PROXY protocol header. Xray accepts that header, connects to a
REALITY target, sends allowed traffic through `freedom`, and blocks destinations matching
`geoip:private`. Nginx provides the public entry point in this deployment.

Inspected Pigsty RPM and DEB artifacts for both architectures provide `/usr/bin/xray`,
`/etc/xray.json` with mode 0640 and ownership `root:xray`, an `xray:xray` service account,
`xray.service`, and geodata in `/usr/share/xray`. Their installation scripts reload systemd without
automatically starting or enabling the service. Public repository indexes list version
26.3.27-1PGSTY. Runtime tests are listed below.

## Alternatives considered {#alternatives}

- **Rename V2Ray paths and keep only VMess.** Insufficient: it does not provide server setup or the
  working reference protocol. The old template passes Xray's configuration validator, but Xray
  emits a VMess deprecation warning.
- **Build a general proxy manager.** Deferred: subscriptions, protocol catalogs, TUN, transparent
  routing, traffic accounting, and arbitrary configuration import are outside the requested pair.
- **Create a new top-level command now.** Deferred: the existing `build proxy` entry can hold the
  two roles without changing the root grammar or creating a second implementation.
- **Copy the reference server verbatim.** Rejected: a loopback listener requiring PROXY protocol
  cannot serve direct public connections, and existing Nginx port ownership must be respected.
- **Require a connection file for client setup.** Rejected: pasting one URI or supplying parameters
  directly is the primary workflow; a protected file or stdin remains useful for automation.
- **Print generated credentials in ordinary success output.** Rejected: structured results and
  routine logs must remain safe to retain. An explicit export can return a URI to the terminal.

## Contract {#contract}

The implemented role grammar is:

```bash
# On a Linux server, explicitly print the connection URI for copying.
pig build proxy server --host proxy.example.com --target www.example.net:443 --export -

# On a Linux or macOS client, paste the URI returned by the server.
pig build proxy client 'vless://UUID@proxy.example.com:443?encryption=none&security=reality&type=tcp&flow=xtls-rprx-vision&sni=www.example.net&fp=chrome&pbk=PUBLIC_KEY&sid=SHORT_ID&pqv=VERIFY_KEY'

# Alternatively supply connection parameters directly.
pig build proxy client --server proxy.example.com:443 --id UUID --sni www.example.net --public-key PUBLIC_KEY --short-id SHORT_ID --pqv VERIFY_KEY

# Optional protected file input.
pig build proxy client --from ./client.uri

# Reuse a running Linux server's credentials without changing its deployment.
pig build proxy server --host proxy.example.com --export-only --export -

# Non-mutating preview using the default client listener, 127.0.0.1:12345.
pig build proxy client --from ./client.uri --plan

# Explicitly match the inspected client's listener.
pig build proxy client --from ./client.uri --listen 127.0.0.1:8888

# Xray backend behind an already-configured trusted ingress; default bind is 127.0.0.1:9443.
pig build proxy server --host proxy.example.com --target www.example.net:443 --proxy-protocol --export ./client.uri
```

**Server inputs and defaults.** Require the advertised `--host` and a REALITY `--target`.
Public `--port` defaults to 443; the direct listener defaults to that port on all IPv4 interfaces.
On first setup with `--proxy-protocol`, the listener instead defaults to `127.0.0.1:9443`.
An explicit `--listen` changes the bind endpoint without changing the advertised public port.
On repeated setup, omitting `--listen` preserves the existing supported listener rather than
replacing it with a first-setup default.
On first setup, derive SNI from the target hostname unless `--sni` is supplied. On repeated setup,
preserve the existing SNI instead of deriving a replacement. Validate the target's TLS
compatibility before applying configuration; do not silently choose an unrelated camouflage host.

Generate a UUID, X25519 key pair, random eight-byte short ID, and a distinct ML-DSA-65 seed and
verification key. Reuse existing PIG-managed material on repeated setup; ordinary reruns do not
rotate credentials. Rotation and multi-user management require a later explicit contract.
Use `warning` logging, disabled access logging, `decryption: none`, the Vision flow, and an
explicit private-destination blocking rule. Set the geodata environment to the packaged asset path.

**Server idempotence.** Repeating setup with the same inputs must leave existing clients usable
without changing their configuration. Preserve the UUID, X25519 private key and corresponding
client authentication material, short ID, ML-DSA seed and verification key, SNI, and protocol defaults.
With the same advertised host and public port, export the same canonical URI. Generate credentials
only when creating a server configuration for the first time, never because setup runs again.

If the managed configuration and service settings already match the requested state and the
service is running and enabled, perform checks without rewriting files or restarting the service.
If only the service has stopped or become disabled, restore the requested service state using the
existing credentials. Missing, malformed, or ambiguous existing authentication material is an error;
do not repair it by silently regenerating credentials. Reject setup inputs that conflict with the
existing authentication or SNI contract. Such changes need an explicit migration or rotation
contract rather than an ordinary rerun.

**Reuse an existing server.** `server --export-only --host HOST --export DESTINATION` reads the
existing packaged server configuration and exports a connection URI using its current UUID, SNI,
short ID, and derived client verification material. The advertised public port still defaults to
443; never infer it from a loopback backend port. Require one unambiguous supported VLESS/REALITY
inbound, client, SNI, and short ID. Reject ambiguity or malformed material instead of selecting an
arbitrary entry. This mode does not install, rewrite server or service configuration, restart,
enable, or rotate credentials. An explicitly requested file export is its only persistent write.
Derive verification material in process so existing private keys and seeds do not enter subprocess
arguments. This branch performs a read-only query with explicit credential export; the shared server command retains its conservative action annotation.

**Client inputs and defaults.** Accept a single quoted `vless://` positional URI as the primary
workflow. Also support direct parameter mode with `--server host:port`, `--id`, `--sni`,
`--public-key`, and `--short-id`; `--pqv` supplies ML-DSA verification when used by the server.
The direct mode supplies the connection fields while REALITY, RAW, Vision, and Chrome retain
the defaults below. All input forms feed the same validated connection model and renderer.
Keep `--from FILE` and `--from -` as optional file/stdin inputs. Reject mixed connection input
forms rather than silently overriding URI fields with flags. Local options such as `--listen`
remain available for every input form.

`client.uri` is an ordinary UTF-8 text file containing one standard `vless://` URI and an optional
trailing newline; the filename is arbitrary. It is not a new PIG configuration format.
Support the reference combination only: REALITY, Vision, and TCP/RAW. Map URI `type=tcp` to JSON
`network: raw`, `pbk` to `password`, `sid` to `shortId`, and `pqv` to `mldsa65Verify`.
Preserve the ML-DSA verification field; never silently drop it. Reject duplicate or unsupported
parameters that would change connection meaning. The mapping follows the
[upstream sharing specification](https://github.com/XTLS/Xray-core/discussions/716).

On first setup, bind a `socks` inbound to `127.0.0.1:12345`, retaining the existing PIG client
endpoint default. `--listen` can explicitly select `127.0.0.1:8888` to match the inspected client.
On repeated setup, omitting `--listen` preserves the existing managed client's listener.
Provide HTTP support on the same port, UDP enabled, and
multiplexing disabled. Use `encryption: none`, the Vision flow, a Chrome fingerprint, and
`spiderX: /`. The upstream [Socks reference](https://xtls.github.io/en/config/inbounds/socks.html)
documents HTTP support. Validate HTTP and SOCKS independently; a TCP listener alone is insufficient.

The CLI fields map to the existing reference configuration as follows. Client paths below are
relative to its VLESS outbound; server paths are relative to its VLESS inbound.

| CLI field | Client configuration | Server configuration or export |
|:---|:---|:---|
| Server `--host`, `--port`; client `--server` | `settings.vnext[0].address` and `.port` | Advertised URI endpoint; not necessarily the bind endpoint |
| Role-specific `--listen` | Local inbound `listen` and `port` | VLESS inbound `listen` and `port` |
| Client `--id` | `settings.vnext[0].users[0].id` | Matches `settings.clients[0].id` |
| `--sni` | `streamSettings.realitySettings.serverName` | Member of `streamSettings.realitySettings.serverNames` |
| Client `--public-key` | `streamSettings.realitySettings.password` | Derived from `streamSettings.realitySettings.privateKey` |
| Client `--short-id` | `streamSettings.realitySettings.shortId` | Member of `streamSettings.realitySettings.shortIds` |
| Client `--pqv` | `streamSettings.realitySettings.mldsa65Verify` | Derived from `streamSettings.realitySettings.mldsa65Seed` |
| Server `--target` | No direct client field; SNI must be compatible | `streamSettings.realitySettings.target` |
| Server `--proxy-protocol` | No client field | `streamSettings.rawSettings.acceptProxyProtocol: true` |

The reference server keeps `realitySettings.xver: 0`: accepting a PROXY header from Nginx and
forwarding a PROXY header to the REALITY target are different settings. Template defaults preserve
the reference flow, transport security, logging, disabled multiplexing, server private-destination
blocking, and client fingerprint. The public endpoint, bind endpoint, and target have separate roles.

**Platform and service ownership.** Linux uses the configured package repositories and the package
account, configuration path, and unit above. macOS clients use Homebrew Xray, a mode-0600 user
configuration at `~/.config/xray/pig-proxy.json`, and a dedicated user LaunchAgent named
`com.pigsty.xray-proxy`; do not take over another Xray daemon or Homebrew service.
Linux setup requires root and a running systemd instance; macOS client setup requires a regular user and existing Homebrew. macOS server setup is outside this change.

New role setup explicitly starts and enables the managed service; install-only does not start or enable it.
On Linux, PIG owns a bounded `pig-proxy.conf` systemd drop-in for asset location and capabilities.
Direct binding to a privileged port requires only `CAP_NET_BIND_SERVICE` in the bounding and
ambient sets. Reset both sets before assignment because systemd combines positive directives
with the package unit. A loopback backend on an unprivileged port does not need that capability.
Do not depend on permissive host sysctl settings or run Xray as root to make port 443 work.

**Application and recovery.** Parse arguments and preflight paths, port ownership, service conflicts,
and existing configuration before mutations. Recognize managed configuration by its complete
supported shape, not merely an inbound tag. Replacing a different or unsupported client configuration requires `--replace --yes`. Unsupported
server configurations and service units using another account, an explicit different group, or
extra configuration arguments are refused. Preview supports `--plan`, with no installation, credential creation, file writes, or restarts.

Render JSON through Go serialization. Stage protected candidates, validate them with Xray under
the actual service identity, then atomically apply configuration and service changes. Compare
content, ownership, and permissions before treating files as unchanged. On macOS, track persistent
disablement separately from loaded state, enable the managed LaunchAgent on start, and restore both
states on failure. Save original
content, ownership, mode, service enablement, and running state for bounded rollback. Check service
health and connectivity before publishing shell aliases or connection exports. Client setup requires
HTTP 204 from `https://www.google.com/generate_204` through both HTTP and SOCKS, so server outbound
access to this endpoint is part of the client acceptance condition. An application
failure returns nonzero and reports which recovery steps succeeded; an installed package may remain.

Never delete or automatically stop V2Ray. A port conflict stops preflight and produces a migration
instruction. Existing V2Ray configuration remains available for operator-controlled rollback.
Retain `po`, `px`, and `pck` for client convenience, including lowercase proxy variables needed by
build tools. Activating those variables in the calling shell still requires sourcing the generated
shell file and invoking `po`; a child process cannot modify its parent shell.

**Secrets and exports.** `--export FILE` writes the explicitly requested connection export with
mode 0600 and no server private key or ML-DSA seed. `--export -` explicitly delivers one complete
URI on stdout, with non-secret setup diagnostics on stderr. This credential-export mode requires
text output; reject JSON/YAML combinations before mutations instead of putting the URI inside a
structured result. Ordinary setup output remains redacted. Command-line input is supported even
though the caller's shell history or process inspection can retain those supplied credentials;
file and stdin input remain available when that matters.

Treat the whole URI as a credential, including its UUID and authentication fields. Default logs,
plans, JSON/YAML results, and failure details contain only redacted summaries and export paths.
Generate and derive key material in process. Capture validator output privately:
upstream configuration errors can include the invalid secret value. Never use a generic subprocess
capture as a safe credential boundary. Re-exporting to an existing protected file containing the
same canonical URI succeeds without rewriting it. A different existing export is a conflict;
never silently overwrite it or print its contents in a diagnostic.

**Ingress and verification boundaries.** Direct mode is the default one-command server setup.
The explicit `--proxy-protocol` backend mode requires loopback binding and a trusted pre-existing
frontend. PIG does not rewrite Nginx or firewall/cloud rules in this change. A busy public port
causes a clear failure instead of stopping its owner. Backend readiness does not prove public ingress;
server setup reports local checks and leaves external reachability pending until a client handshake
and HTTPS request succeed.

Generating equivalent Xray configurations includes all of the reference pair's protocol and
authentication fields. Reproducing the entire reference deployment also requires Nginx's SNI map,
its PROXY-enabled website backend, and OS-specific service registration. This implementation
manages Xray and its own service settings; it does not claim to recreate that Nginx composition
from a server URI. Configuration equivalence does not imply byte-for-byte preservation of tags,
field order, or installation-specific names.

## Consequences {#impact}

Users get two setup operations connected by one standard URI, or supply client parameters directly,
with matching generated parameters instead of manual JSON editing. No connection file is required.
Both new clients and the historical VMess form default to `127.0.0.1:12345` on first setup.
Direct servers default to the public port on all IPv4 interfaces; PROXY-protocol backends default
to `127.0.0.1:9443`. Repeated setup preserves existing listeners when `--listen` is omitted.
Configuration and service management remain explicit about role, OS, ownership,
and external ingress.

The implementation uses `cmd/build.go` and focused files under `cli/build`. It reuses the
existing plan, output, confirmation, privileged-write, and logging helpers.

## Verification and evolution {#verification}

The implementation is recorded in
[`d090b7c`](https://github.com/pgsty/pig/commit/d090b7c9ab9301c06f9ccb2151c732d598f0e40c),
with ownership and service-state fixes in
[`5d8660c`](https://github.com/pgsty/pig/commit/5d8660c8c12e6c00b77d409efd2287b8299377b1).
Tests cover role syntax, equivalent input forms, redaction, listener defaults, credential reuse,
export conflicts, protected files, capability selection, and plans. Full Go tests, vet, static
analysis, dead-code and complexity checks, and repeated command/build race tests passed locally.

Runtime checks used Xray 26.3.27:

| Environment | Verified behavior |
|:---|:---|
| Fresh Debian 13 ARM64 client | Package installation; service running as `xray`; HTTP and SOCKS health checks; unchanged configuration and PID on repeat setup; rollback after a wrong UUID |
| Fresh Debian server | Direct privileged-port listener; `0640` configuration and `0600` export; stable credentials, files, and PID on repeat setup; rollback after export failure |
| Existing Homebrew Xray on macOS | Isolated LaunchAgent; HTTP/SOCKS connectivity; unchanged reruns; restoration of configuration and persistent disabled state after failure |
| Existing reference server | Read-only export and client tests left its configuration hash and PID unchanged |

The new server's exported URI completed REALITY/Vision connections with ML-DSA verification.
HTTP and SOCKS requests to a reachable TLS site returned HTTP 200. Its network could not reach the
Google health endpoint, so full client setup for that pair failed and rolled back. Full client
setup passed against the existing reference server, through its Nginx/PROXY ingress.

Runtime coverage did not include RPM installation, a fresh Homebrew installation, UDP forwarding,
other Xray versions, or deployment of a new PROXY-protocol frontend.

## Current status {#status}

Released in [v1.9.0](https://github.com/pgsty/pig/releases/tag/v1.9.0).
Nginx ingress automation and credential rotation remain future work.
