На главную

telemt self-mask
свой домен + nginx

Один инстанс telemt на 443, маскировка под собственный домен с настоящим Let's Encrypt сертификатом за nginx-фронтом. Полная схема: серт, self-mask, автопродление через webroot, keepalive под мобильных клиентов, BBR+fq, recent-защита от зондирования с проверкой модуля ядра и ребут-тестом.

OS Ubuntu 22.04 / 24.04 telemt 3.4.18 домен + Let's Encrypt root required ~30 мин
1

Схема и зачем self-mask

Здесь другой подход, чем эмуляция чужого домена (apple/cloudflare). Мы берём собственный домен, получаем на него настоящий сертификат Let's Encrypt и поднимаем за telemt реальный nginx-сайт. На «левый» SNI и на прямой заход по IP telemt отдаёт этот наш сайт. Снаружи сервер выглядит как обычный маленький сайт на своём домене — потому что это и есть обычный сайт.

СлойЧто делает
telemt :443принимает MTProto, на чужой SNI маскирует в nginx
nginx :8444реальный сайт-заглушка со своим сертом (локальный)
nginx :80редирект на https + отдаёт ACME для продления серта
self-maskunknown_sni → свой сайт, валидный серт своего домена
Маршрут трафика. Клиент Telegram → :443 telemt (MTProto). Браузер / DPI-зонд по https → :443 telemt → unknown SNI → mask → локальный nginx :8444 → реальный сайт, отдаётся валидный серт своего домена. По http (:80) → редирект на https, плюс отдельная дырка под /.well-known/acme-challenge/ для автопродления.
Плюс схемы: сертификат настоящий и проходит любую проверку цепочки доверия, потому что домен реально твой. Не нужно «подтягивать» чужой серт — DPI видит честный валидный TLS к существующему сайту.
2

Подготовка системы

Обновляем пакеты, ставим зависимости. Заранее нужен домен, A-запись которого указывает на IP этого сервера — без этого Let's Encrypt не выдаст серт.

bash
apt update
apt install -y wget tar jq ufw python3 iptables nginx certbot curl dnsutils

# таймзона в UTC (логи будут единообразны)
timedatectl set-timezone UTC

# ПРОВЕРКА: домен указывает на этот сервер?
MYIP=$(curl -s ifconfig.me)
DNSIP=$(dig +short ВАШ_ДОМЕН | tail -1)
echo "сервер: $MYIP | домен резолвит в: $DNSIP"
# эти два IP должны совпадать, иначе серт не получить
Замени ВАШ_ДОМЕН на свой по всему гайду. До получения серта $MYIP и $DNSIP обязаны совпадать — Let's Encrypt проверяет владение доменом через реальный заход на :80.
3

keepalive + BBR/fq

Два сетевых тюнинга, которые сильно влияют на качество прокси:

пишет /etc/sysctl.d/99-tg-keepalive.conf и 99-bbr.conf
bash
cat > /etc/sysctl.d/99-tg-keepalive.conf << 'EOF'
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 3
EOF

cat > /etc/sysctl.d/99-bbr.conf << 'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF

sysctl --system >/dev/null
IFACE=$(ip route | awk '/default/{print $5; exit}')
tc qdisc replace dev $IFACE root fq

# проверка
echo "keepalive: $(sysctl -n net.ipv4.tcp_keepalive_time)/$(sysctl -n net.ipv4.tcp_keepalive_intvl)/$(sysctl -n net.ipv4.tcp_keepalive_probes)"
echo "bbr: $(sysctl -n net.ipv4.tcp_congestion_control) | qdisc: $(tc qdisc show dev $IFACE | head -1 | awk '{print $2}')"
Должно показать keepalive: 60/15/3 и bbr | fq. На ядре 6.8+ default_qdisc=fq подхватывается сам после ребута; на старых ядрах fq может слетать — это одна из причин брать свежий образ ОС.
Не добавляй секцию [timeouts] в конфиг telemt при таком keepalive — она конфликтует с системным и даёт обратный эффект. keepalive держим только на уровне sysctl.
4

Бинарник + пользователь

Отдельный системный пользователь без shell, рабочие папки, бинарник telemt из последнего релиза.

bash
id telemt &>/dev/null || useradd -r -s /usr/sbin/nologin -d /opt/telemt telemt
mkdir -p /opt/telemt /etc/telemt
chown -R telemt:telemt /opt/telemt

cd /tmp
wget -qO- "https://github.com/telemt/telemt/releases/latest/download/telemt-x86_64-linux-gnu.tar.gz" | tar -xz
mv /tmp/telemt /bin/telemt
chmod +x /bin/telemt
/bin/telemt --version
Должно вывести telemt 3.4.18 (или новее). chown на /opt/telemt обязателен — иначе демон споткнётся на правах при старте (Permission denied в логах).
5

Сертификат Let's Encrypt

Получаем серт на свой домен. Делаем это через webroot, а не standalone — потому что :80 займёт nginx, и standalone-режим в будущем сломает автопродление (не сможет забиндить порт). Сразу настраиваем правильно.

Сначала временно поднимем nginx на :80, чтобы certbot прошёл валидацию:

bash — получить серт
mkdir -p /var/www/html/.well-known/acme-challenge

# минимальный временный конфиг nginx на :80 для валидации
cat > /etc/nginx/sites-available/acme-temp << 'EOF'
server {
    listen 80;
    server_name ВАШ_ДОМЕН;
    root /var/www/html;
    location /.well-known/acme-challenge/ { allow all; }
}
EOF
ln -sf /etc/nginx/sites-available/acme-temp /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl restart nginx

# выдача серта через webroot
certbot certonly --webroot -w /var/www/html \
  -d ВАШ_ДОМЕН --non-interactive --agree-tos \
  -m admin@ВАШ_ДОМЕН --cert-name ВАШ_ДОМЕН

# проверка — серт на месте?
openssl x509 -enddate -noout -in /etc/letsencrypt/live/ВАШ_ДОМЕН/cert.pem
Почему именно webroot. Если получить серт в режиме standalone (когда certbot сам поднимает свой сервер на :80), то потом nginx займёт :80, и certbot renew будет падать с «Could not bind TCP port 80». Серт молча протухнет через 90 дней. webroot + nginx-дырка под ACME (шаг 6) — единственный надёжный вариант, когда на :80 уже что-то слушает.
Вывод notAfter=... примерно через 90 дней — серт получен. Теперь временный конфиг заменим на боевой.
6

nginx-фронт: 3 блока

Боевой конфиг nginx из трёх server-блоков. Каждый со своей задачей:

БлокСлушаетДелает
default:80 + :8444чужой Host / прямой IP → return 444 (обрыв)
:80 домен:80ACME-дырка + редирект на https
:8444 ssl:8444 (локально)сам сайт-заглушка + фильтр-ловушка
пишет /etc/nginx/sites-available/site
python3 — пишет nginx-конфиг
python3 << 'PYEOF'
DOMAIN = "ВАШ_ДОМЕН"   # ← ЗАМЕНИ
cfg = f"""server {{
    listen 80 default_server;
    listen 127.0.0.1:8444 ssl default_server;
    server_name _;
    ssl_certificate /etc/letsencrypt/live/{DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{DOMAIN}/privkey.pem;
    return 444;
}}
server {{
    listen 80;
    server_name {DOMAIN};
    location /.well-known/acme-challenge/ {{
        root /var/www/html;
        allow all;
    }}
    location / {{
        return 301 https://{DOMAIN}$request_uri;
    }}
}}
server {{
    listen 127.0.0.1:8444 ssl;
    server_name {DOMAIN};
    server_tokens off;
    ssl_certificate /etc/letsencrypt/live/{DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{DOMAIN}/privkey.pem;
    root /var/www/html;
    index index.html;
    location ~* "(wget|curl|chmod|/tmp/|eval\\(|base64)" {{
        return 403;
    }}
    location / {{
        try_files $uri $uri/ =404;
    }}
}}
"""
open("/etc/nginx/sites-available/site", "w").write(cfg)
print("nginx-конфиг записан")
PYEOF

# подключаем боевой, убираем временный
ln -sf /etc/nginx/sites-available/site /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/acme-temp
nginx -t && systemctl restart nginx

Кладём простую заглушку-сайт (любой статический HTML). Главное — чтобы при заходе отдавалось что-то осмысленное:

bash — заглушка
cat > /var/www/html/index.html << 'EOF'
<!doctype html><html lang="ru"><head><meta charset="utf-8">
<title>Главная</title></head>
<body><h1>Сайт работает</h1><p>Скоро здесь будет контент.</p></body></html>
EOF
Фильтр-ловушка в :8444 (location ~* "(wget|curl|...)") срабатывает на путь URI, а не на User-Agent — режет автоматические сканеры, которые дёргают пути вида /tmp/x или ?cmd=wget.... Отдаёт им 403 вместо контента.
7

Конфиг telemt + systemd

Один инстанс на :443. Ключевое отличие от эмуляции чужого домена: tls_emulation = false и unknown_sni_action = "mask" — на неизвестный SNI telemt перекидывает на наш локальный nginx (mask_port = 8444), а не подделывает чужой серт.

Почему python, а не heredoc: в мобильных SSH-клиентах cat << EOF с длинными строками склеивается и обрывается. python пишет надёжно. Подставь свой домен и секрет (32 hex — сгенерируй openssl rand -hex 16).

пишет /etc/telemt/telemt1.toml
python3 — конфиг telemt
python3 << 'PYEOF'
DOMAIN = "ВАШ_ДОМЕН"                          # ←
SECRET = "ВАШ_СЕКРЕТ_32_HEX"                   # ← openssl rand -hex 16
cfg = f"""[general]
prefer_ipv6 = false
fast_mode = true
use_middle_proxy = false

[general.modes]
classic = false
secure = false
tls = true

[server]
port = 443
listen_addr_ipv4 = "0.0.0.0"
client_mss = "tspu"

[server.api]
enabled = true
listen = "127.0.0.1:9091"
whitelist = ["127.0.0.1/32"]

[censorship]
tls_domain = "{DOMAIN}"
mask = true
mask_host = "127.0.0.1"
mask_port = 8444
tls_emulation = false
unknown_sni_action = "mask"
fake_cert_len = 2048

[access]
replay_check_len = 65536
ignore_time_skew = false

[access.users]
user1 = "{SECRET}"
"""
open("/etc/telemt/telemt1.toml", "w").write(cfg)
print("конфиг записан, домен:", DOMAIN)
PYEOF
chown -R telemt:telemt /etc/telemt
ПараметрЧто делает
use_middle_proxy = falseпрямой доступ к DC Telegram (быстрее; нужен открытый DC у хостера)
client_mss = "tspu"MSS=92, маскирует размер пакетов под браузер (анти-DPI ТСПУ)
tls_emulation = falseНЕ подделываем чужой серт — у нас свой настоящий
unknown_sni_action = "mask"левый SNI → локальный nginx :8444 (наш сайт)
mask_port = 8444куда внутри слать замаскированный трафик

systemd-юнит с автозапуском и капабилити для bind на 443:

пишет /etc/systemd/system/telemt1.service
bash
cat > /etc/systemd/system/telemt1.service << 'EOF'
[Unit]
Description=Telemt Proxy 1 (port 443 self-mask)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=telemt
Group=telemt
WorkingDirectory=/opt/telemt
ExecStart=/bin/telemt /etc/telemt/telemt1.toml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable telemt1
8

UFW + recent (с проверкой модуля!)

Базовые правила. SSH разреши до ufw enable — лучше с конкретного IP, чтобы не отрезать себе доступ.

ВНИМАНИЕ. Поменяй ВАШ_IP на свой реальный (где ты сейчас). Проверь что SSH-правило есть, прежде чем включать UFW.
bash — порты
# SSH с твоего IP — критично, первым делом
ufw allow from ВАШ_IP to any port 22 proto tcp comment 'SSH'

ufw allow 80/tcp comment 'nginx acme+redirect'
ufw allow 443/tcp comment 'telemt'
ufw default deny incoming
ufw default allow outgoing
ufw --force enable
ufw status verbose

Теперь recent — rate-limit 1 новый SYN/сек с одного IP на :443. Отбивает активное зондирование. Но сначала — модуль ядра, без него правило мёртвое.

Главная ловушка recent. Правило в before.rules — это полдела. Нужен загруженный модуль ядра xt_recent. Если его нет на момент ufw reload — правило молча не применится (в iptables его не будет, но и ошибки не будет). Сначала проверяем и закрепляем модуль, потом вставляем правило.
bash — модуль СНАЧАЛА
# модуль есть в ядре?
modinfo xt_recent | grep filename

# загружаем + закрепляем на автозагрузку (для ребута!)
modprobe xt_recent
echo xt_recent > /etc/modules-load.d/xt_recent.conf

# ПРОВЕРКА — должна быть строка xt_recent:
lsmod | grep xt_recent
Если modinfo ничего не вывел — модуля нет в этой сборке ядра, recent поставить не выйдет. Это маркер неподходящего хостинга/образа: бери стандартный Ubuntu-образ, там xt_recent есть. /etc/modules-load.d/xt_recent.conf — обязателен, без него после ребута модуль не загрузится и recent отвалится.

Вставляем правило в before.rules (после loopback-accept) и применяем:

правит /etc/ufw/before.rules
python3 — recent на 443
cp /etc/ufw/before.rules /etc/ufw/before.rules.bak.$(date +%s)

python3 << 'PYEOF'
path = "/etc/ufw/before.rules"
s = open(path).read()
if "mtp443" in s:
    print("уже есть, пропуск"); raise SystemExit
rule = """# === MTProto rate-limit 443 ===
-A ufw-before-input -p tcp --dport 443 --syn -m recent --name mtp443 --rcheck --seconds 1 -j DROP
-A ufw-before-input -p tcp --dport 443 --syn -m recent --name mtp443 --set -j ACCEPT
"""
marker = "-A ufw-before-input -i lo -j ACCEPT"
if marker in s:
    s = s.replace(marker, marker + "\n" + rule, 1)
    open(path, "w").write(s)
    print("recent вставлен")
else:
    print("ОШИБКА: маркер не найден")
PYEOF

ufw reload

# ПРОВЕРКА — правило реально в живом фаерволе?
iptables -L ufw-before-input -v -n | grep recent
ls /proc/net/xt_recent/        # должен появиться mtp443
Признак успеха: iptables ... grep recent выдаёт две строки (CHECK + SET) с mtp443, и в /proc/net/xt_recent/ появился файл mtp443. Если обе проверки прошли — recent реально работает, а не просто прописан в файле.
9

Запуск и проверка

Стартуем telemt, проверяем весь маршрут.

bash
systemctl start telemt1
sleep 6
systemctl is-active telemt1

# telemt видит DC и слушает 443?
journalctl -u telemt1 --no-pager -n 20 | grep -iE "Initialized|Listening"
ss -tlnp | grep ':443 ' | grep -o telemt

Проверяем браузерный путь и nginx-фронт (подставь свой домен и IP):

bash — диагностика маршрута
# браузерный путь: 443 telemt -> mask -> nginx сайт (ждём 200)
curl -sk https://ВАШ_ДОМЕН/ --resolve ВАШ_ДОМЕН:443:127.0.0.1 \
  -A "Mozilla/5.0" -o /dev/null -w "сайт: HTTP %{http_code}\n" --max-time 8

# http -> https редирект (ждём 301)
curl -sI http://127.0.0.1:80/ -H "Host: ВАШ_ДОМЕН" | grep -iE "HTTP|location"

# ACME-дырка отдаётся по :80?
echo test > /var/www/html/.well-known/acme-challenge/t
curl -s http://127.0.0.1/.well-known/acme-challenge/t -H "Host: ВАШ_ДОМЕН"; rm -f /var/www/html/.well-known/acme-challenge/t

# фильтр-ловушка на :8444 (ждём 403)
curl -sk "https://127.0.0.1:8444/tmp/x" -H "Host: ВАШ_ДОМЕН" -o /dev/null -w "  /tmp/ -> HTTP %{http_code} (ждём 403)\n"

# чужой Host -> обрыв 444 (curl покажет 000)
curl -sI http://127.0.0.1:80/ -H "Host: other.com" -o /dev/null -w "  чужой Host -> %{http_code} (000=обрыв, ок)\n" --max-time 5
Признаки успеха: telemt active, в логах DC/ME Initialized + Listening on 0.0.0.0:443, порт держит telemt, сайт отдаёт 200, http даёт 301, ACME-файл читается, /tmp/ ловится в 403, чужой Host обрывается.
10

Автопродление серта

Серт Let's Encrypt живёт 90 дней. Так как мы получили его через webroot, а в nginx есть ACME-дырка (шаг 6), автопродление работает без остановки nginx. Проверяем dry-run:

bash
# тестовый прогон продления (ничего не меняет)
certbot renew --dry-run

# таймер certbot активен? (продлит автоматически)
systemctl list-timers | grep certbot
Если dry-run падает с «Could not bind TCP port 80» — значит серт когда-то получали в режиме standalone, и метод не переключился на webroot. Лечение — принудительно перевыпустить через webroot один раз:
bash — починка standalone -> webroot
certbot certonly --webroot -w /var/www/html \
  -d ВАШ_ДОМЕН --cert-name ВАШ_ДОМЕН --force-renewal
certbot renew --dry-run   # теперь должно пройти
--force-renewal здесь обязателен: без него certbot скажет «not yet due» и метод не перепишется.
Должно вывести Congratulations, all simulated renewals succeeded и активный таймер certbot.timer. Тогда серт будет продлеваться сам.
11

Ребут-тест

Финальная и обязательная проверка: всё должно подниматься само после перезагрузки. Особенно recent — его модуль и правило это частая точка отказа. Делай это до того как пустишь на сервер боевой трафик.

bash — перед ребутом
# всё в автозапуске?
systemctl is-enabled telemt1 nginx ufw
grep -c mtp443 /etc/ufw/before.rules        # 2
cat /etc/modules-load.d/xt_recent.conf      # xt_recent

reboot

После перезагрузки заходим и проверяем, что recent пережил ребут и всё поднялось:

bash — после ребута
echo "telemt: $(systemctl is-active telemt1) | nginx: $(systemctl is-active nginx) | ufw: $(systemctl is-active ufw)"

# recent пережил ребут? (главное)
lsmod | grep xt_recent && echo "модуль загрузился сам"
iptables -L ufw-before-input -v -n | grep -c recent      # 2
ls /proc/net/xt_recent/                                   # mtp443

# fq + keepalive после ребута?
IFACE=$(ip route | awk '/default/{print $5; exit}')
echo "qdisc: $(tc qdisc show dev $IFACE | head -1 | awk '{print $2}') | keepalive: $(sysctl -n net.ipv4.tcp_keepalive_time)"

# сайт жив?
curl -sk https://ВАШ_ДОМЕН/ --resolve ВАШ_ДОМЕН:443:127.0.0.1 -o /dev/null -w "сайт: HTTP %{http_code}\n" --max-time 8
Всё зелёное = сервер отказоустойчив: telemt/nginx/ufw active, модуль xt_recent загрузился сам (сработал modules-load.d), recent-правил 2, mtp443 на месте, qdisc fq, keepalive 60. Можно пускать трафик.
12

Ссылка + управление

Ссылку telemt собирает сам — берём из API. Получится вид https://t.me/proxy?server=ВАШ_ДОМЕН&port=443&secret=ee....

bash — ссылка
curl -s http://127.0.0.1:9091/v1/users \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data'][0]['links']['tls'][0])"
Ссылка открывается ВНУТРИ Telegram (окно «Подключить прокси»), а не в браузере. Если клиент открыл её как веб-адрес и увидел сайт — он сделал не то: ссылку надо тапнуть в Telegram или вставить в поиск Telegram.

Команды на каждый день:

bash — статистика / управление
# текущие подключения
curl -s http://127.0.0.1:9091/v1/users | jq '.data[] | {user:.username, conns:.current_connections, ips:.active_unique_ips}'

systemctl restart telemt1            # рестарт после правки конфига
journalctl -u telemt1 -f             # логи в реальном времени
bash — обновление telemt
cd /tmp
wget -qO- "https://github.com/telemt/telemt/releases/latest/download/telemt-x86_64-linux-gnu.tar.gz" | tar -xz
systemctl stop telemt1
mv /tmp/telemt /bin/telemt && chmod +x /bin/telemt
systemctl start telemt1
/bin/telemt --version
После правки конфигаsystemctl restart telemt1. Ссылка не меняется, пока не трогаешь домен/порт/секрет. Активные клиенты переподключатся не мгновенно — иногда нужно переоткрыть Telegram.