September 17

Свой VPN с нуля: VLESS + Reality (полный технический гайд)

Гайд для полного новичка. Разворачиваем личный VPN, устойчивый к блокировкам РКН/DPI, на своём сервере. Технология — Xray-core (VLESS + Reality + XTLS-Vision): то, что сейчас используют почти все приличные сервисы. Клиент вставляет одну ссылку в приложение и получает готовый конфиг с правилами (заблокированное — через VPN, российские сайты — напрямую).

Что получится в итоге:

  • Свой сервер, который не детектится обычным DPI (маскируется под обычный HTTPS-сайт).
  • Одна ссылка-подписка на каждого пользователя (работает и в FlClash, и в Happ).
  • Полная независимость: никаких сторонних сервисов, которые могут лечь под блокировками.
Как пользоваться этим гайдом. Иди строго по шагам сверху вниз. Каждую команду можно копировать целиком. Места, которые надо заменить своими значениями, выделены как ВОТ_ТАК — их список в разделе 0. Основной гайд — на Linux (Ubuntu); отличия для Windows Server — в Приложении A в конце. Если что-то не работает — смотри Приложение B (типичные проблемы).

Содержание

  1. Что понадобится и сколько стоит
  2. Как это работает (архитектура простыми словами)
  3. Покупка сервера (VPS)
  4. Покупка домена и настройка DNS
  5. Первое подключение к серверу и базовая защита
  6. Установка Xray
  7. Генерация ключей и клиентов
  8. Конфиг сервера Xray
  9. Установка и настройка nginx (маскировка + подписки)
  10. Выпуск SSL-сертификата (Let's Encrypt)
  11. Запуск и файрвол
  12. Генератор подписок (Clash + Happ)
  13. Подключение клиентов (FlClash, Happ)
  14. Проверка, что всё работает
  15. Обслуживание (добавить/отозвать клиента, обновления)
  16. Приложение A — то же самое на Windows Server
  17. Приложение B — типичные проблемы и решения

0. Что понадобится и сколько стоит

  • VPS (сервер) с Ubuntu 22.04/24.04 — на нём крутится VPN. Цена: 3–6 $/мес.
  • Домен — маскировка + раздача подписок. Цена: 100–300 ₽/год (.fun / .online / .site).
  • Приложение-клиент — FlClash (Win/Android/Linux), Happ (iOS). Цена: бесплатно.

Важно про локацию сервера: бери не Россию (Нидерланды, Германия, Финляндия, Швеция — близко и быстро). Провайдеры: Aeza, 4vps, PQ.Hosting, Hetzner, Vultr, любой другой с почасовой/помесячной оплатой. Требований к железу почти нет — на 10 человек хватит 1 CPU / 1 GB RAM.

Плейсхолдеры, которые встретятся ниже (замени на свои):

  • ВАШ_ДОМЕН — например myrandomsite42.fun
  • IP_СЕРВЕРА — IPv4-адрес вашего VPS, например 203.0.113.10
  • ВАШ_EMAIL — почта для Let's Encrypt (любая ваша)

1. Как это работает (архитектура простыми словами)

                            ВАШ СЕРВЕР (IP_СЕРВЕРА)
                            ┌───────────────────────────────────────┐
  Клиент (телефон/ноут)     │  Xray (слушает порт 443)              │
  ── TLS на порт 443 ──────►│    ├─ «свой» (есть ключ) → ТУННЕЛЬ ───┼──► интернет
     SNI = ВАШ_ДОМЕН        │    │                                  │
                            │    └─ «чужой» (браузер/сканер/DPI)    │
                            │        → отдаём локальному nginx ─────┼─► сайт-обманка
                            │            (127.0.0.1:8443)           │     + подписки
                            └───────────────────────────────────────┘

Ключевые идеи, почему это не детектится:

  • Reality — на порту 443 стоит Xray и принимает обычный TLS. Если пришёл ваш клиент с правильным ключом — открывается зашифрованный туннель. Если пришёл кто угодно другой (браузер, сканер, DPI-зонд РКН) — Xray прозрачно отдаёт его локальному веб-серверу (nginx), и снаружи это выглядит как обычный сайт с валидным сертификатом. DPI не видит разницы между вашим VPN и настоящим сайтом.
  • Свой домен как маскировка. Reality «одалживает» сертификат вашего домена. Домен реально резолвится в ваш IP и отдаёт настоящую HTTPS-страницу — связка честная, не подкопаешься.
  • XTLS-Vision (flow) — почти не грузит процессор (трафик проксируется без пере-шифрования), за счёт этого высокая скорость.
  • Тот же nginx раздаёт подписки — файлы-конфиги для клиентов, по HTTPS.

Больше на 443 ничего нет, поэтому никаких «разводящих по SNI» не нужно — Xray сам и туннель, и обманка. Просто и надёжно.


2. Покупка сервера (VPS)

  1. Регистрируешься у любого провайдера (выше). Создаёшь сервер: Ubuntu 24.04 (или 22.04), локация не-РФ, тариф 1 CPU / 1 GB / любой диск.
  2. Провайдер даёт IP-адрес и root-пароль (или ты загружаешь свой SSH-ключ — лучше).
  3. Проверь у провайдера, что не режется порт 443 на вход. У некоторых бюджетных хостеров есть внешний файрвол — убедись, что входящие 22/80/443 разрешены (обычно да).

Запиши: IP_СЕРВЕРА, логин (обычно root), пароль.


3. Покупка домена и настройка DNS

  1. Купи самый дешёвый домен где угодно (reg.ru, namecheap, porkbun). Имя не важно — можно случайное типа myrandomsite42.fun. Не бери имена со словами vpn/proxy — меньше шансов, что домен попадёт под ковровую блокировку.
  2. В панели управления доменом (DNS-зона) добавь две записи типа A:
    • @ → IP_СЕРВЕРА
    • www → IP_СЕРВЕРА
  3. Подожди, пока DNS разойдётся. Это может занять от 10 минут до пары часов (зависит от регистратора). Проверить можно так (с любого компьютера):nslookup ВАШ_ДОМЕН 8.8.8.8Должно показать IP_СЕРВЕРА. Пока показывает что-то другое (парковку регистратора) — ждём. Не переходи к шагу 9 (сертификат), пока домен не резолвится в ваш IP — выпуск сертификата без этого не сработает.

4. Первое подключение к серверу и базовая защита

Подключаемся по SSH (на Windows — через PowerShell или PuTTY; на Mac/Linux — терминал):

ssh root@IP_СЕРВЕРА

Введи пароль. Дальше выполняем на сервере.

Обновляем систему и ставим базовые утилиты:

apt update && apt upgrade -y
apt install -y curl wget unzip nginx python3 ufw

Настраиваем файрвол (пускаем только SSH, HTTP, HTTPS):

ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
ufw status

5. Установка Xray

Ставим Xray-core официальным скриптом (он же создаёт systemd-сервис автозапуска):

bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install

Проверяем версию (должна быть свежая, 25.x или новее):

xray version

Конфиг Xray будет лежать в /usr/local/etc/xray/config.json, сервис называется xray.


6. Генерация ключей и клиентов

6.1 Ключи Reality

xray x25519

Выведет две строки — сохрани обе:

PrivateKey: <ПРИВАТНЫЙ_КЛЮЧ>       ← пойдёт в конфиг СЕРВЕРА
Password:   <ПУБЛИЧНЫЙ_КЛЮЧ>       ← пойдёт в конфиги КЛИЕНТОВ

(в некоторых версиях вторая строка называется Password (PublicKey) — это и есть публичный ключ)

6.2 Short IDs

Сгенерируй несколько коротких идентификаторов (раздаём клиентам вперемешку, пустой тоже можно):

for i in 1 2 3 4; do openssl rand -hex 4; done

Получишь 4 строки вида a1b2c3d4. Плюс всегда можно использовать пустой "".

6.3 UUID клиентов

Каждому пользователю — свой уникальный UUID (не один на всех! это позволяет отзывать ключи по одному и лучше размазывает нагрузку). Сгенерируй по числу людей:

for i in 1 2 3 4 5; do xray uuid; done

Всё это (ключи, shortIds, UUID) удобнее не вбивать руками, а отдать генератору из шага 11 — он сам всё создаст и разложит. Но ключи Reality (xray x25519) надо получить именно на сервере и вставить в генератор.


7. Конфиг сервера Xray

Этот файл можно написать вручную (ниже), либо — что удобнее — сгенерировать скриптом из шага 11. Сначала покажу структуру, чтобы было понятно, что внутри.

Открываем конфиг:

nano /usr/local/etc/xray/config.json

Вставляем (заменив плейсхолдеры <...> на свои значения из шага 6):

{
  "log": { "loglevel": "warning" },
  "inbounds": [
    {
      "tag": "reality-in",
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [
          { "id": "<UUID-1>", "flow": "xtls-rprx-vision", "email": "user01" },
          { "id": "<UUID-2>", "flow": "xtls-rprx-vision", "email": "user02" }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "dest": "127.0.0.1:8443",
          "xver": 0,
          "serverNames": ["ВАШ_ДОМЕН"],
          "privateKey": "<ПРИВАТНЫЙ_КЛЮЧ>",
          "shortIds": ["", "<shortId1>", "<shortId2>"]
        }
      },
      "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] }
    }
  ],
  "outbounds": [
    { "tag": "direct", "protocol": "freedom" },
    { "tag": "block", "protocol": "blackhole" }
  ]
}

Что здесь важно (не менять без причины):

  • port: 443 — обязательно 443 (TLS на другом порту — аномалия для DPI, плюс провайдеры часто режут нестандартные порты).
  • flow: xtls-rprx-vision — даёт скорость (сплайсинг трафика).
  • dest: 127.0.0.1:8443 — куда Reality отдаёт «чужой» трафик: это наш локальный nginx.
  • serverNames: ["ВАШ_ДОМЕН"] — под какой домен маскируемся. Совпадает с доменом, на который клиент шлёт SNI.
  • xver: 0 — PROXY protocol выключен (он нужен, только если за nginx стоят сайты, которым важен реальный IP клиента; в чистом VPN не нужен).
  • У каждого клиента свой id (UUID) и email (просто метка, любая).

Проверяем валидность и НЕ запускаем пока (nginx ещё не готов):

xray run -test -c /usr/local/etc/xray/config.json

Должно быть Configuration OK. Предупреждение про Reality — ок.


8. Установка и настройка nginx (маскировка + подписки)

nginx делает две вещи: (1) отдаёт «сайт-обманку» на 127.0.0.1:8443 (чтобы Reality было чем прикрываться), (2) раздаёт файлы-подписки клиентам.

8.1 Каталоги и страница-обманка

mkdir -p /var/www/vpnsub/sub /var/www/vpnsub/happ /var/www/vpnsub/acme
echo '<!doctype html><title>Welcome</title><h1>It works</h1>' > /var/www/vpnsub/index.html

8.2 Конфиг сайта

Создаём конфиг:

nano /etc/nginx/sites-available/vpn.conf

Вставляем (заменив ВАШ_ДОМЕН):

# Порт 80 — для выпуска/продления сертификата Let's Encrypt + редирект на HTTPS
server {
    listen 80;
    server_name ВАШ_ДОМЕН www.ВАШ_ДОМЕН;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/vpnsub/acme;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

# Маскировка + подписки. Слушает 127.0.0.1:8443 — сюда Xray Reality отдаёт «чужой» трафик.
server {
    listen 127.0.0.1:8443 ssl;
    http2 on;
    server_name ВАШ_ДОМЕН www.ВАШ_ДОМЕН;

    ssl_certificate     /etc/letsencrypt/live/ВАШ_ДОМЕН/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ВАШ_ДОМЕН/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    root /var/www/vpnsub;

    # Один URL /sub/<токен>.yaml: обычным Clash-клиентам отдаём YAML,
    # приложению Happ (iOS) — base64-список (его формат). Различаем по User-Agent.
    location ~ ^/sub/(?<subtoken>[A-Za-z0-9_-]+)\.yaml$ {
        add_header Cache-Control "no-store" always;
        if ($http_user_agent ~* "happ") {
            rewrite ^ /_happ/$subtoken last;
        }
        default_type "text/yaml; charset=utf-8";
        try_files /sub/$subtoken.yaml =404;
    }
    location ~ ^/_happ/(?<happtoken>[A-Za-z0-9_-]+)$ {
        internal;
        default_type "text/plain; charset=utf-8";
        try_files /happ/$happtoken =404;
    }

    # Всем остальным (браузеры, сканеры, DPI) — страница-обманка.
    location / {
        try_files /index.html =404;
    }
}

Включаем сайт, отключаем дефолтный:

ln -sf /etc/nginx/sites-available/vpn.conf /etc/nginx/sites-enabled/vpn.conf
rm -f /etc/nginx/sites-enabled/default
Пока не проверяй nginx -t — он ругнётся на отсутствующий сертификат. Сначала шаг 9.

9. Выпуск SSL-сертификата (Let's Encrypt)

Убедись, что домен уже резолвится в твой IP (шаг 3)! Иначе выпуск не пройдёт.

Ставим certbot и временно поднимаем nginx только с 80-м портом, чтобы пройти проверку. Проще всего — режим webroot:

apt install -y certbot

# на время выпуска нужен рабочий nginx на порту 80.
# Временно уберём ssl-сервер, чтобы nginx стартанул без сертификата:
mv /etc/nginx/sites-enabled/vpn.conf /root/vpn.conf.bak
cat > /etc/nginx/sites-enabled/acme.conf <<'EOF'
server {
    listen 80;
    server_name _;
    location ^~ /.well-known/acme-challenge/ { root /var/www/vpnsub/acme; }
    location / { return 200 "ok"; }
}
EOF
nginx -t && systemctl restart nginx

# Выпускаем сертификат (webroot):
certbot certonly --webroot -w /var/www/vpnsub/acme \
  -d ВАШ_ДОМЕН -d www.ВАШ_ДОМЕН \
  --email ВАШ_EMAIL --agree-tos --no-eff-email

# Возвращаем полный конфиг:
rm -f /etc/nginx/sites-enabled/acme.conf
mv /root/vpn.conf.bak /etc/nginx/sites-enabled/vpn.conf
nginx -t && systemctl restart nginx

Если certbot написал Successfully received certificate — всё ок, сертификат в /etc/letsencrypt/live/ВАШ_ДОМЕН/. Автопродление certbot настраивает сам (systemd-таймер).

Проверяем nginx:

nginx -t
systemctl restart nginx

10. Запуск и файрвол

Запускаем Xray и ставим в автозапуск:

systemctl enable --now xray
systemctl status xray --no-pager

Должно быть active (running).

Проверяем, что порт 443 слушает Xray, а 8443 — nginx (локально):

ss -tlnp | grep -E ':443|:8443|:80'

Ожидаем: 0.0.0.0:443 → xray, 127.0.0.1:8443 → nginx, 0.0.0.0:80 → nginx.

Файрвол уже настроен (шаг 4). Порт 8443 наружу не открыт и не должен — он только для локального общения Xray↔nginx.


11. Генератор подписок (Clash + Happ)

Теперь главное для удобства: скрипт, который из твоих ключей и списка клиентов генерирует серверный конфиг Xray, файлы-подписки (Clash-YAML для FlClash + base64 для Happ) и печатает готовые ссылки. Добавить нового человека = одна строка.

Создай файл генератора:

nano /root/gen.py

Вставь целиком (в начале — блок НАСТРОЙКИ, замени под себя):

#!/usr/bin/env python3
# Генератор конфигов и подписок для своего VLESS+Reality VPN.
import json, base64, secrets, uuid, urllib.parse, pathlib, subprocess

# ================== НАСТРОЙКИ (замени под себя) ==================
DOMAIN       = "ВАШ_ДОМЕН"            # он же маскировка (serverName) и домен подписок
SERVER_IP    = "IP_СЕРВЕРА"           # IPv4 сервера
PRIVATE_KEY  = "<ПРИВАТНЫЙ_КЛЮЧ>"     # из `xray x25519` (PrivateKey)
PUBLIC_KEY   = "<ПУБЛИЧНЫЙ_КЛЮЧ>"     # из `xray x25519` (Password/PublicKey)
CLIENT_NAMES = ["user01","user02","user03","user04","user05"]  # список людей
GROUP_NAME   = "MyVPN"                # как назвать VPN в приложении
XRAY_CONFIG  = "/usr/local/etc/xray/config.json"
SUB_DIR      = "/var/www/vpnsub"          # webroot (раздаётся наружу!)
STATE_FILE   = "/root/vpn-clients.json"   # СЕКРЕТ: вне webroot, наружу не попадает
# ================================================================

def build_clients():
    # Идемпотентно: существующих клиентов НЕ трогаем (их ссылки остаются рабочими),
    # добавляем только новые имена. Данные храним в STATE_FILE (вне webroot!).
    store = pathlib.Path(STATE_FILE)
    existing = {}
    if store.exists():
        for c in json.loads(store.read_text(encoding="utf-8")):
            existing[c["name"]] = c
    clients = []
    for name in CLIENT_NAMES:
        if name in existing:
            clients.append(existing[name])            # оставляем как есть
        else:
            clients.append({
                "name": name,
                "uuid": str(uuid.uuid4()),
                "short_id": secrets.token_hex(4),      # свой у каждого, стабильный
                "token": secrets.token_urlsafe(24),    # секрет в URL подписки
            })
    return clients

def server_short_ids(clients):
    # shortIds сервера = "" + все short_id клиентов (иначе клиент не пройдёт auth)
    return list(dict.fromkeys([""] + [c["short_id"] for c in clients]))

def write_xray(clients):
    cfg = {
        "log": {"loglevel": "warning"},
        "inbounds": [{
            "tag": "reality-in", "listen": "0.0.0.0", "port": 443, "protocol": "vless",
            "settings": {
                "clients": [{"id": c["uuid"], "flow": "xtls-rprx-vision", "email": c["name"]} for c in clients],
                "decryption": "none",
            },
            "streamSettings": {
                "network": "tcp", "security": "reality",
                "realitySettings": {
                    "show": False, "dest": "127.0.0.1:8443", "xver": 0,
                    "serverNames": [DOMAIN], "privateKey": PRIVATE_KEY,
                    "shortIds": server_short_ids(clients),
                },
            },
            "sniffing": {"enabled": True, "destOverride": ["http", "tls", "quic"]},
        }],
        "outbounds": [
            {"tag": "direct", "protocol": "freedom"},
            {"tag": "block", "protocol": "blackhole"},
        ],
    }
    pathlib.Path(XRAY_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
    print(f"[+] записан {XRAY_CONFIG} ({len(clients)} клиентов)")

def clash_yaml(c):
    G = GROUP_NAME
    return CLASH_TEMPLATE.replace("{{G}}", G).replace("{{SERVER}}", SERVER_IP)\
        .replace("{{UUID}}", c["uuid"]).replace("{{SNI}}", DOMAIN)\
        .replace("{{PBK}}", PUBLIC_KEY).replace("{{SID}}", c["short_id"])

def vless_link(c):
    p = {"encryption":"none","flow":"xtls-rprx-vision","security":"reality","sni":DOMAIN,
         "fp":"firefox","pbk":PUBLIC_KEY,"sid":c["short_id"],"spx":"/","type":"tcp","headerType":"none"}
    q = urllib.parse.urlencode(p, quote_via=urllib.parse.quote)
    name = urllib.parse.quote(f"{GROUP_NAME}")
    return f"vless://{c['uuid']}@{SERVER_IP}:443?{q}#{name}"

def main():
    sub = pathlib.Path(SUB_DIR)
    (sub/"sub").mkdir(parents=True, exist_ok=True)
    (sub/"happ").mkdir(parents=True, exist_ok=True)
    clients = build_clients()
    write_xray(clients)
    print("\nСсылки-подписки (раздать людям):")
    for c in clients:
        (sub/"sub"/f"{c['token']}.yaml").write_text(clash_yaml(c), encoding="utf-8")
        b64 = base64.b64encode((vless_link(c)+"\n").encode()).decode()
        (sub/"happ"/c["token"]).write_text(b64, encoding="utf-8")
        print(f"  {c['name']}: https://{DOMAIN}/sub/{c['token']}.yaml")
    # сохраним таблицу клиентов ВНЕ webroot (секрет! наружу не отдаётся)
    pathlib.Path(STATE_FILE).write_text(json.dumps(clients, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"\n[!] Таблица клиентов сохранена в {STATE_FILE} — это СЕКРЕТ, храни бережно.")
    print("[!] Дальше: systemctl restart xray && systemctl reload nginx")

# ---- шаблон Clash-конфига (богатый, с правилами маршрутизации) ----
CLASH_TEMPLATE = r"""mixed-port: 7890
allow-lan: true
mode: rule
log-level: info
sniffer:
  enable: true
  sniff:
    HTTP: { ports: [80, 8080-8880], override-destination: true }
    TLS:  { ports: [443, 8443] }
    QUIC: { ports: [443, 8443, 853] }
tun:
  enable: true
  stack: mixed
  auto-route: true
  strict-route: true
  auto-detect-interface: true
dns:
  enable: true
  enhanced-mode: redir-host
  nameserver: [https://1.1.1.1/dns-query, https://8.8.8.8/dns-query]
rule-providers:
  ru-blocked:
    type: http
    behavior: domain
    format: mrs
    url: https://github.com/frayZV/runetfreedom-mrs/releases/latest/download/ru-blocked-all.mrs
    path: ./rule-sets/ru-blocked.mrs
    interval: 86400
    proxy: {{G}}
  ru-blocked-ip:
    type: http
    behavior: ipcidr
    format: mrs
    url: https://raw.githubusercontent.com/runetfreedom/russia-blocked-geoip/release/mrs/ru-blocked.mrs
    path: ./rule-sets/ru-blocked-ip.mrs
    interval: 86400
    proxy: {{G}}
proxies:
  - name: "Швеция"
    type: vless
    server: {{SERVER}}
    port: 443
    network: tcp
    udp: true
    uuid: {{UUID}}
    packet-encoding: xudp
    flow: xtls-rprx-vision
    tls: true
    servername: {{SNI}}
    reality-opts:
      public-key: {{PBK}}
      short-id: "{{SID}}"
    client-fingerprint: firefox
proxy-groups:
  - name: {{G}}
    type: select
    proxies: ["Швеция"]
rules:
  - DOMAIN-SUFFIX,t.me,{{G}}
  - DOMAIN-SUFFIX,telegram.org,{{G}}
  - DOMAIN-KEYWORD,telegram,{{G}}
  - IP-CIDR,91.108.0.0/16,{{G}},no-resolve
  - IP-CIDR,149.154.160.0/20,{{G}},no-resolve
  - PROCESS-NAME-REGEX,(?i)discord.*,{{G}}
  - RULE-SET,ru-blocked,{{G}}
  - RULE-SET,ru-blocked-ip,{{G}},no-resolve
  - MATCH,DIRECT
"""

if __name__ == "__main__":
    main()

Заполни блок НАСТРОЙКИ (домен, IP, оба ключа, список людей) и запусти:

python3 /root/gen.py
systemctl restart xray
systemctl reload nginx

Скрипт напечатает готовые ссылки-подписки — по одной на человека. Их и раздаёшь.

client-fingerprint: firefox — по опыту, отпечаток Firefox лучше проходит DPI, чем Chrome. Не меняй на chrome без причины.

12. Подключение клиентов

FlClash (Windows / Android / Linux)

  1. Профили → + → URL → вставить ссылку https://ВАШ_ДОМЕН/sub/<токен>.yaml
  2. Выбрать профиль → в группе MyVPN выбрать ноду
  3. Включить (system proxy или TUN)

Happ (iOS / iPhone)

  1. + → URL подписки → вставить ту же ссылку
  2. Нода добавится сама, подключайся
Копируй ссылку без переноса строки в конце! Если случайно захватить Enter, приложение запросит несуществующий файл и выдаст 404 / «no server links». Выделяй ровно до .yaml.

Сервер сам понимает, кто пришёл (Clash или Happ), и отдаёт нужный формат — ссылка одна и та же для всех приложений.


13. Проверка, что всё работает

На сервере:

systemctl status xray nginx --no-pager    # оба active (running)
ss -tlnp | grep ':443'                      # слушает xray
curl -sk https://ВАШ_ДОМЕН/ --resolve ВАШ_ДОМЕН:443:127.0.0.1   # (см. ниже)

С клиента (после подключения VPN):

  • Зайди на https://ipinfo.io/ip — должен показать IP_СЕРВЕРА (не твой домашний).
  • Открой заблокированный сервис (x.com, youtube.com) — работает.
  • Открой российский сайт (ya.ru) — работает быстро и напрямую (через VPN не идёт, это by design).

Подписка отдаётся? С любого компьютера:

curl -A "FlClash" https://ВАШ_ДОМЕН/sub/<токен>.yaml       # должен вернуться YAML
curl -A "Happ"    https://ВАШ_ДОМЕН/sub/<токен>.yaml       # должен вернуться base64

14. Обслуживание

Добавить человека: впиши новое имя в CLIENT_NAMES в /root/gen.py, запусти python3 /root/gen.py && systemctl restart xray && systemctl reload nginx. Скрипт идемпотентный: старым клиентам ничего не меняет (их ссылки продолжают работать), создаёт только нового и печатает все ссылки. Ссылку новичка отдаёшь ему.

Отозвать человека: убери его имя из CLIENT_NAMES, перегенерируй, перезапусти xray. Его UUID пропадёт из конфига → ключ перестанет пускать, остальные не затронуты.

Данные всех клиентов хранятся в /root/vpn-clients.json (создаётся генератором, ВНЕ каталога, который раздаёт nginx). Это секрет — не потеряй и не публикуй. Именно он делает ссылки стабильными между запусками.

Обновить Xray:

bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
systemctl restart xray

Сертификат продлевается сам (certbot ставит таймер). Проверить: certbot renew --dry-run.

Логи Xray: journalctl -u xray -n 50 --no-pager.


15. Приложение A — то же самое на Windows Server

Если сервер на Windows (дороже и капризнее, но иногда нужно — например, там уже что-то крутится). Архитектура идентична, отличается только «обвязка».

Отличия:

  1. Xray — не через apt, а скачать Xray-windows-64.zip с https://github.com/XTLS/Xray-core/releases, распаковать в C:\Xray\.
  2. Сервис-менеджер — вместо systemd используем NSSM (https://nssm.cc):nssm install Xray "C:\Xray\xray.exe" "run -c C:\Xray\config.json" nssm set Xray Start SERVICE_AUTO_START nssm set Xray AppStdout C:\Xray\logs\xray.log nssm start Xray
  3. nginx — Windows-сборка с nginx.org. Конфиг тот же по смыслу (server на 127.0.0.1:8443 ssl + 80 для ACME). Пути в стиле C:/nginx/....
  4. Сертификат — вместо certbot используем win-acme (https://www.win-acme.com):C:\win-acme\wacs.exe --target manual --host ВАШ_ДОМЕН,www.ВАШ_ДОМЕН ^ --validation filesystem --webroot C:\www\vpnsub\acme ^ --store pemfiles --pemfilespath C:\nginx\ssl --accepttos --emailaddress ВАШ_EMAIL
  5. Файрвол Windows — открыть 443:New-NetFirewallRule -DisplayName "VPN-443" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow
  6. Генератор gen.py — тот же, только пути XRAY_CONFIG/SUB_DIR под Windows (C:/Xray/config.json, C:/www/vpnsub).

⚠️ Три Windows-грабли, которые реально стоили времени (запомни):

  • sc stop nginx НЕ убивает процессы nginx. После смены конфига перезапускай так: net stop nginx & taskkill /F /IM nginx.exe & net start nginx — иначе nginx крутит старый конфиг из памяти.
  • Не пиши русский текст внутри .ps1-скриптов. Windows PowerShell 5.1 читает файл как ANSI и ломается на кириллице (съедает кавычки → синтаксическая ошибка). Только латиница в скриптах.
  • Не сохраняй JSON через Set-Content -Encoding UTF8 — PowerShell 5.1 дописывает BOM в начало файла, и Xray падает с invalid character. Заливай конфиги через scp / пиши без BOM.

Всё остальное (структура конфигов, логика Reality, подписки) — один в один как в Linux-версии.


16. Приложение B — типичные проблемы

  • При добавлении подписки — 404. Причина: в скопированной ссылке лишний перенос строки (%0D%0A в логах). Решение: скопируй ссылку заново, ровно до .yaml, без Enter в конце.
  • Happ: «no server links», ошибка 39. Причина: Happ не понимает Clash-YAML / vless-ссылку без encryption=none. Решение: убедись, что сервер отдаёт Happ base64 (проверь curl -A Happ ...); в vless-ссылке должен быть encryption=none.
  • Клиент подключается, но нет интернета; в логе клиента received real certificate. Причина: маскировочный сертификат протух или домен больше не резолвится в твой IP. Решение: проверь, что домен жив и резолвится в IP; продли/перевыпусти сертификат (certbot renew).
  • certbot: проверка не прошла. Причина: домен ещё не резолвится в твой IP, или 80 порт занят/закрыт. Решение: дождись DNS (nslookup), открой 80 в файрволе, проверь, что nginx на 80 отвечает.
  • VPN работал, потом перестал у всех. Причина: истёк домен → ушёл на парковку → протух сертификат маскировки. Решение: продли домен и верни A-запись на свой IP, перевыпусти сертификат. Следи за датой продления домена!
  • Порт 8888/2053/нестандартный не работает снаружи. Причина: провайдер режет всё, кроме 22/80/443. Решение: используй только 443 (для VPN) — так и задумано.
  • Xray не стартует после правки конфига. Причина: ошибка в JSON (или BOM на Windows). Решение: xray run -test -c <config> покажет причину.

Полезные команды диагностики (Linux):

journalctl -u xray -n 50 --no-pager        # лог Xray
nginx -t                                    # проверка конфига nginx
ss -tlnp | grep -E ':443|:8443|:80'         # кто слушает порты
curl -sv https://ВАШ_ДОМЕН/ 2>&1 | head     # что отдаёт сервер снаружи

Что важно помнить

  1. Reality обязан быть на 443. Другие порты режет провайдер и палит DPI.
  2. Домен нужно вовремя продлевать. Истёк домен → протух сертификат → лёг VPN у всех. Поставь напоминание за месяц до конца.
  3. Секреты не публикуй. Приватный ключ Reality, UUID, токены подписок, clients.json — не выкладывай в открытый git, не кидай в чаты. Ссылку-подписку давай каждому свою.
  4. У каждого свой ключ. Не один UUID на всех — так можно отзывать по одному и не превышать DPI-пороги по числу параллельных соединений.
  5. Если что-то заблокировали — можно сменить домен (маскировку) и раздать новые ссылки; сервер и ключи менять не обязательно.

Готово. Свой VPN, который ни от кого не зависит.