chore: настройка панелей

This commit is contained in:
protokey committed 2026-09-25 17:35:28 +04:00
1 parent 8532557ef6
commit 96213e182e
16 files changed
+529 -64

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
# vars: cert_host - домен или ip, на который выпускается сертификат
#
# Сертификат Let's Encrypt через acme.sh в режиме standalone: проверка HTTP-01 на 80 порту, он должен быть открыт.
# На ip выдается только короткий сертификат (~6 дней), acme.sh продлевает его по cron и перезапускает панель.
# Ставит сертификат панели и подписке. Если выпустить не удалось, падает, а не оставляет панель на http.
- name: Set certificate paths
ansible.builtin.set_fact:
cert_is_ip: "{{ cert_host is match('^[0-9.]+$') or ':' in cert_host }}"
cert_dir: "/root/cert/{{ cert_host }}"
acme: /root/.acme.sh/acme.sh
- name: Install acme.sh dependencies
ansible.builtin.apt:
name: [ curl, socat, cron ]
state: present
- name: Download acme.sh
ansible.builtin.get_url:
url: https://get.acme.sh
dest: /root/acme-install.sh
mode: "0700"
when: not acme is file
- name: Install acme.sh
ansible.builtin.shell: sh /root/acme-install.sh
args:
chdir: /root
creates: "{{ acme }}"
- name: Use Let's Encrypt
ansible.builtin.command: "{{ acme }} --set-default-ca --server letsencrypt"
changed_when: false
# код 2 - действующий сертификат уже есть, продлевать рано
- name: Issue certificate
ansible.builtin.command: >-
{{ acme }} --issue -d {{ cert_host }} --standalone --httpport 80 --server letsencrypt
{{ '--certificate-profile shortlived --days 6' if cert_is_ip else '' }}
register: cert_issue
changed_when: cert_issue.rc == 0
failed_when: cert_issue.rc not in [0, 2]
- name: Create certificate directory
ansible.builtin.file:
path: "{{ cert_dir }}"
state: directory
mode: "0700"
- name: Install certificate
ansible.builtin.command: >-
{{ acme }} --installcert -d {{ cert_host }}
--key-file {{ cert_dir }}/privkey.pem --fullchain-file {{ cert_dir }}/fullchain.pem
--reloadcmd "systemctl restart x-ui"
changed_when: true
- name: Check certificate
ansible.builtin.command: "openssl x509 -in {{ cert_dir }}/fullchain.pem -noout -checkend 3600 -ext subjectAltName"
register: cert_check
changed_when: false
failed_when: cert_check.rc != 0 or cert_host not in cert_check.stdout
- name: Use certificate for panel
ansible.builtin.command: "{{ xui_bin }} cert -webCert {{ cert_dir }}/fullchain.pem -webCertKey {{ cert_dir }}/privkey.pem"
changed_when: true
@@ -0,0 +1,24 @@
# vars: xui_settings_patch - поля настроек панели, которые нужно поменять. Нужен tasks/xui_api.yml.
#
# Настройки меняются только целиком: читаем все, меняем нужные поля, отправляем обратно.
- name: Read panel settings
ansible.builtin.uri:
url: "{{ xui_api_url }}/setting/all"
method: POST
headers: "{{ xui_api_headers }}"
validate_certs: false
register: xui_settings
failed_when: not xui_settings.json.success | default(false)
no_log: true
- name: Update panel settings
ansible.builtin.uri:
url: "{{ xui_api_url }}/setting/update"
method: POST
headers: "{{ xui_api_headers }}"
body_format: json
body: "{{ xui_settings.json.obj | combine(xui_settings_patch) }}"
validate_certs: false
register: xui_settings_update
failed_when: not xui_settings_update.json.success | default(false)
no_log: true
+20 -35
View File
@@ -1,7 +1,9 @@
# extravars: username, password, panel_port, web_base_path, sub_port, sub_path, domain (optional), version (optional)
# extravars: username, password, panel_port, web_base_path, sub_port, sub_path, domain (optional), ssl (optional),
# version (optional)
#
# Ставит 3x-ui скриптом из README проекта в неинтерактивном режиме.
# Сертификат: на домен, если он задан, иначе на ip. Оба варианта требуют открытый 80 порт (acme).
# Ставит 3x-ui скриптом из README проекта в неинтерактивном режиме, SSL скрипта выключен: он при ошибке молча оставляет
# панель на http. Сертификат выпускает tasks/xui_cert.yml на домен, если он задан, иначе на ip. Нужен открытый 80 порт.
# ssl=false - без сертификата, для серверов без публичного адреса.
# Системный DNS - DNS-over-TLS к Cloudflare, Xray резолвит через него. Подписка на sub_port под тем же сертификатом.
- name: Install 3x-ui panel
hosts: target
@@ -9,6 +11,8 @@
vars:
xui_bin: /usr/local/x-ui/x-ui
xui_domain: "{{ domain | default('') }}"
xui_ssl: "{{ ssl | default(true) | bool }}"
cert_host: "{{ xui_domain or ansible_host }}"
tasks:
- ansible.builtin.import_tasks: tasks/firewall.yml
vars:
@@ -33,8 +37,7 @@
XUI_PASSWORD: "{{ password }}"
XUI_PANEL_PORT: "{{ panel_port }}"
XUI_WEB_BASE_PATH: "{{ web_base_path }}"
XUI_SSL_MODE: "{{ 'domain' if xui_domain else 'ip' }}"
XUI_DOMAIN: "{{ xui_domain }}"
XUI_SSL_MODE: none
XUI_SERVER_IP: "{{ ansible_host }}"
no_log: true
@@ -45,6 +48,9 @@
-port {{ panel_port }} -webBasePath {{ web_base_path | quote }}
no_log: true
- ansible.builtin.import_tasks: tasks/xui_cert.yml
when: xui_ssl
- name: Restart panel
ansible.builtin.systemd_service:
name: x-ui
@@ -59,37 +65,14 @@
- ansible.builtin.import_tasks: tasks/xui_api.yml
# настройки меняются только целиком: читаем все, меняем подписку, отправляем обратно
- name: Read panel settings
ansible.builtin.uri:
url: "{{ xui_api_url }}/setting/all"
method: POST
headers: "{{ xui_api_headers }}"
validate_certs: false
register: panel_settings
failed_when: not panel_settings.json.success | default(false)
no_log: true
- name: Enable subscription
- ansible.builtin.import_tasks: tasks/xui_settings.yml
vars:
settings: "{{ panel_settings.json.obj }}"
ansible.builtin.uri:
url: "{{ xui_api_url }}/setting/update"
method: POST
headers: "{{ xui_api_headers }}"
body_format: json
body: >-
{{ settings | combine({
'subEnable': true,
'subPort': sub_port | int,
'subPath': '/' ~ sub_path ~ '/',
'subCertFile': settings.webCertFile,
'subKeyFile': settings.webKeyFile,
}) }}
validate_certs: false
register: sub_update
failed_when: not sub_update.json.success | default(false)
no_log: true
xui_settings_patch:
subEnable: true
subPort: "{{ sub_port | int }}"
subPath: "/{{ sub_path }}/"
subCertFile: "{{ cert_dir ~ '/fullchain.pem' if xui_ssl else '' }}"
subKeyFile: "{{ cert_dir ~ '/privkey.pem' if xui_ssl else '' }}"
- name: Read Xray config
ansible.builtin.uri:
@@ -131,4 +114,6 @@
aggregate: false
data:
api_token: "{{ api_token.stdout }}"
panel_host: "{{ cert_host if xui_ssl else ansible_host }}"
ssl: "{{ xui_ssl }}"
no_log: true
+37
View File
@@ -0,0 +1,37 @@
# extravars: domain (optional)
#
# Выпускает сертификат панели и подписки на domain, без него - на ip. Так можно поставить домен после установки на ip
# или вернуться с домена на ip. Домен должен указывать на этот сервер (или на сервер маскировки перед ним),
# 80 порт должен быть доступен снаружи.
- name: Issue panel certificate
hosts: target
gather_facts: false
vars:
xui_bin: /usr/local/x-ui/x-ui
cert_host: "{{ domain | default('', true) or ansible_host }}"
tasks:
- ansible.builtin.import_tasks: tasks/xui_cert.yml
- name: Restart panel
ansible.builtin.systemd_service:
name: x-ui
state: restarted
- ansible.builtin.import_tasks: tasks/xui_api.yml
- ansible.builtin.import_tasks: tasks/xui_settings.yml
vars:
xui_settings_patch:
subCertFile: "{{ cert_dir }}/fullchain.pem"
subKeyFile: "{{ cert_dir }}/privkey.pem"
- name: Restart panel with subscription certificate
ansible.builtin.systemd_service:
name: x-ui
state: restarted
- name: Return panel host
ansible.builtin.set_stats:
aggregate: false
data:
panel_host: "{{ cert_host }}"
+113
View File
@@ -0,0 +1,113 @@
# extravars: proxy_outbound (outbound Xray или null), warp_keys ({private_key, public_key} или null)
#
# Ставит исходящий прокси первым outbound Xray: он становится выходом по умолчанию для всего трафика клиентов.
# warp_keys - регистрирует устройство Cloudflare WARP через панель и выходит через него по wireguard.
# Оба null - прокси убирается, трафик идет напрямую. Перед применением прокси проверяется через панель.
- name: Set outbound proxy
hosts: target
gather_facts: false
vars:
xui_bin: /usr/local/x-ui/x-ui
proxy_tags: [ proxy, warp ]
tasks:
- ansible.builtin.import_tasks: tasks/xui_api.yml
- name: Read Xray config
ansible.builtin.uri:
url: "{{ xui_api_url }}/xray/"
method: POST
headers: "{{ xui_api_headers }}"
validate_certs: false
register: xray_setting
failed_when: not xray_setting.json.success | default(false)
- name: Use proxy outbound
ansible.builtin.set_fact:
new_outbound: "{{ proxy_outbound }}"
no_log: true
- name: Register Cloudflare WARP
when: warp_keys is not none
block:
- name: Register WARP device
ansible.builtin.uri:
url: "{{ xui_api_url }}/xray/warp/reg"
method: POST
headers: "{{ xui_api_headers }}"
body_format: form-urlencoded
body:
privateKey: "{{ warp_keys.private_key }}"
publicKey: "{{ warp_keys.public_key }}"
validate_certs: false
register: warp_reg
failed_when: not warp_reg.json.success | default(false)
no_log: true
- name: Read WARP config
ansible.builtin.set_fact:
warp: "{{ warp_reg.json.obj | from_json }}"
no_log: true
# reserved - байты client_id, в Jinja нет ord
- name: Decode WARP client id
ansible.builtin.command:
argv: [ python3, -c, "import base64, json, sys; print(json.dumps(list(base64.b64decode(sys.argv[1]))))", "{{ warp.config.config.client_id | default(warp.data.client_id) }}" ]
register: warp_reserved
changed_when: false
- name: Use WARP outbound
vars:
config: "{{ warp.config.config }}"
addresses: "{{ config.interface.addresses }}"
ansible.builtin.set_fact:
new_outbound:
tag: warp
protocol: wireguard
settings:
mtu: 1420
secretKey: "{{ warp.data.private_key }}"
address: "{{ ([addresses.v4 ~ '/32'] if addresses.v4 | default('') else []) + ([addresses.v6 ~ '/128'] if addresses.v6 | default('') else []) }}"
reserved: "{{ warp_reserved.stdout | from_json }}"
domainStrategy: ForceIPv4v6
noKernelTun: true
peers:
- publicKey: "{{ config.peers[0].public_key }}"
endpoint: "{{ config.peers[0].endpoint.host }}"
no_log: true
- name: Test outbound
when: new_outbound is not none
ansible.builtin.uri:
url: "{{ xui_api_url }}/xray/testOutbound"
method: POST
headers: "{{ xui_api_headers }}"
body_format: form-urlencoded
body:
outbound: "{{ new_outbound | to_json }}"
validate_certs: false
timeout: 60
register: outbound_test
failed_when: not outbound_test.json.success | default(false)
no_log: true
- name: Require working outbound
when: new_outbound is not none
ansible.builtin.assert:
that: outbound_test.json.obj.success | default(false)
fail_msg: "Outbound test failed: {{ outbound_test.json.obj.error | default(outbound_test.json.msg) }}"
- name: Apply outbound
vars:
config: "{{ (xray_setting.json.obj | from_json).xraySetting }}"
outbounds: "{{ ([new_outbound] if new_outbound is not none else []) + (config.outbounds | rejectattr('tag', 'in', proxy_tags) | list) }}"
ansible.builtin.uri:
url: "{{ xui_api_url }}/xray/update"
method: POST
headers: "{{ xui_api_headers }}"
body_format: form-urlencoded
body:
xraySetting: "{{ config | combine({'outbounds': outbounds}) | to_json }}"
validate_certs: false
register: xray_update
failed_when: not xray_update.json.success | default(false)
no_log: true