What this guide deploys
This guide keeps exactly two VLESS server profiles:
- TCP + REALITY + Vision on TCP/443 — the active baseline.
- XHTTP + REALITY over HTTP/2 on TCP/443 — a parked alternative for controlled comparison.
Only one profile can listen on the same IP and TCP/443 at a time. Keep the inactive profile outside Xray’s active configuration and switch deliberately. If you need simultaneous A/B testing on port 443, use a second public IP or a second VPS.
I retested both profiles on 24–25 August 2026 with the stable Xray-core v26.3.27. A containerized Xray client connected to an anonymized Ubuntu VPS, and each profile completed the same repeated-request, upload, download, concurrency, post-idle and HLS workload.
The scope of this guide is configuration correctness, client compatibility, data transfer and streaming. Use these tools only on systems and networks you are authorized to administer or test.
Why only these two profiles?
Both profiles use Xray. TCP+REALITY+Vision follows the project’s established TCP example and provides the simpler control. XHTTP changes the transport to HTTP semantics and provides a second experiment without opening another public port.
I also tested VLESS Encryption with Vision, XHTTP and REALITY. It worked, but it was not retained because it used the same server address, TCP/443, XHTTP and REALITY while adding another credential set and client configuration.
Prerequisites
- An Ubuntu 24.04 VPS with administrator access.
- TCP/443 permitted in the provider firewall and in any active host firewall.
- Correct system time; this guide permits at most 60 seconds of clock difference.
- A current client whose core is compatible with Xray 26.3.27 or newer.
- No existing service already bound to TCP/443.
- Docker on the local machine for the QR and containerized client tests.
Check the server clock and confirm that TCP/443 is not already in use:
timedatectl status
sudo ss -lntp 'sport = :443'
Do not open unrelated proxy ports. XHTTP over H2 in this guide still uses only TCP/443; it does not require UDP/443.
1. Install a pinned Xray version on Ubuntu
As of the test date, 26.3.27 was the newest release marked stable. Pinning the tested version avoids silently replacing the baseline with a pre-release. Check the Xray releases before intentionally upgrading, and test the server and actual client core together.
The official installer supports a fixed version, a dedicated service user and an installation without file logs. Download it first so you can inspect it instead of piping a remote script directly into a root shell:
sudo apt-get update
sudo apt-get install -y ca-certificates curl less openssl util-linux
getent passwd xray >/dev/null || \
sudo useradd --system --user-group --no-create-home \
--shell /usr/sbin/nologin xray
install_script="$(mktemp)"
curl -fsSL \
https://github.com/XTLS/Xray-install/raw/main/install-release.sh \
-o "$install_script"
less "$install_script"
sudo bash "$install_script" install \
--version v26.3.27 \
--install-user xray \
--without-logfiles
rm -f "$install_script"
sudo systemctl stop xray
/usr/local/bin/xray version
sha256sum /usr/local/bin/xray
The installer verifies the downloaded release archive’s SHA-256 digest. Recording the installed binary digest makes later audits easier. --without-logfiles avoids creating Xray file logs; service output remains available through the systemd journal. The routing rules below use geoip:private, so keep the installer’s default geodata installation and do not add --without-geodata.
2. Generate separate credentials
Generate a UUID, REALITY X25519 key pair and short ID for the TCP profile:
/usr/local/bin/xray uuid
/usr/local/bin/xray x25519
openssl rand -hex 8
Repeat all three commands for the parked XHTTP alternative, then generate a random XHTTP path component:
openssl rand -hex 16
Record the outputs somewhere private and map them to these placeholders:
| Placeholder | Meaning | Where it belongs |
|---|---|---|
<TCP_UUID> / <XHTTP_UUID> | VLESS client identity | Server and corresponding client |
<TCP_PRIVATE_KEY> / <XHTTP_PRIVATE_KEY> | REALITY server private key | Server only |
<TCP_PUBLIC_VALUE> / <XHTTP_PUBLIC_VALUE> | Password (PublicKey) printed by xray x25519 | Client password, or pbk in a share URL |
<TCP_SHORT_ID> / <XHTTP_SHORT_ID> | 16 hexadecimal characters from openssl | Server and corresponding client |
<XHTTP_PATH> | / followed by the random path component | XHTTP server and client |
<SERVER_ADDRESS> | Public hostname or address of the server you control | Client URLs only |
The Password (PublicKey) output is the server’s public X25519 value. Never put the REALITY privateKey in a client, QR code or share URL. Treat UUIDs, short IDs and complete VLESS URLs as private profile material; a complete URL grants access to the proxy.
3. Select and verify a REALITY target
<TARGET_HOST> is the hostname of the external HTTPS endpoint used by REALITY’s target and by the client’s SNI. It is not the VPS address. The same hostname must appear in server target, server serverNames and client sni.
Current REALITY documentation uses target; dest is the legacy alias. Select a TLS 1.3/H2 endpoint whose use is appropriate for your deployment, and make the client serverName match an SNI accepted by the target certificate. Do not copy a hostname from a tutorial without testing it from your server.
Set TARGET_HOST to each candidate and test it from the VPS:
read -r -p 'Target hostname: ' TARGET_HOST
/usr/local/bin/xray tls ping "$TARGET_HOST:443"
unset TARGET_HOST
Check that the TLS handshake works, the intended SNI appears under Cert's allowed domains, and the certificate behavior is appropriate. REALITY forwards unauthenticated traffic to target, so read the upstream fallback warning and choose the endpoint deliberately.
The examples below deliberately use <TARGET_HOST> rather than revealing the target used by the tested server.
4. Store both profiles root-only
Keep the two complete configurations in a root-only directory. The active copy will be readable by the xray service group, while the parked source profiles remain inaccessible to the service account.
sudo install -d -o root -g root -m 0700 \
/usr/local/etc/xray/profiles
sudoedit /usr/local/etc/xray/profiles/tcp-reality-vision.json
sudoedit /usr/local/etc/xray/profiles/xhttp-reality-h2.json
sudo chmod 0600 \
/usr/local/etc/xray/profiles/tcp-reality-vision.json \
/usr/local/etc/xray/profiles/xhttp-reality-h2.json
Profile A: TCP + REALITY + Vision
Put this in tcp-reality-vision.json and replace every placeholder:
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "vless-tcp-reality-vision",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "<TCP_UUID>",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"target": "<TARGET_HOST>:443",
"xver": 0,
"serverNames": ["<TARGET_HOST>"],
"privateKey": "<TCP_PRIVATE_KEY>",
"minClientVer": "26.3.27",
"maxTimeDiff": 60000,
"shortIds": ["<TCP_SHORT_ID>"]
}
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom"
},
{
"tag": "deny",
"protocol": "blackhole"
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "deny"
},
{
"type": "field",
"protocol": ["bittorrent"],
"outboundTag": "deny"
}
]
}
}
clients is intentional and matches the official server examples. During the stable 26.3.27 test, replacing it with users still passed JSON validation but did not register the inbound user in this configuration path.
Profile B: XHTTP + REALITY over H2
Put this in xhttp-reality-h2.json and use the XHTTP-specific credentials:
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "vless-xhttp-reality-h2",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "<XHTTP_UUID>"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"show": false,
"target": "<TARGET_HOST>:443",
"xver": 0,
"serverNames": ["<TARGET_HOST>"],
"privateKey": "<XHTTP_PRIVATE_KEY>",
"minClientVer": "26.3.27",
"maxTimeDiff": 60000,
"shortIds": ["<XHTTP_SHORT_ID>"]
},
"xhttpSettings": {
"path": "<XHTTP_PATH>"
}
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom"
},
{
"tag": "deny",
"protocol": "blackhole"
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "deny"
},
{
"type": "field",
"protocol": ["bittorrent"],
"outboundTag": "deny"
}
]
}
}
Start with the minimal official XHTTP+REALITY example approach: path plus defaults, without copied extra, XMUX or tuning blocks. XHTTP already has its own multiplexing. Do not add xtls-rprx-vision to this retained XHTTP profile; that would create a different configuration with different client requirements.
In the test, Xray’s automatic direct-REALITY behavior selected stream-one and negotiated HTTP/2. I set mode=stream-one explicitly in the client share URL below because some GUI import paths have mishandled auto.
5. Validate, activate and switch profiles
Never restart Xray with an unvalidated configuration. Validate both parked files first:
sudo /usr/local/bin/xray run -test \
-config /usr/local/etc/xray/profiles/tcp-reality-vision.json
sudo /usr/local/bin/xray run -test \
-config /usr/local/etc/xray/profiles/xhttp-reality-h2.json
Before starting the service, permit TCP/443 in the provider firewall and in any active host firewall. Keep the SSH rule in place while changing firewall settings. Then activate the TCP baseline:
sudo install -o root -g xray -m 0640 \
/usr/local/etc/xray/profiles/tcp-reality-vision.json \
/usr/local/etc/xray/config.json
sudo /usr/local/bin/xray run -test \
-config /usr/local/etc/xray/config.json
sudo systemctl enable xray
sudo systemctl restart xray
Check the listener and journal:
sudo ss -lntp 'sport = :443'
sudo systemctl status xray --no-pager
sudo journalctl -u xray -n 100 --no-pager
Add a safe profile-switch helper
Save the following as ~/switch-xray-profile.sh, then make it executable with chmod 0700 ~/switch-xray-profile.sh. It accepts tcp, xhttp or status, validates before and after replacement, waits for Xray to bind TCP/443, and restores the previous active file if the restart fails:
#!/usr/bin/env bash
set -Eeuo pipefail
readonly XRAY=/usr/local/bin/xray
readonly SERVICE=xray
readonly XRAY_DIR=/usr/local/etc/xray
readonly PROFILES="$XRAY_DIR/profiles"
readonly ACTIVE="$XRAY_DIR/config.json"
readonly TCP="$PROFILES/tcp-reality-vision.json"
readonly XHTTP="$PROFILES/xhttp-reality-h2.json"
readonly LOCK=/run/lock/xray-profile-switch.lock
backup=
candidate=
rollback_pending=0
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
current_profile() {
if cmp -s "$ACTIVE" "$TCP"; then
printf 'tcp\n'
elif cmp -s "$ACTIVE" "$XHTTP"; then
printf 'xhttp\n'
else
printf 'unknown\n'
fi
}
wait_for_listener() {
local attempt
for attempt in {1..20}; do
ss -H -lntp 'sport = :443' | grep -Fq '"xray"' && return 0
sleep 0.25
done
return 1
}
cleanup_temp() {
local path
for path in "$backup" "$candidate"; do
case "$path" in
"$XRAY_DIR"/.switch-*) rm -f -- "$path" ;;
esac
done
}
rollback() {
printf 'Switch failed; restoring the previous profile.\n' >&2
install -o root -g xray -m 0640 -- "$backup" "$ACTIVE"
"$XRAY" run -test -config "$ACTIVE"
systemctl restart "$SERVICE"
systemctl is-active --quiet "$SERVICE"
wait_for_listener
rollback_pending=0
}
on_exit() {
local exit_code=$?
if (( rollback_pending == 1 )); then
rollback || exit_code=1
fi
cleanup_temp
exit "$exit_code"
}
show_status() {
printf 'profile: %s\n' "$(current_profile)"
printf 'service: %s\n' "$(systemctl is-active "$SERVICE" || true)"
"$XRAY" run -test -config "$ACTIVE"
ss -lntp 'sport = :443' || true
}
[[ $# -eq 1 ]] || die 'usage: switch-xray-profile.sh {tcp|xhttp|status}'
case "$1" in tcp|xhttp|status) ;; *) die "unknown argument: $1" ;; esac
if (( EUID != 0 )); then
exec sudo -- "$0" "$@"
fi
exec 9>"$LOCK"
flock -n 9 || die 'another profile switch is already running'
trap on_exit EXIT INT TERM
if [[ "$1" == status ]]; then
show_status
trap - EXIT INT TERM
exit 0
fi
case "$1" in tcp) target=$TCP ;; xhttp) target=$XHTTP ;; esac
[[ -f "$target" ]] || die "missing profile: $target"
[[ -f "$ACTIVE" ]] || die "missing active config: $ACTIVE"
if [[ "$(current_profile)" == "$1" ]]; then
printf '%s is already active.\n' "$1"
show_status
trap - EXIT INT TERM
exit 0
fi
"$XRAY" run -test -config "$target"
backup=$(mktemp "$XRAY_DIR/.switch-backup.XXXXXX")
candidate=$(mktemp "$XRAY_DIR/.switch-candidate.XXXXXX")
install -o root -g xray -m 0640 -- "$ACTIVE" "$backup"
install -o root -g xray -m 0640 -- "$target" "$candidate"
rollback_pending=1
mv -f -- "$candidate" "$ACTIVE"
candidate=
"$XRAY" run -test -config "$ACTIVE"
systemctl restart "$SERVICE"
systemctl is-active --quiet "$SERVICE"
wait_for_listener || die 'Xray did not bind TCP/443'
rollback_pending=0
cleanup_temp
trap - EXIT INT TERM
printf 'Active profile: %s. Switch the client profile too.\n' "$1"
show_status
Use it from the normal SSH account; it requests sudo itself:
~/switch-xray-profile.sh status
~/switch-xray-profile.sh xhttp
~/switch-xray-profile.sh tcp
The corresponding client profile must be switched at the same time. There is no automatic negotiation between TCP+Vision and XHTTP merely because both configurations exist on disk. Run the journal command again after every switch.
6. Build client import URLs
Replace the placeholders locally. Never paste a completed URL into an issue, chat, analytics system or public QR generator.
TCP + REALITY + Vision:
vless://<TCP_UUID>@<SERVER_ADDRESS>:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=<TARGET_HOST>&fp=chrome&pbk=<TCP_PUBLIC_VALUE>&sid=<TCP_SHORT_ID>&type=tcp#reality-tcp-vision
XHTTP + REALITY over H2:
vless://<XHTTP_UUID>@<SERVER_ADDRESS>:443?encryption=none&security=reality&sni=<TARGET_HOST>&fp=chrome&pbk=<XHTTP_PUBLIC_VALUE>&sid=<XHTTP_SHORT_ID>&type=xhttp&path=<URL_ENCODED_XHTTP_PATH>&mode=stream-one#reality-xhttp-h2
The XHTTP path must start with /, and that leading / must be URL-encoded as %2F in the share URL. After importing, inspect the generated client profile rather than trusting the label:
- address and TCP port are correct;
securityis REALITY;- SNI, public value and short ID match the selected server profile;
- the TCP profile has
xtls-rprx-vision; - the XHTTP profile has the exact decoded path and
stream-one, but no Vision flow; - the client’s actual Xray core satisfies
minClientVer.
Current REALITY documentation defaults minClientVer to 26.3.27. If an application cannot connect, identify its bundled core and test with current Xray-core before changing the server’s version requirement.
7. Import from clipboard or a QR code
If the client exposes an “import from clipboard” action, use it when the URL is already on the same device.
For another device, generate the QR code locally. This single helper uses an ephemeral Debian container and asks whether to print the QR in the terminal or create profile.png:
bash
umask 077
make_qr() {
docker run --rm -i debian:13-slim sh -c \
'export DEBIAN_FRONTEND=noninteractive; apt-get update -qq && apt-get install -y -qq --no-install-recommends qrencode >/dev/null && qrencode -t "$1" -o - -r /dev/stdin' \
qrencode "$1"
}
read -r -s -p 'Paste the VLESS URL: ' VLESS_URL
echo
read -r -p 'Output [terminal/png]: ' QR_OUTPUT
case "$QR_OUTPUT" in
terminal)
printf '%s' "$VLESS_URL" | make_qr ANSIUTF8
;;
png)
printf '%s' "$VLESS_URL" | make_qr PNG > profile.png
chmod 0600 profile.png
printf 'Created profile.png\n'
;;
*)
printf 'Expected terminal or png\n' >&2
exit 2
;;
esac
unset VLESS_URL QR_OUTPUT
unset -f make_qr
exit
Scan terminal or PNG output from the client’s “import QR code” action. QR is only an encoding, not encryption: anyone who can see or scan it receives the profile credentials. Avoid screenshots and cloud photo backups. Delete profile.png when it is no longer needed.
Use a different display name for each profile and verify its fields after scanning. If a QR or completed URL has been shared accidentally, generate a new UUID, REALITY key pair and short ID rather than merely changing the display name.
8. Client compatibility
The reference client is the same Xray-core version used by the server. For GUI clients, compatibility depends on the bundled or selected core as well as the application version. Only combinations marked Verified below were run during this update.
| Platform/client | TCP+REALITY+Vision | XHTTP+REALITY H2 | Practical guidance |
|---|---|---|---|
| Xray-core 26.3.27 container | Verified | Verified | Reference client used for configuration and workload tests |
| v2rayNG 2.2.6 on Android | Verified | Verified with stream-one | Android client used in this test |
| v2rayN | Not run in this update | Not run in this update | Use its Xray core and set stream-one explicitly for this XHTTP profile |
| Streisand on iOS/iPadOS | Not run in this update | Not run in this update | Its App Store listing names VLESS (REALITY), but this guide makes no XHTTP compatibility claim |
I checksum-verified the official v2rayNG 2.2.6 release ARM64 APK and tested it on an Android 9 emulator. It imported both share URLs, labeled XHTTP as VLESS / xhttp / reality, established Android’s system VPN for each profile, passed real-connectivity checks, and carried a separate WebView application’s HTTPS request. This validates APK import and Android VPN behavior; it is not a second throughput benchmark.
A v2rayN issue reports that explicitly setting stream-one fixed an XHTTP+REALITY profile that failed with auto. The Android share URL in this update used explicit stream-one; the container log showed automatic selection of stream-one. Test the exact application release and generated runtime configuration you intend to use.
9. Functional, transfer and streaming tests
A successful TCP or TLS handshake does not prove that a proxy carries application data correctly. Test DNS through the proxy, repeated requests, upload and download, a long-lived flow, requests after idle, concurrency and streaming-sized transfers.
If your local Xray client exposes SOCKS on 127.0.0.1:11080, this confirms remote DNS resolution and the proxy egress:
curl --fail --show-error \
--socks5-hostname 127.0.0.1:11080 \
https://www.cloudflare.com/cdn-cgi/trace
Use --socks5-hostname, not plain --socks5, so DNS resolution also goes through the proxy. During XHTTP testing, temporarily use info client logs and verify that Xray reports the expected path, stream-one and HTTP version 2.
The controlled test used the same anonymized VPS TCP/443 endpoint and the same containerized workload for each candidate, with no automatic retries:
- 25 sequential HTTPS requests;
- five requests after 15 seconds idle;
- one 50 MB long-lived download and three 5 MB downloads;
- one 10 MB upload;
- eight concurrent requests;
- one HLS playlist and twelve sequential 720p six-second segments.
Both retained profiles passed all 56 requests, including the sustained download, upload and concurrency checks. Each also downloaded all twelve HLS segments before their six-second playback deadlines.
These results show functional, sustained bidirectional transfer on one client-to-server route; they are not a speed ranking. Both profiles used the same server address and TCP/443, so switching changed the Xray transport and credentials rather than the infrastructure. A high-port test would measure a different listener path.
For repeatable testing, record the source network, timestamp, client/server versions and exact failure stage. Keep automatic retries disabled, run at least 25 initial requests, include sustained data, use the same fixed idle interval for every profile, and distinguish DNS failure, connect timeout, REALITY rejection, XHTTP failure, reset and direction-specific transfer failure.
GUI behavior can change when an application updates its bundled core or import logic, so retain the container test as a reference. XHTTP is a measured alternative, not a universal upgrade: keep TCP active by default and switch only after validating the parked XHTTP file and its matching client profile.
Sources
- Xray-core releases
- Official Xray installer
- Official VLESS inbound reference
- Official REALITY configuration reference
- Official XHTTP transport reference
- Official TCP+Vision+REALITY example
- Official minimal XHTTP+REALITY example
- XHTTP maintainer discussion
- Official v2rayNG 2.2.6 release
- v2rayN XHTTP
stream-oneissue
