Last active
May 1, 2026 14:17
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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