Один инстанс telemt на 443, маскировка под собственный домен с настоящим Let's Encrypt сертификатом за nginx-фронтом. Полная схема: серт, self-mask, автопродление через webroot, keepalive под мобильных клиентов, BBR+fq, recent-защита от зондирования с проверкой модуля ядра и ребут-тестом.
Здесь другой подход, чем эмуляция чужого домена (apple/cloudflare). Мы берём собственный домен, получаем на него настоящий сертификат Let's Encrypt и поднимаем за telemt реальный nginx-сайт. На «левый» SNI и на прямой заход по IP telemt отдаёт этот наш сайт. Снаружи сервер выглядит как обычный маленький сайт на своём домене — потому что это и есть обычный сайт.
| Слой | Что делает |
|---|---|
| telemt :443 | принимает MTProto, на чужой SNI маскирует в nginx |
| nginx :8444 | реальный сайт-заглушка со своим сертом (локальный) |
| nginx :80 | редирект на https + отдаёт ACME для продления серта |
| self-mask | unknown_sni → свой сайт, валидный серт своего домена |
/.well-known/acme-challenge/ для автопродления.Обновляем пакеты, ставим зависимости. Заранее нужен домен, A-запись которого указывает на IP этого сервера — без этого Let's Encrypt не выдаст серт.
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.Два сетевых тюнинга, которые сильно влияют на качество прокси:
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.Отдельный системный пользователь без shell, рабочие папки, бинарник telemt из последнего релиза.
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 в логах).Получаем серт на свой домен. Делаем это через webroot, а не standalone — потому что :80 займёт nginx, и standalone-режим в будущем сломает автопродление (не сможет забиндить порт). Сразу настраиваем правильно.
Сначала временно поднимем nginx на :80, чтобы certbot прошёл валидацию:
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
standalone (когда certbot сам поднимает свой сервер на :80), то потом nginx займёт :80, и certbot renew будет падать с «Could not bind TCP port 80». Серт молча протухнет через 90 дней. webroot + nginx-дырка под ACME (шаг 6) — единственный надёжный вариант, когда на :80 уже что-то слушает.notAfter=... примерно через 90 дней — серт получен. Теперь временный конфиг заменим на боевой.Боевой конфиг nginx из трёх server-блоков. Каждый со своей задачей:
| Блок | Слушает | Делает |
|---|---|---|
| default | :80 + :8444 | чужой Host / прямой IP → return 444 (обрыв) |
| :80 домен | :80 | ACME-дырка + редирект на https |
| :8444 ssl | :8444 (локально) | сам сайт-заглушка + фильтр-ловушка |
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). Главное — чтобы при заходе отдавалось что-то осмысленное:
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
location ~* "(wget|curl|...)") срабатывает на путь URI, а не на User-Agent — режет автоматические сканеры, которые дёргают пути вида /tmp/x или ?cmd=wget.... Отдаёт им 403 вместо контента.Один инстанс на :443. Ключевое отличие от эмуляции чужого домена: tls_emulation = false и unknown_sni_action = "mask" — на неизвестный SNI telemt перекидывает на наш локальный nginx (mask_port = 8444), а не подделывает чужой серт.
cat << EOF с длинными строками склеивается и обрывается. python пишет надёжно. Подставь свой домен и секрет (32 hex — сгенерируй openssl rand -hex 16).
пишет /etc/telemt/telemt1.toml
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.servicecat > /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
Базовые правила. SSH разреши до ufw enable — лучше с конкретного IP, чтобы не отрезать себе доступ.
ВАШ_IP на свой реальный (где ты сейчас). Проверь что SSH-правило есть, прежде чем включать UFW.# 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. Отбивает активное зондирование. Но сначала — модуль ядра, без него правило мёртвое.
before.rules — это полдела. Нужен загруженный модуль ядра xt_recent. Если его нет на момент ufw reload — правило молча не применится (в iptables его не будет, но и ошибки не будет). Сначала проверяем и закрепляем модуль, потом вставляем правило.# модуль есть в ядре? 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) и применяем:
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 реально работает, а не просто прописан в файле.Стартуем telemt, проверяем весь маршрут.
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):
# браузерный путь: 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
active, в логах DC/ME Initialized + Listening on 0.0.0.0:443, порт держит telemt, сайт отдаёт 200, http даёт 301, ACME-файл читается, /tmp/ ловится в 403, чужой Host обрывается.Серт Let's Encrypt живёт 90 дней. Так как мы получили его через webroot, а в nginx есть ACME-дырка (шаг 6), автопродление работает без остановки nginx. Проверяем dry-run:
# тестовый прогон продления (ничего не меняет) certbot renew --dry-run # таймер certbot активен? (продлит автоматически) systemctl list-timers | grep certbot
standalone, и метод не переключился на webroot. Лечение — принудительно перевыпустить через 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. Тогда серт будет продлеваться сам.Финальная и обязательная проверка: всё должно подниматься само после перезагрузки. Особенно recent — его модуль и правило это частая точка отказа. Делай это до того как пустишь на сервер боевой трафик.
# всё в автозапуске? 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 пережил ребут и всё поднялось:
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 собирает сам — берём из API. Получится вид https://t.me/proxy?server=ВАШ_ДОМЕН&port=443&secret=ee....
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])"
Команды на каждый день:
# текущие подключения 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 # логи в реальном времени
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.