chore: добавлен скрипт для создания прозрачного прокси из сервера

This commit is contained in:
protokey committed 2026-09-26 13:20:25 +04:00
1 parent 064bb70f32
commit 9bb3a2fc5d
7 files changed
+100 -14

No files matched your search

+4 -1
View File
@@ -125,7 +125,10 @@ def _install_panel(server: Server):
def _redirect_to_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: def _check(server: Server, result: serverus.Result) -> serverus.Result:
+1
View File
@@ -125,6 +125,7 @@ class InstallTests(TestCase):
_, extravars = runner.call('proxy', '10.0.0.2') _, extravars = runner.call('proxy', '10.0.0.2')
self.assertEqual(extravars['target_ip'], '10.0.0.1') 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): def test_install_point_without_mask(self):
point = create_point(mask=None) point = create_point(mask=None)
+1 -1
View File
@@ -63,7 +63,7 @@ server.release(generate_password())
- `XuiPanel(host).issue_certificate(domain='')` - сертификат панели и подписки на домен или на ip: так домен ставится, меняется или убирается после установки. Домен должен уже указывать на сервер (или на его маскировку). - `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).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`. - `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 <target_ip>`, который перенаправляет весь трафик, кроме ssh, на target_ip. **Сейчас скрипт - заглушка.** - `RedirectProxy(host).redirect(target_ip, ports)` - разрешает транзитный трафик и запускает `playbooks/files/redirect.sh <target_ip> <ports>`, который через 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 символов. Порты и логины: `generate_port(exclude=...)` - порт сервиса из 10000-20000 (ssh - 20000-60000), `generate_username()` - 6 символов.
+59 -6
View File
@@ -1,13 +1,66 @@
#!/bin/sh #!/bin/sh
# Перенаправляет весь входящий трафик сервера на целевой ресурс. # Перенаправляет tcp-порты сервера на те же порты целевого ресурса (прозрачный прокси через nftables).
# Использование: serverus-redirect <target_ip> # Использование: serverus-redirect <target_ip> <port>[,<port>...]
# SSH_PORT - порт ssh этого сервера, его перенаправлять нельзя, иначе доступ к серверу будет потерян. # SSH_PORT - порт ssh этого сервера, его перенаправлять нельзя, иначе доступ к серверу будет потерян.
# #
# TODO: заглушка. Заменить на настоящий скрипт перенаправления через фаервол. # Правила лежат в отдельном файле и подключаются из /etc/nftables.conf через include:
# Правила должны переживать перезапуск nftables: базовый конфиг /etc/nftables.conf начинается с flush ruleset. # базовый конфиг начинается с flush ruleset, поэтому только так они переживают перезапуск nftables.
# Транзитный трафик (forward) и ip_forward должен разрешить вызывающий плейбук.
set -eu set -eu
TARGET_IP="${1:?usage: $0 <target_ip>}" TARGET_IP="${1:?usage: $0 <target_ip> <port>[,<port>...]}"
PORTS="${2:?usage: $0 <target_ip> <port>[,<port>...]}"
SSH_PORT="${SSH_PORT:?SSH_PORT is required}" 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" <<EOF
# Сгенерировано serverus-redirect, не править руками.
table ip serverus_redirect {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
# nat видит только новые соединения: ответы на исходящие соединения самого сервера (apt, dns) не трогаются
fib daddr type local tcp dport { ${PORTS} } dnat to ${TARGET_IP}
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# ответ от цели должен вернуться через этот сервер, иначе клиент получит пакет с чужого адреса
ip daddr ${TARGET_IP} ct status dnat masquerade
}
chain forward {
type filter hook forward priority mangle; policy accept;
# mtu по пути может быть меньше, чем думает клиент
tcp flags & (syn | rst) == syn tcp option maxseg size set rt mtu
}
}
EOF
grep -qxF "include \"${RULES}\"" "$CONF" || printf '\ninclude "%s"\n' "$RULES" >> "$CONF"
nft -c -f "$CONF"
systemctl restart nftables
echo "redirect: tcp/{${PORTS}} -> ${TARGET_IP}"
+27 -3
View File
@@ -1,6 +1,6 @@
# extravars: target_ip # extravars: target_ip, ports
# #
# Превращает сервер в прозрачный прокси: весь трафик, кроме ssh, уходит на target_ip. # Превращает сервер в прозрачный прокси: tcp-порты ports уходят на те же порты target_ip.
- name: Redirect traffic to target - name: Redirect traffic to target
hosts: target hosts: target
gather_facts: false gather_facts: false
@@ -10,12 +10,35 @@
firewall_tcp_ports: [ "{{ ansible_port }}" ] firewall_tcp_ports: [ "{{ ansible_port }}" ]
firewall_forward: true 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 - name: Enable ip forwarding
ansible.builtin.copy: ansible.builtin.copy:
dest: /etc/sysctl.d/90-serverus-forward.conf dest: /etc/sysctl.d/90-serverus-forward.conf
mode: "0644" mode: "0644"
content: | content: |
net.ipv4.ip_forward = 1 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 - name: Apply sysctl
ansible.builtin.command: sysctl --system ansible.builtin.command: sysctl --system
@@ -28,6 +51,7 @@
mode: "0755" mode: "0755"
- name: Redirect traffic - 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: environment:
SSH_PORT: "{{ ansible_port }}" SSH_PORT: "{{ ansible_port }}"
+2
View File
@@ -25,7 +25,9 @@
state: absent state: absent
loop: loop:
- /usr/local/sbin/serverus-redirect - /usr/local/sbin/serverus-redirect
- /etc/nftables.d/serverus-redirect.nft
- /etc/sysctl.d/90-serverus-forward.conf - /etc/sysctl.d/90-serverus-forward.conf
- /etc/modules-load.d/serverus-forward.conf
- name: Reload sysctl - name: Reload sysctl
ansible.builtin.command: sysctl --system ansible.builtin.command: sysctl --system
+6 -3
View File
@@ -1,9 +1,12 @@
from collections.abc import Iterable
from .runner import Result from .runner import Result
from .server import Server from .server import Server
class RedirectProxy(Server): class RedirectProxy(Server):
"""Сервер, который перенаправляет весь трафик на другой сервер, скрывая его адрес.""" """Сервер, который перенаправляет трафик на другой сервер, скрывая его адрес."""
def redirect(self, target_ip: str) -> Result: def redirect(self, target_ip: str, ports: Iterable[int]) -> Result:
return self.run('proxy', target_ip=target_ip) """Перенаправляет tcp-порты ports на те же порты target_ip. Остальные порты сервер обслуживает сам."""
return self.run('proxy', target_ip=target_ip, ports=sorted(set(ports)))