From 9bb3a2fc5d8a7e9c70d669a9939fad20deac9e1a Mon Sep 17 00:00:00 2001 From: protokey Date: Sat, 26 Sep 2026 13:20:25 +0400 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B7=D1=80=D0=B0=D1=87=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App/services.py | 5 +- src/App/tests_services.py | 1 + src/serverus/README.md | 2 +- src/serverus/playbooks/files/redirect.sh | 65 +++++++++++++++++++++--- src/serverus/playbooks/proxy.yml | 30 +++++++++-- src/serverus/playbooks/release.yml | 2 + src/serverus/proxy.py | 9 ++-- 7 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/App/services.py b/src/App/services.py index 56558ce..95a9a58 100644 --- a/src/App/services.py +++ b/src/App/services.py @@ -125,7 +125,10 @@ def _install_panel(server: Server): def _redirect_to_panel(server: Server): - _check(server, serverus.RedirectProxy(server.host()).redirect(server.point.panel.address)) + point = server.point + # 80 - выпуск и продление сертификата (HTTP-01), 443 - подключения клиентов + ports = (80, 443, point.panel_port, point.sub_port) + _check(server, serverus.RedirectProxy(server.host()).redirect(point.panel.address, ports)) def _check(server: Server, result: serverus.Result) -> serverus.Result: diff --git a/src/App/tests_services.py b/src/App/tests_services.py index 07067a5..66fccb8 100644 --- a/src/App/tests_services.py +++ b/src/App/tests_services.py @@ -125,6 +125,7 @@ class InstallTests(TestCase): _, extravars = runner.call('proxy', '10.0.0.2') self.assertEqual(extravars['target_ip'], '10.0.0.1') + self.assertEqual(extravars['ports'], sorted([80, 443, point.panel_port, point.sub_port])) def test_install_point_without_mask(self): point = create_point(mask=None) diff --git a/src/serverus/README.md b/src/serverus/README.md index 328fa2a..432dc6c 100644 --- a/src/serverus/README.md +++ b/src/serverus/README.md @@ -63,7 +63,7 @@ server.release(generate_password()) - `XuiPanel(host).issue_certificate(domain='')` - сертификат панели и подписки на домен или на ip: так домен ставится, меняется или убирается после установки. Домен должен уже указывать на сервер (или на его маскировку). - `XuiPanel(host).set_outbound(proxy='')` - весь исходящий трафик клиентов через прокси: `socks5://user:password@host:port`, `http(s)://...` или `CF_WARP` (устройство WARP регистрирует панель, выход по wireguard). Пустая строка - напрямую. Прокси ставится первым outbound Xray, перед применением панель его проверяет. `parse_proxy(url)` проверяет адрес заранее. - `XuiPanel(host).create_inbound(remark='main', target='')` - подключение vless + xhttp + reality на 443, uTLS firefox, сброс трафика ежемесячно. Цель reality подбирает сканер панели (TLS 1.3, h2, X25519, валидный сертификат, минимальная задержка) из `target` или из своих кандидатов. Ключи x25519 генерирует панель, shortId и путь xhttp - serverus. Существующее подключение с тем же `remark` не трогает. Возвращает `data`: `inbound_id`, `target`, `server_name`, `public_key`, `short_ids`, `path`. -- `RedirectProxy(host).redirect(target_ip)` - разрешает транзитный трафик и запускает `playbooks/files/redirect.sh `, который перенаправляет весь трафик, кроме ssh, на target_ip. **Сейчас скрипт - заглушка.** +- `RedirectProxy(host).redirect(target_ip, ports)` - разрешает транзитный трафик и запускает `playbooks/files/redirect.sh `, который через nftables перенаправляет tcp-порты `ports` на те же порты target_ip с masquerade и MSS clamping. Точка передает 80, 443, порт панели и порт подписки; ssh остается на самом сервере, скрипт откажется перенаправлять его порт. Правила - `/etc/nftables.d/serverus-redirect.nft`, подключаются include из `/etc/nftables.conf`. Заодно включает bbr, fq и увеличенные буферы и таблицу conntrack. Порты и логины: `generate_port(exclude=...)` - порт сервиса из 10000-20000 (ssh - 20000-60000), `generate_username()` - 6 символов. diff --git a/src/serverus/playbooks/files/redirect.sh b/src/serverus/playbooks/files/redirect.sh index efae030..eaf3ba4 100644 --- a/src/serverus/playbooks/files/redirect.sh +++ b/src/serverus/playbooks/files/redirect.sh @@ -1,13 +1,66 @@ #!/bin/sh -# Перенаправляет весь входящий трафик сервера на целевой ресурс. -# Использование: serverus-redirect +# Перенаправляет tcp-порты сервера на те же порты целевого ресурса (прозрачный прокси через nftables). +# Использование: serverus-redirect [,...] # SSH_PORT - порт ssh этого сервера, его перенаправлять нельзя, иначе доступ к серверу будет потерян. # -# TODO: заглушка. Заменить на настоящий скрипт перенаправления через фаервол. -# Правила должны переживать перезапуск nftables: базовый конфиг /etc/nftables.conf начинается с flush ruleset. +# Правила лежат в отдельном файле и подключаются из /etc/nftables.conf через include: +# базовый конфиг начинается с flush ruleset, поэтому только так они переживают перезапуск nftables. +# Транзитный трафик (forward) и ip_forward должен разрешить вызывающий плейбук. set -eu -TARGET_IP="${1:?usage: $0 }" +TARGET_IP="${1:?usage: $0 [,...]}" +PORTS="${2:?usage: $0 [,...]}" SSH_PORT="${SSH_PORT:?SSH_PORT is required}" -echo "redirect stub: all traffic except tcp/${SSH_PORT} -> ${TARGET_IP}" +RULES=/etc/nftables.d/serverus-redirect.nft +CONF=/etc/nftables.conf + +# ip route get заодно проверяет, что это ipv4-адрес и что до него есть маршрут +ROUTE=$(ip -4 route get "$TARGET_IP" 2>/dev/null) || { echo "no route to ${TARGET_IP}" >&2; exit 1; } +case "$ROUTE" in + local\ *) echo "${TARGET_IP} is this server, refusing to redirect to itself" >&2; exit 1 ;; +esac + +for port in $(echo "$PORTS" | tr ',' ' '); do + case "$port" in + ''|*[!0-9]*) echo "bad port: ${port}" >&2; exit 1 ;; + esac + if [ "$port" -eq "$SSH_PORT" ]; then + echo "refusing to redirect ssh port ${SSH_PORT}" >&2 + exit 1 + fi +done + +mkdir -p "$(dirname "$RULES")" +cat > "$RULES" <> "$CONF" + +nft -c -f "$CONF" +systemctl restart nftables + +echo "redirect: tcp/{${PORTS}} -> ${TARGET_IP}" diff --git a/src/serverus/playbooks/proxy.yml b/src/serverus/playbooks/proxy.yml index 829ae40..73273d1 100644 --- a/src/serverus/playbooks/proxy.yml +++ b/src/serverus/playbooks/proxy.yml @@ -1,6 +1,6 @@ -# extravars: target_ip +# extravars: target_ip, ports # -# Превращает сервер в прозрачный прокси: весь трафик, кроме ssh, уходит на target_ip. +# Превращает сервер в прозрачный прокси: tcp-порты ports уходят на те же порты target_ip. - name: Redirect traffic to target hosts: target gather_facts: false @@ -10,12 +10,35 @@ firewall_tcp_ports: [ "{{ ansible_port }}" ] firewall_forward: true + # sysctl при загрузке применяется раньше nftables: без модулей ключи conntrack и bbr не выставятся + - name: Load forwarding kernel modules on boot + ansible.builtin.copy: + dest: /etc/modules-load.d/serverus-forward.conf + mode: "0644" + content: | + nf_conntrack + tcp_bbr + + - name: Load forwarding kernel modules + ansible.builtin.command: modprobe -a nf_conntrack tcp_bbr + changed_when: false + - name: Enable ip forwarding ansible.builtin.copy: dest: /etc/sysctl.d/90-serverus-forward.conf mode: "0644" content: | net.ipv4.ip_forward = 1 + net.core.default_qdisc = fq + net.ipv4.tcp_congestion_control = bbr + net.netfilter.nf_conntrack_max = 2000000 + net.core.netdev_max_backlog = 250000 + net.core.rmem_max = 16777216 + net.core.wmem_max = 16777216 + net.ipv4.tcp_rmem = 4096 87380 16777216 + net.ipv4.tcp_wmem = 4096 65536 16777216 + net.ipv4.tcp_mtu_probing = 1 + net.ipv4.tcp_slow_start_after_idle = 0 - name: Apply sysctl ansible.builtin.command: sysctl --system @@ -28,6 +51,7 @@ mode: "0755" - name: Redirect traffic - ansible.builtin.command: "/usr/local/sbin/serverus-redirect {{ target_ip }}" + ansible.builtin.command: + argv: [ /usr/local/sbin/serverus-redirect, "{{ target_ip }}", "{{ ports | join(',') }}" ] environment: SSH_PORT: "{{ ansible_port }}" diff --git a/src/serverus/playbooks/release.yml b/src/serverus/playbooks/release.yml index ad3d84d..ee5e062 100644 --- a/src/serverus/playbooks/release.yml +++ b/src/serverus/playbooks/release.yml @@ -25,7 +25,9 @@ state: absent loop: - /usr/local/sbin/serverus-redirect + - /etc/nftables.d/serverus-redirect.nft - /etc/sysctl.d/90-serverus-forward.conf + - /etc/modules-load.d/serverus-forward.conf - name: Reload sysctl ansible.builtin.command: sysctl --system diff --git a/src/serverus/proxy.py b/src/serverus/proxy.py index 936a060..6719aa9 100644 --- a/src/serverus/proxy.py +++ b/src/serverus/proxy.py @@ -1,9 +1,12 @@ +from collections.abc import Iterable + from .runner import Result from .server import Server class RedirectProxy(Server): - """Сервер, который перенаправляет весь трафик на другой сервер, скрывая его адрес.""" + """Сервер, который перенаправляет трафик на другой сервер, скрывая его адрес.""" - def redirect(self, target_ip: str) -> Result: - return self.run('proxy', target_ip=target_ip) + def redirect(self, target_ip: str, ports: Iterable[int]) -> Result: + """Перенаправляет tcp-порты ports на те же порты target_ip. Остальные порты сервер обслуживает сам.""" + return self.run('proxy', target_ip=target_ip, ports=sorted(set(ports)))