This commit is contained in:
Gabenov Stanislav
2026-02-09 20:42:24 +03:00
parent 0a8f11397d
commit a6d2623802
38 changed files with 2198 additions and 1 deletions

View File

@@ -0,0 +1,2 @@
---
ssh_public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChdhIFLJ1nwmqkFJzgHKvPJXPdGRZ3tmN0IqAJyP61DSsQf4rJcw4fL9Z4UKGfIw4Mn4f8FFcAUCmD/2OZxSKjjQBYiq/+q3AS12PVUbDR3fE5IyZx3uDmAB3m9yJuIFGl3mqldefnnH98UA3+kwZgFoJXJwHgp6mTV+S5N0aJdX1gCwoVMz3imkUAkGQrK4eDp9T4YH94yVh8or3si+mQJe+5j5xjF7oFGWWVYIr7iNS6zrtgRQdFklAt8cFIx2Mxql6PuTNJo83zS1NUxijEEGC/aZMLhafsOWm8M63EvkXsGOYS2JN5ycKJaTTDiD22tyXP4a1Oe6SGzB7of2NJ9RUXYnq7IYNlMpLJ4D7tHyYJUEckRQy9yugYWSUFxPgLc6n1PxDUEk1hTVwm5xsha9kYYXtuAY4RI3uXkvYIMsuO92kwqnOwt6Yw285DrXQQYSLt2NGBYvGfVczrSM8TYwsCZ0wHOaNeZKieeOmNDeHcOX8smSsiYlF08NOJ6pk= stanito@MacM5"

View File

@@ -8,7 +8,7 @@ ansible_port=22
ansible_ssh_private_key_file=~/.ssh/id_rsa ansible_ssh_private_key_file=~/.ssh/id_rsa
# VPN серверы # VPN серверы
[vpnservers] [vpn]
access.stanito.me ansible_user=root access.stanito.me ansible_user=root
# Группы по ОС # Группы по ОС

12
playbooks/vpn.yml Normal file
View File

@@ -0,0 +1,12 @@
- name: Setup VLESS VPN server
hosts: vpn
become: yes
roles:
- role: xray-vps-setup
vars:
domain: access.stanito.me
setup_variant: xray
user_to_create: vpnuser
ssh_public_key: "{{ ssh_public_key }}"

View File

@@ -2,6 +2,11 @@
roles: roles:
# Install a role from Ansible Galaxy. # Install a role from Ansible Galaxy.
# note that ranges are not supported for roles # note that ranges are not supported for roles
- name: xray-vps-setup
src: https://github.com/Akiyamov/xray-vps-setup.git
scm: git
version: master
- name: geerlingguy.certbot - name: geerlingguy.certbot
version: "5.4.1" version: "5.4.1"

4
roles/xray-vps-setup/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
gist*.md
ansible.cfg
inventory.yml
playbook.yml

View File

@@ -0,0 +1,60 @@
# xray-vps-setup
VLESS со своим доменом. А что еще нужно для счастья?
В данном варианте VLESS слушает на 443 и принимате все запросы, делая запрос на локальный Caddy только для сертификатов. В таком варианте задержка будет меньше, чем в варианте с Caddy/NGINX перед VLESS, где происходит множество лишних запросов.
## Скрипт
- Установит Xray/Marzban на ваш выбор. Для маскировки страницы используется [Conflunce](https://github.com/Jolymmiles/confluence-marzban-home)
- На ваше усмотрение настроит:
- - Iptables, запретив все подключения, кроме SSH, 80 и 443.
- - Создаст пользователя для подключения, запретив вход от рута
- - Добавит этому пользователю ключ для SSH, запретив вход по паролю
- Настроит WARP для ру-сайтов.
```bash
tmux
bash <(wget -qO- https://raw.githubusercontent.com/Akiyamov/xray-vps-setup/refs/heads/main/vps-setup.sh)
```
## Плейбук
[Ansible-galaxy](https://galaxy.ansible.com/ui/standalone/roles/Akiyamov/xray-vps-setup/install/)
```yaml
- name: Setup vps
hosts: some_host
roles:
- Akiyamov.xray-vps-setup
vars:
domain: example.com # домен, уровень неважен
setup_variant: marzban # marzban or xray
setup_warp: false # true or false
configure_security: true # true or false
user_to_create: xray_user # если configure_security: true, то обязательно
user_password: "xray_password" # если configure_security: true, то обязательно
SSH_PORT: 22 # если configure_security: true, то обязательно
ssh_public_key: "" # если configure_security: true, то обязательно
```
## Добавляем подписку и поддержку Mihomo
```
bash <(wget -qO- https://github.com/legiz-ru/marz-sub/raw/main/marz-sub.sh)
```
После этого сделайте `docker compose -f /opt/xray-vps-setup/docker-compose.yml down && docker compose -f /opt/xray-vps-setup/docker-compose.yml up -d`
## Ручная установка
Описана [здесь](https://github.com/Akiyamov/xray-vps-setup/blob/main/install_in_docker.md).
## Почему не nginx, haproxy, 3x-ui, x-ui, sing-box...
Caddy сам получит сертификаты, поэтому нам не придется их получать через `acme.sh` или `certbot`.
3X-ui мерзотная панель.
Sing-box не очень.
XHTTP позже, а больше не надо. Уже точно.
## Связь
Issues, PR ну или мой [тг](https://t.me/Akiyamov).
> [!IMPORTANT]
> Дайте секс

View File

@@ -0,0 +1,2 @@
---
# defaults file for vps-setup

View File

@@ -0,0 +1,15 @@
---
# handlers file for vps-setup
- name: Restart xray
service:
name: xray
state: restarted
- name: Restart caddy
service:
name: caddy
state: restarted
- name: Restart ssh
service:
name: ssh
state: restarted
daemon_reload: true

View File

@@ -0,0 +1,356 @@
<h1 align="center">VLESS + Reality Self Steal в Docker</h2>
### Что потребуется:
- VPS
- Свой домен
В статье будет рассмотрена установка как чистого Xray, так и Marzban.
## Настройка сервера
### Настройка SSH
На своем ПК, неважно, GNU/Linux или Windows. __На Windows используйте Powershell__. Открываем терминал и выполняем следующую команду:
```bash
ssh-keygen -t ed25519
```
После выполнения команды вам предложат изменить место хранения ключа и добавить пароль к нему. Менять локацию не надо, пароль же можете добавить ради безопасности.
Создав ключ, вам будет выведена локация публичной и приватной его части, нам нужно перекинуть публичную часть этого ключа на нашу VPS.
На Linux:
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub ваш_пользователь@ваша_vps
```
На Windows:
```powershell
ssh-copy-id -i $env:USERPROFILE\.ssh\id_ed25519.pub ваш_пользовательаша_vps
```
Если данная команда у вас не сработала на Windows, то нужно выполнить следующую:
```powershell
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh ваш_пользовательаша_vps "cat >> .ssh/authorized_keys"
```
__Далее все делается на VPS.__
Для отключения входа по паролю выполняем следующую команду:
```bash
grep -r PasswordAuthentication /etc/ssh -l | xargs -n 1 sed -i -e "/PasswordAuthentication /c\PasswordAuthentication no"
```
Сделав это можно перезапустить SSH.
```bash
sudo systemctl restart ssh
```
### Настройки iptables
Нам нужно оставить открытыми порты для SSH, 80(HTTP) и 443(HTTPS).
Для этого нужно выполнить следующие команды:
```bash
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
iptables -P INPUT DROP
iptables-save > /etc/network/iptables.rules
```
### Включение BBR
Достаточно выполнить следующие команды:
```bash
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p
```
## Создание прокси
### Установка Docker
Для установки нужно выполнить следующую команду:
```bash
bash <(wget -qO- https://get.docker.com) @ -o get-docker.sh
```
Если вы работаете не от админа, то выполните следующие команды, чтобы не писать `sudo` каждый раз:
```bash
sudo groupadd docker
sudo usermod -aG docker $USER
```
### Получение данных для прокси
В этой части будут описаны необходимые данные, а также способ их получения. Позже эти данные будут использованы в конфигурации.
- __VLESS_DOMAIN__: Ваш домен. Если используется punycode, то далее используется ТОЛЬКО на латинице.
- __XRAY_PBK+PIK__: `docker run --rm ghcr.io/xtls/xray-core x25519`
Оба значения для нас важны, Public key = PBK, Private key = PIK.
- __XRAY_SID__: `openssl rand -hex 8`
Short id, используется для различения разных клиентов
Следующие данные нужны только если вы будете устанавливать панель Marzban.
- __MARZBAN_USER__: `tr -dc A-Za-z0-9 </dev/urandom | head -c 8; echo`
Пользователь панели
- __MARZBAN_PASS__: `tr -dc A-Za-z0-9 </dev/urandom | head -c 13; echo`
Пароль пользователя панели
- __MARZBAN_PATH__: `openssl rand -hex 8`
URL панели
- __MARZBAN_SUB_PATH__: `openssl rand -hex 8`
URL подписок
### Настройка прокси
Создадим папку `/opt/xray-vps-setup` командой `mkdir -p /opt/xray-vps-setup`.
После этого переходим в папку и создаем в ней файл `docker-compose.yml`
<details>
<summary>Marzban</summary>
```yaml
services:
caddy:
image: caddy:2.9
restart: always
network_mode: host
volumes:
- ./caddy/data:/data
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./marzban_lib:/run/marzban
marzban:
image: gozargah/marzban:latest
restart: always
env_file: ./marzban/.env
network_mode: host
volumes:
- ./marzban_lib:/var/lib/marzban
- ./marzban/xray_config.json:/code/xray_config.json
- ./marzban/templates:/var/lib/marzban/templates
```
</details>
<details>
<summary>Xray</summary>
```yaml
services:
caddy:
image: caddy:2.9
restart: always
network_mode: host
volumes:
- ./caddy/data:/data
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/templates:/srv
xray:
image: ghcr.io/xtls/xray-core:latest
restart: always
network_mode: host
volumes:
- ./xray:/etc/xray
```
</details>
Создаем папку `/opt/xray-vps-setup/caddy` и в ней создаем файл `Caddyfile` и меняем его следующим образом.
<details><summary>Marzban</summary>
```yaml
{
https_port 4123
default_bind 127.0.0.1
servers {
listener_wrappers {
proxy_protocol {
allow 127.0.0.1/32
}
tls
}
}
auto_https disable_redirects
}
https://$VLESS_DOMAIN {
reverse_proxy * unix//run/marzban/marzban.socket
}
http://$VLESS_DOMAIN {
bind 0.0.0.0
redir https://$VLESS_DOMAIN{uri} permanent
}
:4123 {
tls internal
respond 204
}
:80 {
bind 0.0.0.0
respond 204
}
```
</details>
<details><summary>Чистый Xray</summary>
```yaml
{
https_port 4123
default_bind 127.0.0.1
servers {
listener_wrappers {
proxy_protocol {
allow 127.0.0.1/32
}
tls
}
}
auto_https disable_redirects
}
https://$VLESS_DOMAIN {
root * /srv
file_server
}
http://$VLESS_DOMAIN {
bind 0.0.0.0
redir https://$VLESS_DOMAIN{uri} permanent
}
:4123 {
tls internal
respond 204
}
:80 {
bind 0.0.0.0
respond 204
}
```
</details>
Настроив caddy требуется добавить страницу для маскировки. Для xray и marzban команды отличаются:
Xray
```bash
wget -qO- https://raw.githubusercontent.com/Jolymmiles/confluence-marzban-home/main/index.html | envsubst > /opt/xray-vps-setup/caddy/templates/index.html
```
Marzban
```bash
wget -qO- https://raw.githubusercontent.com/Jolymmiles/confluence-marzban-home/main/index.html | envsubst > /opt/xray-vps-setup/marzban/templates/home/index.html
```
После этого надо создать файл конфигурации Xray, если вы ставите marzban, то он будет находится в `/opt/xray-vps-setup/marzban/xray_config.json`, если чистый xray, то `/opt/xray-vps-setup/xray/config.json`
```json
{
"log": {
"loglevel": "debug"
},
"inbounds": [
{
"tag": "VLESS TCP VISION REALITY",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "XRAY_UUDI", // ПОМЕНЯТЬ НА СВОЕ
"email": "default",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"xver": 1,
"dest": "127.0.0.1:4123",
"serverNames": [
"VLESS_DOMAIN" // ПОМЕНЯТЬ НА СВОЕ
],
"privateKey": "XRAY_PIK", // ПОМЕНЯТЬ НА СВОЕ
"shortIds": [
"XRAY_SID" // ПОМЕНЯТЬ НА СВОЕ
]
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
],
"routeOnly": true
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct",
"settings": {
"domainStrategy": "UseIPv4"
}
},
{
"protocol": "blackhole",
"tag": "block"
}
],
"routing": {
"rules": [
{
"protocol": "bittorrent",
"outboundTag": "block"
}
],
"domainStrategy": "IPIfNonMatch"
},
"dns": {
"servers": [
"1.1.1.1",
"8.8.8.8"
],
"queryStrategy": "UseIPv4",
"disableFallback": false,
"tag": "dns-aux"
}
}
```
Для Marzban необходимо также добавить `.env` файл. Создайте файл `/opt/xray-vps-setup/marzban/.env` и вставьте следующее:
```
SUDO_USERNAME = "xray_admin"
SUDO_PASSWORD = "$MARZBAN_PASS"
UVICORN_UDS = "/var/lib/marzban/marzban.socket"
DASHBOARD_PATH = "/$MARZBAN_PATH/"
XRAY_JSON = "xray_config.json"
XRAY_SUBSCRIPTION_URL_PREFIX = "https://$VLESS_DOMAIN"
XRAY_SUBSCRIPTION_PATH = "$MARZBAN_SUB_PATH"
SQLALCHEMY_DATABASE_URL = "sqlite:////var/lib/marzban/db.sqlite3"
CUSTOM_TEMPLATES_DIRECTORY="/var/lib/marzban/templates/"
SUBSCRIPTION_PAGE_TEMPLATE="subscription/index.html"
HOME_PAGE_TEMPLATE="home/index.html"
```
## Настройка WARP
Для того, чтобы доабвить WARP для того, чтобы в Россию наш юзер ходил черзе него, то надо сделать следующее.
Устанавливаем WARP:
```bash
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/cloudflare-client.list
apt update
apt install cloudflare-warp -y
```
Настроим WARP:
```bash
warp-cli registration new
warp-cli mode proxy
warp-cli proxy port 40000
warp-cli connect
```
Если на этом этапе ловим ошибку подключения, то не продолжайте, WARP не рабоатет.
Установка `yq`:
```bash
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq && chmod +x /usr/bin/yq
```
Далее с помощью `yq` мы установим в уже существующий кофниг WARP:
```bash
yq eval '.outbounds += {"tag": "warp","protocol": "socks","settings": {"servers": [{"address": "127.0.0.1","port": 40000}]}}' -i $XRAY_CONFIG_WARP
yq eval '.routing.rules += {"outboundTag": "warp", "domain": ["geosite:category-ru", "regexp:.*\\.xn--$", "regexp:.*\\.ru$", "regexp:.*\\.su$"]}' -i $XRAY_CONFIG_WARP
```
Заменяем $XRAY_CONFIG_WARP на `/opt/xray-vps-setup/marzban/xray_config.json` для marzban и на `/opt/xray-vps-setup/xray/config.json` для чистого xray. После этого перезапускаем все:
```bash
docker compose -f /opt/xray-vps-setup/docker-compose.yml down && docker compose -f /opt/xray-vps-setup/docker-compose.yml up -d
```
#
Если вы хотите помочь что-то исправить, добавить и тд, то делайте PR или пишите в [ТГ](https://t.me/Akiyamov).

View File

@@ -0,0 +1,2 @@
install_date: Mon Feb 9 17:35:32 2026
version: null

View File

@@ -0,0 +1,18 @@
galaxy_info:
author: Akiyamov
description: Setup VPS for XRay
company: your company (optional)
license: BSD-3-Clause
min_ansible_version: 2.1
platforms:
- name: Ubuntu
versions:
- 16
- 18
- 20
- 22
- 24
galaxy_tags:
- xray
- vps
dependencies: []

View File

@@ -0,0 +1,10 @@
- name: Set BBR
ansible.posix.sysctl:
name: net.core.default_qdisc
value: "fq"
state: present
- name: Set queue
ansible.posix.sysctl:
name: net.ipv4.tcp_congestion_control
value: "bbr"
state: present

View File

@@ -0,0 +1,63 @@
- name: Print clipboard string
debug:
msg: "vless://{{ xray_uuid.stdout }}@{{ vless.domain }}:443?type=tcp&security=reality&pbk={{ x25519_pbk.stdout }}&fp=chrome&sni={{ vless.domain }}&sid={{ short_id.stdout }}&spx=%2F&flow=xtls-rprx-vision"
- name: XRay outbound config
debug:
msg: |
{
"tag": "default",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "{{ vless.domain }}",
"port": 443,
"users": [
{
"id": "{{ xray_uuid.stdout }}",
"encryption": "none",
"flow": "xtls-rprx-vision"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "{{ vless.domain }}",
"fingerprint": "chrome",
"publicKey": "{{ x25519_pbk.stdout }}",
"shortId": "{{ short_id.stdout }}",
"spiderX": ""
}
}
}
- name: Sing-box outbound config
debug:
msg: |
{
"type": "vless",
"server": "{{ vless.domain }}",
"server_port": 443,
"uuid": "{{ xray_uuid.stdout }}",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"insecure": false,
"server_name": "{{ vless.domain }}",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "{{ x25519_pbk.stdout }}",
"short_id": "{{ short_id.stdout }}"
}
}
}
- name: Print PBK, SID and UUID to connect to server.
debug:
msg: "UUID: {{ xray_uuid.stdout }}, SID: {{ short_id.stdout }}, PBK: {{ x25519_pbk.stdout }}"

View File

@@ -0,0 +1,13 @@
- name: Add Docker GPG apt Key
ansible.builtin.apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker Repository
ansible.builtin.apt_repository:
repo: deb https://download.docker.com/linux/ubuntu focal stable
state: present
- name: Update install docker-ce
ansible.builtin.apt:
name: docker-ce
state: latest
update_cache: true

View File

@@ -0,0 +1,31 @@
- name: Generate marzban specific values
block:
- name: Generate marzban password
set_fact:
MARZBAN_PASS: "{{ lookup('password', '/dev/null length=13 chars=ascii_letters') }}"
- name: Generate marzban password
set_fact:
MARZBAN_PATH: "{{ lookup('password', '/dev/null length=8 chars=ascii_letters') }}"
- name: Generate marzban password
set_fact:
MARZBAN_SUB_PATH: "{{ lookup('password', '/dev/null length=8 chars=ascii_letters') }}"
- name: Create dirs
file:
path: "{{ item }}"
state: directory
loop:
- /opt/xray-vps-setup/caddy
- /opt/xray-vps-setup/marzban
- /opt/xray-vps-setup/marzban/templates/home
- name: Copy config files
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: "caddyfile.j2", dest: "/opt/xray-vps-setup/caddy/Caddyfile" }
- { src: "xray.json.j2", dest: "/opt/xray-vps-setup/marzban/xray_config.json" }
- { src: "marzban.j2", dest: "/opt/xray-vps-setup/marzban/.env" }
- { src: "confluence.j2", dest: "/opt/xray-vps-setup/marzban/templates/home/index.html" }
- { src: "marzban_docker.j2", dest: "/opt/xray-vps-setup/docker-compose.yml" }
- debug:
msg: "Marzban password: {{ MARZBAN_PASS }}, marzban path: {{ MARZBAN_PATH }}"

View File

@@ -0,0 +1,16 @@
- name: Create dirs
file:
path: "{{ item }}"
state: directory
loop:
- /opt/xray-vps-setup/caddy/templates
- /opt/xray-vps-setup/xray
- name: Copy config files
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: "caddyfile.j2", dest: "/opt/xray-vps-setup/caddy/Caddyfile" }
- { src: "confluence.j2", dest: "/opt/xray-vps-setup/caddy/templates/index.html" }
- { src: "xray.json.j2", dest: "/opt/xray-vps-setup/xray/config.json" }
- { src: "xray_docker.j2", dest: "/opt/xray-vps-setup/docker-compose.yml" }

View File

@@ -0,0 +1,5 @@
- name: Download yq
ansible.builtin.get_url:
url: https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
dest: /usr/bin/yq
mode: '0755'

View File

@@ -0,0 +1,46 @@
- name: IPTables rules
block:
- name: Install netfilter-persistent
apt:
name: netfilter-persistent
state: present
- name: Allow related and established connections
ansible.builtin.iptables:
chain: INPUT
ctstate: ESTABLISHED,RELATED
jump: ACCEPT
become: yes
- name: Allow new incoming SYN packets on specified port
ansible.builtin.iptables:
chain: INPUT
protocol: tcp
destination_port: "{{ SSH_PORT }}"
ctstate: NEW
syn: match
jump: ACCEPT
- name: Allow ICMP
ansible.builtin.iptables:
chain: INPUT
protocol: icmp
jump: ACCEPT
- name: Allow 80, 443 connections
ansible.builtin.iptables:
chain: INPUT
protocol: tcp
destination_ports:
- "80"
- "443"
jump: ACCEPT
- name: Allow loopback in
shell:
cmd: iptables -A INPUT -i lo -j ACCEPT
- name: Allow loopback out
shell:
cmd: iptables -A OUTPUT -o lo -j ACCEPT
- name: INPUT DROP
ansible.builtin.iptables:
chain: INPUT
policy: DROP
- name: Save iptables rules
shell:
cmd: netfilter-persistent save

View File

@@ -0,0 +1,54 @@
---
- name: Populate service facts
ansible.builtin.service_facts:
- name: Enable BBR
include_tasks: bbr.yml
- name: Install docker
include_tasks: install_docker.yml
when: ansible_facts.services['docker'] is undefined
- name: Install/update yq
include_tasks: install_yq.yml
- name: Security block
block:
- name: Edit SSHD config
include_tasks: ssh.yml
- name: Edit iptables
include_tasks: iptables.yml
- name: Add user
include_tasks: user.yml
when: configure_security|default(false)|bool == true
- name: Generate values
block:
- name: Generate x25519 PIK
shell:
cmd: docker run --rm ghcr.io/xtls/xray-core x25519 | head -n1 | cut -d' ' -f 3
register: x25519_pik
- name: Generate x25519 PBK
shell:
cmd: docker run --rm ghcr.io/xtls/xray-core x25519 -i {{ x25519_pik.stdout }} | tail -1 | cut -d' ' -f 3
register: x25519_pbk
- name: Generate SID
shell:
cmd: openssl rand -hex 8
register: short_id
- name: Generate default user
shell:
cmd: docker run --rm ghcr.io/xtls/xray-core uuid
register: xray_uuid
- name: Install marzban
include_tasks: install_marzban.yml
when: setup_variant == "marzban"
- name: Install xray
include_tasks: install_xray.yml
when: setup_variant == "xray"
- name: Install warp
include_tasks: setup_warp.yml
when: setup_warp|default(false)|bool == true
- name: Start proxy
community.docker.docker_compose_v2:
project_src: /opt/xray-vps-setup
files:
- docker-compose.yml
- name: End xray
include_tasks: end_xray.yml
when: setup_variant == "xray"

View File

@@ -0,0 +1,61 @@
#- name: Add WARP GPG key
# ansible.builtin.get_url:
# url: https://pkg.cloudflareclient.com/pubkey.gpg
# dest: /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
# mode: '0644'
# force: true
- name: Add WARP GPG key
ansible.builtin.shell:
cmd: curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
- name: Add WARP repo
ansible.builtin.apt_repository:
filename: cloudflare-client
repo: "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ {{ ansible_facts['distribution_release'] }} main"
- name:
apt:
name: cloudflare-warp
state: present
update_cache: yes
- name: Register WARP
shell:
cmd: echo "y" | warp-cli registration new
- shell:
cmd: warp-cli mode proxy
- shell:
cmd: warp-cli proxy port 40000
- shell:
cmd: warp-cli connect
- name: Edit xray config
block:
- command:
argv:
- yq
- eval
- '.outbounds += {"tag": "warp","protocol": "socks","settings": {"servers": [{"address": "127.0.0.1","port": 40000}]}}'
- -i
- /opt/xray-vps-setup/xray/config.json
- command:
argv:
- yq
- eval
- '.routing.rules += {"outboundTag": "warp", "domain": ["geosite:category-ru", "regexp:.*\\.xn--$", "regexp:.*\\.ru$", "regexp:.*\\.su$"]}'
- -i
- /opt/xray-vps-setup/xray/config.json
when: setup_variant == "xray"
- name: Edit marzban config
block:
- command:
argv:
- yq
- eval
- '.outbounds += {"tag": "warp","protocol": "socks","settings": {"servers": [{"address": "127.0.0.1","port": 40000}]}}'
- -i
- /opt/xray-vps-setup/marzban/xray_config.json
- command:
argv:
- yq
- eval
- '.routing.rules += {"outboundTag": "warp", "domain": ["geosite:category-ru", "regexp:.*\\.xn--$", "regexp:.*\\.ru$", "regexp:.*\\.su$"]}'
- -i
- /opt/xray-vps-setup/marzban/xray_config.json
when: setup_variant == "marzban"

View File

@@ -0,0 +1,14 @@
- name: Change SSH port
shell:
cmd: grep -r Port /etc/ssh -l | xargs -n 1 sed -i -e "/Port /c\Port {{ SSH_PORT }}"
- name: Disable password login
shell:
cmd: grep -r PasswordAuthentication /etc/ssh -l | xargs -n 1 sed -i -e "/PasswordAuthentication /c\PasswordAuthentication no"
- name: Disable root login
shell:
cmd: grep -r PermitRootLogin /etc/ssh -l | xargs -n 1 sed -i -e "/PermitRootLogin /c\PermitRootLogin no"
- name: Restart ssh
service:
name: ssh
state: restarted
daemon_reload: true

View File

@@ -0,0 +1,14 @@
- name: Add user
ansible.builtin.user:
name: "{{ user_to_create }}"
shell: /bin/bash
groups: sudo,docker
password: "{{ user_password | password_hash('sha512') }}"
append: yes
update_password: on_create
register: "xray_user"
- name: Add ssh_pbk to user
ansible.posix.authorized_key:
user: "{{ user_to_create }}"
state: "present"
key: "{{ ssh_public_key }}"

View File

@@ -0,0 +1,50 @@
{
https_port 4123
default_bind 127.0.0.1
servers {
listener_wrappers {
proxy_protocol {
allow 127.0.0.1/32
}
tls
}
}
auto_https disable_redirects
}
https://{{ domain }} {
{% if setup_variant == "marzban" %}
reverse_proxy * unix//run/marzban/marzban.socket
{% else %}
root * /srv
file_server
{% endif %}
header {
-Server
}
handle_errors {
header {
-Server
}
}
}
http://{{ domain }} {
bind 0.0.0.0
redir https://{host}{uri} permanent
header {
-Server
}
handle_errors {
header {
-Server
}
}
}
:4123 {
tls internal
respond 204
}
:80 {
bind 0.0.0.0
respond 204
}

View File

@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход в Confluence</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f5f7;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background-color: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
width: 350px;
text-align: center;
}
.logo {
margin-bottom: 20px;
}
.logo img {
width: 120px;
}
h2 {
margin-bottom: 20px;
font-size: 24px;
color: #0052cc;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #dfe1e6;
border-radius: 4px;
box-sizing: border-box;
font-size: 16px;
}
.error {
border-color: red;
}
.error-message {
color: red;
font-size: 14px;
display: none;
margin-top: 10px;
}
button {
width: 100%;
padding: 10px;
background-color: #0052cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
}
button:hover {
background-color: #0747a6;
}
.help-links {
margin-top: 20px;
font-size: 14px;
}
.help-links a {
color: #0052cc;
text-decoration: none;
}
.help-links a:hover {
text-decoration: underline;
}
/* Стили для модального окна */
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
padding-top: 60px;
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 400px;
border-radius: 8px;
text-align: center;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<img src="https://cdn.icon-icons.com/icons2/2429/PNG/512/confluence_logo_icon_147305.png" alt="Confluence">
</div>
<h2 id="login-title">Войти в Confluence</h2>
<form id="login-form">
<input type="text" id="username" name="username" placeholder="Адрес электронной почты">
<input type="password" id="password" name="password" placeholder="Введите пароль">
<button type="submit" id="login-button">Войти</button>
</form>
<div id="error-message" class="error-message">Неправильное имя пользователя или пароль.</div>
<div class="help-links">
<a href="#" id="forgot-link">Не удается войти?</a> • <a href="#" id="create-link">Создать аккаунт</a>
</div>
</div>
<div id="myModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<p id="modal-text">Для создания аккаунта обратитесь к администратору.</p>
</div>
</div>
<script>
function setLanguage(lang) {
const elements = {
'ru': {
loginTitle: 'Войти в Confluence',
usernamePlaceholder: 'Адрес электронной почты',
passwordPlaceholder: 'Введите пароль',
loginButton: 'Войти',
forgotLink: 'Не удается войти?',
createLink: 'Создать аккаунт',
createAccountText: 'Для создания аккаунта обратитесь к администратору.',
forgotPasswordText: 'Для восстановления доступа обратитесь к администратору.',
errorMessage: 'Неправильное имя пользователя или пароль.'
},
'en': {
loginTitle: 'Login to Confluence',
usernamePlaceholder: 'Email address',
passwordPlaceholder: 'Enter password',
loginButton: 'Login',
forgotLink: 'Can\'t log in?',
createLink: 'Create an account',
createAccountText: 'To create an account, please contact your administrator.',
forgotPasswordText: 'To recover access, please contact your administrator.',
errorMessage: 'Incorrect username or password.'
}
};
document.getElementById('login-title').innerText = elements[lang].loginTitle;
document.getElementById('username').placeholder = elements[lang].usernamePlaceholder;
document.getElementById('password').placeholder = elements[lang].passwordPlaceholder;
document.getElementById('login-button').innerText = elements[lang].loginButton;
document.getElementById('forgot-link').innerText = elements[lang].forgotLink;
document.getElementById('create-link').innerText = elements[lang].createLink;
// Устанавливаем тексты для модальных окон и сообщений об ошибках
document.getElementById('create-link').dataset.modalText = elements[lang].createAccountText;
document.getElementById('forgot-link').dataset.modalText = elements[lang].forgotPasswordText;
document.getElementById('error-message').innerText = elements[lang].errorMessage;
}
function detectLanguage() {
const userLang = navigator.language || navigator.userLanguage;
if (userLang.startsWith('ru')) {
setLanguage('ru');
} else {
setLanguage('en');
}
}
document.addEventListener('DOMContentLoaded', detectLanguage);
var modal = document.getElementById("myModal");
var span = document.getElementsByClassName("close")[0];
function openModal(text) {
document.getElementById('modal-text').innerText = text;
modal.style.display = "block";
}
document.getElementById("create-link").onclick = function(event) {
event.preventDefault();
openModal(this.dataset.modalText);
}
document.getElementById("forgot-link").onclick = function(event) {
event.preventDefault();
openModal(this.dataset.modalText);
}
span.onclick = function() {
modal.style.display = "none";
}
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
document.getElementById('login-form').onsubmit = function(event) {
event.preventDefault();
var username = document.getElementById('username');
var password = document.getElementById('password');
var errorMessage = document.getElementById('error-message');
username.classList.remove('error');
password.classList.remove('error');
errorMessage.style.display = 'none';
var hasError = false;
if (username.value.trim() === '') {
username.classList.add('error');
hasError = true;
}
if (password.value.trim() === '') {
password.classList.add('error');
hasError = true;
}
if (hasError) {
return;
}
errorMessage.style.display = 'block';
};
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,95 @@
SUDO_USERNAME = "xray_admin"
SUDO_PASSWORD = "{{ MARZBAN_PASS }}"
UVICORN_UDS = "/var/lib/marzban/marzban.socket"
DASHBOARD_PATH = "/{{ MARZBAN_PATH }}/"
XRAY_JSON = "xray_config.json"
XRAY_SUBSCRIPTION_URL_PREFIX = "https://{{ domain }}"
XRAY_SUBSCRIPTION_PATH = "{{ MARZBAN_SUB_PATH }}"
SQLALCHEMY_DATABASE_URL = "sqlite:////var/lib/marzban/db.sqlite3"
# ALLOWED_ORIGINS=http://localhost,http://localhost:8000,http://example.com
# XRAY_EXECUTABLE_PATH = "/usr/local/bin/xray"
# XRAY_ASSETS_PATH = "/usr/local/share/xray"
# XRAY_EXCLUDE_INBOUND_TAGS = "INBOUND_X INBOUND_Y"
# XRAY_FALLBACKS_INBOUND_TAG = "INBOUND_X"
# TELEGRAM_API_TOKEN = 123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# TELEGRAM_ADMIN_ID = 987654321, 123456789
# TELEGRAM_LOGGER_CHANNEL_ID = -1234567890123
# TELEGRAM_DEFAULT_VLESS_FLOW = "xtls-rprx-vision"
# TELEGRAM_PROXY_URL = "http://localhost:8080"
# DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/xxxxxxx"
CUSTOM_TEMPLATES_DIRECTORY="/var/lib/marzban/templates/"
# CLASH_SUBSCRIPTION_TEMPLATE="clash/my-custom-template.yml"
SUBSCRIPTION_PAGE_TEMPLATE="subscription/index.html"
HOME_PAGE_TEMPLATE="home/index.html"
# V2RAY_SUBSCRIPTION_TEMPLATE="v2ray/default.json"
# V2RAY_SETTINGS_TEMPLATE="v2ray/settings.json"
# SINGBOX_SUBSCRIPTION_TEMPLATE="singbox/default.json"
# SINGBOX_SETTINGS_TEMPLATE="singbox/settings.json"
# MUX_TEMPLATE="mux/default.json"
## Enable JSON config for compatible clients to use mux, fragment, etc. Default False.
# USE_CUSTOM_JSON_DEFAULT=True
## Your preferred config type for different clients
## If USE_CUSTOM_JSON_DEFAULT is set True, all following programs will use the JSON config
# USE_CUSTOM_JSON_FOR_V2RAYN=False
# USE_CUSTOM_JSON_FOR_V2RAYNG=True
# USE_CUSTOM_JSON_FOR_STREISAND=False
## Set headers for subscription
# SUB_PROFILE_TITLE = "Susbcription"
# SUB_SUPPORT_URL = "https://t.me/support"
# SUB_UPDATE_INTERVAL = "12"
## External config to import into v2ray format subscription
# EXTERNAL_CONFIG = "config://..."
# SQLALCHEMY_POOL_SIZE = 10
# SQLIALCHEMY_MAX_OVERFLOW = 30
## Custom text for STATUS_TEXT variable
# ACTIVE_STATUS_TEXT = "Active"
# EXPIRED_STATUS_TEXT = "Expired"
# LIMITED_STATUS_TEXT = "Limited"
# DISABLED_STATUS_TEXT = "Disabled"
# ONHOLD_STATUS_TEXT = "On-Hold"
### Use negative values to disable auto-delete by default
# USERS_AUTODELETE_DAYS = -1
# USER_AUTODELETE_INCLUDE_LIMITED_ACCOUNTS = false
## Customize all notifications
# NOTIFY_STATUS_CHANGE = True
# NOTIFY_USER_CREATED = True
# NOTIFY_USER_UPDATED = True
# NOTIFY_USER_DELETED = True
# NOTIFY_USER_DATA_USED_RESET = True
# NOTIFY_USER_SUB_REVOKED = True
# NOTIFY_IF_DATA_USAGE_PERCENT_REACHED = True
# NOTIFY_IF_DAYS_LEF_REACHED = True
# NOTIFY_LOGIN = True
## Whitelist of IPs/hosts to disable login notifications
# LOGIN_NOTIFY_WHITE_LIST = '1.1.1.1,127.0.0.1'
### for developers
# DOCS=True
# DEBUG=True
# If You Want To Send Webhook To Multiple Server Add Multi Address
# WEBHOOK_ADDRESS = "http://127.0.0.1:9000/,http://127.0.0.1:9001/"
# WEBHOOK_SECRET = "something-very-very-secret"
# VITE_BASE_API="https://example.com/api/"
# JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 1440
# JOB_CORE_HEALTH_CHECK_INTERVAL = 10
# JOB_RECORD_NODE_USAGES_INTERVAL = 30
# JOB_RECORD_USER_USAGES_INTERVAL = 10
# JOB_REVIEW_USERS_INTERVAL = 10
# JOB_SEND_NOTIFICATIONS_INTERVAL = 30

View File

@@ -0,0 +1,20 @@
services:
caddy:
image: caddy:2.9
container_name: caddy
restart: always
network_mode: host
volumes:
- ./caddy/data:/data
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./marzban_lib:/run/marzban
marzban:
image: gozargah/marzban:v0.8.4
container_name: marzban
restart: always
env_file: ./marzban/.env
network_mode: host
volumes:
- ./marzban_lib:/var/lib/marzban
- ./marzban/xray_config.json:/code/xray_config.json
- ./marzban/templates:/var/lib/marzban/templates

View File

@@ -0,0 +1,76 @@
{
"log": {
"loglevel": "info"
},
"inbounds": [
{
"tag": "VLESS TCP VISION REALITY",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "{{ xray_uuid.stdout}}",
"email": "default",
"flow": "xtls-rprx-vision"
}],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"xver": 1,
"dest": "127.0.0.1:4123",
"serverNames": [
"{{ domain }}"
],
"privateKey": "{{ x25519_pik.stdout }}",
"shortIds": [
"{{ short_id.stdout }}"
]
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
],
"routeOnly": true
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct",
"settings": {
"domainStrategy": "UseIPv4"
}
},
{
"protocol": "blackhole",
"tag": "block"
}
],
"routing": {
"rules": [
{
"protocol": "bittorrent",
"outboundTag": "block"
}
],
"domainStrategy": "IPIfNonMatch"
},
"dns": {
"servers": [
"1.1.1.1",
"8.8.8.8"
],
"queryStrategy": "UseIPv4",
"disableFallback": false,
"tag": "dns-aux"
}
}

View File

@@ -0,0 +1,19 @@
services:
caddy:
image: caddy:2.9
container_name: caddy
restart: always
network_mode: host
volumes:
- ./caddy/data:/data
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/templates:/srv
xray:
image: ghcr.io/xtls/xray-core:25.6.8
restart: always
container_name: xray
user: root
command: run -c /etc/xray/config.json
network_mode: host
volumes:
- ./xray:/etc/xray

View File

@@ -0,0 +1,47 @@
{
https_port 4123
default_bind 127.0.0.1
servers {
listener_wrappers {
proxy_protocol {
allow 127.0.0.1/32
}
tls
}
}
auto_https disable_redirects
}
https://$VLESS_DOMAIN {
$CADDY_REVERSE
header {
-Server
}
handle_errors {
header {
-Server
}
}
}
http://$VLESS_DOMAIN {
bind 0.0.0.0
redir https://$VLESS_DOMAIN{uri} permanent
header {
-Server
}
handle_errors {
header {
-Server
}
}
}
:4123 {
tls internal
respond 204
}
:80 {
bind 0.0.0.0
respond 204
}

View File

@@ -0,0 +1,9 @@
services:
caddy:
image: caddy:2.9
container_name: caddy
restart: always
network_mode: host
volumes:
- ./caddy/data:/data
- ./caddy/Caddyfile:/etc/caddy/Caddyfile

View File

@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход в Confluence</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f5f7;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background-color: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
width: 350px;
text-align: center;
}
.logo {
margin-bottom: 20px;
}
.logo img {
width: 120px;
}
h2 {
margin-bottom: 20px;
font-size: 24px;
color: #0052cc;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #dfe1e6;
border-radius: 4px;
box-sizing: border-box;
font-size: 16px;
}
.error {
border-color: red;
}
.error-message {
color: red;
font-size: 14px;
display: none;
margin-top: 10px;
}
button {
width: 100%;
padding: 10px;
background-color: #0052cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
}
button:hover {
background-color: #0747a6;
}
.help-links {
margin-top: 20px;
font-size: 14px;
}
.help-links a {
color: #0052cc;
text-decoration: none;
}
.help-links a:hover {
text-decoration: underline;
}
/* Стили для модального окна */
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
padding-top: 60px;
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 400px;
border-radius: 8px;
text-align: center;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<img src="https://cdn.icon-icons.com/icons2/2429/PNG/512/confluence_logo_icon_147305.png" alt="Confluence">
</div>
<h2 id="login-title">Войти в Confluence</h2>
<form id="login-form">
<input type="text" id="username" name="username" placeholder="Адрес электронной почты">
<input type="password" id="password" name="password" placeholder="Введите пароль">
<button type="submit" id="login-button">Войти</button>
</form>
<div id="error-message" class="error-message">Неправильное имя пользователя или пароль.</div>
<div class="help-links">
<a href="#" id="forgot-link">Не удается войти?</a> • <a href="#" id="create-link">Создать аккаунт</a>
</div>
</div>
<div id="myModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<p id="modal-text">Для создания аккаунта обратитесь к администратору.</p>
</div>
</div>
<script>
function setLanguage(lang) {
const elements = {
'ru': {
loginTitle: 'Войти в Confluence',
usernamePlaceholder: 'Адрес электронной почты',
passwordPlaceholder: 'Введите пароль',
loginButton: 'Войти',
forgotLink: 'Не удается войти?',
createLink: 'Создать аккаунт',
createAccountText: 'Для создания аккаунта обратитесь к администратору.',
forgotPasswordText: 'Для восстановления доступа обратитесь к администратору.',
errorMessage: 'Неправильное имя пользователя или пароль.'
},
'en': {
loginTitle: 'Login to Confluence',
usernamePlaceholder: 'Email address',
passwordPlaceholder: 'Enter password',
loginButton: 'Login',
forgotLink: 'Can\'t log in?',
createLink: 'Create an account',
createAccountText: 'To create an account, please contact your administrator.',
forgotPasswordText: 'To recover access, please contact your administrator.',
errorMessage: 'Incorrect username or password.'
}
};
document.getElementById('login-title').innerText = elements[lang].loginTitle;
document.getElementById('username').placeholder = elements[lang].usernamePlaceholder;
document.getElementById('password').placeholder = elements[lang].passwordPlaceholder;
document.getElementById('login-button').innerText = elements[lang].loginButton;
document.getElementById('forgot-link').innerText = elements[lang].forgotLink;
document.getElementById('create-link').innerText = elements[lang].createLink;
// Устанавливаем тексты для модальных окон и сообщений об ошибках
document.getElementById('create-link').dataset.modalText = elements[lang].createAccountText;
document.getElementById('forgot-link').dataset.modalText = elements[lang].forgotPasswordText;
document.getElementById('error-message').innerText = elements[lang].errorMessage;
}
function detectLanguage() {
const userLang = navigator.language || navigator.userLanguage;
if (userLang.startsWith('ru')) {
setLanguage('ru');
} else {
setLanguage('en');
}
}
document.addEventListener('DOMContentLoaded', detectLanguage);
var modal = document.getElementById("myModal");
var span = document.getElementsByClassName("close")[0];
function openModal(text) {
document.getElementById('modal-text').innerText = text;
modal.style.display = "block";
}
document.getElementById("create-link").onclick = function(event) {
event.preventDefault();
openModal(this.dataset.modalText);
}
document.getElementById("forgot-link").onclick = function(event) {
event.preventDefault();
openModal(this.dataset.modalText);
}
span.onclick = function() {
modal.style.display = "none";
}
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
document.getElementById('login-form').onsubmit = function(event) {
event.preventDefault();
var username = document.getElementById('username');
var password = document.getElementById('password');
var errorMessage = document.getElementById('error-message');
username.classList.remove('error');
password.classList.remove('error');
errorMessage.style.display = 'none';
var hasError = false;
if (username.value.trim() === '') {
username.classList.add('error');
hasError = true;
}
if (password.value.trim() === '') {
password.classList.add('error');
hasError = true;
}
if (hasError) {
return;
}
errorMessage.style.display = 'block';
};
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,95 @@
SUDO_USERNAME = "xray_admin"
SUDO_PASSWORD = "$MARZBAN_PASS"
UVICORN_UDS = "/var/lib/marzban/marzban.socket"
DASHBOARD_PATH = "/$MARZBAN_PATH/"
XRAY_JSON = "xray_config.json"
XRAY_SUBSCRIPTION_URL_PREFIX = "https://$VLESS_DOMAIN"
XRAY_SUBSCRIPTION_PATH = "$MARZBAN_SUB_PATH"
SQLALCHEMY_DATABASE_URL = "sqlite:////var/lib/marzban/db.sqlite3"
# ALLOWED_ORIGINS=http://localhost,http://localhost:8000,http://example.com
# XRAY_EXECUTABLE_PATH = "/usr/local/bin/xray"
# XRAY_ASSETS_PATH = "/usr/local/share/xray"
# XRAY_EXCLUDE_INBOUND_TAGS = "INBOUND_X INBOUND_Y"
# XRAY_FALLBACKS_INBOUND_TAG = "INBOUND_X"
# TELEGRAM_API_TOKEN = 123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# TELEGRAM_ADMIN_ID = 987654321, 123456789
# TELEGRAM_LOGGER_CHANNEL_ID = -1234567890123
# TELEGRAM_DEFAULT_VLESS_FLOW = "xtls-rprx-vision"
# TELEGRAM_PROXY_URL = "http://localhost:8080"
# DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/xxxxxxx"
CUSTOM_TEMPLATES_DIRECTORY="/var/lib/marzban/templates/"
# CLASH_SUBSCRIPTION_TEMPLATE="clash/my-custom-template.yml"
SUBSCRIPTION_PAGE_TEMPLATE="subscription/index.html"
HOME_PAGE_TEMPLATE="home/index.html"
# V2RAY_SUBSCRIPTION_TEMPLATE="v2ray/default.json"
# V2RAY_SETTINGS_TEMPLATE="v2ray/settings.json"
# SINGBOX_SUBSCRIPTION_TEMPLATE="singbox/default.json"
# SINGBOX_SETTINGS_TEMPLATE="singbox/settings.json"
# MUX_TEMPLATE="mux/default.json"
## Enable JSON config for compatible clients to use mux, fragment, etc. Default False.
# USE_CUSTOM_JSON_DEFAULT=True
## Your preferred config type for different clients
## If USE_CUSTOM_JSON_DEFAULT is set True, all following programs will use the JSON config
# USE_CUSTOM_JSON_FOR_V2RAYN=False
# USE_CUSTOM_JSON_FOR_V2RAYNG=True
# USE_CUSTOM_JSON_FOR_STREISAND=False
## Set headers for subscription
# SUB_PROFILE_TITLE = "Susbcription"
# SUB_SUPPORT_URL = "https://t.me/support"
# SUB_UPDATE_INTERVAL = "12"
## External config to import into v2ray format subscription
# EXTERNAL_CONFIG = "config://..."
# SQLALCHEMY_POOL_SIZE = 10
# SQLIALCHEMY_MAX_OVERFLOW = 30
## Custom text for STATUS_TEXT variable
# ACTIVE_STATUS_TEXT = "Active"
# EXPIRED_STATUS_TEXT = "Expired"
# LIMITED_STATUS_TEXT = "Limited"
# DISABLED_STATUS_TEXT = "Disabled"
# ONHOLD_STATUS_TEXT = "On-Hold"
### Use negative values to disable auto-delete by default
# USERS_AUTODELETE_DAYS = -1
# USER_AUTODELETE_INCLUDE_LIMITED_ACCOUNTS = false
## Customize all notifications
# NOTIFY_STATUS_CHANGE = True
# NOTIFY_USER_CREATED = True
# NOTIFY_USER_UPDATED = True
# NOTIFY_USER_DELETED = True
# NOTIFY_USER_DATA_USED_RESET = True
# NOTIFY_USER_SUB_REVOKED = True
# NOTIFY_IF_DATA_USAGE_PERCENT_REACHED = True
# NOTIFY_IF_DAYS_LEF_REACHED = True
# NOTIFY_LOGIN = True
## Whitelist of IPs/hosts to disable login notifications
# LOGIN_NOTIFY_WHITE_LIST = '1.1.1.1,127.0.0.1'
### for developers
# DOCS=True
# DEBUG=True
# If You Want To Send Webhook To Multiple Server Add Multi Address
# WEBHOOK_ADDRESS = "http://127.0.0.1:9000/,http://127.0.0.1:9001/"
# WEBHOOK_SECRET = "something-very-very-secret"
# VITE_BASE_API="https://example.com/api/"
# JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 1440
# JOB_CORE_HEALTH_CHECK_INTERVAL = 10
# JOB_RECORD_NODE_USAGES_INTERVAL = 30
# JOB_RECORD_USER_USAGES_INTERVAL = 10
# JOB_REVIEW_USERS_INTERVAL = 10
# JOB_SEND_NOTIFICATIONS_INTERVAL = 30

View File

@@ -0,0 +1,21 @@
{
"type": "vless",
"server": "$VLESS_DOMAIN",
"server_port": 443,
"uuid": "$XRAY_UUID",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"insecure": false,
"server_name": "$VLESS_DOMAIN",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "$XRAY_PBK",
"short_id": "$XRAY_SID"
}
}
}

View File

@@ -0,0 +1,77 @@
{
"log": {
"loglevel": "none"
},
"inbounds": [
{
"tag": "VLESS TCP VISION REALITY",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "$XRAY_UUID",
"email": "default",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"xver": 1,
"dest": "127.0.0.1:4123",
"serverNames": [
"$VLESS_DOMAIN"
],
"privateKey": "$XRAY_PIK",
"shortIds": [
"$XRAY_SID"
]
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
],
"routeOnly": true
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct",
"settings": {
"domainStrategy": "UseIPv4"
}
},
{
"protocol": "blackhole",
"tag": "block"
}
],
"routing": {
"rules": [
{
"protocol": "bittorrent",
"outboundTag": "block"
}
],
"domainStrategy": "IPIfNonMatch"
},
"dns": {
"servers": [
"1.1.1.1",
"8.8.8.8"
],
"queryStrategy": "UseIPv4",
"disableFallback": false,
"tag": "dns-aux"
}
}

View File

@@ -0,0 +1,30 @@
{
"tag": "default",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "$VLESS_DOMAIN",
"port": 443,
"users": [
{
"id": "$XRAY_UUID",
"encryption": "none",
"flow": "xtls-rprx-vision"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "$VLESS_DOMAIN",
"fingerprint": "chrome",
"publicKey": "$XRAY_PBK",
"shortId": "$XRAY_SID",
"spiderX": ""
}
}
}

View File

@@ -0,0 +1,5 @@
---
- hosts: localhost
remote_user: root
roles:
- vps-setup

View File

@@ -0,0 +1,2 @@
---
# vars file for vps-setup

View File

@@ -0,0 +1,304 @@
#/bin/bash
set -e
export GIT_BRANCH="main"
export GIT_REPO="Akiyamov/xray-vps-setup"
# Check if script started as root
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
# Install idn
apt-get update
apt-get install idn sudo dnsutils -y
# Read domain input
read -ep "Enter your domain:"$'\n' input_domain
export VLESS_DOMAIN=$(echo $input_domain | idn)
SERVER_IPS=($(hostname -I))
RESOLVED_IP=$(dig +short $VLESS_DOMAIN | tail -n1)
if [ -z "$RESOLVED_IP" ]; then
echo "Warning: Domain has no DNS record"
read -ep "Are you sure? That domain has no DNS record. If you didn't add that you will have to restart xray and caddy by yourself [y/N]"$'\n' prompt_response
if [[ "$prompt_response" =~ ^([yY])$ ]]; then
echo "Ok, proceeding without DNS verification"
else
echo "Come back later"
exit 1
fi
else
MATCH_FOUND=false
for server_ip in "${SERVER_IPS[@]}"; do
if [ "$RESOLVED_IP" == "$server_ip" ]; then
MATCH_FOUND=true
break
fi
done
if [ "$MATCH_FOUND" = true ]; then
echo "✓ DNS record points to this server ($RESOLVED_IP)"
else
echo "Warning: DNS record exists but points to different IP"
echo " Domain resolves to: $RESOLVED_IP"
echo " This server's IPs: ${SERVER_IPS[*]}"
read -ep "Continue anyway? [y/N]"$'\n' prompt_response
if [[ "$prompt_response" =~ ^([yY])$ ]]; then
echo "Ok, proceeding"
else
echo "Come back later"
exit 1
fi
fi
fi
read -ep "Do you want to install marzban? [y/N] "$'\n' marzban_input
read -ep "Do you want to configure server security? Do this on first run only. [y/N] "$'\n' configure_ssh_input
if [[ ${configure_ssh_input,,} == "y" ]]; then
# Read SSH port
read -ep "Enter SSH port. Default 22, can't use ports: 80, 443 and 4123:"$'\n' input_ssh_port
while [[ "$input_ssh_port" -eq "80" || "$input_ssh_port" -eq "443" || "$input_ssh_port" -eq "4123" ]]; do
read -ep "No, ssh can't use $input_ssh_port as port, write again:"$'\n' input_ssh_port
done
# Read SSH Pubkey
read -ep "Enter SSH public key:"$'\n' input_ssh_pbk
echo "$input_ssh_pbk" > ./test_pbk
ssh-keygen -l -f ./test_pbk
PBK_STATUS=$(echo $?)
if [ "$PBK_STATUS" -eq 255 ]; then
echo "Can't verify the public key. Try again and make sure to include 'ssh-rsa' or 'ssh-ed25519' followed by 'user@pcname' at the end of the file."
exit
fi
rm ./test_pbk
fi
read -ep "Do you want to install WARP and use it on russian websites? [y/N] "$'\n' configure_warp_input
if [[ ${configure_warp_input,,} == "y" ]]; then
if ! curl -I https://api.cloudflareclient.com --connect-timeout 10 > /dev/null 2>&1; then
echo "Warp can't be used"
configure_warp_input="n"
fi
fi
# Check congestion protocol
if sysctl net.ipv4.tcp_congestion_control | grep bbr; then
echo "BBR is already used"
else
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p > /dev/null
echo "Enabled BBR"
fi
export ARCH=$(dpkg --print-architecture)
yq_install() {
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_$ARCH -O /usr/bin/yq && chmod +x /usr/bin/yq
}
yq_install
docker_install() {
curl -fsSL https://get.docker.com | sh
}
if ! command -v docker 2>&1 >/dev/null; then
docker_install
fi
# Generate values for XRay
export SSH_USER=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 8; echo)
export SSH_USER_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13; echo)
export SSH_PORT=${input_ssh_port:-22}
export ROOT_LOGIN="yes"
export IP_CADDY=$(hostname -I | cut -d' ' -f1)
export CADDY_BASIC_AUTH=$(docker run --rm caddy caddy hash-password --plaintext $SSH_USER_PASS)
export XRAY_PIK=$(docker run --rm ghcr.io/xtls/xray-core x25519 | head -n1 | cut -d' ' -f 2)
export XRAY_PBK=$(docker run --rm ghcr.io/xtls/xray-core x25519 -i $XRAY_PIK | tail -2 | head -1 | cut -d' ' -f 2)
export XRAY_SID=$(openssl rand -hex 8)
export XRAY_UUID=$(docker run --rm ghcr.io/xtls/xray-core uuid)
export XRAY_CFG="/usr/local/etc/xray/config.json"
# Install marzban
xray_setup() {
mkdir -p /opt/xray-vps-setup
cd /opt/xray-vps-setup
if [[ "${marzban_input,,}" == "y" ]]; then
export MARZBAN_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13; echo)
export MARZBAN_PATH=$(openssl rand -hex 8)
export MARZBAN_SUB_PATH=$(openssl rand -hex 8)
wget -qO- https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/compose | envsubst > ./docker-compose.yml
yq eval \
'.services.marzban.image = "gozargah/marzban:v0.8.4" |
.services.marzban.container_name = "marzban" |
.services.marzban.restart = "always" |
.services.marzban.env_file = "./marzban/.env" |
.services.marzban.network_mode = "host" |
.services.marzban.volumes[0] = "./marzban_lib:/var/lib/marzban" |
.services.marzban.volumes[1] = "./marzban/xray_config.json:/code/xray_config.json" |
.services.marzban.volumes[2] = "./marzban/templates:/var/lib/marzban/templates" |
.services.caddy.volumes[2] = "./marzban_lib:/run/marzban"' -i /opt/xray-vps-setup/docker-compose.yml
mkdir -p marzban caddy
wget -qO- https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/marzban | envsubst > ./marzban/.env
mkdir -p /opt/xray-vps-setup/marzban/templates/home
wget -qO- https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/confluence_page | envsubst > ./marzban/templates/home/index.html
export CADDY_REVERSE="reverse_proxy * unix//run/marzban/marzban.socket"
wget -qO- "https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/caddy" | envsubst > ./caddy/Caddyfile
wget -qO- "https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/xray" | envsubst > ./marzban/xray_config.json
else
wget -qO- https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/compose | envsubst > ./docker-compose.yml
mkdir -p /opt/xray-vps-setup/caddy/templates
yq eval \
'.services.xray.image = "ghcr.io/xtls/xray-core:25.6.8" |
.services.xray.container_name = "xray" |
.services.xray.user = "root" |
.services.xray.command = "run -c /etc/xray/config.json" |
.services.xray.restart = "always" |
.services.xray.network_mode = "host" |
.services.caddy.volumes[2] = "./caddy/templates:/srv" |
.services.xray.volumes[0] = "./xray:/etc/xray"' -i /opt/xray-vps-setup/docker-compose.yml
wget -qO- https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/confluence_page | envsubst > ./caddy/templates/index.html
export CADDY_REVERSE="root * /srv
file_server"
mkdir -p xray caddy
wget -qO- "https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/xray" | envsubst > ./xray/config.json
wget -qO- "https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/caddy" | envsubst > ./caddy/Caddyfile
fi
}
xray_setup
sshd_edit() {
grep -r Port /etc/ssh -l | xargs -n 1 sed -i -e "/Port /c\Port $SSH_PORT"
grep -r PasswordAuthentication /etc/ssh -l | xargs -n 1 sed -i -e "/PasswordAuthentication /c\PasswordAuthentication no"
grep -r PermitRootLogin /etc/ssh -l | xargs -n 1 sed -i -e "/PermitRootLogin /c\PermitRootLogin no"
systemctl daemon-reload
systemctl restart ssh
}
add_user() {
useradd $SSH_USER -s /bin/bash
usermod -aG sudo $SSH_USER
echo $SSH_USER:$SSH_USER_PASS | chpasswd
mkdir -p /home/$SSH_USER/.ssh
touch /home/$SSH_USER/.ssh/authorized_keys
echo $input_ssh_pbk >> /home/$SSH_USER/.ssh/authorized_keys
chmod 700 /home/$SSH_USER/.ssh/
chmod 600 /home/$SSH_USER/.ssh/authorized_keys
chown $SSH_USER:$SSH_USER -R /home/$SSH_USER
usermod -aG docker $SSH_USER
}
debconf-set-selections <<EOF
iptables-persistent iptables-persistent/autosave_v4 boolean true
iptables-persistent iptables-persistent/autosave_v6 boolean true
EOF
# Configure iptables
edit_iptables() {
apt-get install iptables-persistent netfilter-persistent -y
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport $SSH_PORT -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
iptables -P INPUT DROP
netfilter-persistent save
}
if [[ ${configure_ssh_input,,} == "y" ]]; then
add_user
sshd_edit
edit_iptables
fi
# WARP Install function
warp_install() {
apt install gpg -y
echo "If this fails then warp won't be added to routing and everything will work without it"
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/cloudflare-client.list
apt update
apt install cloudflare-warp -y
echo "y" | warp-cli registration new
export TRY_WARP=$(echo $?)
if [[ $TRY_WARP != 0 ]]; then
echo "Couldn't connect to WARP"
exit 0
else
warp-cli mode proxy
warp-cli proxy port 40000
warp-cli connect
if [[ "${marzban_input,,}" == "y" ]]; then
export XRAY_CONFIG_WARP="/opt/xray-vps-setup/marzban/xray_config.json"
else
export XRAY_CONFIG_WARP="/opt/xray-vps-setup/xray/config.json"
fi
yq eval \
'.outbounds += {"tag": "warp","protocol": "socks","settings": {"servers": [{"address": "127.0.0.1","port": 40000}]}}' \
-i $XRAY_CONFIG_WARP
yq eval \
'.routing.rules += {"outboundTag": "warp", "domain": ["geosite:category-ru", "regexp:.*\\.xn--$", "regexp:.*\\.ru$", "regexp:.*\\.su$"]}' \
-i $XRAY_CONFIG_WARP
docker compose -f /opt/xray-vps-setup/docker-compose.yml down && docker compose -f /opt/xray-vps-setup/docker-compose.yml up -d
fi
}
end_script() {
if [[ ${configure_warp_input,,} == "y" ]]; then
warp_install
fi
if [[ "${marzban_input,,}" == "y" ]]; then
docker run -v /opt/xray-vps-setup/caddy/Caddyfile:/opt/xray-vps-setup/Caddyfile --rm caddy caddy fmt --overwrite /opt/xray-vps-setup/Caddyfile
docker compose -f /opt/xray-vps-setup/docker-compose.yml up -d
final_msg="Marzban panel location: https://$VLESS_DOMAIN/$MARZBAN_PATH
User: xray_admin
Password: $MARZBAN_PASS
"
if [[ ${configure_ssh_input,,} == "y" ]]; then
echo "New user for ssh: $SSH_USER, password for user: $SSH_USER_PASS. New port for SSH: $SSH_PORT."
fi
else
docker run -v /opt/xray-vps-setup/caddy/Caddyfile:/opt/xray-vps-setup/Caddyfile --rm caddy caddy fmt --overwrite /opt/xray-vps-setup/Caddyfile
docker compose -f /opt/xray-vps-setup/docker-compose.yml up -d
xray_config=$(wget -qO- "https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/xray_outbound" | envsubst)
singbox_config=$(wget -qO- "https://raw.githubusercontent.com/$GIT_REPO/refs/heads/$GIT_BRANCH/templates_for_script/sing_box_outbound" | envsubst)
final_msg="Clipboard string format:
vless://$XRAY_UUID@$VLESS_DOMAIN:443?type=tcp&security=reality&pbk=$XRAY_PBK&fp=chrome&sni=$VLESS_DOMAIN&sid=$XRAY_SID&spx=%2F&flow=xtls-rprx-vision
XRay outbound config:
$xray_config
Sing-box outbound config:
$singbox_config
Plain data:
PBK: $XRAY_PBK, SID: $XRAY_SID, UUID: $XRAY_UUID
"
fi
docker rmi ghcr.io/xtls/xray-core:latest caddy:latest
clear
echo "$final_msg"
if [[ ${configure_ssh_input,,} == "y" ]]; then
echo "New user for ssh: $SSH_USER, password for user: $SSH_USER_PASS. New port for SSH: $SSH_PORT."
fi
}
end_script
set +e