todo/Zapret/mtproto/07-nginx-haproxy.md
loop-uh 0ad0bcc8dd
Some checks failed
Published content check / validate (push) Failing after 5s
seo+зеркало: [!mirror]-плашка со ссылкой на вики и description во frontmatter всех заметок
Каждая публикуемая заметка получила callout-шапку со ссылкой на свою страницу
wiki.zapret.moe (на самой вики она вырезается транформером RemoveMirrorCallout,
видна только на зеркале Obsidian Publish и в Forgejo) и SEO-поле description —
1–2 предложения для meta description обоих сайтов.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 22:05:57 +03:00

7.6 KiB
Raw Permalink Blame History

description
MTProxy FakeTLS и сайт на одном порту 443: SNI-маршрутизация через nginx stream или HAProxy без терминации TLS, Let's Encrypt и проверка openssl.

[!mirror] Резервное зеркало Актуальная версия этой страницы — на основной вики: wiki.zapret.moe/Zapret/mtproto/07-nginx-haproxy

SNI Routing — MTProxy + сайт на одном порту 443

Архитектура

FakeTLS — это НЕ настоящий TLS. Nginx/HAProxy не могут его терминировать. Единственный рабочий вариант — L4 (TCP) маршрутизация по SNI без терминации.

Subpath (/mtproto) — невозможен. MTProto работает на уровне TCP, не HTTP. Нет понятия "path".

                      Порт 443
                         |
                  [nginx stream / haproxy]
                  L4 TCP — ssl_preread / inspect
                         |
           ┌─────────────┴──────────────┐
           |                            |
  SNI = example.com              SNI пустой / неизвестный
                                 (iOS, Android, Desktop)
           |                            |
           ▼                            ▼
 [127.0.0.1:8443]              [127.0.0.1:2443]
   Nginx HTTPS                  MTProto Proxy
   (реальный сайт)             (FakeTLS)

Проблема с SNI в Telegram-клиентах

Критически важно: Не все Telegram-клиенты отправляют SNI в FakeTLS:

Клиент SNI? Статус (март 2026) Примечание
Android ДА Работает Всегда отправлял при корректных ee-секретах. Имеет fallback на старый алгоритм
iOS НЕСТАБИЛЬНО Регрессия 12.2+ Issue #1912 открыт. Часть юзеров 12.4.x видит SNI, часть нет
Desktop ДА Работает (с 6.3) Новый алгоритм ClientHello. Требует обновлённый прокси-сервер

Причина путаницы: Desktop 6.3 (ноябрь 2025) обновил алгоритм TLS handshake — старые прокси (mtg v1, старые MTProxy) не понимали новый формат. Это выглядело как "Desktop не шлёт SNI", но на самом деле проблема была в несовместимости протокола.

Вывод: MTProxy должен быть default backend (для пустого/неизвестного SNI), потому что iOS нестабильно отправляет SNI. Сайт маршрутизируется по конкретному SNI.

Nginx Stream (рекомендуемый)

# /etc/nginx/nginx.conf

user www-data;
worker_processes auto;

events {
    worker_connections 4096;
}

# ============ STREAM: L4 маршрутизатор ============
stream {
    log_format stream_log '$remote_addr [$time_local] '
                          'sni="$ssl_preread_server_name" '
                          'upstream=$upstream_addr';
    access_log /var/log/nginx/stream.log stream_log;

    map $ssl_preread_server_name $backend {
        example.com         web_backend;
        www.example.com     web_backend;
        # Добавьте поддомены при необходимости

        default             mtproto_backend;   # ← ВСЁ остальное → MTProxy
    }

    upstream mtproto_backend {
        server 127.0.0.1:2443;
    }

    upstream web_backend {
        server 127.0.0.1:8443;
    }

    server {
        listen 443;
        listen [::]:443;
        ssl_preread on;
        proxy_pass $backend;
        proxy_connect_timeout 10s;
        proxy_timeout 300s;
        proxy_buffer_size 16k;   # Для длинных ClientHello
    }
}

# ============ HTTP: реальный сайт ============
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Порт 80: ACME challenge + редирект
    server {
        listen 80;
        listen [::]:80;
        server_name example.com www.example.com;

        location ^~ /.well-known/acme-challenge/ {
            root /var/www/letsencrypt;
        }

        location / {
            return 301 https://$host$request_uri;
        }
    }

    # Порт 8443: HTTPS (получает трафик от stream)
    server {
        listen 127.0.0.1:8443 ssl;
        server_name example.com www.example.com;

        ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        ssl_protocols       TLSv1.2 TLSv1.3;

        root /var/www/example.com;
        index index.html;
    }
}

HAProxy (альтернатива)

# /etc/haproxy/haproxy.cfg

global
    log /dev/log local0
    maxconn 50000
    daemon

defaults
    log global
    timeout connect 10s
    timeout client 300s
    timeout server 300s

frontend ft_ssl
    bind *:443
    mode tcp
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    use_backend bk_web if { req.ssl_sni -i example.com }
    use_backend bk_web if { req.ssl_sni -i www.example.com }

    default_backend bk_mtproto

backend bk_mtproto
    mode tcp
    server mtproto 127.0.0.1:2443 check

backend bk_web
    mode tcp
    server web 127.0.0.1:8443 check

Let's Encrypt с stream блоком

Порт 443 занят stream — certbot --nginx не работает.

Решение 1: Webroot через порт 80 (рекомендуемое)

mkdir -p /var/www/letsencrypt/.well-known/acme-challenge

certbot certonly \
    --webroot \
    -w /var/www/letsencrypt \
    -d example.com \
    -d www.example.com \
    --agree-tos \
    --non-interactive

Решение 2: DNS challenge (Cloudflare)

apt install python3-certbot-dns-cloudflare

# /etc/letsencrypt/cloudflare.ini:
# dns_cloudflare_api_token = YOUR_TOKEN
chmod 600 /etc/letsencrypt/cloudflare.ini

certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
    -d "example.com" \
    -d "*.example.com"

Автообновление

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
nginx -t 2>/dev/null && systemctl reload nginx
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
certbot renew --dry-run   # проверка

Проверка маршрутизации

# Должен показать сертификат вашего сайта
openssl s_client -connect IP:443 -servername example.com </dev/null 2>/dev/null | head -5

# Без SNI — должен попасть в MTProxy
openssl s_client -connect IP:443 -noservername </dev/null 2>/dev/null | head -5

# Логи
tail -f /var/log/nginx/stream.log

Важно

  1. $ssl_preread_protocol бесполезен — FakeTLS тоже показывает TLSv1.3
  2. Маршрутизация только по SNI ($ssl_preread_server_name)
  3. Caddy не подходит — нет stream модуля
  4. Proxy Protocol нужен для передачи реального IP клиента в бэкенд