feat: добавить аддон hysteria2-server + playbook.yml для всех аддонов

hysteria2-server:
- Устанавливает Hysteria2 v2 на удалённый VPS (группа [hysteria2_server])
- TLS: selfsigned (IP, insecure=1) | acme (Let's Encrypt) | custom
- Obfuscation salamander, masquerade, лимиты полосы
- systemd-сервис, открытие firewall (ufw/firewalld)
- Вывод готового URL hysteria2://... для vault_hysteria2_url
- SSH с паролем: make addon-hysteria2-server ARGS="-k" (интерактивный ввод)

playbook.yml:
- addons/mediaserver/playbook.yml — hosts: k3s_master[0]
- addons/hysteria2-server/playbook.yml — hosts: hysteria2_server

Интеграция:
- inventory/hosts.ini: группа [hysteria2_server] (закомментированный пример)
- group_vars/all/addons.yml: addon_hysteria2_server
- vault.yml.example: vault_hysteria2_server_password, vault_hysteria2_obfs_password
- playbooks/addons.yml: оба аддона
- Makefile: addon-hysteria2-server target
This commit is contained in:
Sergey Antropoff
2026-04-26 06:39:50 +03:00
parent ba580f883e
commit fb5dcbc3af
13 changed files with 699 additions and 2 deletions

View File

@@ -0,0 +1,266 @@
# Hysteria2 Server
Устанавливает [Hysteria2](https://v2.hysteria.network/) VPN-сервер на удалённый VPS. На выходе даёт готовый URL для вставки в Shadowrocket, NekoBox, Hiddify или `vault_hysteria2_url` (Prowlarr sidecar).
## Что устанавливается
- Бинарник `hysteria` последней версии с GitHub
- Системный пользователь `hysteria`
- Конфиг в `/etc/hysteria/config.yaml`
- systemd-сервис `hysteria2` (autostart)
- Правило firewall (ufw / firewalld)
- TLS: self-signed cert **или** Let's Encrypt (ACME) **или** свой сертификат
## Быстрый старт
### 1. Добавить VPS в inventory
```ini
# inventory/hosts.ini
[hysteria2_server]
myvps ansible_host=1.2.3.4 ansible_user=root
```
### 2. Vault секреты
```bash
make vault-edit
```
```yaml
# group_vars/all/vault.yml
vault_hysteria2_server_password: "my-secure-password-32chars+"
vault_hysteria2_obfs_password: "obfs-secret" # если obfs включён
```
### 3. Деплой
**С SSH-ключом** (рекомендуется):
```bash
make addon-hysteria2-server
```
**С SSH-паролем** (ввод в терминале):
```bash
make addon-hysteria2-server ARGS="-k"
```
**С SSH-паролем + sudo-паролем**:
```bash
make addon-hysteria2-server ARGS="-k -K"
```
**На конкретный хост** (без inventory):
```bash
make addon-hysteria2-server ARGS="-i '1.2.3.4,' -u root -k"
```
После деплоя Ansible выведет готовый URL:
```
vault_hysteria2_url: "hysteria2://password@1.2.3.4:443?insecure=1#MyVPS"
```
---
## Конфигурация
### Режимы TLS
#### Self-signed (по умолчанию) — без домена, IP-only
```yaml
# group_vars/all/main.yml
hysteria2_server_tls_mode: "selfsigned"
```
Клиент подключается с `insecure=1`. URL автоматически получает `?insecure=1`.
#### ACME — Let's Encrypt (нужен домен)
```yaml
hysteria2_server_tls_mode: "acme"
hysteria2_server_domain: "vpn.example.com"
hysteria2_server_acme_email: "admin@example.com"
```
Требования: порт 80 открыт, DNS A-запись указывает на VPS. Hysteria2 сам получает и обновляет сертификат.
#### Custom — свои cert/key файлы
```yaml
hysteria2_server_tls_mode: "custom"
hysteria2_server_tls_cert_path: "/etc/hysteria/server.crt"
hysteria2_server_tls_key_path: "/etc/hysteria/server.key"
```
Сертификаты должны уже лежать на сервере до запуска роли.
---
### Obfuscation (salamander)
Скрывает протокол от DPI — важно если Hysteria2 блокируется провайдером:
```yaml
hysteria2_server_obfs_enabled: true
```
```yaml
# vault.yml
vault_hysteria2_obfs_password: "random-obfs-phrase"
```
---
### Лимиты полосы на клиента
```yaml
hysteria2_server_up_mbps: 100 # исходящий с сервера (= входящий у клиента)
hysteria2_server_down_mbps: 100 # входящий на сервер (= исходящий у клиента)
```
`0` — без ограничений.
---
### Все переменные
```yaml
hysteria2_server_version: "" # "" = автоопределение последней версии
hysteria2_server_port: 443
hysteria2_server_password: "{{ vault_hysteria2_server_password }}"
hysteria2_server_tls_mode: "selfsigned" # selfsigned | acme | custom
hysteria2_server_domain: ""
hysteria2_server_acme_email: ""
hysteria2_server_tls_cert_path: "/etc/hysteria/server.crt"
hysteria2_server_tls_key_path: "/etc/hysteria/server.key"
hysteria2_server_obfs_enabled: false
hysteria2_server_obfs_password: "{{ vault_hysteria2_obfs_password | default('') }}"
hysteria2_server_masquerade_enabled: true
hysteria2_server_masquerade_url: "https://bing.com/"
hysteria2_server_up_mbps: 0
hysteria2_server_down_mbps: 0
hysteria2_server_name: "MyVPS" # метка в URL (для клиентов)
```
---
## Подключение SSH
### Ключ (рекомендуется)
```bash
# Скопировать ключ на VPS
ssh-copy-id root@1.2.3.4
# Деплой
make addon-hysteria2-server
```
### Пароль
```bash
# Ввод пароля в терминале:
make addon-hysteria2-server ARGS="-k"
```
Или хранить в vault (не рекомендуется для SSH):
```yaml
# inventory/hosts.ini
[hysteria2_server]
myvps ansible_host=1.2.3.4 ansible_user=root ansible_ssh_pass="{{ vault_hysteria2_vps_ssh_password }}"
```
### Нестандартный SSH порт
```ini
# inventory/hosts.ini
[hysteria2_server]
myvps ansible_host=1.2.3.4 ansible_user=root ansible_port=2222
```
---
## Примеры inventory
### Один VPS, root + ключ
```ini
[hysteria2_server]
myvps ansible_host=1.2.3.4 ansible_user=root
```
### Один VPS, обычный пользователь + sudo
```ini
[hysteria2_server]
myvps ansible_host=1.2.3.4 ansible_user=ubuntu
```
```bash
make addon-hysteria2-server ARGS="-K" # -K = prompt sudo password
```
### Несколько серверов (мульти-регион)
```ini
[hysteria2_server]
vps-nl ansible_host=1.2.3.4 ansible_user=root
vps-de ansible_host=5.6.7.8 ansible_user=root
```
---
## Что делать с URL после деплоя
Ansible выведет в конце:
```
vault_hysteria2_url: "hysteria2://mypassword@1.2.3.4:443?insecure=1&obfs=salamander&obfs-password=secret#MyVPS"
```
**Вариант 1 — вставить в vault для Prowlarr:**
```yaml
# group_vars/all/vault.yml
vault_hysteria2_url: "hysteria2://mypassword@1.2.3.4:443?insecure=1#MyVPS"
```
Затем: `make addon-mediaserver`
**Вариант 2 — вставить в Shadowrocket / NekoBox / Hiddify:**
Скопируй строку `hysteria2://...` и добавь как новый профиль в клиент.
---
## Управление сервисом
```bash
# Статус
ssh root@1.2.3.4 systemctl status hysteria2
# Логи в реальном времени
ssh root@1.2.3.4 journalctl -u hysteria2 -f
# Перезапуск после изменения конфига
ssh root@1.2.3.4 systemctl restart hysteria2
# Обновить до последней версии
make addon-hysteria2-server
```
## Обновление
```bash
# Скачает последнюю версию и перезапустит сервис
make addon-hysteria2-server
```
Для фиксации версии:
```yaml
hysteria2_server_version: "app/v2.5.1"
```

View File

@@ -0,0 +1,7 @@
---
- name: Install Hysteria2 VPN Server
hosts: hysteria2_server
gather_facts: true
become: true
roles:
- role: "{{ playbook_dir }}/role"

View File

@@ -0,0 +1,50 @@
---
# ─── Hysteria2 Server ─────────────────────────────────────────────────────────
# Устанавливает Hysteria2 на удалённый VPS как системный сервис
# Версия — пустая строка = авто-определение последней с GitHub
hysteria2_server_version: ""
# Порт прослушивания
hysteria2_server_port: 443
# Пароль аутентификации клиентов
hysteria2_server_password: "{{ vault_hysteria2_server_password }}"
# ─── TLS ──────────────────────────────────────────────────────────────────────
# Режим: acme | selfsigned | custom
# acme — Let's Encrypt (нужен домен + порт 80 открыт)
# selfsigned — самоподписанный сертификат (клиент: insecure=1)
# custom — указать свои cert/key файлы
hysteria2_server_tls_mode: "selfsigned"
# Для acme:
hysteria2_server_domain: "" # домен, e.g. "vpn.example.com"
hysteria2_server_acme_email: "" # e-mail для Let's Encrypt
# Для custom:
hysteria2_server_tls_cert_path: "/etc/hysteria/server.crt"
hysteria2_server_tls_key_path: "/etc/hysteria/server.key"
# ─── Obfuscation (salamander) ─────────────────────────────────────────────────
# Скрывает протокол от DPI — рекомендуется для обхода блокировок
hysteria2_server_obfs_enabled: false
hysteria2_server_obfs_password: "{{ vault_hysteria2_obfs_password | default('') }}"
# ─── Masquerade ───────────────────────────────────────────────────────────────
# Делает трафик похожим на обычный HTTPS (важно для DPI)
hysteria2_server_masquerade_enabled: true
hysteria2_server_masquerade_url: "https://bing.com/"
# ─── Лимиты полосы пропускания (на клиента) ───────────────────────────────────
# 0 = без ограничений
hysteria2_server_up_mbps: 0
hysteria2_server_down_mbps: 0
# ─── Пути ─────────────────────────────────────────────────────────────────────
hysteria2_server_install_dir: /usr/local/bin
hysteria2_server_config_dir: /etc/hysteria
hysteria2_server_log_dir: /var/log/hysteria
# ─── Имя сервера (для метки в URL) ────────────────────────────────────────────
hysteria2_server_name: "MyVPS"

View File

@@ -0,0 +1,11 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
become: true
- name: Restart hysteria2
ansible.builtin.systemd:
name: hysteria2
state: restarted
become: true

View File

@@ -0,0 +1,264 @@
---
# ── Проверка обязательных переменных ─────────────────────────────────────────
- name: Assert required variables
ansible.builtin.assert:
that:
- hysteria2_server_password != ""
- hysteria2_server_password != "vault_hysteria2_server_password"
fail_msg: |
Необходимо задать vault_hysteria2_server_password в group_vars/all/vault.yml
Пример: vault_hysteria2_server_password: "my-secure-password"
run_once: true
- name: Assert ACME requires domain
ansible.builtin.assert:
that:
- hysteria2_server_domain != ""
- hysteria2_server_acme_email != ""
fail_msg: |
Для tls_mode=acme обязательны:
hysteria2_server_domain: "vpn.example.com"
hysteria2_server_acme_email: "admin@example.com"
when: hysteria2_server_tls_mode == "acme"
run_once: true
# ── Определить архитектуру ───────────────────────────────────────────────────
- name: Set CPU architecture fact
ansible.builtin.set_fact:
_hy2_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
# ── Получить последнюю версию с GitHub ───────────────────────────────────────
- name: Get latest Hysteria2 release from GitHub
ansible.builtin.uri:
url: https://api.github.com/repos/apernet/hysteria/releases/latest
return_content: true
headers:
Accept: "application/vnd.github.v3+json"
register: _hy2_github_release
when: hysteria2_server_version == ""
delegate_to: localhost
become: false
run_once: true
- name: Set Hysteria2 version fact
ansible.builtin.set_fact:
_hy2_version: >-
{{ (hysteria2_server_version != '')
| ternary(hysteria2_server_version,
_hy2_github_release.json.tag_name | default('app/latest')) }}
run_once: true
- name: Show Hysteria2 version
ansible.builtin.debug:
msg: "Installing Hysteria2 {{ _hy2_version }} ({{ _hy2_arch }})"
# ── Установить зависимости ────────────────────────────────────────────────────
- name: Install dependencies
ansible.builtin.package:
name:
- curl
- openssl
- ca-certificates
state: present
become: true
# ── Создать системного пользователя и директории ─────────────────────────────
- name: Create hysteria group
ansible.builtin.group:
name: hysteria
system: true
state: present
become: true
- name: Create hysteria user
ansible.builtin.user:
name: hysteria
group: hysteria
system: true
shell: /sbin/nologin
home: /var/lib/hysteria
create_home: false
comment: "Hysteria2 VPN Server"
become: true
- name: Create directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: hysteria
group: hysteria
mode: "0750"
loop:
- "{{ hysteria2_server_config_dir }}"
- "{{ hysteria2_server_log_dir }}"
- /var/lib/hysteria
become: true
# ── Скачать и установить бинарник ────────────────────────────────────────────
- name: Set download URL
ansible.builtin.set_fact:
_hy2_download_url: >-
https://github.com/apernet/hysteria/releases/download/{{ _hy2_version }}/hysteria-linux-{{ _hy2_arch }}
- name: Download Hysteria2 binary
ansible.builtin.get_url:
url: "{{ _hy2_download_url }}"
dest: "{{ hysteria2_server_install_dir }}/hysteria"
mode: "0755"
owner: root
group: root
become: true
register: _hy2_binary_downloaded
notify: Restart hysteria2
# ── TLS сертификат ───────────────────────────────────────────────────────────
- name: Generate self-signed TLS certificate
ansible.builtin.command: >
openssl req -x509 -newkey rsa:4096 -keyout {{ hysteria2_server_tls_key_path }}
-out {{ hysteria2_server_tls_cert_path }} -sha256 -days 3650 -nodes
-subj "/CN={{ ansible_default_ipv4.address }}"
args:
creates: "{{ hysteria2_server_tls_cert_path }}"
become: true
when: hysteria2_server_tls_mode == "selfsigned"
notify: Restart hysteria2
- name: Set permissions on TLS files
ansible.builtin.file:
path: "{{ item }}"
owner: hysteria
group: hysteria
mode: "0600"
loop:
- "{{ hysteria2_server_tls_cert_path }}"
- "{{ hysteria2_server_tls_key_path }}"
become: true
when: hysteria2_server_tls_mode in ["selfsigned", "custom"]
# ── Конфигурация ─────────────────────────────────────────────────────────────
- name: Template Hysteria2 config
ansible.builtin.template:
src: config.yaml.j2
dest: "{{ hysteria2_server_config_dir }}/config.yaml"
owner: hysteria
group: hysteria
mode: "0640"
become: true
notify: Restart hysteria2
# ── systemd сервис ────────────────────────────────────────────────────────────
- name: Template systemd service
ansible.builtin.template:
src: hysteria2.service.j2
dest: /etc/systemd/system/hysteria2.service
owner: root
group: root
mode: "0644"
become: true
notify:
- Reload systemd
- Restart hysteria2
- name: Enable and start Hysteria2
ansible.builtin.systemd:
name: hysteria2
enabled: true
state: started
daemon_reload: true
become: true
# ── Firewall ─────────────────────────────────────────────────────────────────
- name: Check if ufw is active
ansible.builtin.command: ufw status
register: _ufw_status
changed_when: false
failed_when: false
become: true
- name: Allow Hysteria2 port in ufw (TCP + UDP)
community.general.ufw:
rule: allow
port: "{{ hysteria2_server_port | string }}"
proto: "{{ item }}"
comment: "Hysteria2 VPN"
loop:
- tcp
- udp
become: true
when: "'active' in (_ufw_status.stdout | default(''))"
- name: Check if firewalld is running
ansible.builtin.command: firewall-cmd --state
register: _firewalld_status
changed_when: false
failed_when: false
become: true
- name: Allow Hysteria2 port in firewalld
ansible.posix.firewalld:
port: "{{ hysteria2_server_port }}/{{ item }}"
permanent: true
state: enabled
immediate: true
loop:
- tcp
- udp
become: true
when: "_firewalld_status.stdout | default('') == 'running'"
# ── Проверить что сервис запустился ──────────────────────────────────────────
- name: Wait for Hysteria2 to start listening
ansible.builtin.wait_for:
port: "{{ hysteria2_server_port }}"
host: "{{ ansible_default_ipv4.address }}"
timeout: 30
when: hysteria2_server_tls_mode != "acme"
- name: Verify Hysteria2 service status
ansible.builtin.command: systemctl status hysteria2 --no-pager
register: _hy2_service_status
changed_when: false
become: true
# ── Вывести итоговый URL для клиента ─────────────────────────────────────────
- name: Build client connection URL
ansible.builtin.set_fact:
_hy2_client_url: >-
hysteria2://{{ hysteria2_server_password | urlencode
}}@{{ ansible_default_ipv4.address }}:{{ hysteria2_server_port
}}{% if hysteria2_server_tls_mode == "selfsigned" %}?insecure=1{% else %}?insecure=0{% endif
%}{% if hysteria2_server_obfs_enabled and hysteria2_server_obfs_password != ""
%}&obfs=salamander&obfs-password={{ hysteria2_server_obfs_password | urlencode }}{% endif
%}#{{ hysteria2_server_name | urlencode }}
- name: "=== Hysteria2 Server Ready ==="
ansible.builtin.debug:
msg:
- "╔══════════════════════════════════════════════════════════════╗"
- "║ Hysteria2 Server установлен ║"
- "╚══════════════════════════════════════════════════════════════╝"
- ""
- " Сервер: {{ ansible_default_ipv4.address }}:{{ hysteria2_server_port }}"
- " TLS: {{ hysteria2_server_tls_mode }}"
- " Obfs: {{ 'salamander' if hysteria2_server_obfs_enabled else 'отключён' }}"
- ""
- " ── URL для клиента (Shadowrocket / NekoBox / Hiddify) ───────"
- " {{ _hy2_client_url }}"
- ""
- " ── vault.yml для использования с Prowlarr ───────────────────"
- " vault_hysteria2_url: \"{{ _hy2_client_url }}\""
- ""
- " Логи: journalctl -u hysteria2 -f"
- " Статус: systemctl status hysteria2"

View File

@@ -0,0 +1,43 @@
listen: :{{ hysteria2_server_port }}
{% if hysteria2_server_tls_mode == "acme" %}
acme:
domains:
- {{ hysteria2_server_domain }}
email: {{ hysteria2_server_acme_email }}
{% elif hysteria2_server_tls_mode == "selfsigned" or hysteria2_server_tls_mode == "custom" %}
tls:
cert: {{ hysteria2_server_tls_cert_path }}
key: {{ hysteria2_server_tls_key_path }}
{% endif %}
auth:
type: password
password: {{ hysteria2_server_password }}
{% if hysteria2_server_obfs_enabled and hysteria2_server_obfs_password != "" %}
obfs:
type: salamander
salamander:
password: {{ hysteria2_server_obfs_password }}
{% endif %}
{% if hysteria2_server_masquerade_enabled %}
masquerade:
type: proxy
proxy:
url: {{ hysteria2_server_masquerade_url }}
rewriteHost: true
{% endif %}
{% if hysteria2_server_up_mbps | int > 0 or hysteria2_server_down_mbps | int > 0 %}
bandwidth:
{% if hysteria2_server_up_mbps | int > 0 %}
up: {{ hysteria2_server_up_mbps }} mbps
{% endif %}
{% if hysteria2_server_down_mbps | int > 0 %}
down: {{ hysteria2_server_down_mbps }} mbps
{% endif %}
{% endif %}

View File

@@ -0,0 +1,21 @@
[Unit]
Description=Hysteria2 VPN Server
Documentation=https://v2.hysteria.network/
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=hysteria
Group=hysteria
ExecStart={{ hysteria2_server_install_dir }}/hysteria server --config {{ hysteria2_server_config_dir }}/config.yaml
Restart=on-failure
RestartSec=5s
LimitNOFILE=1048576
# Логи
StandardOutput=append:{{ hysteria2_server_log_dir }}/hysteria2.log
StandardError=append:{{ hysteria2_server_log_dir }}/hysteria2.log
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,7 @@
---
- name: Install MediaServer (Plex, *arr, Transmission, Prowlarr+Hysteria2, Samba)
hosts: k3s_master[0]
gather_facts: false
become: true
roles:
- role: "{{ playbook_dir }}/role"