Шейпинг в ipfw

Думаю, многим сисадминам не раз и не два приходилось сталкиваться с проблемой ограничения скорости канала тому или иному юзеру/группе_юзеров. Как правило, ограничивать нужно качальщиков всего и вся, иногда начальство, которое любит качать фильмы. Столкнулся с такой проблемой и я. Наиболее очевидное решение – пускать весь трафик через проксик (у меня используется Squid), а уже на нём ограничивать скорость скачки. Однако данный метод не всегда подходит. Например, человек приносит на работу свой личный ноутбук, подключается к внутренней wi-fi сетке и запускает торрент-клиент, качая целый день новинки кинорынка. Не переставлять же ему постоянно галочки «Использовать/Не использовать прокси-сервер»? Поэтому и было решено попробовать ограничить скорость непосредственно на уровне гейта.
В качестве гейта используется машина с FreeBSD 7.1, ipfw + natd. Интернет раздаёт связка squid + rejik, однако, как я уже говорил выше, резать трафик будем на уровне гейта, а именно ipfw. Вот так вот я и подошёл к понятию шейпинг. Шейпинг — это одно из направлений ритмической гимнастики, призванное целенаправленно изменять фигуру. Такое вот определение даст нам в числе первых Гугл. В нашем же случае под термином “шейпинг” мы будем подразумевать механизм, который позволяет нам целенаправленно изменять ширину Интернет-канала для того или иного пользователя сети. Тот же Гугл даёт нам неплохое количество информации на тему шейпинга в ipfw, однако большинство этой инфы является передратыми друг у друга конфигами файрвола, при этом толком ничего не объясняется: что, куда, зачем, а главное подводные камни. Я вот в силу своих своих возможностей попробую это сделать.

Итак, мы имеем: Интернет-канал 2 Мбит/сек, гейт FreeBSD 7.1 + ipfw + natd.

Задача: на локальной машине с ip-адресом 192.168.50.150 ограничить скорость скачки в пределах 1 Мбит/сек
На всякий случай: Все операции производятся на гейте, клиент нас не интересует вообще.
Для начала пересобираем ядро, нас интересует формирователь трафика dummynet

# cd /usr/src/sys/i386/conf
# vi MYKERNEL

Добавляем туда строчку:

options DUMMYNET

Далее перекомпилируем ядро, на FreeBSD 7 и 8 проблем быть не должно:

# cd /usr/src
# make buildkernel KERNCONF=MYKERNEL
# make installkernel KERNCONF=MYKERNEL

Ядро перекомпилировано, перезагружаемся.
Изначальный конфиг файрвола:

00100 allow ip from any to any via fxp0 # разрешаем трафик внутри локалки
00200 allow ip from any to any via lo0 # loopback-интерфейс
01000 divert 8668 ip from any to any in via vr0 # заворачиваем весь входящий трафик на natd
01100 check-state # чекаем динамические правила
01102 skipto 10000 icmp from any to any icmptypes 0,8,11 keep-state # разрешаем icmp-пакеты, но не все
01103 skipto 10000 ip from me to any out setup via vr0 uid root keep-state # разрешаем исходящий трафик суперпользователю
01200 skipto 10000 tcp from any to any dst-port 53 out setup via vr0 keep-state # DNS transfer zone
01201 skipto 10000 udp from any to any dst-port 53 out via vr0 keep-state # разрешаем запросы DNS
08000 allow ip from any to me dst-port 11222 setup in via vr0 keep-state # демон ssh, порт нестандартный
09000 deny ip from any to any in via vr0 # режем весь входящий трафик, что не попал в вышестоящие правила
09100 deny ip from any to any out via vr0 # режем весь исходящий трафик, что не попал в вышестоящие правила
10000 divert 8668 ip from any to any out via vr0 # заворачиваем на NAT всё, что не порезалось
11001 allow ip from any to any # пропускаем всё, что вернулось с natd
65535 deny ip from any to any # дефолтное правило, в нашем случае на него ничего не доходит (как показывает практика, какие-то байты сюда всё таки падают)

Это минимальный рабочий конфиг, здесь fxp0 — интерфейс, смотрящий в локалку, vr0 — интерфейс, смотрящий во внешку. Конфиг немного отличается от того, что обычно предлагают в Интернетах. ИМХО, считаю гораздо правильней гнать на NAT не всё и сразу, а только то, что удовлетворяет нашим правилам. Также применяются динамические правила (keep-state). Теперь разбираемся непосредственно с шейпингом, а точнее — с формирователем трафика dummynet. Вот небольшая цитата из мануала ipfw на русском языке:

Формирователь работает путем деления пакетов на потоки в соответствии с заданной пользователем
маской по различным полям заголовка IP. Пакеты, принадлежащие к одному потоку, затем передаются двум различным объектам — каналу или очереди.

Канал эмулирует связь с заданными пропускной способностью, задержкой при передаче, размером очереди и процентом потери пакетов. Пакеты проходят по каналу в соответствии с его параметрами.

Очередь — абстракция, используемая для реализации правил WF2Q+.
Очередь связывает с каждым потоком вес и соответствующий канал. Далее, все потоки, связанные с одним и тем же каналом, переключаются с частотой, определяемой каналом в соответствии с правилами WF2Q+.

Итак, сначала мы создаём канал, куда будем пускать трафик 192.168.50.150:

# ipfw pipe 1 config bw 1024 Kbit/s queue 20

У каналов есть несколько свойств, основные из них: пропускная способность (bw) и размер очереди пакетов (queue). С пропускной способностью думаю всё ясно: bw 1024 Kbit/s даёт нам нам тот самый мегабит ограничения. Размер очереди пакетов, queue. Дефолтное значение равно 50 слотам (можно кстати указывать ещё и в килобайтах). Руководство ipfw говорит о том, что 50 слотов – это обычный размер очереди для устройств Ethernet, мы тоже будем считать, что у нас Ethernet-соединение с размером пакета 1400 байт (по дефолту). Отталкиваться здесь нужно от скорости: чем меньше пропускная способность нашего канала, тем ниже число слотов. Я выставил для мегабита 20 слотов, однако тут можно экспериментировать, ничего страшного не произойдёт.

В общем, с каналом разобрались. Теперь создаём очередь для нашего пайпа:

# ipfw queue 1 config pipe 1 weight 100

Параметры те же самые, что и для канала (к примеру, размер мы можем указывать не при конфигурировании канала, а при добавлении этой самой очереди, при этом, правда, у нас в одной строчке будет примерно следующее: ipfw queue 2 config pipe 2 queue 20. Немного сбивает с толку, на мой взгляд). Также у очереди есть один важный параметр – weight, приоритет, принимает значения от 1 до 100. Чем выше число – тем выше приоритет. Он важен, когда очередей несколько, в нашем же случае очередь одна, ставим 100 (можно вообще ничего не ставить, указал его для наглядности).

С приготовлениями закончено, добавляем правила в файрвол (обращаем внимания на номера правил). Правил будет два.

# ipfw add 01101 skipto 10000 ip from 192.168.50.150 to any out via vr0 setup keep-state
# ipfw add 10001 queue 1 ip from any to 192.168.50.150

Первое правило передаёт весь трафик от нашего клиента на правило 10000, которое отдаёт пакеты на NAT. А так как наше правило 01101 динамическое, то и весь возвращающийся трафик к клиенту тоже будет пропускаться. Второе же правило как раз и заворачивает весь траф, уже пришедший из natd, в нашу очередь. Таким образом, конечный конфиг файрвола имеет вид:

00100 allow ip from any to any via fxp0
00200 allow ip from any to any via lo0
01000 divert 8668 ip from any to any in via vr0
01100 check-state
01101 skipto 10000 ip from 192.168.50.150 to any out setup via vr0 keep-state
01102 skipto 10000 icmp from any to any icmptypes 0,8,11 keep-state
01103 skipto 10000 ip from me to any out setup via vr0 uid root keep-state
01200 skipto 10000 tcp from any to any dst-port 53 out setup via vr0 keep-state
01201 skipto 10000 udp from any to any dst-port 53 out via vr0 keep-state
08000 allow ip from any to me dst-port 11122 setup in via vr0 keep-state
09000 deny ip from any to any in via vr0
09100 deny ip from any to any out via vr0
10000 divert 8668 ip from any to any out via vr0
10001 queue 1 ip from any to 192.168.50.150
11001 allow ip from any to any
65535 deny ip from any to any

Небольшое примечание:
В sysctl есть переменная net.inet.ip.fw.one_pass, по умолчанию она равна 1. При этом значении правило “10001 queue … “ является разрешающим, типа allow. Если же net.inet.ip.fw.one_pass равна 0, то трафик, попав в нашу очередь, выйдет из неё и пойдёт дальше по правилам файрвола. Иногда может и пригодиться.

Share