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

+12 -8
View File
@@ -27,7 +27,7 @@ Django приложение связывающее все части сайта
## Модули
- `services.py` - сценарии поверх serverus: `install_point`, `reinstall`, `release_point`.
- `services.py` - сценарии поверх serverus: `install_point`, `reinstall`, `release_point`, у готовой точки `set_domain` (новый сертификат) и `set_outbound_proxy`.
- `tasks.py` - задачи dramatiq: запускают сценарий и присылают результат в чат. Воркер: `make dj_worker`.
- `bot/interface.py` - интерфейс бота `MyPointInterface` (выбирается у бота в админке).
- `bot/texts.py` - кнопки и тексты.
@@ -35,16 +35,20 @@ Django приложение связывающее все части сайта
## Установка точки
1. bootstrap обоих серверов (ключ, пароль root, обновление, ssh на случайном порту, nftables)
2. панель: 3x-ui с логином, паролем, портом и путем, которые сгенерировал App
2. панель: 3x-ui с логином, паролем, портом и путем, которые сгенерировал App, сертификат на домен или ip, подписка, подключение reality `main`, исходящий прокси точки
3. маскировка: перенаправление трафика на адрес панели
4. facts: `uname -a` для карточки сервера
"Переустановить" сбрасывает отпечаток ssh сервера и повторяет установку с новым паролем от провайдера или, по кнопке "Использовать старый", с паролем, присланным при добавлении сервера. Отпечаток можно сбросить и в админке.
# TODO:
## TODO
-[ ] для панели 3x-ui нужно активировать ssl, панель умеет сама создавать сертификаты для домена или ip адреса.
-[ ] можно ли поставить домен после того как настроили сервер на ip? я имею ввиду поставить домен и выпустить ssl сертификат от letsencrypt.
-[ ] что нужно сделать чтобы сервис мог нормально работать и раздавать прокси интернет через cloudflare? когда домен за прокси клауда.
-[ ] если используется и прокси сервер и домен за claudflare, то будет работать?
-[ ] нужна функция установки прокси в исходящие (outbound)
- [x] для панели 3x-ui нужно активировать ssl, панель умеет сама создавать сертификаты для домена или ip адреса.
Скрипт 3x-ui при ошибке молча оставлял панель на http и при повторной установке SSL не трогал. Теперь сертификат выпускает serverus и падает, если не вышло. На ip без публичного адреса сертификат не выпустить: `ssl=no`.
- [x] можно ли поставить домен после того как настроили сервер на ip? я имею ввиду поставить домен и выпустить ssl сертификат от letsencrypt.
Да: `services.set_domain(point, domain)`, в serverus - `XuiPanel.issue_certificate(domain)`. В боте кнопки пока нет.
- [x] что нужно сделать чтобы сервис мог нормально работать и раздавать прокси интернет через cloudflare? когда домен за прокси клауда.
- [x] если используется и прокси сервер и домен за claudflare, то будет работать?
Решение: за Cloudflare не работаем. Reality через проксирование Cloudflare невозможен: Cloudflare сам завершает TLS.
- [x] нужна функция установки прокси в исходящие (outbound)
При установке ставится `point.outbound_proxy`, у готовой точки - `services.set_outbound_proxy(point, proxy)`. Поддерживаются socks5, http(s) и cf_warp.
+3 -4
View File
@@ -1,7 +1,7 @@
import ipaddress
import re
from urllib.parse import urlsplit
import serverus
from Telegram.interfaces import Interface, command, register, state, text
from .. import services, tasks
@@ -22,7 +22,6 @@ POINT_REINSTALL = 'point.reinstall'
DRAFT_CREDENTIALS = {DRAFT_SERVER: 'server', DRAFT_MASK: 'mask'}
DOMAIN_RE = re.compile(r'^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$')
PROXY_SCHEMES = ('http', 'https', 'socks5')
HTML = 'HTML'
@@ -42,10 +41,10 @@ def is_domain(value: str) -> bool:
def is_proxy_url(value: str) -> bool:
try:
url = urlsplit(value)
return url.scheme in PROXY_SCHEMES and bool(url.hostname) and bool(url.port)
serverus.parse_proxy(value)
except ValueError:
return False
return True
@register
+2 -2
View File
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser
from django.db import models, transaction
from serverus import Host, generate_password, generate_port, generate_username
from serverus import CF_WARP, Host, generate_password, generate_port, generate_username
class UserManager(BaseUserManager):
@@ -127,7 +127,7 @@ class Point(models.Model):
Создание: Point.objects.create_point. Поиск: методы PointQuerySet. Удаление: point.delete() удаляет и серверы.
"""
CF_WARP = 'cf_warp'
CF_WARP = CF_WARP
class Status(models.TextChoices):
INSTALLING = 'installing', 'устанавливается'
+25
View File
@@ -55,6 +55,29 @@ def release_point(point: Point) -> tuple[dict[str, str], dict[str, str]]:
return passwords, errors
def set_domain(point: Point, domain: str):
"""Ставит или меняет домен готовой точки: выпускает сертификат панели на домен, без домена - на ip.
Домен должен уже указывать на панель или на сервер маскировки.
"""
if not domain and (point.mask or point.cloudflare):
raise ValueError('маскировка и cloudflare работают только с доменом')
with _installing(point):
_check(point.panel, serverus.XuiPanel(point.panel.host()).issue_certificate(domain))
point.domain = domain
point.save(update_fields=['domain'])
def set_outbound_proxy(point: Point, proxy: str):
"""Меняет исходящий прокси готовой точки: protocol://user:password@host:port, Point.CF_WARP или '' - напрямую."""
if proxy and proxy != Point.CF_WARP:
serverus.parse_proxy(proxy)
with _installing(point):
_check(point.panel, serverus.XuiPanel(point.panel.host()).set_outbound(proxy))
point.outbound_proxy = proxy
point.save(update_fields=['outbound_proxy'])
def provision(server: Server):
"""Базовая настройка сервера и специализация по его роли."""
_bootstrap(server)
@@ -97,6 +120,8 @@ def _install_panel(server: Server):
result = _check(server, panel.create_inbound())
point.panel_inbound_id = result.data.get('inbound_id')
point.save(update_fields=['panel_inbound_id'])
if point.outbound_proxy:
_check(server, panel.set_outbound(point.outbound_proxy))
def _redirect_to_panel(server: Server):
+52
View File
@@ -160,6 +160,58 @@ class InstallTests(TestCase):
self.assertEqual(Server.objects.get(pk=panel.pk).private_key, private_key)
def test_install_sets_outbound_proxy(self):
point = create_point(mask=None, outbound_proxy=Point.CF_WARP)
runner = FakeRunner()
with patch_runner(runner):
services.install_point(point)
self.assertEqual(runner.playbooks('10.0.0.1'), ['bootstrap', 'xui', 'xui_inbound', 'xui_outbound', 'facts'])
self.assertTrue(runner.call('xui_outbound', '10.0.0.1')[1]['warp_keys'])
class ReadyPointTests(TestCase):
"""Настройки готовой точки: домен и исходящий прокси."""
def setUp(self):
self.point = create_point(mask=None)
with patch_runner(FakeRunner()):
services.install_point(self.point)
def test_set_domain_issues_certificate(self):
runner = FakeRunner()
with patch_runner(runner):
services.set_domain(self.point, 'example.com')
self.assertEqual(runner.call('xui_cert', '10.0.0.1')[1], {'domain': 'example.com'})
self.point.refresh_from_db()
self.assertEqual((self.point.domain, self.point.status), ('example.com', Point.Status.READY))
def test_failed_certificate_keeps_old_domain(self):
with patch_runner(FakeRunner(fail='xui_cert')), self.assertRaises(services.ProvisionError):
services.set_domain(self.point, 'example.com')
self.point.refresh_from_db()
self.assertEqual((self.point.domain, self.point.status), ('', Point.Status.ERROR))
def test_mask_requires_domain(self):
Server.objects.create_server(self.point, Server.Role.MASK, Credentials('10.0.0.3', 'pw'))
with self.assertRaises(ValueError):
services.set_domain(self.point, '')
def test_set_outbound_proxy(self):
runner = FakeRunner()
with patch_runner(runner):
services.set_outbound_proxy(self.point, 'socks5://user:pw@proxy.example:1080')
outbound = runner.call('xui_outbound', '10.0.0.1')[1]['proxy_outbound']
self.assertEqual(outbound['settings']['servers'][0]['address'], 'proxy.example')
self.point.refresh_from_db()
self.assertEqual(self.point.outbound_proxy, 'socks5://user:pw@proxy.example:1080')
def test_invalid_proxy_is_rejected_before_run(self):
runner = FakeRunner()
with patch_runner(runner), self.assertRaises(ValueError):
services.set_outbound_proxy(self.point, 'ftp://proxy.example:21')
self.assertEqual(runner.calls, [])
class ReinstallTests(TestCase):
def setUp(self):
self.point = create_point()