...
...
...

управление трафиком на Linux-маршрутизаторе небольшой сети

Подключился я однажды к ADSL и обнаружил, что хотя у него неплохие характеристики (8/1 Mbit in/out), но бывают ситуации, когда разные потоки трафика начинают мешать работе, играм... Даже пинг до гейта поднимался с 8 до 200-300 ms. Усугубляло ситуацию то, что я был не единственным пользователем этого подключения. То есть я в принципе могу (хотя это и нежелательно) притормозить выкачивание чего-то, когда хочется поиграть в quake, но что делать с другими пользователями?

Зарезать их на время игры – это, конечно, сильно :) но также нежелательно. Неудобно даже свои процессы постоянно прибивать/запускать. Ну и просто снижается КПД канала. Ради нескольких процентов трафика и минимальных лагов убивать все остальное... Поэтому начал копать то, что наваяли в Линуксе для разделения траффика по приоритетам и ограничения полосы. Применил это для роутера, который подключен в Интернет через ADSL-модем и раздает трафик пользователям через какой-то сетевой интерфейс (у меня их несколько и они объединены в виртуальный интерфейс bridge). По большому счету ничего не мешает применять мое решение и для других ситуаций, конечно, с некоторыми изменениями.

Результат моих трудов представляю вам на растерзание :)

теория

Для ограничения трафика я решил использовать только входящие в стандартное ядро (2.4, 2.6) модули. Это QoS, netfilter и стандартные пакеты для работы с ядром, входящие в большиство дистрибутивов - iproute2, iptables. Для большинства случаев этого достаточно.

Эксперименты показали, что эффективно управлять потоками пакетов можно только при неполной загрузке канала в обоих направлениях. То есть нужно ограничить скорость канала. Это очень важно! Достаточно забить одно направление на 100%, и качество связи упадет. Решать что важнее: ограничить пропускную способность канала на 10-30% или маяться с лагами в момент, когда он полностью забит - личное дело каждого.

Я описываю метод с ограничением скорости канала.

Теперь совсем немного теории. Мы не можем управлять входящим трафиком, который поступает ADSL-модему на вход. И это факт. Но большинство IP- потоков зависят от исходящего трафика и от дропания или задержки пакетов. Именно IP, а не только TCP. Так пишут во всех руководствах. Но я не буду управлять входящим трафиком через задержку исходящего, как обычно советуют. Не потому что это невозможно, а потому что это сложно и требуется долгая настройка, а потом и постоянная подстройка.

Например: тянем что-то по FTP. Входящий трафик 8 Mbit, а исходящий всего 200 Kbit, что в разы меньше пропускной способности исходящего канала. То есть для ограничения входящего FTP-трафика надо сделать исходящий меньше 200 Kbit. Но это только примерная величина и ее нужно подгонять. А ситуация может меняться...

Есть еще один важный момент: ADSL-модем - устройство с разной пропускной способностью для исходящих и входящих пакетов. Поэтому нужно ограничивать исходящий с роутера или сети трафик даже в том случае, если он не является подтверждением для входящих потоков. Например, поставили у себя какой-то FTP/WWW/игровой сервер, также в сети могут жить любители пирингового обмена. И своим небольшим (в 10 раз меньше входящего) трафиком они могут сильно испортить качество канала. Значит нужно делить исходящий трафик на исходящий с хоста/сети и подтверждения входящих пакетов.

Будем решать эту проблему в лоб, то есть ограничивать исходящий трафик для исходящих потоков отдельно и входящий для входящих тоже отдельно) Для примера берем мою сеть. Есть два интерфейса: ppp0 - для выхода в Интернет и br0 - интерфейс локальной сети.

На ppp0 будем контролировать только исходящий трафик и все ограничения делать, исходя из скорости передачи ADSL-модема наружу. Политику ограничений строить так, чтобы не забить исходящий канал, и только. Кроме некоторых особых ситуаций.
На br0 будем контролировать тоже только исходящий трафик (стандартная реализация QoS в линуксе не позволяет делать это для исходящего). Но так как это маршрутизатор, то это будет и контроль входящего на ppp0 трафика :)

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

настройка ядра

Для тех, кто собирает ядра сам, я дам общие советы (вряд ли они нуждаются в разжевывании мелочей :) Я буду давать названия для ядра 2.6. Для 2.4 используются аналогичные, но с другими путями.

В первую очередь нужно разрешить Device Drivers ->Networking support ->Networking options ->QoS and/or fair queueing.
Дальше собираем модулями или вставляем в ядро следующие вещи:
- HTB packet scheduler;
- SFQ queue;
- Packet classifier API;
- Firewall based classifier (NEW);
- U32 classifier.
Остальное на ваше усмотрение. Это минимальный набор для ограничения трафика.

ОК, перейдем от слов к делу. Вот пример скрипта поблочно, с комментариями после каждого блока.
Запускается так:

adsl_qos {start|stop|status} ppp_интерфейс адрес_интерфейса

#!/bin/sh
DEV_OUT=$2
IP=$3
QLEN_OUT=10
RATE_OUT=650
MTU_OUT=1400
DEV_IN=br0
RATE_IN=6000
QLEN_IN=1000
RATE_LOCAL=100mbit
IP_LOCAL=10.0.0.1
ICMP="match ip protocol 1 0xff"
TCP="match ip protocol 6 0xff"
UDP="match ip protocol 17 0xff"
DPORT="match ip dport"
SPORT="match ip sport"
SRC="match ip src"
DST="match ip dst"
U32="protocol ip u32"

Сделано это для запуска из ip-up/ip-down скрипта ppp.
xxx_OUT - относится к ADSL-интерфейсу, исходящему потоку.
xxx_IN - относится к интерфейсу локальной сети, но входящему на ADSL-модем потоку :)
Ставлю ограничение 650 Kbit для исходящего потока и 6000 Kbit для входящего.

status(){
echo "[qdisc out]"
tc -s -d qdisc show dev $DEV_OUT
echo
echo "[class out]"
tc -s -d class show dev $DEV_OUT
echo
echo "[filter out]"
tc -s filter show dev $DEV_OUT
echo
echo "---------------------------------------"
echo "[qdisc in]"
tc -s -d qdisc show dev $DEV_IN
echo
echo "[class in]"
tc -s -d class show dev $DEV_IN
echo
echo "[filter in]"
tc -s filter show dev $DEV_IN
exit
}

Без комментариев :)

stop(){
tc qdisc del dev $DEV_OUT root 2>/dev/null >/dev/null
tc qdisc del dev $DEV_IN root 2>/dev/null >/dev/null
}

Сбрасываю все в начальное состояние:

start_out(){
ip link set dev $DEV_OUT qlen $QLEN_OUT mtu $MTU_OUT
tc qdisc add dev $DEV_OUT root handle 1: htb default 17
tc class add dev $DEV_OUT parent 1: classid 1:1 htb rate ${RATE_OUT}kbit
tc class add dev $DEV_OUT parent 1:1 classid 1:10 htb rate $[$RATE_OUT/8]kbit ceil $[$RATE_OUT/3]kbit prio 0
tc class add dev $DEV_OUT parent 1:1 classid 1:11 htb rate 20kbit prio 1
tc class add dev $DEV_OUT parent 1:1 classid 1:12 htb rate $[$RATE_OUT/8]kbit ceil $[$RATE_OUT/3]kbit prio 2
tc class add dev $DEV_OUT parent 1:1 classid 1:13 htb rate $[$RATE_OUT/8]kbit ceil $[$RATE_OUT/3]kbit prio 3
tc class add dev $DEV_OUT parent 1:1 classid 1:14 htb rate $[$RATE_OUT/8]kbit ceil $[$RATE_OUT/3]kbit prio 4
tc class add dev $DEV_OUT parent 1:1 classid 1:15 htb rate $[$RATE_OUT/8]kbit ceil ${RATE_OUT}kbit prio 5
tc class add dev $DEV_OUT parent 1:1 classid 1:17 htb rate $[$RATE_OUT/8]kbit ceil ${RATE_OUT}kbit prio 7

Разделил исходящий трафик на 8 полос с разным приоритетом и разной пропускной способностью каждой полосы. Одну полосу (classid 1:16) пока ничем не занял – это резерв.

Поясню некоторые поля:
- rate - гарантированная полоса пропускания;
- ceil – максимальная полоса, если очереди с более высоким приоритетом не загружены;
- prio - приоритет полосы, 0 – максимальный.
Остальную абракадабру вычитывайте в документации ;)

Еще один момент. Трафик, не попадающий в полосы 1:10 - 1:16, по умолчанию идет в 1:17.
Ниже будут заданы фильтры, которые направляют пакеты в нужные полосы (классы).

Прошу обратить внимание на prio. Чем меньше значение, тем выше приоритет. Это не имеет отношения к приоритету полос. Это приоритет для выборки пакета фильтром. Иначе может получиться, что сначала отработает

tc filter add dev $DEV_OUT parent 1:0 $U32 $UDP classid 1:13

а

tc filter add dev $DEV_OUT parent 1:0 $U32 $DPORT 53 0xffff classid 1:12

будет сосать лапу :)

Я сделал несколько простых макросов, чтобы задание правил не было таким громоздким.

0xffff после порта - маска. Например, 22 0xfffe позволяет ловить и 23 порт.

Все условия, которые задаются в одной команде, работают как AND.

tc filter add dev $DEV_OUT parent 1:0 prio 2 $U32 $DPORT 53 0xffff classid 1:12

Важный момент. Тут не отслеживается тип пакета. Формат UDP- и TCP-пакета одинаковый в самом начале, и поэтому можно проверять условие на dest port сразу для этих двух протоколов.

tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $TCP $SPORT 22 0xfffe classid 1:10
tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $TCP $SPORT 222 0xfffe classid 1:10
tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $TCP $SPORT 2200 0xfffe classid 1:10
tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $TCP $DPORT 22 0xfffe classid 1:10
tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $TCP $DPORT 222 0xfffe classid 1:10
tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $TCP $DPORT 2200 0xfffe classid 1:10

Самым приоритетным для меня считаются исходящие соединения на внешние ssh/telnet-серверы. К тому же я использую нестандартные порты типа 222, 2200. Также хочу, чтобы те же сервисы, что подняты на роутере, были доступны снаружи без тормозов.
Выше стоит максимальное ограничение скорости в 1/3. Нечего забивать канал всякими scp и sftp!

tc filter add dev $DEV_OUT parent 1:0 prio 1 $U32 $ICMP classid 1:11

Пусть пингуется с максимальным качеством, но не более 30-50 пакетов в секунду (20kbit/8/60). Нечего флудить!

tc filter add dev $DEV_OUT parent 1:0 prio 2 $U32 $SPORT 53 0xffff classid 1:12
tc filter add dev $DEV_OUT parent 1:0 prio 2 $U32 $DPORT 53 0xffff classid 1:12

Пропускаю тут запросы к DNS-серверам наружу. Также у меня есть DNS-сервер на хосте, и он должен отвечать на запросы. С таким приоритетом следует маркировать пакеты для своих любимых сетевых игр, именно поэтому максимальная скорость разрешена в 1/3. Например:

tc filter add dev $DEV_OUT parent 1:0 prio 2 $U32 $UDP $DPORT 27505 0xffff classid 1:12
tc filter add dev $DEV_OUT parent 1:0 prio 2 $U32 $TCP $DPORT 4000 0xffff classid 1:12
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $UDP classid 1:13

Здесь - все прочие UDP-пакеты. Возможно, некоторые пиринговые сети будут жить в этой полосе. Но в основном здесь место для игрушек, которые не попали в более приоритетную полосу.

tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $DPORT 3389 0xffff classid 1:14
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $SPORT 3389 0xffff classid 1:14

Хочу использовать rdp на внешних серверах без сильных тормозов, но и не хочу мешать игрушкам. Также есть некий компьютер в локальной сети, на который можно зайти по rdp снаружи. Где-то в другом скрипте сделан dnat.

tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $DPORT 80 0xffff classid 1:15
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $DPORT 443 0xffff classid 1:15
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $DPORT 3128 0xffff classid 1:15
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $DPORT 8080 0xffff classid 1:15
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $SPORT 25 0xffff classid 1:15
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $SPORT 80 0xffff classid 1:15
tc filter add dev $DEV_OUT parent 1:0 prio 3 $U32 $TCP $SPORT 443 0xffff classid 1:15

Веб серфинг пусть получит приоритет повыше. Сюда же добавлять ICQ, IRC, SMTP, POP3 ...

Тут же ограничиваю свои веб- и почтовые сервисы. Приоритет выше, чем у пакетов по умолчанию.

tc qdisc add dev $DEV_OUT parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev $DEV_OUT parent 1:11 handle 11: sfq perturb 10
tc qdisc add dev $DEV_OUT parent 1:12 handle 12: sfq perturb 10
tc qdisc add dev $DEV_OUT parent 1:13 handle 13: sfq perturb 10
tc qdisc add dev $DEV_OUT parent 1:14 handle 14: sfq perturb 10
tc qdisc add dev $DEV_OUT parent 1:15 handle 15: sfq perturb 10
tc qdisc add dev $DEV_OUT parent 1:17 handle 17: sfq perturb 10
echo "Outbound shaping added to $DEV_OUT. Rate: ${RATE_OUT}Kbit/sec."
}

К каждому классу добавляются алгоритмы обработки очереди. Кстати можно вместо sfq выбрать другой тип очереди, например, esfq.

Esfq не входит в стандартное ядро и поэтому нужно устанавливать этот модуль отдельно (http://fatooh.org/esfq-2.6/).

Процедура для ограничения исходящего трафика закончена.

start_in(){
ip link set dev $DEV_IN qlen $QLEN_IN
tc qdisc add dev $DEV_IN root handle 1: htb default 17
tc class add dev $DEV_IN parent 1: classid 1:1 htb rate ${RATE_IN}kbit
tc class add dev $DEV_IN parent 1:1 classid 1:10 htb rate $[$RATE_IN/8]kbit prio 0
tc class add dev $DEV_IN parent 1:1 classid 1:11 htb rate $[$RATE_IN/8]kbit prio 1
tc class add dev $DEV_IN parent 1:1 classid 1:12 htb rate $[$RATE_IN/8]kbit ceil $[$RATE_IN/3]kbit prio 2
tc class add dev $DEV_IN parent 1:1 classid 1:13 htb rate $[$RATE_IN/8]kbit ceil $[$RATE_IN/3]kbit prio 3
tc class add dev $DEV_IN parent 1:1 classid 1:14 htb rate $[$RATE_IN/8]kbit ceil $[$RATE_IN/3]kbit prio 4
tc class add dev $DEV_IN parent 1:1 classid 1:15 htb rate $[$RATE_IN/8]kbit ceil ${RATE_IN}kbit prio 5
tc class add dev $DEV_IN parent 1:1 classid 1:17 htb rate $[$RATE_IN/8]kbit ceil ${RATE_IN}kbit prio 7

Примерно то же самое, только этот интерфейс работает с локальной сетью.

tc class add dev $DEV_IN parent 1: classid 1:2 htb rate $RATE_LOCAL prio 7

Через этот класс пропускается не транзитный с ADSL-модема трафик, а исходящий с роутера, поэтому используется другой, более высокий rate.
tc filter add dev $DEV_IN parent 1:0 prio 1 $U32 $SRC $IP_LOCAL classid 1:2
а это фильтр для него
tc filter add dev $DEV_IN parent 1:0 prio 2 $U32 $TCP $SPORT 22 0xfffe classid 1:10
tc filter add dev $DEV_IN parent 1:0 prio 2 $U32 $TCP $SPORT 222 0xffff classid 1:10
tc filter add dev $DEV_IN parent 1:0 prio 2 $U32 $TCP $SPORT 2200 0xffff classid 1:10
tc filter add dev $DEV_IN parent 1:0 prio 2 $U32 $ICMP classid 1:11
tc filter add dev $DEV_IN parent 1:0 prio 2 $U32 $SPORT 53 0xffff classid 1:12
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $UDP classid 1:13
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $TCP $SPORT 3389 0xffff classid 1:14
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $TCP $DST 10.0.0.100 $DPORT 3389 0xffff classid 1:14
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $TCP $SPORT 80 0xffff classid 1:15
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $TCP $SPORT 443 0xffff classid 1:15
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $TCP $SPORT 3128 0xffff classid 1:15
tc filter add dev $DEV_IN parent 1:0 prio 3 $U32 $TCP $SPORT 8080 0xffff classid 1:15

Тут используются обычно src port вместо dst port, потому что это входящий трафик.

Хотя никто не мешает вам сделать другие политики и наслаждаться бардаком :)

tc qdisc add dev $DEV_IN parent 1:2 handle 2: pfifo

Использую самую простую и легкую для процессора очередь pfifo для исходящего с роутера трафика (не транзитный!).

tc qdisc add dev $DEV_IN parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev $DEV_IN parent 1:11 handle 11: sfq perturb 10
tc qdisc add dev $DEV_IN parent 1:12 handle 12: sfq perturb 10
tc qdisc add dev $DEV_IN parent 1:13 handle 13: sfq perturb 10
tc qdisc add dev $DEV_IN parent 1:14 handle 14: sfq perturb 10
tc qdisc add dev $DEV_IN parent 1:15 handle 15: sfq perturb 10
tc qdisc add dev $DEV_IN parent 1:17 handle 17: sfq perturb 10
echo "Outbound shaping added to $DEV_IN. Rate: ${RATE_IN}Kbit/sec."
}

Процедура для ограничения входящего трафика закончена.

case "$1" in
start)
stop
start_in
start_out
;;
stop)
stop
echo "Shaping removed on $DEV_OUT/$DEV_IN."
;;

status)
status
;;

*)
echo "Usage: $0 {start|stop|status} ppp_interface ip"
esac

Теперь добавляем в /etc/ppp/ip-up что-то типа:

if [ $4 = "1.2.3.4" ]; then
/etc/ppp/adsl_qos start $1 $4
fi

в /etc/ppp/ip-down:

if [ $4 = "1.2.3.4" ]; then
/etc/ppp/adsl_qos stop $1 $4
fi

Проверку можно и не делать, но у меня еще есть и dialup-сервер и/или туннель на pppd.

Все. Если циферки RATE_IN/RATE_OUT подобраны правильно, то максимальная пропускная способность канала пострадает не сильно, но зато высокоприоритетные потоки не будут лагать даже при сильной загрузке в обоих направлениях одновременно.

Скорость входящих потоков реально зажимается без прямого воздействия на исходящие и дропы пакетов минимальны. Скорее всего это происходит из-за притормаживания пакета в очереди. Разумеется, от флуда это не спасет, но эти случаи лечат обычно административными методами.

Если критично, что ограничиваются пакеты на интерфейсе локальной сети, то нужно ставить IMQ-модуль и переносить обработку с $DEV_IN на интерфейс imq#.



Антон Щуко aka SG, системный администратор.


© Сетевые решения