chore: настройка панелей
This commit is contained in:
1 parent
8532557ef6
commit
96213e182e
16 files changed
+529
-64
No files matched your search
+12
-8
@@ -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.
|
||||
@@ -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
@@ -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', 'устанавливается'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user