Skip to content

Instantly share code, notes, and snippets.

@philomuzzi
Last active May 1, 2026 14:17
Show Gist options
  • Select an option

  • Save philomuzzi/bb13876fda86daad2b0e49b30d04d054 to your computer and use it in GitHub Desktop.

Select an option

Save philomuzzi/bb13876fda86daad2b0e49b30d04d054 to your computer and use it in GitHub Desktop.
Minimal Xray setup for Codex: VLESS Reality + Shadowsocks 2022 + VLESS WS TLS with certbot cert sync.
#!/usr/bin/env bash
# For Codex:
# - Run this script as root on an apt-based Debian/Ubuntu VPS with systemd.
# - Set WS_TLS_DOMAIN, or CADDY_DOMAIN, to the domain served by Caddy.
# - The script is non-interactive: it installs minimal dependencies, installs
# Xray/Caddy if missing, overwrites Xray and Caddy configs after backup,
# enables/restarts services, and prints the generated client values.
# - Generated secrets are persisted in /usr/local/etc/xray/codex-xray-secrets.env
# so reruns keep the same UUIDs, keys, password, WS path, and local WS port.
# - Current layout:
# public 443/tcp -> Caddy TLS -> 127.0.0.1:<random> -> Xray VLESS WS
# public 8443/tcp IPv4+IPv6 -> Xray VLESS REALITY Vision
# public 38443/tcp+udp IPv4+IPv6 -> Xray Shadowsocks 2022
# - Typical invocation:
# WS_TLS_DOMAIN=your.domain.tld bash setup-xray-codex-minimal.sh
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
XRAY_CONFIG_PATH="/usr/local/etc/xray/config.json"
XRAY_INSTALL_URL="https://github.com/XTLS/Xray-install/raw/main/install-release.sh"
XRAY_DIR="/usr/local/etc/xray"
XRAY_SECRETS_PATH="${XRAY_DIR}/codex-xray-secrets.env"
CADDY_CONFIG_PATH="${CADDY_CONFIG_PATH:-/etc/caddy/Caddyfile}"
REALITY_PORT="${REALITY_PORT:-8443}"
SHADOWSOCKS_PORT="${SHADOWSOCKS_PORT:-38443}"
PUBLIC_LISTEN="${PUBLIC_LISTEN:-::}"
VLESS_WS_BACKEND_HOST="${VLESS_WS_BACKEND_HOST:-127.0.0.1}"
VLESS_WS_BACKEND_PORT_OVERRIDE="${VLESS_WS_BACKEND_PORT:-}"
REALITY_SERVER_NAME="${REALITY_SERVER_NAME:-www.microsoft.com}"
REALITY_DEST="${REALITY_DEST:-${REALITY_SERVER_NAME}:443}"
WS_TLS_DOMAIN="${WS_TLS_DOMAIN:-}"
CADDY_DOMAIN="${CADDY_DOMAIN:-${WS_TLS_DOMAIN}}"
CADDY_PUBLIC_PORT="${CADDY_PUBLIC_PORT:-443}"
CADDY_ROOT="${CADDY_ROOT:-/var/www/speedtest}"
CADDY_LOG_PATH="${CADDY_LOG_PATH:-/var/log/caddy/speedtest.log}"
CADDY_SPEEDTEST_PATH="${CADDY_SPEEDTEST_PATH:-/cf-00cfc0dcd2f2111afeca-100mb.bin}"
CADDY_CERT_PATH="${CADDY_CERT_PATH:-}"
CADDY_KEY_PATH="${CADDY_KEY_PATH:-}"
CONFIGURE_UFW="${CONFIGURE_UFW:-1}"
APT_UPDATED=0
TMP_FILES=()
CADDY_TLS_BLOCK=""
usage() {
cat <<EOF
Usage:
WS_TLS_DOMAIN=your.domain.tld bash ${SCRIPT_NAME}
For Codex:
- Run as root on Debian/Ubuntu with apt and systemd.
- This script writes the current recommended layout:
- Caddy owns public 443/tcp and terminates TLS.
- Xray VLESS WS listens only on 127.0.0.1:<random> without TLS.
- Caddy reverse_proxy sends the WS path to that local Xray backend.
- Xray VLESS REALITY listens on 8443/tcp over IPv4+IPv6 by default.
- Xray Shadowsocks 2022 listens on 38443/tcp+udp over IPv4+IPv6 by default.
- Xray direct outbound uses UseIP so IPv4 and IPv6 destinations can both work.
- Persistent secrets are stored in ${XRAY_SECRETS_PATH}.
Required environment:
WS_TLS_DOMAIN Domain served by Caddy; CADDY_DOMAIN is also accepted.
Optional environment:
REALITY_PORT Default: 8443
SHADOWSOCKS_PORT Default: 38443
PUBLIC_LISTEN Default: ::, dual-stack when net.ipv6.bindv6only=0
VLESS_WS_BACKEND_PORT Default: generated high local port, persisted on rerun
CADDY_PUBLIC_PORT Default: 443
CADDY_ROOT Default: /var/www/speedtest
CADDY_LOG_PATH Default: /var/log/caddy/speedtest.log
CADDY_SPEEDTEST_PATH Default: /cf-00cfc0dcd2f2111afeca-100mb.bin
CADDY_CERT_PATH Optional explicit certificate path
CADDY_KEY_PATH Optional explicit private key path
CONFIGURE_UFW Default: 1; add allow rules when ufw is installed
REALITY_SERVER_NAME Default: www.microsoft.com
REALITY_DEST Default: \${REALITY_SERVER_NAME}:443
TLS behavior:
- If CADDY_CERT_PATH and CADDY_KEY_PATH are set, Caddy uses them.
- Else if /etc/caddy/certs/<domain>/fullchain.pem and privkey.pem exist, Caddy uses them.
- Else if /etc/letsencrypt/live/<domain>/fullchain.pem and privkey.pem exist, Caddy uses them.
- Else Caddy uses its normal automatic HTTPS behavior.
Notes:
- This script overwrites ${XRAY_CONFIG_PATH} and ${CADDY_CONFIG_PATH} after backups.
- Xray no longer stores or serves the WS TLS certificate; Caddy owns TLS on 443.
EOF
}
log() {
printf '[%s] %s\n' "$1" "$2"
}
fail() {
log "ERROR" "$1" >&2
exit 1
}
run() {
log "RUN" "$*"
"$@"
}
cleanup_tmp_files() {
local path
for path in "${TMP_FILES[@]}"; do
[ -e "${path}" ] && rm -f "${path}"
done
}
trap cleanup_tmp_files EXIT
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
-h|--help)
usage
exit 0
;;
*)
fail "unknown argument: $1"
;;
esac
shift
done
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
package_installed() {
dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q '^install ok installed$'
}
random_hex() {
local bytes="$1"
od -An -N"${bytes}" -tx1 /dev/urandom | tr -d ' \n'
}
random_base64_32() {
head -c 32 /dev/urandom | base64 -w0
}
random_high_port() {
local n port unused
for unused in $(seq 1 64); do
n="$(od -An -N2 -tu2 /dev/urandom | tr -d ' ')"
port=$((20000 + n % 41000))
if ! command_exists ss; then
printf '%s\n' "${port}"
return 0
fi
if ! ss -H -ltn "sport = :${port}" 2>/dev/null | grep -q .; then
printf '%s\n' "${port}"
return 0
fi
done
fail "could not find an unused high port"
}
require_root() {
[ "$(id -u)" -eq 0 ] || fail "this script must run as root"
}
require_apt() {
command_exists apt-get || fail "this script only supports apt-based Debian/Ubuntu systems"
}
require_systemd() {
command_exists systemctl || fail "systemctl is not available"
}
require_domain() {
[ -n "${CADDY_DOMAIN}" ] || fail "set WS_TLS_DOMAIN or CADDY_DOMAIN"
}
install_packages_if_missing() {
local package missing_packages=()
for package in "$@"; do
package_installed "${package}" || missing_packages+=("${package}")
done
if [ "${#missing_packages[@]}" -eq 0 ]; then
return 0
fi
if [ "${APT_UPDATED}" -eq 0 ]; then
run apt-get update
APT_UPDATED=1
fi
run apt-get install -y --no-install-recommends "${missing_packages[@]}"
}
ensure_base_packages() {
install_packages_if_missing ca-certificates curl unzip
}
ensure_xray_installed() {
local installer
if command_exists xray; then
log "INFO" "xray already installed: $(xray version | head -n 1)"
return 0
fi
installer="$(mktemp)"
TMP_FILES+=("${installer}")
run curl -fsSL "${XRAY_INSTALL_URL}" -o "${installer}"
run bash "${installer}" install --without-geodata --without-logfiles
command_exists xray || fail "xray command not found after install"
}
ensure_caddy_available() {
if command_exists caddy; then
log "INFO" "caddy already installed: $(caddy version)"
return 0
fi
install_packages_if_missing caddy
command_exists caddy || fail "caddy command not found after install"
}
load_or_init_secrets() {
local reality_keys backend_port_override
backend_port_override="${VLESS_WS_BACKEND_PORT_OVERRIDE}"
run mkdir -p "${XRAY_DIR}"
if [ -f "${XRAY_SECRETS_PATH}" ]; then
# shellcheck disable=SC1090
. "${XRAY_SECRETS_PATH}"
fi
if [ -z "${REALITY_PRIVATE_KEY:-}" ] || [ -z "${REALITY_PUBLIC_KEY:-}" ]; then
reality_keys="$(xray x25519)"
REALITY_PRIVATE_KEY="$(printf '%s\n' "${reality_keys}" | awk -F': ' '/^PrivateKey:/{print $2}')"
REALITY_PUBLIC_KEY="$(printf '%s\n' "${reality_keys}" | awk -F': ' '/^Password \(PublicKey\):/{print $2}')"
fi
VLESS_REALITY_UUID="${VLESS_REALITY_UUID:-$(xray uuid)}"
REALITY_SHORT_ID="${REALITY_SHORT_ID:-$(random_hex 8)}"
SHADOWSOCKS_PASSWORD="${SHADOWSOCKS_PASSWORD:-$(random_base64_32)}"
VLESS_WS_TLS_UUID="${VLESS_WS_TLS_UUID:-$(xray uuid)}"
VLESS_WS_TLS_PATH="${VLESS_WS_TLS_PATH:-/$(random_hex 12)}"
if [ -n "${backend_port_override}" ]; then
VLESS_WS_BACKEND_PORT="${backend_port_override}"
else
VLESS_WS_BACKEND_PORT="${VLESS_WS_BACKEND_PORT:-$(random_high_port)}"
fi
umask 077
cat > "${XRAY_SECRETS_PATH}" <<EOF
REALITY_PRIVATE_KEY='${REALITY_PRIVATE_KEY}'
REALITY_PUBLIC_KEY='${REALITY_PUBLIC_KEY}'
VLESS_REALITY_UUID='${VLESS_REALITY_UUID}'
REALITY_SHORT_ID='${REALITY_SHORT_ID}'
SHADOWSOCKS_PASSWORD='${SHADOWSOCKS_PASSWORD}'
VLESS_WS_TLS_UUID='${VLESS_WS_TLS_UUID}'
VLESS_WS_TLS_PATH='${VLESS_WS_TLS_PATH}'
VLESS_WS_BACKEND_PORT='${VLESS_WS_BACKEND_PORT}'
EOF
chmod 600 "${XRAY_SECRETS_PATH}"
}
backup_file() {
local path backup_path
path="$1"
[ -f "${path}" ] || return 0
backup_path="${path}.bak.$(date -u +%Y%m%d%H%M%S)"
run cp -a "${path}" "${backup_path}"
log "INFO" "backed up ${path} to ${backup_path}"
}
detect_caddy_tls() {
local cert key
if { [ -n "${CADDY_CERT_PATH}" ] && [ -z "${CADDY_KEY_PATH}" ]; } ||
{ [ -z "${CADDY_CERT_PATH}" ] && [ -n "${CADDY_KEY_PATH}" ]; }; then
fail "set both CADDY_CERT_PATH and CADDY_KEY_PATH, or neither"
fi
if [ -n "${CADDY_CERT_PATH}" ]; then
cert="${CADDY_CERT_PATH}"
key="${CADDY_KEY_PATH}"
elif [ -r "/etc/caddy/certs/${CADDY_DOMAIN}/fullchain.pem" ] &&
[ -r "/etc/caddy/certs/${CADDY_DOMAIN}/privkey.pem" ]; then
cert="/etc/caddy/certs/${CADDY_DOMAIN}/fullchain.pem"
key="/etc/caddy/certs/${CADDY_DOMAIN}/privkey.pem"
elif [ -r "/etc/letsencrypt/live/${CADDY_DOMAIN}/fullchain.pem" ] &&
[ -r "/etc/letsencrypt/live/${CADDY_DOMAIN}/privkey.pem" ]; then
cert="/etc/letsencrypt/live/${CADDY_DOMAIN}/fullchain.pem"
key="/etc/letsencrypt/live/${CADDY_DOMAIN}/privkey.pem"
else
CADDY_TLS_BLOCK=""
return 0
fi
[ -r "${cert}" ] || fail "certificate not readable: ${cert}"
[ -r "${key}" ] || fail "private key not readable: ${key}"
CADDY_TLS_BLOCK=" tls ${cert} ${key}"
}
ensure_caddy_paths() {
run mkdir -p "${CADDY_ROOT}" "$(dirname "${CADDY_LOG_PATH}")"
if id caddy >/dev/null 2>&1; then
run chown caddy:caddy "$(dirname "${CADDY_LOG_PATH}")"
fi
}
configure_firewall() {
if [ "${CONFIGURE_UFW}" != "1" ] || ! command_exists ufw; then
return 0
fi
run ufw allow "${CADDY_PUBLIC_PORT}/tcp"
run ufw allow "${REALITY_PORT}/tcp"
run ufw allow "${SHADOWSOCKS_PORT}/tcp"
run ufw allow "${SHADOWSOCKS_PORT}/udp"
}
write_xray_config() {
backup_file "${XRAY_CONFIG_PATH}"
cat > "${XRAY_CONFIG_PATH}" <<EOF
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "vless_reality_in",
"listen": "${PUBLIC_LISTEN}",
"port": ${REALITY_PORT},
"protocol": "vless",
"settings": {
"clients": [
{
"id": "${VLESS_REALITY_UUID}",
"flow": "xtls-rprx-vision",
"level": 0
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"dest": "${REALITY_DEST}",
"serverNames": [
"${REALITY_SERVER_NAME}"
],
"privateKey": "${REALITY_PRIVATE_KEY}",
"shortIds": [
"${REALITY_SHORT_ID}"
],
"fingerprint": "chrome"
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
]
}
},
{
"tag": "ss_in",
"listen": "${PUBLIC_LISTEN}",
"port": ${SHADOWSOCKS_PORT},
"protocol": "shadowsocks",
"settings": {
"method": "2022-blake3-chacha20-poly1305",
"password": "${SHADOWSOCKS_PASSWORD}",
"network": "tcp,udp",
"level": 0
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
]
}
},
{
"tag": "vless_ws_in",
"listen": "${VLESS_WS_BACKEND_HOST}",
"port": ${VLESS_WS_BACKEND_PORT},
"protocol": "vless",
"settings": {
"clients": [
{
"id": "${VLESS_WS_TLS_UUID}",
"email": "vless-ws-tls@server",
"level": 0
}
],
"decryption": "none"
},
"streamSettings": {
"network": "ws",
"security": "none",
"wsSettings": {
"path": "${VLESS_WS_TLS_PATH}"
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct",
"settings": {
"domainStrategy": "UseIP"
}
},
{
"protocol": "blackhole",
"tag": "blocked"
}
]
}
EOF
run chmod 644 "${XRAY_CONFIG_PATH}"
run chown root:root "${XRAY_CONFIG_PATH}"
}
write_caddy_config() {
backup_file "${CADDY_CONFIG_PATH}"
ensure_caddy_paths
detect_caddy_tls
cat > "${CADDY_CONFIG_PATH}" <<EOF
{
auto_https disable_redirects
}
${CADDY_DOMAIN}:${CADDY_PUBLIC_PORT} {
${CADDY_TLS_BLOCK}
root * ${CADDY_ROOT}
header {
Cache-Control "no-store, no-cache, must-revalidate"
Pragma "no-cache"
X-Content-Type-Options "nosniff"
}
handle ${CADDY_SPEEDTEST_PATH} {
file_server
}
handle ${VLESS_WS_TLS_PATH} {
reverse_proxy ${VLESS_WS_BACKEND_HOST}:${VLESS_WS_BACKEND_PORT}
}
handle {
respond 404
}
log {
output file ${CADDY_LOG_PATH}
}
}
EOF
run chmod 644 "${CADDY_CONFIG_PATH}"
run chown root:root "${CADDY_CONFIG_PATH}"
}
validate_and_restart() {
run xray run -test -config "${XRAY_CONFIG_PATH}"
run caddy validate --config "${CADDY_CONFIG_PATH}"
run systemctl enable xray
run systemctl restart xray
run systemctl enable caddy
if ! systemctl reload caddy; then
run systemctl restart caddy
fi
}
print_summary() {
cat <<EOF
[VLESS Reality]
Port: ${REALITY_PORT}
UUID: ${VLESS_REALITY_UUID}
Public Key: ${REALITY_PUBLIC_KEY}
Short ID: ${REALITY_SHORT_ID}
Server Name: ${REALITY_SERVER_NAME}
Flow: xtls-rprx-vision
Security: reality
[Shadowsocks-2022]
Port: ${SHADOWSOCKS_PORT}
Method: 2022-blake3-chacha20-poly1305
Password: ${SHADOWSOCKS_PASSWORD}
Network: tcp,udp
[VLESS WS via Caddy]
Client host: ${CADDY_DOMAIN}
Client port: ${CADDY_PUBLIC_PORT}
Client TLS: enabled
Network: ws
UUID: ${VLESS_WS_TLS_UUID}
Path: ${VLESS_WS_TLS_PATH}
Backend: ${VLESS_WS_BACKEND_HOST}:${VLESS_WS_BACKEND_PORT}
Xray TLS: none
[Files]
Xray config: ${XRAY_CONFIG_PATH}
Caddyfile: ${CADDY_CONFIG_PATH}
Secrets: ${XRAY_SECRETS_PATH}
EOF
}
main() {
parse_args "$@"
require_root
require_apt
require_systemd
require_domain
ensure_base_packages
ensure_xray_installed
ensure_caddy_available
load_or_init_secrets
write_xray_config
write_caddy_config
configure_firewall
validate_and_restart
print_summary
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment