Skip to content

Latest commit

 

History

History
162 lines (115 loc) · 12.3 KB

File metadata and controls

162 lines (115 loc) · 12.3 KB

Как это работает

Подробное описание внутренней механики telemt-shaper.

Общая схема

                           ┌───────────────────┐
                           │  Главный цикл     │
                           │  каждые 5 секунд  │
                           └─────────┬─────────┘
                                     │
                ┌────────────────────┴────────────────────┐
                │                                         │
        ┌───────▼────────┐                       ┌────────▼────────┐
        │  ss -tnip      │                       │  Решения по     │
        │  → парсинг     │                       │  каждому IP     │
        │  → дельты      │                       │  (state machine)│
        │  → скорость    │                       └────────┬────────┘
        │  bytes×8 → bps │                                │
        └───────┬────────┘                                │
                │                                         │
                └─────────────► Скорости по IP ◄──────────┘
                                                          │
                                              ┌───────────┴────────────┐
                                              │                        │
                                       ┌──────▼──────┐         ┌───────▼──────┐
                                       │  add_shape  │         │ change_level │
                                       │  remove_    │         │ (апгрейд     │
                                       │  shape      │         │  без разрыва)│
                                       └──────┬──────┘         └───────┬──────┘
                                              │                        │
                                              └──────────┬─────────────┘
                                                         │
                                                  ┌──────▼──────┐
                                                  │  tc class   │
                                                  │  tc qdisc   │
                                                  │  tc filter  │
                                                  │  (HTB+cake) │
                                                  └─────────────┘

Шаг 1. Сбор данных

Каждые CHECK_INTERVAL секунд (по умолчанию 5) скрипт вызывает ss -tnip state established. Из вывода берутся только строки, в которых упоминается процесс telemt — то есть отслеживаются только клиенты MTProto-прокси.

Для каждого сокета извлекаются:

  • local_addr (IP:port сервера)
  • remote_addr (IP:port клиента)
  • bytes_sent и bytes_received из TCP-инфо

Почему по сокетам, а не по IP сразу

bytes_sent/received — счётчики конкретного сокета, а не суммарно по IP. Если клиент держит несколько подключений к прокси (что нормально), нужно агрегировать.

Сначала считаем дельту по каждому сокету ((local, remote) → разница байт от прошлого тика), а потом суммируем дельты по remote_ip. Это устойчиво к переоткрытию соединений: если клиент закрыл сокет и открыл новый — старый счётчик пропадает, новый стартует с baseline, дельты не «прыгают».

Перевод в Мбит/с

(сумма дельт байт по IP) × 8 / dt = биты/сек

Делим на 1_000_000 — получаем Мбит/с.

Шаг 2. Список исключений

IP, попадающие в SKIP_NETWORKS, никогда не шейпятся:

Это нужно чтобы сам трафик прокси «к Telegram» не зарезался — иначе мы бы ломали то, для чего прокси и работает.

Шаг 3. State machine для каждого IP

У каждого IP в каждый момент одно из двух состояний: не шейпится или шейпится на уровне N.

Не шейпится → попадает на L0

Условие: скорость ≥ SHAPE_LEVELS[0].threshold_mbps в течение SHAPE_LEVELS[0].exceed_ticks тиков подряд.

Накапливается счётчик exceed_count[ip]. Если на каком-то тике скорость упала ниже порога — счётчик сбрасывается в 0, копить надо заново. Это защита от ложных срабатываний на коротких всплесках.

Когда счётчик достиг порога → add_shape(ip, level=0):

  1. Аллоцируем уникальный class_id (берём из пула освобождённых, если есть; иначе инкрементим)
  2. Создаём HTB-класс с rate = ceil = limit_mbps
  3. Внутрь класса вешаем cake qdisc с bandwidth = limit_mbps
  4. Добавляем фильтр u32 по dst ip → flowid класса

Шейпится на L_N → апгрейд до L_(N+1)

Условие: скорость ≥ SHAPE_LEVELS[N+1].threshold_mbps в течение SHAPE_LEVELS[N+1].exceed_ticks тиков подряд.

Логика та же, что для входа на L0, только счётчик хранится в состоянии шейпленного IP (upgrade_count).

Когда накопилось → change_shape_level(ip, N+1):

  • tc class change — меняет rate/ceil/burst у существующего класса
  • tc qdisc change — меняет bandwidth у cake

Важно: фильтр НЕ пересоздаётся. Это значит, нет даже миллисекундного окна, в котором трафик уходил бы нешейпленным. Один и тот же class_id, один и тот же фильтр — меняется только лимит внутри.

Шейпится → снятие шейпа

Условие «успокоился»: скорость < CALM_RATIO × текущий_лимит_уровня (по умолчанию 0.5, т.е. ниже половины лимита).

При выполнении этого условия запускается отсчёт COOLDOWN_SECS (по умолчанию 120 секунд). Если за это время скорость снова поднимется выше calm-порога — отсчёт сбрасывается. Если выдержал тишину все 120 секунд — remove_shape(ip):

  1. tc filter del
  2. tc qdisc del
  3. tc class del
  4. class_id возвращается в пул свободных

Снимается шейп сразу до полного отсутствия (а не пошаговое понижение L2 → L1 → L0). Логика: если клиент действительно перестал качать — отпускаем целиком, не мучаем. Если потом снова разгонится — пройдёт весь путь L0 → L1 → L2 заново.

Шаг 4. tc-стек

Базовая структура qdisc на интерфейсе:

1: htb (root)
├── 1:10 (default class, rate=10gbit, qdisc=fq)
│   └── весь обычный трафик идёт сюда
├── 1:100 (htb, rate=40mbit) — шейпленный IP A
│   └── cake (bandwidth=40mbit)
├── 1:101 (htb, rate=24mbit) — шейпленный IP B  
│   └── cake (bandwidth=24mbit)
└── ...

При старте, если корневого HTB ещё нет, скрипт его создаёт (setup_htb()). Если есть — оставляет как есть и добавляет свои классы рядом.

Почему HTB+cake, а не cake целиком?

cake умеет шейпить сам, без HTB-обёртки. Но нам нужна изоляция per-IP: чтобы один шейпленный клиент не мешал шейпу другого. HTB даёт древовидную структуру с независимыми классами, cake внутри каждого класса делает грамотное управление очередью (fairness между потоками одного клиента, борьба с буферблоутом).

Точность шейпа

cake шейпит на link layer с учётом ethernet/IP/TCP overhead. На уровне ss (TCP payload) реальная скорость получается на 5-10% меньше заявленной cake. Например, при cake bandwidth 40mbit реально на ss видно ~38 Мбит/с.

Это учтено в дефолтных порогах: L1.threshold=36 при L0.limit=40 (запас 10%). При старте скрипт валидирует конфиг и ругается, если порог следующего уровня выше 90% от лимита предыдущего.

Шаг 5. Чистка состояния

Раз в минуту (gc_state()) удаляются:

  • Записи ip_last_seen, которым больше STATE_TTL_SECS (по умолчанию 600 секунд) и которые сейчас не шейпятся
  • Соответствующие им exceed_count

Это защита от бесконечного роста словарей при большом потоке клиентов.

Шаг 6. Завершение

При получении SIGTERM/SIGINT (shutdown()):

  1. По всем активным шейпам вызывается remove_shape() — все tc-классы и фильтры удаляются
  2. Снимается flock на PID-файле, файл удаляется
  3. Выход с кодом 0

systemd ждёт graceful shutdown до TimeoutStopSec=30s, дальше SIGKILL. На практике укладывается в пару секунд даже при сотнях активных шейпов.

Что НЕ делается

  • IPv6. Сейчас только v4. Если нужен v6 — нужно дописать отдельную ветку с inet6 и protocol ipv6 фильтрами.
  • Прометей-метрики. Если кому-то надо — экспорт можно прикрутить отдельным процессом, читающим лог.
  • Глобальный лимит. Шейпер работает per-IP, не считает суммарную нагрузку. Если 100 клиентов одновременно качают по 50 Мбит/с — шейпер может не успеть всех зарезать сразу. На практике одновременных «качков» обычно единицы.
  • Поддержка других прокси. Хардкод фильтра "telemt" в выводе ss. Для xray/sing-box/etc нужно править исходник.