Add Salamander obfs branch: replace masquerade with packet obfuscation.
- ACME TLS challenge on 443 (no port 80 or nginx decoy) - Auto-generate and persist obfs password per server - Update client export, HTML catalog, and vault examples - Document Salamander vs main and ACME auto-renewal in README
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
---
|
||||
# Домен сервера (A-запись → IP VPS). Задаётся per-host в inventory.
|
||||
# Режим развёртывания (ветка salamander)
|
||||
hysteria2_mode: salamander
|
||||
|
||||
# Домен сервера (A-запись → IP VPS). Используется для TLS/SNI и ACME.
|
||||
hysteria2_domain: ""
|
||||
|
||||
# Email для ACME / Let's Encrypt
|
||||
@@ -9,34 +12,31 @@ hysteria2_acme_email: ""
|
||||
hysteria2_users: []
|
||||
|
||||
# Опционально: фиксированные пароли { username: password }
|
||||
# Пустое значение или отсутствие ключа — автогенерация через pwgen
|
||||
|
||||
# Пароль обфускации Salamander (общий для сервера).
|
||||
# Пусто — автогенерация через pwgen или загрузка из output/<server>/server-info.yml
|
||||
hysteria2_obfs_password: ""
|
||||
|
||||
hysteria2_password_length: 40
|
||||
hysteria2_obfs_password_length: 32
|
||||
hysteria2_listen_port: 443
|
||||
hysteria2_upgrade_system: true
|
||||
hysteria2_configure_firewall: true
|
||||
|
||||
hysteria2_masq_dir: /var/www/masq
|
||||
hysteria2_config_path: /etc/hysteria/config.yaml
|
||||
hysteria2_service_name: hysteria-server
|
||||
|
||||
# Локальный каталог для экспорта URL и QR (на control node)
|
||||
hysteria2_output_dir: "{{ playbook_dir }}/output"
|
||||
hysteria2_output_name: "{{ inventory_hostname }}"
|
||||
|
||||
# Генерировать PNG QR-коды через qrencode (apt на VPS, fetch на control node)
|
||||
hysteria2_generate_qr_png: true
|
||||
hysteria2_qr_png_size: 6
|
||||
hysteria2_qr_png_margin: 2
|
||||
hysteria2_qr_png_error_correction: M
|
||||
|
||||
# Ждать ACME при первом запуске (отключите при update: make update)
|
||||
hysteria2_wait_for_acme: true
|
||||
|
||||
# Открыть output/index.html в браузере после install/update/export
|
||||
hysteria2_open_browser: true
|
||||
|
||||
# --- uninstall ---
|
||||
hysteria2_uninstall_remove_config: true
|
||||
hysteria2_uninstall_remove_masq: true
|
||||
hysteria2_uninstall_remove_local_output: false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
galaxy_info:
|
||||
role_name: hysteria2
|
||||
author: inecs
|
||||
description: Install Hysteria2 VPN server with masquerade site and export client URLs with QR codes
|
||||
description: Install Hysteria2 VPN server with Salamander obfs and export client URLs with QR codes
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
---
|
||||
- name: Create masquerade web directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ hysteria2_masq_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Deploy masquerade index.html
|
||||
ansible.builtin.template:
|
||||
src: masq/index.html.j2
|
||||
dest: "{{ hysteria2_masq_dir }}/index.html"
|
||||
mode: "0644"
|
||||
notify: Restart hysteria-server
|
||||
|
||||
- name: Remove default Hysteria config if present
|
||||
ansible.builtin.file:
|
||||
path: "{{ hysteria2_config_path }}"
|
||||
state: absent
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Deploy Hysteria2 server config
|
||||
- name: Deploy Hysteria2 Salamander server config
|
||||
ansible.builtin.template:
|
||||
src: config.yaml.j2
|
||||
dest: "{{ hysteria2_config_path }}"
|
||||
@@ -42,12 +29,11 @@
|
||||
failed_when: false
|
||||
when: hysteria2_configure_firewall | bool
|
||||
|
||||
- name: Allow HTTP and HTTPS in ufw
|
||||
- name: Allow Hysteria2 port in ufw (Salamander — только {{ hysteria2_listen_port }})
|
||||
ansible.builtin.command: "ufw allow {{ item }}"
|
||||
loop:
|
||||
- 80/tcp
|
||||
- 443/tcp
|
||||
- 443/udp
|
||||
- "{{ hysteria2_listen_port }}/tcp"
|
||||
- "{{ hysteria2_listen_port }}/udp"
|
||||
register: _hysteria2_ufw_allow
|
||||
changed_when: "'Skipping' not in (_hysteria2_ufw_allow.stdout | default(''))"
|
||||
failed_when: false
|
||||
@@ -55,10 +41,10 @@
|
||||
- hysteria2_configure_firewall | bool
|
||||
- "'active' in (_hysteria2_ufw_status.stdout | default(''))"
|
||||
|
||||
- name: Wait for ACME certificate (first start may take several minutes)
|
||||
- name: Wait for ACME TLS certificate (first start may take several minutes)
|
||||
ansible.builtin.pause:
|
||||
seconds: 30
|
||||
prompt: "Ожидание получения ACME-сертификата для {{ hysteria2_domain }}..."
|
||||
prompt: "Ожидание получения ACME TLS-сертификата для {{ hysteria2_domain }}..."
|
||||
when: hysteria2_wait_for_acme | default(true) | bool
|
||||
|
||||
- name: Verify hysteria-server is running
|
||||
|
||||
@@ -31,8 +31,10 @@
|
||||
mode: "0600"
|
||||
content: |
|
||||
server: {{ hysteria2_output_name }}
|
||||
mode: salamander
|
||||
domain: {{ hysteria2_domain }}
|
||||
port: {{ hysteria2_listen_port }}
|
||||
obfs_password: "{{ hysteria2_obfs_password }}"
|
||||
users:
|
||||
{% for user in hysteria2_export_users %}
|
||||
- name: {{ user.name }}
|
||||
@@ -53,6 +55,7 @@
|
||||
mode: "0644"
|
||||
vars:
|
||||
generated_at: "{{ ansible_date_time.date }} {{ ansible_date_time.time }}"
|
||||
hysteria2_obfs_password: "{{ hysteria2_obfs_password }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
name: "{{ _info.server }}"
|
||||
domain: "{{ _info.domain }}"
|
||||
port: "{{ _info.port }}"
|
||||
mode: "{{ _info.mode | default('salamander') }}"
|
||||
obfs_password: "{{ _info.obfs_password | default('') }}"
|
||||
dir: "{{ item.item.path | dirname | basename }}"
|
||||
users: "{{ _info.users }}"
|
||||
become: false
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
ansible.builtin.import_tasks: users.yml
|
||||
tags: [install, update, export]
|
||||
|
||||
- name: Resolve Salamander obfs password
|
||||
ansible.builtin.import_tasks: obfs.yml
|
||||
tags: [install, update, export]
|
||||
|
||||
- name: Install packages and Hysteria2 binary
|
||||
ansible.builtin.import_tasks: install.yml
|
||||
tags: [install]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
- name: Check for saved Salamander password from previous install
|
||||
ansible.builtin.stat:
|
||||
path: "{{ hysteria2_output_dir }}/{{ hysteria2_output_name }}/server-info.yml"
|
||||
register: _hysteria2_saved_info
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
tags:
|
||||
- install
|
||||
- update
|
||||
- export
|
||||
|
||||
- name: Load saved Salamander obfs password
|
||||
when: _hysteria2_saved_info.stat.exists
|
||||
block:
|
||||
- name: Read server-info.yml for obfs password
|
||||
ansible.builtin.slurp:
|
||||
path: "{{ hysteria2_output_dir }}/{{ hysteria2_output_name }}/server-info.yml"
|
||||
register: _hysteria2_saved_info_raw
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
- name: Parse saved obfs password
|
||||
ansible.builtin.set_fact:
|
||||
_hysteria2_saved_obfs_password: "{{ (_hysteria2_saved_info_raw.content | b64decode | from_yaml).obfs_password | default('') }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
tags:
|
||||
- install
|
||||
- update
|
||||
- export
|
||||
|
||||
- name: Resolve Salamander obfs password
|
||||
ansible.builtin.set_fact:
|
||||
hysteria2_obfs_password: >-
|
||||
{{
|
||||
hysteria2_obfs_password
|
||||
| default(_hysteria2_saved_obfs_password | default(''), true)
|
||||
}}
|
||||
tags:
|
||||
- install
|
||||
- update
|
||||
- export
|
||||
|
||||
- name: Generate Salamander obfs password with pwgen
|
||||
ansible.builtin.command:
|
||||
cmd: "pwgen -s {{ hysteria2_obfs_password_length }} 1"
|
||||
register: _hysteria2_obfs_pwgen
|
||||
changed_when: false
|
||||
when: hysteria2_obfs_password | length == 0
|
||||
tags:
|
||||
- install
|
||||
- update
|
||||
- export
|
||||
|
||||
- name: Apply generated Salamander obfs password
|
||||
ansible.builtin.set_fact:
|
||||
hysteria2_obfs_password: "{{ _hysteria2_obfs_pwgen.stdout }}"
|
||||
when:
|
||||
- _hysteria2_obfs_pwgen is defined
|
||||
- not (_hysteria2_obfs_pwgen.skipped | default(false))
|
||||
tags:
|
||||
- install
|
||||
- update
|
||||
- export
|
||||
@@ -34,6 +34,8 @@
|
||||
Domain: {{ hysteria2_domain }}:{{ hysteria2_listen_port }}
|
||||
User: {{ hysteria2_current_user.name }}
|
||||
Password: {{ hysteria2_current_user.password }}
|
||||
Mode: salamander
|
||||
Obfs password: {{ hysteria2_obfs_password }}
|
||||
|
||||
URL:
|
||||
{{ _hysteria2_client_url }}
|
||||
|
||||
@@ -19,12 +19,6 @@
|
||||
state: absent
|
||||
when: hysteria2_uninstall_remove_config | bool
|
||||
|
||||
- name: Remove masquerade web directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ hysteria2_masq_dir }}"
|
||||
state: absent
|
||||
when: hysteria2_uninstall_remove_masq | bool
|
||||
|
||||
- name: Reload systemd after uninstall
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
@@ -32,7 +26,7 @@
|
||||
- name: Show uninstall result
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Hysteria2 удалён с {{ inventory_hostname }}.
|
||||
Hysteria2 (Salamander) удалён с {{ inventory_hostname }}.
|
||||
{% if not hysteria2_uninstall_remove_local_output | bool %}
|
||||
Локальные URL/QR в {{ hysteria2_output_dir }}/{{ hysteria2_output_name }}/ сохранены.
|
||||
{% endif %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- hysteria2_users | length > 0
|
||||
fail_msg: |
|
||||
Задайте для каждого хоста:
|
||||
hysteria2_domain — домен с A-записью на IP сервера
|
||||
hysteria2_domain — домен с A-записью на IP сервера (используется для TLS/SNI)
|
||||
hysteria2_users — список имён пользователей, например [my, friend]
|
||||
И в group_vars/all.yml:
|
||||
hysteria2_acme_email — email для Let's Encrypt
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
server: {{ hysteria2_domain }}:{{ hysteria2_listen_port }}
|
||||
|
||||
auth: {{ hysteria2_current_user.name }}:{{ hysteria2_current_user.password }}
|
||||
|
||||
obfs:
|
||||
type: salamander
|
||||
salamander:
|
||||
password: {{ hysteria2_obfs_password }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
listen: 0.0.0.0:{{ hysteria2_listen_port }}
|
||||
|
||||
acme:
|
||||
type: http
|
||||
type: tls
|
||||
domains:
|
||||
- {{ hysteria2_domain }}
|
||||
email: {{ hysteria2_acme_email }}
|
||||
@@ -13,10 +13,7 @@ auth:
|
||||
{{ user.name }}: "{{ user.password }}"
|
||||
{% endfor %}
|
||||
|
||||
masquerade:
|
||||
type: file
|
||||
file:
|
||||
dir: {{ hysteria2_masq_dir }}
|
||||
listenHTTP: :80
|
||||
listenHTTPS: :{{ hysteria2_listen_port }}
|
||||
forceHTTPS: true
|
||||
obfs:
|
||||
type: salamander
|
||||
salamander:
|
||||
password: "{{ hysteria2_obfs_password }}"
|
||||
|
||||
@@ -160,6 +160,17 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.server-link:hover { background: var(--accent-soft); border-color: var(--accent); }
|
||||
.badge-salamander {
|
||||
display: inline-block;
|
||||
margin-top: 0.35rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
background: var(--accent-soft-2);
|
||||
color: var(--accent-2);
|
||||
}
|
||||
.server-obfs { margin-top: 0.75rem; width: 100%; }
|
||||
.users-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
@@ -286,8 +297,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero">
|
||||
<h1>Hysteria2</h1>
|
||||
<p>Общий каталог VPN-подключений</p>
|
||||
<h1>Hysteria2 · Salamander</h1>
|
||||
<p>Общий каталог VPN-подключений с обфускацией Salamander</p>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>{{ hysteria2_global_servers | length }}</span> серверов</div>
|
||||
<div class="stat"><span>{{ total_users }}</span> пользователей</div>
|
||||
@@ -307,6 +318,18 @@
|
||||
<div>
|
||||
<h2>{{ server.name | e }}</h2>
|
||||
<p class="domain">{{ server.domain | e }}:{{ server.port }}</p>
|
||||
<span class="badge-salamander">Salamander</span>
|
||||
{% if server.obfs_password | default('') | length > 0 %}
|
||||
<div class="field server-obfs">
|
||||
<label for="obfs-{{ server.dir | e }}">Пароль обфускации</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="obfs-{{ server.dir | e }}" value="{{ server.obfs_password | e }}" readonly>
|
||||
<button type="button" class="btn-copy" onclick="copyField('obfs-{{ server.dir | e }}', this)" title="Копировать obfs-password" aria-label="Копировать obfs-password">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a class="server-link" href="{{ server.dir | e }}/index.html">Страница сервера →</a>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,15 @@
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-salamander {
|
||||
margin-left: 0.35rem;
|
||||
background: rgba(163, 113, 247, 0.18);
|
||||
color: #a371f7;
|
||||
}
|
||||
.server-obfs {
|
||||
margin-top: 1.25rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.user-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
@@ -235,6 +244,17 @@
|
||||
Домен: <strong>{{ hysteria2_domain }}:{{ hysteria2_listen_port }}</strong>
|
||||
</p>
|
||||
<span class="badge">{{ hysteria2_export_users | length }} {{ 'пользователь' if hysteria2_export_users | length == 1 else 'пользователей' }}</span>
|
||||
<span class="badge badge-salamander">Salamander</span>
|
||||
|
||||
<div class="field server-obfs">
|
||||
<label for="obfs-server">Пароль обфускации (Salamander)</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="obfs-server" value="{{ hysteria2_obfs_password | e }}" readonly>
|
||||
<button type="button" class="btn-copy" onclick="copyField('obfs-server', this)" title="Копировать obfs-password" aria-label="Копировать obfs-password">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% for user in hysteria2_export_users %}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to nginx!</title>
|
||||
<style>
|
||||
html { color-scheme: light dark; }
|
||||
body { width: 35em; margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to nginx!</h1>
|
||||
<p>If you see this page, nginx is successfully installed and working.
|
||||
Further configuration is required for the web server, reverse proxy,
|
||||
API gateway, load balancer, content cache, or other features.</p>
|
||||
|
||||
<p>For online documentation and support please refer to
|
||||
<a href="https://nginx.org/">nginx.org</a>.<br/>
|
||||
To engage with the community please visit
|
||||
<a href="https://community.nginx.org/">community.nginx.org</a>.<br/>
|
||||
For enterprise grade support, professional services, additional
|
||||
security features and capabilities please refer to
|
||||
<a href="https://f5.com/nginx">f5.com/nginx</a>.</p>
|
||||
|
||||
<p><em>Thank you for using nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user