На главную

telemt
отлов мёртвых соединений

Фикс залипания мобильных клиентов через TCP keepalive. telemt ставит SO_KEEPALIVE на сокеты, поэтому достаточно подкрутить sysctl — ядро само быстро пробивает тихие коннекты и шлёт RST. Клиент после выхода из фона не висит на дохлом сокете, а сразу переподключается.

OS Ubuntu 20.04 / 22.04 / 24.04 telemt SO_KEEPALIVE ✓ root required ~5 мин
1

В чём проблема

Мобильный клиент сворачивается → ОС усыпляет приложение, но сокет закрывается не чисто (FIN/RST не уходит, процесс заморожен). Сервер продолжает держать established-коннект открытым — по умолчанию чуть ли не сутками. Юзер разворачивает Telegram → клиент пытается ожить на старом сокете, а путь (NAT) его состояние уже выкинул → пакеты в никуда → висяк.

ЭтапЧто происходит
фонприложение заморожено, сокет не закрыт
сервердержит мёртвый established без таймаута
возвратклиент залипает на протухшем коннекте
Решение: ускорить TCP keepalive. telemt уже выставляет SO_KEEPALIVE, поэтому ядро при правильном sysctl само быстро обнаружит тихий коннект, отправит пробы, и через несколько неотвеченных — порвёт его RST-ом. Клиент на возврате сразу видит «мёртв» и делает чистый реконнект.
2

Убедиться, что keepalive есть

Проверки ниже на Python — он есть в любой Ubuntu из коробки. На всякий случай убедись:

bash
python3 --version # если не найден: apt install -y python3

Скрипт сам достаёт порты инстансов из /etc/telemt/*.toml, парсит ss (он выдаёт по две строки на соединение — питон их корректно склеивает) и считает таймеры на клиентских коннектах. Ничего подставлять руками не нужно.

python3
python3 << 'PYEOF'
import subprocess, re, glob
from collections import Counter

# порты инстансов из конфигов
ports = set()
for f in glob.glob("/etc/telemt/*.toml"):
    for line in open(f):
        m = re.match(r'\s*port\s*=\s*(\d+)', line)
        if m: ports.add(m.group(1))

# ss: established с таймерами; склеиваем двустрочные записи
out = subprocess.run(["ss","-tinoH","state","established"],
                     capture_output=True, text=True).stdout
records, buf = [], ""
for ln in out.split("\n"):
    if not ln.strip(): continue
    if ln[0].isspace(): buf += " " + ln.strip()
    else:
        if buf: records.append(buf)
        buf = ln.strip()
if buf: records.append(buf)

# считаем таймеры на соединениях с локальным портом из наших
timers, total = Counter(), 0
for r in records:
    f = r.split()
    local = f[2] if len(f) > 2 else ""
    if not any(local.endswith(":"+p) for p in ports): continue
    total += 1
    m = re.search(r'timer:\((\w+)', r)
    timers[m.group(1) if m else "—"] += 1

# красивый вывод
W = 44
print("╭" + "─"*W + "╮")
print("│ " + "KEEPALIVE CHECK".ljust(W-1) + "│")
print("├" + "─"*W + "┤")
print("│ " + f"Порты telemt:  {', '.join(sorted(ports)) or '—'}".ljust(W-1) + "│")
print("│ " + f"Соединений:    {total}".ljust(W-1) + "│")
print("├" + "─"*W + "┤")
for name, cnt in timers.most_common():
    bar = "█" * min(cnt, 24)
    print("│ " + f"{name:<10} {cnt:>4}  {bar}".ljust(W-1) + "│")
print("╰" + "─"*W + "╯")
PYEOF
Что должно быть: большинство соединений с таймером keepalive — значит keepalive армирован, метод подходит. Несколько on — это retransmit-таймер активных передач, норма, к делу не относятся. Если соединений 0 — просто сейчас никто не подключён, проверь когда будет трафик.
3

Применяем sysctl

Задаём агрессивные тайминги keepalive глобально. Дохлый коннект будет рваться примерно за 60 + 15×3 ≈ 105с после ухода в тишину.

создаёт /etc/sysctl.d/99-tg-keepalive.conf
bash
cat > /etc/sysctl.d/99-tg-keepalive.conf << 'EOF'
# после 60с тишины — первая keepalive-проба
net.ipv4.tcp_keepalive_time = 60
# повтор пробы каждые 15с
net.ipv4.tcp_keepalive_intvl = 15
# 3 неотвеченных пробы → RST
net.ipv4.tcp_keepalive_probes = 3
EOF

sysctl --system

Сразу убедимся, что значения прописались в ядро:

bash
sysctl net.ipv4.tcp_keepalive_time \
       net.ipv4.tcp_keepalive_intvl \
       net.ipv4.tcp_keepalive_probes
4

Главная проверка — отсчёт таймера

Это источник правды: применился ли sysctl к живым сокетам. Тот же подход, но вытаскиваем сами значения обратного отсчёта keepalive-таймера — видно, сползают ли они к заданным 60с.

python3
python3 << 'PYEOF'
import subprocess, re, glob
from collections import Counter

ports = set()
for f in glob.glob("/etc/telemt/*.toml"):
    for line in open(f):
        m = re.match(r'\s*port\s*=\s*(\d+)', line)
        if m: ports.add(m.group(1))

out = subprocess.run(["ss","-tinoH","state","established"],
                     capture_output=True, text=True).stdout
records, buf = [], ""
for ln in out.split("\n"):
    if not ln.strip(): continue
    if ln[0].isspace(): buf += " " + ln.strip()
    else:
        if buf: records.append(buf)
        buf = ln.strip()
if buf: records.append(buf)

# собираем значения обратного отсчёта keepalive-таймера
countdown = Counter()
for r in records:
    f = r.split()
    local = f[2] if len(f) > 2 else ""
    if not any(local.endswith(":"+p) for p in ports): continue
    m = re.search(r'timer:\(keepalive,([^,)]+)', r)
    if m: countdown[m.group(1)] += 1

# сортируем по реальной длительности (ms < sec < min)
def secs(v):
    n = float(re.findall(r'[0-9.]+', v)[0])
    if 'ms' in v: return n/1000
    if 'min' in v: return n*60
    return n

W = 44
print("╭" + "─"*W + "╮")
print("│ " + "KEEPALIVE COUNTDOWN".ljust(W-1) + "│")
print("├" + "─"*W + "┤")
if not countdown:
    print("│ " + "нет активных соединений сейчас".ljust(W-1) + "│")
else:
    for val, cnt in sorted(countdown.items(), key=lambda x: secs(x[0])):
        bar = "█" * min(cnt, 24)
        print("│ " + f"{val:<10} {cnt:>3}  {bar}".ljust(W-1) + "│")
print("╰" + "─"*W + "╯")
PYEOF
Норма для этой инсталляции: telemt не фиксирует интервалы per-socket, поэтому отсчёт сползает к заданным 60с. После применения новые и существующие коннекты реапятся за ~105с тишины.
5

Тайминги в самом telemt — не трогать

У telemt есть своя секция таймаутов [timeouts] (client_handshake, client_keepalive, tg_connect, client_ack). Возникает соблазн «ускорить» отлов мёртвых соединений ещё и через неё. Не надо. Для этой задачи правильный слой — ядро (шаги 3–4), а не конфиг telemt.

Рекомендация: секции [timeouts] в конфиге быть не должно. Если её нет — telemt берёт разумные дефолты (client_handshake = 15, client_keepalive = 60, tg_connect = 10, client_ack = 300). Этого достаточно. Если секция есть и ты её правил — лучше удалить целиком и вернуться к дефолтам.

Почему именно ядро, а не тайминги telemt:

проверить, есть ли секция в конфиге
bash
# если что-то вывелось — секция есть, лучше убрать её и перезапустить telemt
grep -n '\[timeouts\]' /etc/telemt/*.toml
Про use_middle_proxy = false: при прямой маршрутизации (direct relay) вся группа ME-таймаутов (me_keepalive_*, me_reconnect_*) вообще не применяется. Работают только клиентские тайминги и ядерный keepalive — ещё одна причина держать всё на ядре, а конфиг telemt не усложнять.
6

Чего это НЕ чинит

Метод лечит симптом — залипание клиента на мёртвом сокете после фона. Границы:

Итог: keepalive убирает залипание мобильного клиента после фона — это его прямая задача, и он с ней справляется. Остальные ограничения относятся к слоям, которые на самом сервере не лечатся.
7

Как откатить всё назад

Если нужно вернуть систему в исходное состояние — удаляем файл sysctl и сбрасываем значения ядра к дефолтам. Одной командой:

bash
# удаляем наш конфиг
rm -f /etc/sysctl.d/99-tg-keepalive.conf

# возвращаем дефолтные значения ядра Linux
sysctl -w net.ipv4.tcp_keepalive_time=7200
sysctl -w net.ipv4.tcp_keepalive_intvl=75
sysctl -w net.ipv4.tcp_keepalive_probes=9

# перечитываем оставшиеся sysctl-файлы
sysctl --system

Проверяем, что вернулись дефолты (7200 / 75 / 9):

bash
sysctl net.ipv4.tcp_keepalive_time \
       net.ipv4.tcp_keepalive_intvl \
       net.ipv4.tcp_keepalive_probes
Без перезагрузки: sysctl -w применяет дефолты к ядру сразу, новые соединения подхватят их немедленно. Уже открытые сокеты дойдут до старого таймаута один раз, дальше — на дефолтных. Полностью чисто будет после reboot, но он не обязателен.