Skip to content

fix(provisioning): remove redundant ufw reload and avoid ephemeral ports - #19

Open
Inhibit0r wants to merge 1 commit into
howdeploy:mainfrom
Inhibit0r:fix/provisioning-ufw-reload-and-ephemeral-ports
Open

fix(provisioning): remove redundant ufw reload and avoid ephemeral ports#19
Inhibit0r wants to merge 1 commit into
howdeploy:mainfrom
Inhibit0r:fix/provisioning-ufw-reload-and-ephemeral-ports

Conversation

@Inhibit0r

@Inhibit0r Inhibit0r commented Sep 3, 2026

Copy link
Copy Markdown

Проблема

Создание HAPP-подписки (create_profile_all_routes, 7 маршрутов) занимает 22.9 с. Вызывающий с таймаутом 20 с (у меня — бот-стек с VPNBOT_TIMEOUT_SECONDS=20) получает timeout и откатывает профиль.

Замер по фазам, Debian 13 / Xray 26.3.27, конфиг из 214 inbound:

фаза время
open_firewall_port × 7 20 182 мс (88 %)
add_inbound без firewall ~60 мс × 7
safe_restart_xray 1 740 мс
итого 22 912 мс

Параллельно наблюдались перемежающиеся отказы старта:

Failed to start: failed to listen TCP on 56216 > bind: address already in use

Причина 1 — избыточный ufw reload

open_firewall_port зовёт ufw reload после ufw allow. Он не нужен — ufw allow уже применяет правило к живому iptables:

$ ufw allow 9999/tcp
Rule added
$ iptables -S ufw-user-input | grep 9999        # никакого reload между командами
-A ufw-user-input -p tcp -m tcp --dport 9999 -j ACCEPT

Один ufw reload на 453 правилах стоит 2 353 мс и вдобавок сбрасывает conntrack — то есть создание одной подписки семь раз рвёт живые пользовательские сессии. То же самое в close_firewall_port на пути удаления; ufw delete allow тоже применяется сразу (проверено тем же способом: правило исчезает из ufw-user-input без reload).

Причина 2 — порты внутри эфемерного диапазона ядра

find_available_random_port выбирает из 30000-60000, что почти целиком лежит внутри ip_local_port_range (по умолчанию 32768-60999). Проверка ss -tuln показывает только слушающие сокеты, поэтому порт, занятый исходящим соединением в ESTABLISHED/TIME_WAIT, проверку проходит — а bind() падает уже после рестарта, и safe_restart_xray трактует это как отказ конфига и откатывает профиль.

Окно гонки — сам рестарт: xray освобождает свои inbound-порты на ~300 мс, и TIME_WAIT только что закрытых им же исходящих соединений могут их занять. Наблюдалось 3 отказа старта из 6.

Правка опускает только потолок, под ip_local_port_range. Нижняя граница 30000 остаётся нетронутой — и это принципиально: замер с мобильного оператора МегаФон (RU) показал, что исходящие TCP на низкие порты у него не проходят вообще.

порты назначения успешных TCP-соединений с MegaFon RU
10256, 13473 0 / 8
20074, 20358, 23295 0 / 12
30330 … 31213 (10 портов) 40 / 40
30000-39999 (выборка) 16 / 16
40000-49999 (выборка) 16 / 16
50000-60999 (выборка) 16 / 16

Со стороны сервера все эти порты отвечают одинаково (ufw ALLOW v4+v6, xray listen, self-connect на публичный IP — OK), то есть режется именно путь. Инбаунд ниже ~30000 у таких абонентов был бы просто недостижим, поэтому нижнюю границу трогать нельзя.

Если ядро отдаёт эфемерный диапазон выше 60000 или ниже 31000, окно не сужается вовсе — правка в этом случае no-op.

Результат

метрика до после
create_profile_all_routes 22 912 мс 6 717 / 6 404 мс
удаление 7-маршрутного профиля ~21 с 6.3 с
open_firewall_port 2 932 мс 476 мс
рестартов xray без коллизии 3 из 6 6 из 6

Проверено

  • bash -n на xrayebator, install.sh, update.sh, uninstall.sh — чисто.
  • validation/15/16 на Debian 13 / bash 5.2. Единственный сбой, test-legacy-udp443-migration.sh, требует rg (нет в тестовом окружении) и с правкой не связан.
  • На macOS (bash 3.2) падают 3 теста на declare -A / mapfileконтрольный прогон на чистом main даёт ровно те же 3, регрессии нет.
  • Реальный жизненный цикл (создание подписки → отдача /sub → пинг всех 7 маршрутов → удаление) — 5 прогонов из 5 успешно.

Совместимость

Существующие профили и их порты не затрагиваются: новый диапазон действует только на вновь выбираемые порты, ссылки у клиентов остаются валидными. Для инстансов, где inbound'ы уже стоят в эфемерном диапазоне, остаточная гонка снимается на стороне хоста через net.ipv4.ip_local_reserved_ports — правки кода это не требует.

@Inhibit0r
Inhibit0r force-pushed the fix/provisioning-ufw-reload-and-ephemeral-ports branch from 8a097bd to 20a57c9 Compare September 3, 2026 10:38
…ephemeral range

Создание HAPP-подписки (`create_profile_all_routes`, 7 маршрутов) занимало 22.9с
и не укладывалось в 20-секундный таймаут вызывающего бот-стека.

1. open_firewall_port/close_firewall_port звали `ufw reload` после
   `ufw allow`/`ufw delete allow`. Он избыточен: правило уже применено к живому
   iptables (проверено через `iptables -S ufw-user-input` до и после). Каждый
   вызов стоил 2353мс и сбрасывал conntrack, разрывая активные сессии. На 7
   маршрутах это 16.5с из 22.9с. После правки — 6.7с.

2. find_available_random_port выбирал из 30000-60000, что почти целиком лежит
   внутри эфемерного диапазона ядра (ip_local_port_range, по умолчанию
   32768-60999). Проверка `ss -tuln` видит только слушающие сокеты, поэтому
   порт, занятый исходящим соединением в ESTABLISHED/TIME_WAIT, проверку
   проходит — а bind() падает уже после рестарта, и safe_restart_xray трактует
   это как отказ конфига и откатывает профиль. Окно гонки — сам рестарт: xray
   освобождает свои inbound-порты на ~300мс, и TIME_WAIT только что закрытых им
   же исходящих соединений могут их занять. Наблюдалось 3 отказа старта из 6.

   Правка опускает ТОЛЬКО потолок, под ip_local_port_range. Нижняя граница
   30000 остаётся нетронутой: замер с мобильного оператора МегаФон (RU)
   показал, что исходящие TCP на порты <=23295 у него не проходят вообще
   (0/20), а >=30330 проходят полностью (16/16) — инбаунд на низком порту у
   таких абонентов был бы недостижим. Если ядро отдаёт эфемерный диапазон выше
   60000 или ниже 31000, окно не сужается вовсе.

Проверено на Debian 13 / Xray 26.3.27, конфиг из 214 inbound:
create-subscription 22912мс -> 6717 и 6404мс, удаление 7-маршрутного профиля
~21с -> 6.3с, 6/6 рестартов xray без коллизий (до правки 3 отказа из 6).
validation: 15/16 на Linux/bash5 (единственный сбой — отсутствие `rg` в
окружении, с правкой не связан); на macOS/bash3.2 три теста падают на
`declare -A`/`mapfile` одинаково до и после правки.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01QPztpCoQCACXjBdeTFJaPj
@Inhibit0r
Inhibit0r force-pushed the fix/provisioning-ufw-reload-and-ephemeral-ports branch from 20a57c9 to 3f7c768 Compare September 3, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant