#!/usr/bin/env bash # xray-install.sh — ставит Xray (VLESS + Reality, маскировка ya.ru) и команду xr на свежем сервере: # wget -qO- https://x.dev.onov.ru | sudo bash # Перезагрузка не нужна. Повторный запуск безопасен: ключи Reality, адрес и клиенты сохраняются, # Xray-core ставится закреплённой версии, xr обновляется. # Свой адрес и имя сервера: sudo XR_HOST=1.2.3.4 XR_NAME="My VPN" bash xray-install.sh set -euo pipefail # Неизменяемые константы Xray. scripts/build.sh встраивает этот файл в dist/xray-install.sh # и dist/xr вместо строки-маркера (отдельно от констант AmneziaWG: те скрипты не меняются). # BOOTSTRAP_ROOT — префикс всех абсолютных путей, нужен только тестам (в проде пуст). # shellcheck disable=SC2034 readonly XR_BASE_URL="https://x.dev.onov.ru" readonly XRAY_VERSION="v26.10.21" # Официальный установщик Xray закреплён коммитом и проверяется по sha256: он выполняется от root, # а его поведение меняется (например, пользователь сервиса nobody → xray). readonly XRAY_INSTALL_URL="https://raw.githubusercontent.com/XTLS/Xray-install/e42df6a772e60dd7b058f852262c4dbeb0bc231d/install-release.sh" readonly XRAY_INSTALL_SHA256="954666bb028ba8561b2218372b7db76ba051f8252b2db9195321e672263aab94" # Сервис xray.service работает от пользователя xray: конфиг должен быть root:xray 640. readonly XRAY_GROUP="xray" readonly XRAY_RELEASE_URL="https://github.com/XTLS/Xray-core/releases/download" readonly XR_IP_SERVICES="https://api.ipify.org https://ifconfig.me https://icanhazip.com" readonly XR_PORT=443 readonly XR_SNI="ya.ru" readonly XR_FLOW="xtls-rprx-vision" readonly XR_FINGERPRINT="chrome" readonly XR_INBOUND_TAG="vless-reality" readonly XR_API="127.0.0.1:10085" readonly XR_LOCAL_SOCKS_PORT=10808 readonly XR_DNS1="1.1.1.1" readonly XR_DNS2="1.0.0.1" readonly XR_EXPIRY_CHECK_MIN=5 readonly XR_BACKUPS_KEEP=10 readonly XR_LOCK_WAIT=30 readonly FS_ROOT="${BOOTSTRAP_ROOT:-}" readonly XRAY_BINARY="$FS_ROOT/usr/local/bin/xray" readonly XRAY_CONFIG="$FS_ROOT/usr/local/etc/xray/config.json" readonly XR_DIR="$FS_ROOT/root/xray" readonly XR_KEYS="$XR_DIR/keys.env" readonly XR_SERVER_ENV="$XR_DIR/server.env" readonly XR_CLIENTS_DIR="$XR_DIR/clients" readonly XR_EXPIRY_DIR="$XR_DIR/expiry" readonly XR_BACKUPS_DIR="$XR_DIR/backups" readonly XR_BIN="$FS_ROOT/usr/local/bin/xr" readonly XR_CRON="$FS_ROOT/etc/cron.d/xr-expiry" readonly XR_LOG="$FS_ROOT/var/log/xr.log" readonly XR_LOCK="$FS_ROOT/run/lock/xr.lock" CHANGED=0 log() { echo "[xray-install] $*"; } die() { echo "ОШИБКА: $*" >&2 exit 1 } require_root() { [[ ${BOOTSTRAP_EUID:-$EUID} -eq 0 ]] || die "нужны права root: запустите через sudo" } # fetch fetch() { curl -fsSL --retry 3 -o "$2" "$1" } install_deps() { log "Ставлю зависимости (curl, jq, qrencode, perl, unzip)..." export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq curl jq qrencode perl unzip ca-certificates >/dev/null } # public_ip — внешний IPv4 сервера по первому ответившему сервису. public_ip() { local url ip for url in $XR_IP_SERVICES; do ip=$(curl -4 -fsS --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]') || continue if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then echo "$ip" return 0 fi done return 1 } # ensure_server_env — адрес и имя сервера: переменная окружения > сохранённое > определение. ensure_server_env() { local host="" name="" if [[ -f $XR_SERVER_ENV ]]; then host=$(sed -n 's/^XR_HOST=//p' "$XR_SERVER_ENV") name=$(sed -n 's/^XR_NAME=//p' "$XR_SERVER_ENV") fi host=${XR_HOST:-$host} name=${XR_NAME:-$name} [[ -n $host ]] || host=$(public_ip) || die "не удалось определить внешний IPv4 сервера. Укажите его сами: sudo XR_HOST=1.2.3.4 bash xray-install.sh" [[ -n $name ]] || name=$(hostname) printf 'XR_HOST=%s\nXR_NAME=%s\n' "$host" "$name" >"$XR_SERVER_ENV" chmod 600 "$XR_SERVER_ENV" log "Адрес сервера: $host, имя: $name" } # check_port — 443/tcp не должен быть занят чужим процессом (сам xray при повторном запуске — можно). check_port() { local out proc out=$(ss -Hltnp "sport = :$XR_PORT" 2>/dev/null || true) [[ -n $out ]] || return 0 proc=$(grep -o 'users:(("[^"]*"' <<<"$out" | head -n1 | sed 's/users:(("//; s/"$//') [[ $proc == xray ]] && return 0 die "порт $XR_PORT/tcp уже занят процессом '${proc:-неизвестно}' — освободите его и повторите" } xray_zip_name() { case $(uname -m) in x86_64 | amd64) echo "Xray-linux-64.zip" ;; aarch64 | arm64) echo "Xray-linux-arm64-v8a.zip" ;; *) die "архитектура $(uname -m) не поддерживается" ;; esac } installed_xray_version() { [[ -x $XRAY_BINARY ]] || return 0 "$XRAY_BINARY" version 2>/dev/null | sed -n '1s/^Xray \([0-9][0-9.]*\).*/\1/p' } # install_xray_core — Xray-core закреплённой версии из проверенного по sha256 архива. install_xray_core() { if [[ $(installed_xray_version) == "${XRAY_VERSION#v}" ]]; then log "Xray $XRAY_VERSION уже установлен." return 0 fi local zip tmp want have zip=$(xray_zip_name) tmp=$(mktemp -d) log "Скачиваю Xray $XRAY_VERSION ($zip)..." fetch "$XRAY_RELEASE_URL/$XRAY_VERSION/$zip" "$tmp/$zip" || die "не удалось скачать $zip" fetch "$XRAY_RELEASE_URL/$XRAY_VERSION/$zip.dgst" "$tmp/$zip.dgst" || die "не удалось скачать $zip.dgst" want=$(sed -n 's/^SHA2-256= *//p' "$tmp/$zip.dgst" | head -n1) have=$(sha256sum "$tmp/$zip" | cut -d' ' -f1) [[ -n $want && $want == "$have" ]] || die "sha256 архива $zip не совпадает с $zip.dgst — установка прервана" fetch "$XRAY_INSTALL_URL" "$tmp/install-release.sh" || die "не удалось скачать установщик Xray" have=$(sha256sum "$tmp/install-release.sh" | cut -d' ' -f1) [[ $have == "$XRAY_INSTALL_SHA256" ]] || die "install-release.sh: sha256 не совпадает с закреплённым — установка прервана" bash "$tmp/install-release.sh" install --local "$tmp/$zip" rm -rf "$tmp" CHANGED=1 } # ensure_keys — ключи Reality и shortId создаются один раз. ensure_keys() { if [[ -f $XR_KEYS ]]; then log "Ключи Reality уже есть — не трогаю." return 0 fi local out priv pub sid out=$("$XRAY_BINARY" x25519) priv=$(sed -n 's/^PrivateKey: *//p' <<<"$out") pub=$(sed -n 's/^Password (PublicKey): *//p' <<<"$out") [[ -n $priv && -n $pub ]] || die "не удалось разобрать вывод 'xray x25519'" sid=$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n') ( umask 077 printf 'XR_PRIVATE_KEY=%s\nXR_PUBLIC_KEY=%s\nXR_SHORT_ID=%s\n' "$priv" "$pub" "$sid" >"$XR_KEYS" ) log "Ключи Reality созданы." } # config_perms <файл> — конфиг читает сервис от пользователя xray: root:xray 640. config_perms() { chown "root:$XRAY_GROUP" "$1" || die "не удалось выставить группу $XRAY_GROUP на $1" chmod 640 "$1" } has_our_inbound() { [[ -f $XRAY_CONFIG ]] && jq -e --arg tag "$XR_INBOUND_TAG" 'any(.inbounds[]?; .tag == $tag)' "$XRAY_CONFIG" >/dev/null 2>&1 } # ensure_config — конфиг сервера пишется, только если в нём ещё нет нашего inbound. ensure_config() { if has_our_inbound; then log "Конфиг Xray уже настроен — клиенты сохраняются." else local priv sid tmp priv=$(sed -n 's/^XR_PRIVATE_KEY=//p' "$XR_KEYS") sid=$(sed -n 's/^XR_SHORT_ID=//p' "$XR_KEYS") mkdir -p "${XRAY_CONFIG%/*}" if [[ -s $XRAY_CONFIG ]] && [[ $(jq -c . "$XRAY_CONFIG" 2>/dev/null) != "{}" ]]; then cp "$XRAY_CONFIG" "$XRAY_CONFIG.bak-$(date +%F_%H-%M-%S)" fi tmp="${XRAY_CONFIG%.json}.tmp-$$.json" jq -n --arg tag "$XR_INBOUND_TAG" --argjson port "$XR_PORT" --arg sni "$XR_SNI" \ --arg priv "$priv" --arg sid "$sid" --arg api "$XR_API" ' ($api | split(":")) as [$apiHost, $apiPort] | { log: {loglevel: "warning"}, api: {tag: "api", services: ["HandlerService", "StatsService"]}, stats: {}, policy: {levels: {"0": {statsUserUplink: true, statsUserDownlink: true, statsUserOnline: true}}}, inbounds: [ {tag: "api", listen: $apiHost, port: ($apiPort | tonumber), protocol: "dokodemo-door", settings: {address: $apiHost}}, {tag: $tag, listen: "0.0.0.0", port: $port, protocol: "vless", settings: {clients: [], decryption: "none"}, streamSettings: {network: "tcp", security: "reality", realitySettings: {dest: ($sni + ":443"), serverNames: [$sni], privateKey: $priv, shortIds: [$sid]}}, sniffing: {enabled: true, destOverride: ["http", "tls", "quic"]}} ], outbounds: [{protocol: "freedom", tag: "direct"}, {protocol: "blackhole", tag: "block"}], routing: {rules: [{type: "field", inboundTag: ["api"], outboundTag: "api"}]} }' >"$tmp" config_perms "$tmp" mv "$tmp" "$XRAY_CONFIG" CHANGED=1 log "Конфиг Xray записан (VLESS + Reality на $XR_PORT/tcp, маскировка $XR_SNI)." fi # чинит права и у конфига, записанного раньше (повторный запуск) config_perms "$XRAY_CONFIG" "$XRAY_BINARY" run -test -format json -c "$XRAY_CONFIG" >/dev/null || die "конфиг $XRAY_CONFIG не прошёл проверку xray run -test" } open_firewall() { if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q '^Status: active'; then ufw allow "$XR_PORT/tcp" >/dev/null log "UFW: открыт $XR_PORT/tcp." fi } # install_xr — команда xr: рядом с запущенным файлом или с XR_BASE_URL (при запуске через pipe). install_xr() { local tmp="$XR_DIR/xr.tmp" local_xr local_xr="$(dirname "$0")/xr" if [[ -f $0 && -f $local_xr ]]; then cp "$local_xr" "$tmp" else fetch "$XR_BASE_URL/xr" "$tmp" || die "не удалось скачать $XR_BASE_URL/xr" fi bash -n "$tmp" || die "скачанный xr повреждён" mkdir -p "${XR_BIN%/*}" install -m 755 "$tmp" "$XR_BIN" rm -f "$tmp" mkdir -p "${XR_CRON%/*}" printf '*/%s * * * * root /usr/local/bin/xr expire-check >>/var/log/xr.log 2>&1\n' "$XR_EXPIRY_CHECK_MIN" >"$XR_CRON" chmod 644 "$XR_CRON" } start_service() { systemctl enable xray >/dev/null 2>&1 || systemctl enable xray if [[ $CHANGED -eq 1 ]]; then systemctl restart xray else systemctl start xray fi local _ for _ in 1 2 3 4 5; do systemctl is-active --quiet xray && return 0 sleep 1 done journalctl -u xray -n 20 --no-pager || true die "сервис xray не запустился — журнал выше (systemctl status xray)" } main() { require_root mkdir -p "$XR_DIR" "$XR_CLIENTS_DIR" "$XR_EXPIRY_DIR" "$XR_BACKUPS_DIR" chmod 700 "$XR_DIR" install_deps ensure_server_env check_port install_xray_core ensure_keys ensure_config open_firewall install_xr start_service XR_SKIP_SUDO=1 "$XR_BIN" check || log "Проверка нашла проблемы — подробности выше." log "Xray готов — наберите: xr help" } main "$@"