Skip to content

Instantly share code, notes, and snippets.

@DavidOsipov
Last active February 27, 2026 15:50
Show Gist options
  • Select an option

  • Save DavidOsipov/ee38483fbdb10913346e3ef526980430 to your computer and use it in GitHub Desktop.

Select an option

Save DavidOsipov/ee38483fbdb10913346e3ef526980430 to your computer and use it in GitHub Desktop.
Monolithic installation script of Xray-Core for OpenBSD
#!/bin/ksh
#
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 David Osipov <personal@david-osipov.vision>
# Website: https://david-osipov.vision/
#
# $OpenBSD$
#
# Xray-core installer for OpenBSD.
# Strictly follows hier(7), bsd.own.mk, rc.d(8) and login.conf(5) standards.
# Designed for doas/sudo + cron usage.
set -eu
umask 022
# ==================== CONFIGURATION (hier(7) compliant) ====================
BIN_PATH="/usr/local/bin/xray"
SHARE_PATH="/usr/local/share/xray" # Architecture-independent geo assets
CONF_DIR="/etc/xray"
LOG_DIR="/var/log/xray"
RC_SCRIPT="/etc/rc.d/xray"
DAEMON_USER="_xray"
# daemon_class is READ-ONLY in rc.subr — it is automatically derived from
# the rc.d script name by searching login.conf(5) for a matching class.
DAEMON_CLASS="xray"
GITHUB_REPO="XTLS/Xray-core"
# Script operation flags
QUIET=0
FORCE=0
# ========================================================================
# Sanity check: Ensure script is running with root privileges
if [ "$(id -u)" -ne 0 ]; then
echo "error: This script requires root privileges. Please run using doas or sudo." >&2
exit 1
fi
# OpenBSD native architecture detection for downloading the correct binary
case $(uname -m) in
amd64) XRAY_ARCH="64" ;;
i386) XRAY_ARCH="32" ;;
arm64) XRAY_ARCH="arm64-v8a" ;;
*) echo "error: Architecture $(uname -m) is currently not supported by Xray releases." >&2; exit 1 ;;
esac
# Cleanup function to remove temporary files upon script exit or interruption
cleanup() {
[ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ] && rm -rf "$TMP_DIR"
[ -n "${CLIENT_INFO_FILE:-}" ] && [ -f "$CLIENT_INFO_FILE" ] && rm -f "$CLIENT_INFO_FILE"
}
trap cleanup EXIT INT TERM
# Logging utilities
log() { [ "$QUIET" -eq 1 ] && logger -t xray-install -p user.info "$1" || echo "info: $1"; }
error() { [ "$QUIET" -eq 1 ] && logger -t xray-install -p user.err "$1" || echo "error: $1" >&2; }
# POSIX/OpenBSD compliant version comparison (workaround since there is no GNU sort -V)
version_gt() {
local top
top=$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -n1)
[ "$top" != "$1" ]
}
# Extracts the version number of the currently installed binary
get_current_version() {
[ -x "$BIN_PATH" ] || { echo ""; return; }
"$BIN_PATH" version 2>/dev/null | head -n1 | awk '{print $2}' | tr -d 'v'
}
# Queries GitHub API for the latest release tag
get_latest_version() {
ftp -V -o - "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null |
tr ',' '\n' | awk -F'"' '/"tag_name":/ {print $4}' | tr -d 'v'
}
# Creates a dedicated, unprivileged user for running the daemon securely
create_daemon_user() {
if ! id -u "$DAEMON_USER" >/dev/null 2>&1; then
log "Creating dedicated system user '$DAEMON_USER' to isolate the daemon..."
useradd -s /sbin/nologin -d /var/empty -c "Xray Proxy Service" "$DAEMON_USER"
else
log "System user '$DAEMON_USER' already exists, proceeding..."
fi
}
# Adds the xray login class to /etc/login.conf.
setup_login_class() {
if grep -q "^${DAEMON_CLASS}:" /etc/login.conf 2>/dev/null; then
log "Login class '${DAEMON_CLASS}' already exists in /etc/login.conf."
if ! grep -q "setenv=XRAY_LOCATION_ASSET" /etc/login.conf 2>/dev/null; then
echo "warning: Login class '${DAEMON_CLASS}' exists but lacks XRAY_LOCATION_ASSET." >&2
echo " Please manually add: :setenv=XRAY_LOCATION_ASSET=${SHARE_PATH}:" >&2
fi
return
fi
log "Appending '${DAEMON_CLASS}' login class to /etc/login.conf..."
cat >> /etc/login.conf << EOF
${DAEMON_CLASS}:\\
:setenv=XRAY_LOCATION_ASSET=${SHARE_PATH}:\\
:openfiles-cur=4096:\\
:openfiles-max=8192:\\
:tc=daemon:
EOF
cap_mkdb /etc/login.conf
log "Rebuilt /etc/login.conf database via cap_mkdb(8)."
}
# Generates and installs a canonical, pristine rc.d(8) service script.
install_rc_script() {
log "Generating and installing OpenBSD rc.d(8) service script at $RC_SCRIPT..."
cat > "$RC_SCRIPT" << EOF
#!/bin/ksh
#
# \$OpenBSD\$
#
# Xray-core daemon control script.
# Auto-generated by install-xray.sh
daemon="${BIN_PATH}"
daemon_flags="run -confdir ${CONF_DIR}"
daemon_user="${DAEMON_USER}"
daemon_timeout="60"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_cmd \$1
EOF
chmod 555 "$RC_SCRIPT"
chown root:wheel "$RC_SCRIPT"
}
# Generates a default configuration with best practices for DPI evasion
generate_default_config() {
local conf_file="${CONF_DIR}/config.json"
if [ -f "$conf_file" ]; then
log "Configuration file already exists at $conf_file. Skipping default config generation."
return
fi
log "Generating default XHTTP + REALITY + Vision configuration..."
# Generate cryptographic materials using Xray
local uuid=$("$BIN_PATH" uuid)
local path_uuid=$("$BIN_PATH" uuid)
local keys=$("$BIN_PATH" x25519)
local priv_key=$(echo "$keys" | awk '/Private key:/ {print $3}')
local pub_key=$(echo "$keys" | awk '/Public key:/ {print $3}')
local short_id=$(openssl rand -hex 8)
local vlessenc=$("$BIN_PATH" vlessenc)
local dec_key=$(echo "$vlessenc" | awk '/Decryption:/ {print $2}')
local enc_key=$(echo "$vlessenc" | awk '/Encryption:/ {print $2}')
cat > "$conf_file" << EOF
{
"log": {
"loglevel": "warning",
"access": "${LOG_DIR}/access.log",
"error": "${LOG_DIR}/error.log",
"dnsLog": false
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"ip": ["geoip:private", "geoip:ru"],
"outboundTag": "block"
},
{
"type": "field",
"domain": ["geosite:ru", "geosite:category-ads-all"],
"outboundTag": "block"
},
{
"type": "field",
"protocol": ["bittorrent"],
"outboundTag": "block"
}
]
},
"dns": {
"servers": [
"https://8.8.8.8/dns-query",
"https://1.1.1.1/dns-query",
"localhost"
]
},
"inbounds": [
{
"listen": "127.0.0.1",
"port": 10443,
"protocol": "vless",
"tag": "vless_reality_xhttp",
"settings": {
"clients": [
{
"id": "${uuid}",
"email": "user@example.com",
"flow": "xtls-rprx-vision"
}
],
"decryption": "${dec_key}"
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"xhttpSettings": {
"host": "YOUR_DOMAIN_HERE",
"path": "/${path_uuid}",
"mode": "auto",
"extra": {
"xPaddingBytes": "100-5000",
"noSSEHeader": false,
"scMaxEachPostBytes": 1000000,
"scMaxBufferedPosts": 30,
"scStreamUpServerSecs": "20-80",
"xmux": {
"maxConcurrency": "16-32",
"hMaxRequestTimes": "600-900",
"hMaxReusableSecs": "1800-3000"
}
}
},
"realitySettings": {
"show": false,
"target": "127.0.0.1:8443",
"xver": 1,
"serverNames": [
"YOUR_DOMAIN_HERE"
],
"privateKey": "${priv_key}",
"shortIds": [
"${short_id}"
]
}
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct",
"streamSettings": {
"sockopt": {
"tcpKeepAliveIdle": 300
}
}
},
{
"protocol": "blackhole",
"tag": "block"
}
]
}
EOF
chown root:"$DAEMON_USER" "$conf_file"
chmod 640 "$conf_file"
# Save client details to a temporary file to display at the end
CLIENT_INFO_FILE=$(mktemp /tmp/xray_client_info.XXXXXX)
cat > "$CLIENT_INFO_FILE" << EOF
[CLIENT CONFIGURATION DETAILS]
UUID : ${uuid}
Path : /${path_uuid}
Public Key : ${pub_key}
Short ID : ${short_id}
Encryption : ${enc_key}
(Use 'Encryption' value in the client's 'Decryption' or 'Fingerprint' field depending on the app)
EOF
}
install_xray() {
local version="$1"
log "Starting Xray-core deployment process..."
if [ "$version" = "latest" ]; then
log "Querying GitHub for the latest release version..."
version=$(get_latest_version)
[ -z "$version" ] && { error "Failed to fetch the latest version tag from GitHub API."; exit 1; }
version="v${version}"
elif [ "${version#v}" = "$version" ]; then
version="v${version}"
fi
local current
current=$(get_current_version)
local target=${version#v}
if [ -n "$current" ] && [ "$current" = "$target" ] && [ "$FORCE" -eq 0 ]; then
log "Xray v${current} is already installed and matches the requested version."
[ "$QUIET" -eq 0 ] && echo "Hint: Use '$0 install --force' if you need to cleanly reinstall."
exit 0
fi
# ==================== DOWNLOAD & VERIFICATION ====================
TMP_DIR=$(mktemp -d /tmp/xray.XXXXXX)
local zip="${TMP_DIR}/xray.zip"
local dgst="${TMP_DIR}/xray.zip.dgst"
log "Downloading Xray release ${version} for architecture ${XRAY_ARCH}..."
ftp -V -o "$zip" "https://github.com/${GITHUB_REPO}/releases/download/${version}/Xray-openbsd-${XRAY_ARCH}.zip" \
|| { error "Failed to download the Xray zip archive."; exit 1; }
log "Downloading SHA256 signature file..."
ftp -V -o "$dgst" "https://github.com/${GITHUB_REPO}/releases/download/${version}/Xray-openbsd-${XRAY_ARCH}.zip.dgst" \
|| { error "Failed to download the .dgst signature file."; exit 1; }
log "Verifying package integrity via SHA256 checksum..."
local expected actual
expected=$(sed -n 's/.*256= *//p' "$dgst" | head -n1)
actual=$(sha256 -q "$zip")
if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then
error "CRITICAL: SHA256 checksum verification failed! Archive might be corrupted or compromised."
exit 1
fi
log "Checksum verification passed successfully."
log "Extracting archive contents..."
unzip -q -o "$zip" -d "$TMP_DIR"
# ==================== DEPLOYMENT ====================
create_daemon_user
setup_login_class
log "Installing binary to $BIN_PATH..."
install -m 555 -o root -g bin "${TMP_DIR}/xray" "$BIN_PATH"
log "Deploying geo-assets to $SHARE_PATH..."
install -d -m 755 -o root -g bin "$SHARE_PATH"
install -m 644 -o root -g bin "${TMP_DIR}/geoip.dat" "${TMP_DIR}/geosite.dat" "$SHARE_PATH/"
log "Configuring directory permissions..."
# Config directory: root:_xray 750 — daemon reads, only root modifies
install -d -m 750 -o root -g "$DAEMON_USER" "$CONF_DIR"
# Generate default config if it doesn't exist
generate_default_config
chown -R root:"$DAEMON_USER" "$CONF_DIR"
find "$CONF_DIR" -name '*.json' -exec chmod 640 {} + 2>/dev/null || true
# Log directory: _xray:_xray 750 — daemon has full control over its logs
install -d -m 750 -o "$DAEMON_USER" -g "$DAEMON_USER" "$LOG_DIR"
install_rc_script
# ==================== FINAL SYSTEM MESSAGES ====================
if [ "$QUIET" -eq 0 ]; then
echo ""
echo "================================================================================"
echo " Xray-core ${version} has been deployed successfully!"
echo "--------------------------------------------------------------------------------"
echo " Executable : $BIN_PATH"
echo " Geo Assets : $SHARE_PATH/ (geoip.dat, geosite.dat)"
echo " Configs : $CONF_DIR/ (root:_xray, 640 on *.json)"
echo " Logs : $LOG_DIR/ (_xray:_xray 750)"
echo " rc.d script : $RC_SCRIPT"
echo " login class : ${DAEMON_CLASS} (env injected, openfiles tuned)"
if [ -f "${CLIENT_INFO_FILE:-}" ]; then
echo ""
echo " [!] A DEFAULT CONFIGURATION HAS BEEN GENERATED"
echo " File: $CONF_DIR/config.json"
echo " It uses the ultimate anti-DPI stack: VLESS + XHTTP + REALITY + Vision."
echo ""
cat "$CLIENT_INFO_FILE"
fi
echo ""
echo "================================================================================"
echo " NEXT STEPS & INSTRUCTIONS:"
echo "================================================================================"
echo " 1. EDIT YOUR CONFIGURATION"
echo " Open $CONF_DIR/config.json and replace 'YOUR_DOMAIN_HERE'"
echo " with your actual domain (e.g., game.example.com)."
echo ""
echo " 2. SETUP YOUR LOCAL WEB SERVER (REALITY Fallback)"
echo " Xray expects a valid HTTPS server running on 127.0.0.1:8443."
echo " - It MUST have a valid TLS certificate for your domain."
echo " - It MUST be configured to accept PROXY protocol v1 (xver: 1)."
echo ""
echo " 3. CONFIGURE PF (PORT FORWARDING & ISOLATION)"
echo " OpenBSD strictly prohibits unprivileged users from binding to ports < 1024."
echo " Add the following to your /etc/pf.conf:"
echo ""
echo " # Redirect incoming port 443 to Xray's unprivileged port 10443"
echo " pass in on egress inet proto tcp to port 443 rdr-to 127.0.0.1 port 10443"
echo ""
echo " # Allow Xray daemon to make outbound connections"
echo " pass out on egress inet proto tcp from any to any port { 80, 443 } user ${DAEMON_USER}"
echo ""
echo " Apply changes: doas pfctl -f /etc/pf.conf"
echo ""
echo " 4. START THE SERVICE"
echo " doas rcctl enable xray"
echo " doas rcctl start xray"
echo "================================================================================"
else
logger -t xray-install -p user.notice "Successfully installed/updated Xray to ${version}"
fi
}
check_update() {
local current latest
current=$(get_current_version)
latest=$(get_latest_version)
[ -z "$latest" ] && { error "Failed to fetch latest version data."; exit 1; }
if [ -z "$current" ]; then
[ "$QUIET" -eq 0 ] && echo "Xray is currently not installed. The latest available release is: v${latest}"
exit 0
fi
if version_gt "$latest" "$current"; then
[ "$QUIET" -eq 0 ] && echo "Update available! v${current} -> v${latest}"
return 0 # update available
else
[ "$QUIET" -eq 0 ] && echo "Your Xray installation is fully up to date (v${current})."
return 1 # already up to date
fi
}
uninstall_xray() {
local purge=${1:-0}
log "Initiating Xray-core removal procedure..."
# Gracefully stop and disable the service before removing files
rcctl stop xray 2>/dev/null || true
rcctl disable xray 2>/dev/null || true
for p in "$BIN_PATH" "$SHARE_PATH" "$RC_SCRIPT"; do
[ -e "$p" ] && rm -rf "$p" && log "Deleted system path: $p"
done
if [ "$purge" = 1 ]; then
rm -rf "$CONF_DIR" "$LOG_DIR" 2>/dev/null || true
log "Purged configuration directory: $CONF_DIR"
log "Purged log directory: $LOG_DIR"
if id -u "$DAEMON_USER" >/dev/null 2>&1; then
userdel "$DAEMON_USER" 2>/dev/null || true
log "Deleted system user: $DAEMON_USER"
fi
echo "warning: The login class '${DAEMON_CLASS}' was left in /etc/login.conf."
echo " It is generally unsafe to automate its removal. Please remove it manually."
else
echo "warning: User configurations ($CONF_DIR) and logs ($LOG_DIR) were preserved."
echo " To completely obliterate all traces, run: '$0 remove --purge'"
fi
log "Xray-core removal completed."
}
print_help() {
cat << EOF
Usage: $0 <command> [options]
Commands:
install [version] Install or upgrade Xray-core (defaults to the latest release)
check Check GitHub for available updates
update Silent auto-update if a newer version is available
remove [--purge] Uninstall Xray-core
help Display this comprehensive help message
Options:
--force, -f Force reinstallation even if the exact version is already installed
--quiet, -q Enable quiet mode (suppresses stdout, logs to syslog; ideal for cron jobs)
--purge Obliterate configs, logs, and the dedicated user (only valid with 'remove')
Examples:
doas $0 install
doas $0 install v1.8.4
0 4 * * * doas $0 update --quiet # Recommended cron job schedule for automatic updates
EOF
}
# ====================== ARGUMENT PARSING & MAIN EXECUTION ======================
COMMAND="install"
VERSION="latest"
PURGE=0
# Determine primary command
if [ $# -gt 0 ] && [ "${1#-}" = "$1" ]; then
COMMAND="$1"
shift
fi
# Parse additional runtime options
while [ $# -gt 0 ]; do
case "$1" in
--quiet|-q) QUIET=1; shift ;;
--force|-f) FORCE=1; shift ;;
--purge) PURGE=1; shift ;;
-*) error "Unrecognized option '$1' provided."; print_help >&2; exit 1 ;;
*) VERSION="$1"; shift ;;
esac
done
# Route to the appropriate function based on the requested command
case "$COMMAND" in
install)
if ! command -v unzip >/dev/null 2>&1; then
log "Dependency 'unzip' is missing. Installing via pkg_add..."
pkg_add -I unzip || { error "Failed to install required dependency: unzip."; exit 1; }
fi
install_xray "$VERSION"
;;
check)
check_update
;;
update)
QUIET=1
if check_update; then
log "Auto-update triggered via cron/manual execution."
install_xray latest
rcctl restart xray 2>/dev/null || true
fi
;;
remove)
uninstall_xray "$PURGE"
;;
help)
print_help
;;
*)
error "Unknown command '$COMMAND' requested."
print_help >&2
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment