tc (MARK): ограничение скорости по порту
tc + iptables (MARK) — способ ограничить скорость конкретного трафика на уровне ядра Linux. Например, если HTTP-прокси слушает порт 8080, можно задать жёсткий лимит независимо от его конфигурации.
Задача
Debian, сервис на порту 8080. Нужно ограничить скорость до 1 Mbit/s.
Помечаем трафик
iptables -t mangle -A OUTPUT -p tcp --sport 8080 -j MARK --set-mark 10
iptables -t mangle -A PREROUTING -p tcp --dport 8080 -j MARK --set-mark 10
Трафик прокси получает метку 10. Дальше tc будет работать именно с этой меткой.
Настройка tc (HTB)
Интерфейс допустим eth0.
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 1mbit ceil 1mbit
tc filter add dev eth0 parent 1: protocol ip handle 10 fw flowid 1:10
Что означает каждая строка
1.
tc qdisc add dev eth0 root handle 1: htb default 30
qdisc— очередь обработки трафика.root— корневая дисциплина для интерфейса.handle 1:— идентификатор этой очереди.htb— Hierarchical Token Bucket (иерархическое ограничение скорости).default 30— класс по умолчанию для трафика без фильтра.
2.
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
parent 1:— родительский qdisc.classid 1:1— основной класс.rate 100mbit— базовая пропускная способность (например максимум интерфейса).
3.
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 1mbit ceil 1mbit
parent 1:1— дочерний класс основного.classid 1:10— идентификатор класса.rate— гарантированная скорость (минимум, который класс может использовать).ceil— максимальная скорость, выше которой класс не поднимется.
Если rate и ceil одинаковые — лимит жёсткий.
Если ceil больше rate — класс может «разгоняться» до указанного потолка при свободном канале.
4.
tc filter add dev eth0 parent 1: protocol ip handle 10 fw flowid 1:10
handle 10— та самая MARK из iptables.fw— фильтрация по firewall-метке.flowid 1:10— направляет помеченный трафик в класс 1:10.
Mbit и MB
1mbit ≈ 125 KB/s.
Если нужно примерно 1 MB/s — указывать 8mbit.
Проверка
tc -s class show dev eth0
Покажет активные классы и статистику переданных байт.
Итог
Ограничение работает на уровне ядра, не зависит от приложения и подходит для прокси, VPN, веб-сервисов и любого другого трафика, который можно пометить через iptables.
Входящий трафик (ingress)
С входящим трафиком всё иначе например : напрямую замедлить входящий поток на интерфейсе нельзя. Обычно делают костыль через IFB — виртуальный интерфейс, куда перенаправляют входящий трафик, и уже там применяют шейпинг.
1) Создаём IFB
modprobe ifb
ip link add ifb0 type ifb
ip link set ifb0 up
2) Включаем ingress qdisc на eth0
tc qdisc add dev eth0 handle ffff: ingress
3) Перенаправляем входящий трафик (порт 8080) на ifb0
tc filter add dev eth0 parent ffff: protocol ip \
u32 match ip dport 8080 0xffff \
action mirred egress redirect dev ifb0
4) Ограничиваем скорость уже на ifb0
tc qdisc add dev ifb0 root handle 2: htb
tc class add dev ifb0 parent 2: classid 2:1 htb rate 1mbit ceil 1mbit
Проверка для входящего:
tc -s class show dev ifb0
Теперь получается так: исходящий трафик шейпится на eth0, а входящий — через ifb0.
Чтобы НЕ пропадало после ребута (systemd)
tc и правила iptables после перезагрузки обычно не сохраняются. Проще всего сделать запуск через systemd.
1) Скрипт
nano /usr/local/sbin/limit-proxy.sh
Очистка старых правил (чтобы не плодились дубли)
Если скрипт запускается несколько раз, правила firewall могут накапливаться. Перед добавлением можно удалить старые правила по номеру строки.
# удалить все совпадения OUTPUT (sport 8080, MARK 10)
while iptables -t mangle -C OUTPUT -p tcp --sport 8080 -j MARK --set-mark 10 2>/dev/null; do
iptables -t mangle -D OUTPUT -p tcp --sport 8080 -j MARK --set-mark 10
done
# удалить все совпадения PREROUTING (dport 8080, MARK 10)
while iptables -t mangle -C PREROUTING -p tcp --dport 8080 -j MARK --set-mark 10 2>/dev/null; do
iptables -t mangle -D PREROUTING -p tcp --dport 8080 -j MARK --set-mark 10
done
#!/bin/bash
DEV=eth0
MARK=10
# очистка дублей iptables (mangle)
while iptables -t mangle -C OUTPUT -p tcp --sport 8080 -j MARK --set-mark $MARK 2>/dev/null; do
iptables -t mangle -D OUTPUT -p tcp --sport 8080 -j MARK --set-mark $MARK
done
while iptables -t mangle -C PREROUTING -p tcp --dport 8080 -j MARK --set-mark $MARK 2>/dev/null; do
iptables -t mangle -D PREROUTING -p tcp --dport 8080 -j MARK --set-mark $MARK
done
# iptables
iptables -t mangle -A OUTPUT -p tcp --sport 8080 -j MARK --set-mark $MARK
iptables -t mangle -A PREROUTING -p tcp --dport 8080 -j MARK --set-mark $MARK
# tc
tc qdisc del dev $DEV root 2>/dev/null
tc qdisc add dev $DEV root handle 1: htb default 30
tc class add dev $DEV parent 1: classid 1:1 htb rate 100mbit
tc class add dev $DEV parent 1:1 classid 1:$MARK htb rate 1mbit ceil 1mbit
tc filter add dev $DEV parent 1: protocol ip handle $MARK fw flowid 1:$MARK
chmod +x /usr/local/sbin/limit-proxy.sh
2) systemd unit
nano /etc/systemd/system/limit-proxy.service
[Unit]
Description=Limit HTTP proxy bandwidth (port 8080)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/limit-proxy.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable limit-proxy.service
Теперь переживёт любой reboot.
Глава 2: nftables + tc (MARK)
Если система переведена на nftables и хочется вообще не трогать iptables, то меняется только маркировка. tc остаётся тем же, потому что он работает с fwmark.
Помечаем трафик (nftables)
Маркировка делается через meta mark. Ниже минимальный вариант, который ставит метку 10 для трафика прокси на 8080.
# таблица (если ещё нет)
nft add table inet mangle
# цепочка для исходящего (если ещё нет)
nft 'add chain inet mangle output { type route hook output priority mangle; }'
# цепочка для входящего (если ещё нет)
nft 'add chain inet mangle prerouting { type filter hook prerouting priority mangle; }'
# исходящий трафик прокси (локальный сервис, sport 8080)
nft 'add rule inet mangle output tcp sport 8080 meta mark set 10'
# входящий трафик к прокси (dport 8080)
nft 'add rule inet mangle prerouting tcp dport 8080 meta mark set 10'
Настройка tc (та же самая)
Фильтр fw читает метку из firewall. Не важно, кто её поставил — iptables или nftables.
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 1mbit ceil 1mbit
tc filter add dev eth0 parent 1: protocol ip handle 10 fw flowid 1:10
Входящий трафик (ingress) — тот же IFB
Для входящего shaping всё остаётся таким же: IFB + redirect. Маркировка nftables тут не обязательна, потому что мы матчим порт прямо в tc filter.
modprobe ifb
ip link add ifb0 type ifb
ip link set ifb0 up
tc qdisc add dev eth0 handle ffff: ingress
tc filter add dev eth0 parent ffff: protocol ip \
u32 match ip dport 8080 0xffff \
action mirred egress redirect dev ifb0
tc qdisc add dev ifb0 root handle 2: htb
tc class add dev ifb0 parent 2: classid 2:1 htb rate 1mbit ceil 1mbit
Чтобы НЕ пропадало после ребута (systemd) — nftables версия
Принцип тот же, только вместо iptables внутри скрипта — nft.
# чтобы не плодились дубли — сначала гарантируем наличие таблицы и цепочек
nft add table inet mangle 2>/dev/null
nft 'add chain inet mangle output { type route hook output priority mangle; }' 2>/dev/null
nft 'add chain inet mangle prerouting { type filter hook prerouting priority mangle; }' 2>/dev/null
# теперь можно безопасно очистить правила
nft flush chain inet mangle output 2>/dev/null
nft flush chain inet mangle prerouting 2>/dev/null
1) Скрипт
nano /usr/local/sbin/limit-proxy-nft.sh
#!/bin/bash
DEV=eth0
MARK=10
# nftables — гарантируем структуру
nft add table inet mangle 2>/dev/null
nft 'add chain inet mangle output { type route hook output priority mangle; }' 2>/dev/null
nft 'add chain inet mangle prerouting { type filter hook prerouting priority mangle; }' 2>/dev/null
# очистка старых правил
nft flush chain inet mangle output 2>/dev/null
nft flush chain inet mangle prerouting 2>/dev/null
# добавляем правила заново
nft add rule inet mangle output tcp sport 8080 meta mark set $MARK
nft add rule inet mangle prerouting tcp dport 8080 meta mark set $MARK
# tc
tc qdisc del dev $DEV root 2>/dev/null
tc qdisc add dev $DEV root handle 1: htb default 30
tc class add dev $DEV parent 1: classid 1:1 htb rate 100mbit
tc class add dev $DEV parent 1:1 classid 1:$MARK htb rate 1mbit ceil 1mbit
tc filter add dev $DEV parent 1: protocol ip handle $MARK fw flowid 1:$MARK
chmod +x /usr/local/sbin/limit-proxy-nft.sh
2) systemd unit
nano /etc/systemd/system/limit-proxy-nft.service
[Unit]
Description=Limit HTTP proxy bandwidth (port 8080) using nftables + tc
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/limit-proxy-nft.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable limit-proxy-nft.service
Теперь также переживёт любой reboot.
Уточнения например
1) OUTPUT и контейнеры
Правило: iptables -t mangle -A OUTPUT -p tcp --sport 8080 -j MARK --set-mark 10 работает для локального сервиса, запущенного непосредственно на хосте.
Если прокси работает в Docker / LXC / другом network namespace, трафик может проходить через цепочку FORWARD, а не OUTPUT. В таком случае маркировку нужно настраивать на соответствующем уровне (например, в FORWARD или внутри контейнера).
2) Ingress: policing vs shaping
Непосредственно замедлить входящий поток на интерфейсе нельзя — ingress qdisc поддерживает только policing (drop при превышении лимита).
Для полноценного shaping входящего трафика используется IFB — входящий поток перенаправляется на виртуальный интерфейс и уже там применяется обычный HTB.
3) nftables: flush chain
Команды вида: nft flush chain inet mangle output удаляют все правила в указанной цепочке.
В примере предполагается, что цепочка используется только для этого сервиса. Если в ней есть другие правила — они будут удалены. Поэтому нужно будет из скрипта убрать удаление старых правил, если они есть.
4) Родительский класс HTB
В примере используется: rate 100mbit для родительского класса.
Это значение должно соответствовать реальной пропускной способности интерфейса. Если интерфейс гигабитный — имеет смысл указать 1000mbit. Иначе можно искусственно занизить общий потолок скорости.
5) burst и cburst (важно при низких скоростях)
HTB использует механизм token bucket. Параметр burst задаёт, сколько байт класс может отправить сверх "моментального" лимита без ожидания новых токенов.
Если указано: rate = ceil и burst не задан вручную, ядро рассчитывает его автоматически. При низких скоростях (например 1mbit и ниже) это значение может оказаться небольшим.
Слишком маленький burst делает ограничение жёстким: пакеты отправляются строго по расписанию токенов, что может увеличить задержки (RTT) и при перегрузке вызвать TCP retransmit.
Для более плавного поведения имеет смысл задать burst явно. Например:
tc class add dev eth0 parent 1:1 classid 1:10 htb \
rate 1mbit ceil 1mbit \
burst 32k cburst 32k
burst— буфер для гарантированной скорости (rate)cburst— буфер для потолка (ceil)
Больший burst сглаживает трафик и снижает jitter. Слишком большой — может временно допускать короткие всплески выше лимита.
Для скоростей 1–10 mbit обычно достаточно 16–64 KB. Оптимальное значение зависит от RTT и характера нагрузки (HTTP, VPN, прокси, QUIC и т.д.).
6) Скрипты ограничивают только исходящий трафик
Приведённые systemd-скрипты настраивают root qdisc на интерфейсе (например eth0), то есть ограничивают только исходящий (egress) трафик.
Настройка входящего (ingress) shaping через IFB в скрипты не включена. В статье она показана отдельно, но автоматически при перезагрузке не применяется.
Если требуется ограничение входящего трафика после reboot, необходимо дополнительно:
- загружать модуль
ifb - создавать интерфейс
ifb0 - добавлять ingress qdisc на основной интерфейс
- настраивать shaping на
ifb0
Без этого будет ограничиваться только исходящий поток.
7) Альтернативы и расширение сценариев
В статье рассматривается классическая схема tc + HTB + fwmark. Не упоминаются более современные или упрощённые альтернативы, такие как:
cake(Common Applications Kept Enhanced) — современный qdisc с встроенным AQM и автонастройкойwondershaper— обёртка надtcдля быстрого ограничения интерфейсаsystemd-networkd— поддерживает встроенный traffic shaping на уровне конфигурации интерфейса
Эти инструменты могут быть проще в настройке, но имеют другую философию и уровень абстракции. В данной статье используется низкоуровневый подход с прямым управлением классами HTB.
Также приведён пример только для одного TCP-порта (8080). Не рассматриваются варианты:
- ограничение по подсети (
-s 192.168.1.0/24) - ограничение по конкретному IP-адресу
- ограничение нескольких портов
- комбинация нескольких классов с разными лимитами
Все эти сценарии реализуются аналогично — через соответствующую маркировку (iptables/nftables) и добавление дополнительных классов HTB.