Skip to content

Latest commit

 

History

History
152 lines (123 loc) · 9.35 KB

File metadata and controls

152 lines (123 loc) · 9.35 KB

Архитектура

Один сервер, порт 443, и за одним SNI-роутером — MTProto-прокси, VLESS+Reality и веб-панель. Снаружи всё выглядит как обычный сайт с настоящим сертификатом, а не как «прокси, который притворяется».

Тупик с лимитом MTProto

Клиенты Telegram, которые не могут достучаться до MTProto-прокси, долбят SYN без остановки — бесконечные переподключения. Известный фикс — SYN rate limit на nftables, его используют проекты вроде MEKO и Liafanx. Пока на сервере один MTProto-прокси, всё просто: лимит вешается прямо на порт 443.

Проблема начинается, когда на тот же 443 добавляют VPN за nginx с SNI-роутингом. Поставить лимит становится некуда.

Вариант Где лимит Почему ломается
А — на :443, перед nginx на голом TCP, до TLS SYN приходит раньше SNI: ядро ещё не знает, MTProto это или VPN, и режет всё подряд под профиль MTProto — VPN отваливается
Б — на локальном порту, за nginx после того как nginx принял соединение nginx терминирует клиентский TCP и открывает к бэкенду своё, новое соединение; исходный SYN туда не доезжает, источник для бэкенда — всегда 127.0.0.1; per-IP лимит теряет смысл, все клиенты выглядят как один localhost

Развязка. До nginx, в ядре, нужен не лимит, а классификация. redirect в nftables — это DNAT, а не проксирование: порт назначения переписывается, но пакет остаётся тем же самым, исходный SYN клиента никуда не девается. По отпечатку TCP SYN (JA4T) iOS уводится на отдельный порт, минуя гейт HAProxy. Остальные идут через nginx, и там HAProxy лимитирует их, видя настоящий IP клиента через PROXY protocol. Лимит живёт на ветке MTProto, а не приколочен к порту — механика классификации разобрана ниже.

Почему сплит по портам, а не одно правило

JA4T — отпечаток TCP SYN (window size, порядок опций, MSS). Виден в самом первом пакете, до TLS.

SNI — домен из TLS ClientHello. Появляется только после того, как TCP-соединение уже установлено.

flowchart LR
    A["1 · Приходит TCP SYN<br/><b>виден JA4T</b><br/>SNI ещё не существует"]
    B["2 · Соединение установлено<br/>nginx его принял"]
    C["3 · Приходит TLS ClientHello<br/><b>виден SNI</b><br/>исходный SYN уже недоступен"]
    A --> B --> C
Loading

Два сигнала существуют в разные моменты времени — проверить оба в одной точке нельзя. Отсюда решение: не совмещать, а развести по портам.

conntrack протаскивает остальные пакеты соединения тем же DNAT-маппингом автоматически: классификация происходит один раз, на SYN, дальше ядро само доносит пакеты до выбранного порта.

Классификация (DNAT) живёт в prerouting/nat. Лимит для iOS сейчас выключен — старое правило матчило только dport 8443 и SYN, не различая сервисы (SNI ещё не существует), и резало заодно Reality и панель. Цепочка input/filter с meter'ом создаётся, только если включена переменная IOS_LIMIT_ENABLED (по умолчанию false) — тогда нужны именно две цепочки, потому что reject недопустим в nat.

Общая схема

flowchart TD
    subgraph K["Ядро · видит только TCP, SNI ещё не существует"]
        E["Все клиенты → :443"] --> Q{"Отпечаток iOS?"}
        Q -->|"да"| R["DNAT на :8443<br/>лимита нет"]
        Q -->|"нет"| S["Остаётся :443"]
    end
    subgraph W["nginx · теперь виден SNI"]
        M{"Разбор SNI"}
    end
    R --> M
    S --> M
    M -->|"plug1 с :443"| HA["HAProxy · 54 за 60 сек"] --> TG["telemt"]
    M -->|"plug1 с :8443"| TG
    M -->|"plug2"| XR["Xray · Reality"]
    M -->|"dashboard"| PN["3x-ui"]
    M -->|"не опознан"| DR["Разрыв"]
    TG -.-> FB["Заглушка"]
    XR -.-> FB
    style DR fill:#2d1418,stroke:#f85149,color:#c9d1d9
    style FB fill:#1c2128,stroke:#6e7681,color:#8b949e
Loading

Клиент всегда подключается на :443. Порт :8443 — не отдельный вход, а результат DNAT внутри nat prerouting, срабатывающего ДО SNI-роутера; для клиента и conntrack соединение всё время выглядит как :443. Механика DNAT — в разделе выше.

JA4T-сплит режет ВСЕ сервисы на 443, не только MTProto. На момент SYN ядро не знает, куда идёт соединение — SNI ещё не существует. Поэтому iOS-клиент любого сервиса (VPN, панель, MTProto) уходит на :8443. Следствие: единственное, что реально отличается между :443 и :8443, — маршрутизация MTProto (мимо HAProxy). Для VPN и панели оба порта ведут на один и тот же бэкенд.

Кто Лимит
iOS, любой сервис нет
не-iOS, MTProto 54 соединения за 60 сек (HAProxy)
не-iOS, VPN и панель нет

Self-steal: две заглушки

Классическая маскировка проксирует непрошеных гостей на чужой сайт — тот вам не принадлежит и может в любой момент измениться или начать блокировать ваш IP.

Self-steal — маскировка под собственный домен. Заглушка реально работает: открывается в браузере, отдаёт валидный сертификат Let's Encrypt, индексируется.

В стеке две независимые заглушки, каждая на своём домене:

flowchart LR
    subgraph MT["MTProto"]
        T["telemt<br/>:11443"] -->|"mask_host<br/>mask_port"| F1["nginx TLS<br/>:10443"]
        F1 --> H1["контент<br/>:20443"]
    end
    subgraph RL["Reality"]
        X["Xray<br/>:51443"] -->|"target"| F2["nginx TLS<br/>:10444"]
        F2 --> H2["контент<br/>:20444"]
    end
    MT -.->|"домен plug1"| D1["plug1.example.com"]
    RL -.->|"домен plug2"| D2["plug2.example.com"]
Loading

Фоллбэк указывает на TLS-фронт (:10443/:10444), а не на HTTP-бэкенд с контентом. Указание на HTTP-порт ломает маскировку — TLS не терминируется, клиент получает мусор вместо хендшейка.

Порты

Порт Кто слушает Назначение
80 nginx редирект на HTTPS, заглушки ведут себя как обычные сайты
443 nginx stream единственный «настоящий» вход
8443 nginx stream технический: iOS попадает сюда через DNAT, снаружи клиенты сюда не подключаются; в UFW открыт вынужденно — DNAT переписывает dport раньше, чем пакет доходит до filter/input
1443 HAProxy rate-limit гейт для не-iOS MTProto
10443 / 10444 nginx TLS-фронты заглушек
20443 / 20444 docker HTTP-контент заглушек
11443 telemt MTProto
11444 telemt локальный API
12443 nginx TLS-фронт панели
30443 / 30444 3x-ui панель и подписки
51443 Xray VLESS+Reality, TCP

Наружу через UFW открыты только 22, 80, 443, 8443. Всё остальное — loopback или закрыто файрволом.

Что дальше

Установка сервера — 02-install.md.