Если такой туториал уже был на Хабре — не вините строго, я не нашел, и мне, на удивление, понадобился большой бубен и пол дня чтобы настроить сабж.

Вводные:

1) у вас есть крохотная виртуалка на которой крутится nginx и пара сайтов. Конечно https и скорее всего let's encrypt — короче стандартный набор рядового девелопера.

2) вы бы хотели поднять телеграм-прокси, но так чтобы трафик на него был максимально похож на обычный https, а это значит 443 порт — но вы хотите чтобы nginx и ваши сайты продолжили работать как и раньше.

Если это про вас и для вас — вот объяснение механизма и небольшой туториал.

Если вы хотя бы раз настраивали nginx для виртуальных https хостов, то могли заметить неочевидную деталь: каким-то образом nginx знает о hostname запроса ещё до того как найти соответствующий https сертификат и начать "рукопожатие" или расшифровывать входящий трафик. А всё дело в том что request hostname передаётся в SSL-протоколе без шифрования. Это даёт возможность nginx найти соответствующий server {...} конфиг и обработать запрос. Весь остальной payload запроса зашифрован.

Однако протокол телеграм-прокси не https, просто написать server { server_name xxx; proxy_pass ... } не получается. Но вопрос можно решить через stream.

Как в итоге это работает:

  • меняем listen порт на всех https виртуальных хостах сконфигурированных в nginx

  • настраиваем nginx stream listener на осободившемся 443 порту

  • nginx получает входящий SSL запрос на 443 порт, достаёт оттуда hostname и если это наш особенный hostname который мы выбрали для телеграм-прокси — проксирует весь поток данных на mtproxy, а если это один из hostname которые должен обработать сам nginx — отправляем поток на localhost:4443 — там его снова поймает тот же nginx и обработает как раньше.

  • далее, mtproxy получает запрос (от nginx), проверяет его переданный секрет, и если это "правильный" запрос — проксирует его на сервера телеграма. А если это "неправильный" запрос (например https запрос на этот же hostname) — прозрачно проксирует его на настоящий сайт.

Итак, настраиваем по пунктам:

0) Начнём с выбора доменного имени который будет использоваться для прокси: я предлагаю вам взять главный домен вашего же хостинг провайдера. Тогда запрос на IP вашей виртуалки на домен этого же провайдера не будет выглядеть подозрительным. Вам не нужен доступ к настройкам DNS этого домена — вам просто нужно выбрать практически любой существующий. Пусть это будет myprovider.ru

1) Находим все виртуальные https хосты в настройках nginx. Что-то типа

server {
  server_name mywebsite.ru;
  listen 443 ssl http2;
  ...
}

Меняем listen на

server {
  server_name mywebsite.ru;
  listen 127.0.0.1:4443 ssl http2 proxy_protocol;
  ...
}

Обратите внимание на proxy_protocol — это фича nginx чтобы не потерять реальный IP и порт клиента при внутреннем проксировании с 443 на 4443.

2) Создаём conf файл для настройки nginx stream. Пусть это будет /etc/nginx/conf.d/stream.conf

stream {
    log_format stream '$remote_addr [$time_local] host=$ssl_preread_server_name '
                      'prot=$protocol status=$status out=$bytes_sent in=$bytes_received';

    # включите логи если вам интересно
    #access_log /var/log/nginx/stream_access.log stream;
    #error_log /var/log/nginx/stream_error.log;

    map $ssl_preread_server_name $backend_name {
        myprovider.ru      tg_proxy; # запросы на myprovider.ru будут отправляться на mtproxy
        default            nginx_https; # все остальные запросы будут отправляться на этот же nginx
    }

    upstream nginx_https {
        server 127.0.0.1:4443;
    }

    upstream tg_proxy {
        server 127.0.0.1:7788;
    }

    server {
        listen 443 reuseport;
        proxy_pass $backend_name;
        ssl_preread on; # включаем pre-read ssl протокола чтобы получить hostname из запроса
        proxy_protocol on; # включаем proxy_protocol чтобы передать реальный IP адрес клиента по цепочке
    }
}

Если в вашем nginx нет модуля stream — просто поставьте полную версию nginx. На Debian/Ubuntu и производных от них это будет выглядеть так:

$ sudo apt install nginx-full

Перезапускаем nginx, смотрим что ваши вебсайты работают в браузере как надо.

# именно restart на случай если вы обновились на nginx-full
$ nginx -t && sudo service nginx restart

3) Устанавливаем mtproxy. Тут надо сделать оговорочку: я не смог настроить такую схему с официальным mtproxy от Дуровых, но смог настроить реализацию этого протокола на Python.

Установка:

# bash
(cd /opt || mkdir /opt) && cd /opt
git clone https://github.com/alexbers/mtprotoproxy.git
# важно: в README этого проекта написано клонировать на ветку stable, но на момент когда я пишу эту статью, настройка работает только на ветке master
cd mtprotoproxy

# сгенерируйте и скопируйте значение секрета
head -c 16 /dev/urandom | xxd -ps 

nano config.py

Настройка:

PORT = 7788 # порт можете поменять, но тогда и nginx stream config обновите
LISTEN_ADDR_IPV4 = "127.0.0.1" # рекомендую бинд на localhost чтобы ваш прокси не было напрямую видно снаружи
LISTEN_ADDR_IPV6 = None # я думаю тем кому нужен IPv6 не надо объяснять что делать =)

# name -> secret (32 hex chars)
USERS = {
    "tg":  "значение секрета",
    # удобная фича - вы можете создать несколько секретов, например один для семьи/друзей, и один гостевой — и в любой момент вы сможете его отключить без эффекта дл семьи/друзей  
    #"tg2": "0123456789abcdef0123456789abcdef", 
}

MODES = {
    # Classic mode, easy to detect
    "classic": False,

    # Makes the proxy harder to detect
    # Can be incompatible with very old clients
    "secure": False,

    # Makes the proxy even more hard to detect
    # Can be incompatible with old clients
    "tls": True
}

# The domain for TLS mode, bad clients are proxied there
# Use random existing domain, proxy checks it on start
TLS_DOMAIN = "myprovider.ru" # доменное имя которое вы выбрали в путкте (0)
PROXY_PROTOCOL = True # это важная настройка — для поддержки nginx proxy_protocol on;

# Tag for advertising, obtainable from @MTProxybot
#AD_TAG = "получите это значение от @MTProxybot"

4) Проверка

Запускаем прокси (пока в ручном режиме)

$ python3 mtprotoproxy.py 

Он должен написать вам в консоль что-то типа

tg: tg://proxy?server=IP_ВАШЕЙ_ВИРТУАЛКИ&port=7788&secret=ДЛИННЫЙ_СЕКРЕТ

Обратите внимание: параметр secret будет другим, не такой как вы указали в файле config.py у него будет префикс "ee" и длинный хвост в котором будет hex значение доменного имени из пункта (0). Телеграм клиент будет подключаться на ваш IP, но в SSL заголовке будет передавать myprovider.ru — вот собстсвенно где вся магия. Nginx поймает этот заголовок и запроксирует соединение на mtproxy.

Теперь скопируйте эту ссылку из консоли, поменяйте порт на 443 и отправьте себе в "saved messages" в Телегу. Ссылка станет кликабельной, тыкайте на неё и нажимайте "проверить статус". Если всё хорошо, вы должны получить что-то типа ping 123

Теперь нужно обязательно проверить что myprovider.ru так же нормально открывается в браузере. Для этого добавьте в свой /etc/hosts файл (или аналог на винде, не знаю что там):

IP_ВАШЕЙ_ВИРТУАЛКИ   myprovider.ru

сохраните файл, и откройте в браузере https://myprovider.ru — и если всё хорошо, вы должны просто увидеть сайт своего провайдера, который был запроксирован через ваш nginx и ваш mtproxy. Не забудьте убрать эту строку из hosts файла =)

5) Настройка mtproxy как Linux сервиса (на примере Ubuntu)

Остановите в ручную запущенный mtproxy. Создайте файл /etc/systemd/system/mtprotoproxy.service

[Unit]
Description=MTProto Proxy
After=network.target

[Service]
Type=simple
User=nobody
Group=nogroup
WorkingDirectory=/opt/mtprotoproxy
ExecStart=/usr/bin/python3 /opt/mtprotoproxy/mtprotoproxy.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Далее

$ sudo systemctl daemon-reload  
$ sudo systemctl enable mtprotoproxy.service  
$ sudo systemctl start mtprotoproxy.service  
$ systemctl status mtprotoproxy.service

Если нужно, посмотреть логи

$ journalctl -u mtprotoproxy.service -e

Ну и напоследок: сколько это проработает одному богу известно.