Ограничение скорости и трафика на пользователя VPN-сервера
Если на одном VPN-сервере сидит несколько пользователей — семья, команда, клиенты реселлера — рано или поздно один из них займёт весь канал торрентом или бэкапом, и у остальных начнутся тормоза. Без учёта трафика вы вообще не узнаете, кто и сколько потребляет, пока не придёт счёт от провайдера за перерасход или сервер не упрётся в лимит порта. Разберём, как посчитать трафик по каждому пользователю и ограничить ему скорость средствами Linux — tc и iptables, без сторонних панелей.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Зачем лимитировать скорость и трафик на общем сервере
На выделенном VPN-сервере для одного человека шейпинг обычно не нужен — весь канал и так его. Но задача меняется, если:
- сервер общий на несколько пользователей (семья, друзья, небольшая команда) и нужна справедливость — никто не должен вытеснять остальных;
- вы поднимаете VPN как инфраструктуру для нескольких клиентов с разными тарифами по скорости;
- у сервера лимитированный исходящий трафик по тарифу хостера, и превышение означает доплату или отключение;
- нужно поймать аномалию — скомпрометированный конфиг клиента, который внезапно гоняет через сервер терабайты.
Решение во всех случаях одно: считать трафик по каждому клиенту отдельно и, если нужно, резать скорость или отключать при превышении квоты. Ниже — как это сделать для WireGuard и OpenVPN, двух самых частых протоколов.
Учёт трафика по пользователю: что реально работает
Первая ошибка — пытаться считать VPN-трафик через vnstat на интерфейсе. vnstat отлично показывает трафик по интерфейсу целиком, но у типичного VPN-сервера все клиенты сидят на одном и том же wg0 или tun0 — vnstat не различит, кто из них накачал 500 ГБ, а кто 2 ГБ. vnstat пригодится только если вы физически развели клиентов по разным интерфейсам (что избыточно для большинства сценариев).
Три рабочих варианта учёта по пользователю:
1. Встроенные счётчики WireGuard. У WireGuard есть штатная команда, которая показывает трафик по каждому peer:
wg show wg0 transfer
Вывод — публичный ключ, полученные и отправленные байты:
1c3F...aB2Q= 184320000 93184000
9kLp...zR7w= 2481200000 512000000
Счётчики обнуляются при перезапуске интерфейса, поэтому для истории их нужно периодически снимать и накапливать — скриптом на cron раз в 5 минут, дописывающим значения в файл или SQLite.
2. Статус-лог OpenVPN. Если сервер запущен с директивой status, OpenVPN сам пишет файл со списком клиентов и их трафиком:
status /var/log/openvpn/status.log
status-version 2
В файле по каждому подключённому клиенту есть строка вида:
CLIENT_LIST,ivan,203.0.113.5:41221,10.8.0.10,,2481200000,512000000,...
Пятое и шестое числа — байты, полученные и отправленные сервером от этого клиента. Файл перезаписывается с заданным интервалом (status <file> <seconds>), так что можно просто парсить его по cron.
3. Счётчики iptables/nftables по IP клиента. Универсальный вариант, если протокол не даёт удобной встроенной статистики или нужно единое место учёта для WireGuard и OpenVPN сразу. Создаём отдельную цепочку учёта на каждого клиента:
iptables -N acct_ivan
iptables -A FORWARD -s 10.8.0.10/32 -j acct_ivan
iptables -A FORWARD -d 10.8.0.10/32 -j acct_ivan
iptables -A acct_ivan -j RETURN
Смотрим накопленные байты:
iptables -L acct_ivan -v -x -n
Флаг -x обязателен — без него iptables округляет большие числа до K/M/G, что для точного учёта не годится. Счётчики тоже сбрасываются при перезагрузке и обнуляются командой iptables -Z, поэтому логика накопления та же: снимать дельту периодически и складывать в постоянное хранилище.
| Способ | Работает для | Точность | Переживает reboot |
|---|---|---|---|
wg show transfer | WireGuard | По peer | Нет, нужен внешний сборщик |
status.log | OpenVPN | По client-cn | Нет, нужен внешний сборщик |
| iptables-счётчики | Любой протокол | По IP клиента | Нет, нужен внешний сборщик |
| vnstat | Интерфейс целиком | Не по пользователю | Да (своя БД) |
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверОграничение скорости через tc: общая схема HTB
Для шейпинга используется tc (traffic control) с дисциплиной очереди HTB (Hierarchical Token Bucket) — она задаёт гарантированную и максимальную скорость на класс трафика и делит канал между классами. Схема простая: корневой qdisc, под ним классы с лимитами и фильтр, направляющий пакеты клиента в нужный класс по IP.
Ограничение исходящего от сервера к клиенту трафика (то, что клиент скачивает) вешается на исходящую очередь VPN-интерфейса:
tc qdisc add dev wg0 root handle 1: htb default 30
tc class add dev wg0 parent 1: classid 1:1 htb rate 1000mbit
tc class add dev wg0 parent 1:1 classid 1:10 htb rate 20mbit ceil 20mbit
tc filter add dev wg0 protocol ip parent 1:0 prio 1 u32 \
match ip dst 10.8.0.10/32 flowid 1:10
Это ограничит скорость к клиенту с туннельным IP 10.8.0.10 до 20 Мбит/с. Но tc из коробки шейпит только исходящий с интерфейса трафик — значит, входящий на сервер трафик (то, что клиент заливает наружу) через обычный dev wg0 root не ограничить. Для этого нужен трюк с виртуальным интерфейсом IFB:
modprobe ifb numifbs=1
ip link set ifb0 up
tc qdisc add dev wg0 ingress
tc filter add dev wg0 parent ffff: protocol ip u32 \
match ip src 10.8.0.10/32 \
action mirred egress redirect dev ifb0
tc qdisc add dev ifb0 root handle 1: htb default 30
tc class add dev ifb0 parent 1: classid 1:10 htb rate 20mbit ceil 20mbit
tc filter add dev ifb0 protocol ip parent 1: prio 1 u32 \
match ip src 10.8.0.10/32 flowid 1:10
Входящий от клиента трафик зеркалится на ifb0, и уже там к нему применяется тот же HTB-класс. В сумме получается симметричный лимит 20 Мбит/с в обе стороны для одного клиента.
Для сравнения — более простые дисциплины очереди, которые тоже встречаются:
| Дисциплина | Что умеет | Когда уместна |
|---|---|---|
| HTB | Иерархия классов, гарантия + потолок (ceil), приоритеты | Несколько клиентов с разными тарифами |
| TBF | Один простой лимит скорости на весь интерфейс | Быстрый общий лимит без деления на пользователей |
police в фильтре | Жёсткий обрез (drop) без буферизации | Простое ограничение входящего трафика без ifb |
Если не хочется возиться с ifb, для входящего трафика можно обойтись фильтром с action police прямо на ingress — он просто дропает лишние пакеты сверх заданной скорости в одну команду, без честной очереди и плавного сглаживания:
tc filter add dev wg0 parent ffff: protocol ip u32 \
match ip src 10.8.0.10/32 \
police rate 20mbit burst 100k drop flowid :1
Шейпинг на WireGuard: привязка к peer
У WireGuard каждому клиенту соответствует статический туннельный IP из AllowedIPs — удобный стабильный идентификатор для tc-фильтров, в отличие от внешнего IP клиента.
Практичный подход — держать соответствие "публичный ключ → туннельный IP → лимит скорости" в отдельном файле и накатывать правила tc скриптом при поднятии интерфейса, через PostUp в /etc/wireguard/wg0.conf:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server_key>
PostUp = /usr/local/bin/wg-shape.sh add
PostDown = /usr/local/bin/wg-shape.sh del
[Peer]
# ivan, 20 Мбит/с
PublicKey = 1c3F...aB2Q=
AllowedIPs = 10.8.0.10/32
Скрипт wg-shape.sh читает список IP:лимит и накатывает связку class + filter для каждого клиента — по образцу команд из предыдущего раздела, в цикле. Это заметно удобнее, чем прописывать tc-правила руками при каждом добавлении клиента.
Шейпинг на OpenVPN: ccd и стабильный IP клиента
С OpenVPN сложнее, потому что по умолчанию IP клиенту выдаётся динамически из пула при каждом подключении — а фильтр tc должен знать конкретный IP заранее. Решение — client-config-dir (ccd), который закрепляет за клиентом постоянный адрес:
# /etc/openvpn/server.conf
client-config-dir /etc/openvpn/ccd
# /etc/openvpn/ccd/ivan
ifconfig-push 10.8.0.10 255.255.255.0
После этого клиент ivan всегда получает 10.8.0.10, и к этому адресу применимы те же tc-команды, что и в примере с WireGuard, только на интерфейсе tun0. Если клиентов много, tc-правила удобно генерировать скриптом, который проходит по файлам в ccd/ и читает лимит из отдельного ini-файла с тарифами.
Есть нюанс: если сервер использует --client-connect скрипт (например, для логирования), туда же можно добавить вызов наката tc-правил при подключении конкретного клиента — тогда правила будут появляться и исчезать вместе с сессией.
Квоты трафика: считаем и отключаем при превышении
Ограничение скорости не мешает клиенту накачать терабайты за месяц на низкой скорости — для этого нужна отдельная логика квот поверх учёта из второго раздела. Общая схема:
- Раз в N минут cron-скрипт снимает текущие счётчики (
wg show transfer,status.logилиiptables -L -v -x). - Считает дельту от предыдущего снятия (учитывая, что счётчик мог обнулиться при перезапуске — если новое значение меньше старого, считаем, что был reboot, и просто прибавляем новое значение как есть).
- Копит накопленный трафик за период (например, за календарный месяц) в файле или SQLite.
- Если накопленный трафик превысил квоту — применяет действие.
Действие при превышении квоты — на выбор:
- Жёсткое отключение. Для WireGuard —
wg set wg0 peer <pubkey> remove(или обнулитьAllowedIPs, чтобы пакеты просто не матчились). Для OpenVPN — командаkill <common_name>через management-интерфейс, либо DROP-правило iptables на IP клиента из ccd. - Мягкое ограничение. Вместо отключения — резко снизить скорость через tc (например, до 512 Кбит/с), чтобы клиент понял, что квота исчерпана, но не потерял связь совсем. Технически это изменение
rate/ceilу существующего HTB-класса командойtc class change, без пересоздания фильтров — и обычно удобнее жёсткого отключения.
Пример шага 2 на bash для WireGuard, вызывается по cron:
#!/bin/bash
STATE=/var/lib/wg-quota/state.tsv
QUOTA_BYTES=$((100 * 1024 * 1024 * 1024)) # 100 ГБ
wg show wg0 transfer | while read -r pubkey rx tx; do
total=$((rx + tx))
prev=$(awk -v k="$pubkey" '$1==k{print $2}' "$STATE")
prev=${prev:-0}
if [ "$total" -lt "$prev" ]; then
# интерфейс перезапускался, считаем текущее значение как есть
acc=$total
else
acc=$((prev))
acc=$((acc + total - prev))
fi
sed -i "/^$pubkey/d" "$STATE"
echo -e "$pubkey\t$acc" >> "$STATE"
if [ "$acc" -gt "$QUOTA_BYTES" ]; then
/usr/local/bin/wg-shape.sh throttle "$pubkey"
fi
done
Это каркас, а не готовое production-решение — в реальном скрипте нужно аккуратнее обработать конкурентный доступ к файлу состояния и сброс накопителя в начале нового расчётного периода, но идея та же: снять счётчик → сравнить с квотой → применить действие.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
tc-правила пропадают после перезагрузки сервера — это нормально?
Да, tc и учётные цепочки iptables не персистентны сами по себе. Правила нужно накатывать заново при старте — через PostUp в конфиге WireGuard или systemd-юнитом после поднятия интерфейса. Пакет iptables-persistent сохраняет сами правила, но накопленные счётчики байт при перезагрузке всё равно обнулятся.
Можно ли ограничить скорость на VPS с виртуализацией OpenVZ/LXC вместо KVM?
Не всегда. В части контейнерных виртуализаций доступ к tc и модулю ifb ограничен политикой хостера — модули ядра общие на весь физический сервер. Проверьте заранее: tc qdisc add dev eth0 root handle 1: htb default 30 && echo OK. На KVM таких ограничений обычно нет.
iptables или nftables — что использовать для учёта?
Логика та же, просто синтаксис другой (nftables использует именованные counter-объекты). Если на сервере уже nftables как основной фаервол — используйте его counters, чтобы не держать два инструмента параллельно.
Есть готовые панели с лимитами из коробки?
Частично — например, панель wg-easy для WireGuard упрощает управление клиентами через веб-интерфейс, но встроенных квот и шейпинга в базовой версии нет — это по-прежнему придётся добавлять поверх скриптами tc.
Что делать, если у клиента резко вырос трафик — это может быть компрометация конфига?
Заведите алерт по резкому скачку (тот же скрипт учёта, но с порогом "трафик за час выше обычного в N раз") — это ловит утечку ключа раньше, чем набежит счёт от хостера за перерасход.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →