Инструкция: telemt inbound SYN limiter через nftables (v2)

Итоговая инструкция для сервера с telemt в Docker. Основной метод — входящий per-client limiter по SYN от внешнего клиента к контейнеру telemt.

Для чего нужен метод: он помогает решить проблему с подключением клиента к серверу telemt на некоторых провайдерах, где обычное подключение может зависать, долго устанавливаться или нестабильно проходить начальный TCP-этап.
Итоговый рабочий метод: ограничивать входящие SYN к telemt:443 отдельно для каждого внешнего IP. Рабочее правило использует meter { ip saddr ... limit rate over 1/second burst 1 packets }.
Смысл настройки: не ускорять сам telemt, а аккуратно ограничить частые повторные входящие SYN от одного и того же внешнего IP. Для клиента это выглядит мягче, чем глобально резать ответы сервера.
Deprecated: старый метод с исходящими SYN,ACK от сервера, tcp sport 443 и глобальным limit rate over ... оставлен ниже только как архив/история проверки. Для текущей рабочей схемы использовать новый inbound per-client метод.

Проблемы после применения указанных здесь настроек

1. Иногда бывает долгое подключение к Telemt. Много клиентов одновременно пытаются установить соединение. Просто подождите ~ 10 минут после перезагрузки telemt и станет лучше.

2. Telemt может продолжать писать Telegram handshake timeout и Operation timed out, но при inbound per-client SYN limiter такие ошибки должны идти заметно менее плотными пачками.

Содержание
  1. Простая команда без Docker
  2. Что именно ограничивается
  3. Настройки telemt
  4. Ручной тест нового inbound per-client метода
  5. Скрипт применения nft-правила
  6. Watcher для автоматического обновления правила
  7. systemd service
  8. Проверка работы
  9. Подбор параметров
  10. Временное отключение и возврат правила
  11. Deprecated: старый sport/SYN-ACK метод

1. Простая команда без Docker

Если telemt запущен прямо на этом же сервере без Docker, VM или отдельного bridge, пакет приходит в локальный процесс. Для такого случая нужен hook input, а не hook forward.

IP="1.1.1.1"
PORT="443"

nft delete table inet telemt_limit 2>/dev/null

nft add table inet telemt_limit
nft 'add chain inet telemt_limit input { type filter hook input priority 0; policy accept; }'

nft "add rule inet telemt_limit input \
ip daddr $IP tcp dport $PORT \
tcp flags & (syn | ack) == syn \
meter telemt_in_syn_per_client { ip saddr timeout 60s limit rate over 1/second burst 1 packets } \
counter drop comment \"telemt_in_syn_per_client_1ps_burst1\""

nft list chain inet telemt_limit input

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

2. Что именно ограничивается

Новый метод режет не ответ сервера SYN,ACK, а первый входящий SYN от клиента:

client_ip:random_port → telemt_container:443  SYN

Так как telemt работает в Docker bridge, пакет к контейнеру проходит через netfilter hook forward. Поэтому правило создаётся в chain с hook forward.

Ключ per-client лимита — ip saddr, потому что во входящем SYN внешний клиент является источником:

ip daddr $TELEMT_IP tcp dport 443
tcp flags & (syn | ack) == syn
meter telemt_in_syn_per_client { ip saddr timeout 60s limit rate over 1/second burst 1 packets }
counter drop
Часть правилаСмысл
ip daddr $TELEMT_IPПакет направлен в контейнер telemt.
tcp dport 443Вход на порт 443.
tcp flags & (syn | ack) == synМатчится именно первый SYN клиента, без ACK.
meter ... { ip saddr ... }Каждому внешнему IP — свой bucket, а не одна глобальная очередь на всех.
timeout 60sСостояние meter для IP очищается через 60 секунд неактивности.

3. Настройки telemt

Эти параметры не обязательны. Их можно не трогать, если после nft-правила подключение работает нормально. Ниже указан базовый пример со значениями по умолчанию. Если остаются долгие подключения, обрывы или нестабильность на отдельных провайдерах, эти значения можно аккуратно увеличить.

[general]
tg_connect = 10

[timeouts]
client_handshake = 15
client_keepalive = 60
ПараметрЗа что отвечает
tg_connect = 10Базовый таймаут подключения telemt к upstream Telegram DC. Если upstream Telegram отвечает нестабильно, можно попробовать увеличить до 30.
client_handshake = 15Базовое ожидание начального handshake клиента. Если клиент долго проходит начальное подключение, можно попробовать увеличить до 120.
client_keepalive = 60Базовое ожидание активности клиента. При мобильной сети, NAT и нестабильных соединениях можно попробовать увеличить до 90.
Вариант для тюнинга: если базовые значения не помогают, можно попробовать tg_connect = 30, client_handshake = 120, client_keepalive = 90. Это не обязательные значения, а запасной вариант для проблемных сетей.

Если параметры менялись, после изменения конфига перезапустить контейнер:

cd /opt/telemt
docker compose restart telemt

4. Ручной тест нового inbound per-client метода

Команды для быстрого теста без установки watcher:

nft delete table inet telemt_limit 2>/dev/null

TELEMT_IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{"\n"}}{{end}}' telemt | awk 'NF {print; exit}')"

echo "TELEMT_IP=$TELEMT_IP"

nft add table inet telemt_limit
nft 'add chain inet telemt_limit forward { type filter hook forward priority 0; policy accept; }'

nft "add rule inet telemt_limit forward \
ip daddr $TELEMT_IP tcp dport 443 \
tcp flags & (syn | ack) == syn \
meter telemt_in_syn_per_client { ip saddr timeout 60s limit rate over 1/second burst 1 packets } \
counter drop comment \"telemt_in_syn_per_client_1ps_burst1\""

nft list chain inet telemt_limit forward

Ожидаемый вид:

table inet telemt_limit {
    chain forward {
        type filter hook forward priority filter; policy accept;
        ip daddr 172.x.x.x tcp dport 443 tcp flags syn / syn,ack meter telemt_in_syn_per_client size 65535 { ip saddr timeout 1m limit rate over 1/second burst 1 packets } counter packets 0 bytes 0 drop comment "telemt_in_syn_per_client_1ps_burst1"
    }
}

5. Скрипт применения nft-правила

Постоянный скрипт для нового метода:

cat > /usr/local/sbin/telemt-in-syn-limit.sh <<'EOF'
#!/bin/sh
set -eu

CONTAINER="${1:-telemt}"
TABLE="telemt_limit"
CHAIN="forward"
PORT="${PORT:-443}"
RATE="${RATE:-1/second}"
BURST="${BURST:-1}"
METER_TIMEOUT="${METER_TIMEOUT:-60s}"

IP=""

for i in $(seq 1 60); do
    RUNNING="$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)"
    if [ "$RUNNING" = "true" ]; then
        IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{"\n"}}{{end}}' "$CONTAINER" | awk 'NF {print; exit}')"
        if [ -n "$IP" ]; then
            break
        fi
    fi
    sleep 1
done

if [ -z "$IP" ]; then
    echo "Could not get IP for container: $CONTAINER" >&2
    exit 1
fi

nft delete table inet "$TABLE" 2>/dev/null || true
nft add table inet "$TABLE"
nft "add chain inet $TABLE $CHAIN { type filter hook forward priority 0; policy accept; }"

nft "add rule inet $TABLE $CHAIN ip daddr $IP tcp dport $PORT tcp flags & (syn | ack) == syn meter telemt_in_syn_per_client { ip saddr timeout $METER_TIMEOUT limit rate over $RATE burst $BURST packets } counter drop comment \"telemt_in_syn_per_client_${RATE}_burst_${BURST}\""

echo "Applied telemt inbound SYN per-client limiter:"
echo "container=$CONTAINER ip=$IP port=$PORT rate=$RATE burst=$BURST meter_timeout=$METER_TIMEOUT"
nft list chain inet "$TABLE" "$CHAIN"
EOF

chmod +x /usr/local/sbin/telemt-in-syn-limit.sh

Применение вручную:

/usr/local/sbin/telemt-in-syn-limit.sh telemt

Применение с временным переопределением значений:

RATE="1/second" BURST="1" METER_TIMEOUT="60s" /usr/local/sbin/telemt-in-syn-limit.sh telemt

6. Watcher для автоматического обновления правила

Watcher проверяет IP контейнера каждые 5 секунд. Если IP изменился, правило пересоздаётся.

cat > /usr/local/sbin/telemt-in-syn-watch.sh <<'EOF'
#!/bin/sh
set -u

CONTAINER="${1:-telemt}"
INTERVAL="${INTERVAL:-5}"
LAST_IP=""

echo "Watching Docker container for inbound SYN limiter: $CONTAINER"

while true; do
    RUNNING="$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)"

    if [ "$RUNNING" = "true" ]; then
        IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{"\n"}}{{end}}' "$CONTAINER" 2>/dev/null | awk 'NF {print; exit}')"

        if [ -n "$IP" ] && [ "$IP" != "$LAST_IP" ]; then
            echo "Container IP changed: ${LAST_IP:-none} -> $IP"
            if /usr/local/sbin/telemt-in-syn-limit.sh "$CONTAINER"; then
                LAST_IP="$IP"
            else
                echo "Failed to apply nft inbound SYN rule for $CONTAINER" >&2
            fi
        fi
    else
        if [ -n "$LAST_IP" ]; then
            echo "Container $CONTAINER is not running"
            LAST_IP=""
        fi
    fi

    sleep "$INTERVAL"
done
EOF

chmod +x /usr/local/sbin/telemt-in-syn-watch.sh

7. systemd service

Новый systemd service для inbound SYN метода:

cat > /etc/systemd/system/telemt-in-syn-watch.service <<'EOF'
[Unit]
Description=Watch telemt Docker container and refresh nft inbound SYN per-client limiter
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/sbin/telemt-in-syn-watch.sh telemt
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now telemt-in-syn-watch.service

Если старый сервис ещё существует, лучше остановить его, чтобы он не пересоздавал таблицу старым методом:

systemctl disable --now telemt-synack-watch.service 2>/dev/null || true

systemctl restart telemt-in-syn-watch.service

systemctl status telemt-in-syn-watch.service --no-pager
journalctl -u telemt-in-syn-watch.service -n 30 --no-pager

8. Проверка работы

Проверить IP контейнера:

docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} {{.State.Status}}' telemt

Проверить nft-правило:

nft list chain inet telemt_limit forward

Основной счётчик в новом методе:

telemt_in_syn_per_client_1ps_burst1
СчётчикЧто означает
telemt_in_syn_per_client_1ps_burst1Дропы сверх лимита 1/second burst 1 для конкретного внешнего IP.

Проверка через tcpdump:

tcpdump -ni any 'tcp and dst port 443 and tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'

Ожидаемое направление:

client_ip.random_port > 172.x.x.x.443: Flags [S]

9. Подбор параметров

Текущий рабочий вариант:

RATE=1/second
BURST=1
METER_TIMEOUT=60s
ВариантКогда пробовать
1/second burst 1Предпочтительный рабочий вариант. Жёсткий per-client режим для входящих SYN. Использовать как основной вариант, если подключение клиента к telemt нестабильно у некоторых провайдеров.
1/second burst 3Если отдельным клиентам не хватает совсем короткого burst, но хочется сохранить rate 1/sec.
2/second burst 5Более мягкая альтернатива для сервера с большим числом клиентов.
METER_TIMEOUT=30sБыстрее очищать состояние per-IP meter.
METER_TIMEOUT=120sДольше помнить IP, если клиенты часто ретраят с паузами.

Пример временного теста другого значения:

nft delete table inet telemt_limit 2>/dev/null
RATE="1/second" BURST="3" METER_TIMEOUT="60s" /usr/local/sbin/telemt-in-syn-limit.sh telemt
nft list chain inet telemt_limit forward

10. Временное отключение и возврат правила

Для чистого теста без правила:

systemctl stop telemt-in-syn-watch.service
nft delete table inet telemt_limit 2>/dev/null

Проверка, что таблицы нет:

nft list tables | grep telemt_limit

Вернуть новый inbound per-client метод:

/usr/local/sbin/telemt-in-syn-limit.sh telemt
systemctl start telemt-in-syn-watch.service

11. Deprecated: старый sport/SYN-ACK метод

Этот раздел оставлен специально как архив. Метод ограничивал исходящие SYN,ACK от telemt через tcp sport 443. На текущем тесте более удачным оказался новый входящий per-client SYN limiter. Если нет отдельной причины возвращаться к старому варианту, использовать новый метод выше.
Показать старый метод с исходящими SYN,ACK

Старый тестовый вариант:

nft delete table inet telemt_test 2>/dev/null

nft add table inet telemt_test
nft 'add chain inet telemt_test forward { type filter hook forward priority 0; policy accept; }'

nft 'add rule inet telemt_test forward ip saddr 172.21.0.4 tcp sport 443 tcp flags & (syn | ack) == (syn | ack) limit rate over 1/second burst 1 packets counter drop comment "telemt_synack_drop_over_1ps"'

Старый постоянный вариант был завязан на направление:

telemt_container:443 → client_ip:random_port  SYN,ACK

И матчился через:

ip saddr $TELEMT_IP tcp sport 443
tcp flags & (syn | ack) == (syn | ack)

Почему метод отправлен в deprecated:

Короткий итог

telemt в Docker
client_mss не используется
основной nft limiter: inbound SYN per-client
match: ip daddr $TELEMT_IP tcp dport 443 tcp flags syn / syn,ack
meter key: ip saddr
rate: 1/second
burst: 1
meter timeout: 60s
tg_connect = 10
client_handshake = 15
client_keepalive = 60
старый sport/SYN-ACK метод: deprecated