Управление сетевым трафиком посредством очередей
Сети/интернет
Статья была опубликована 1 февраля 2010 года в 00:00, а последний раз правилась 11 июля 2010 года в 16:05.
Постоянная ссылка: http://www.nixp.ru/articles/44.html
Начну я с некоторых пояснений терминологии. Что есть очередь в данном контексте? Очередь — это механизм, позволяющий управлять приемом и передачей пакетов.
Данная статья является продолжением описания пакета маршрутизации GNU/Linux iproute2 (см. «Настройка маршрутизации с помощью iproute»). Далее будут описаны принципы управления сетевым трафиком посредством очередей. Эта тема зачастую не упоминается в руководствах, но на самом деле с помощью очередей сетевых пакетов можно выполнять широкий круг исключительно полезных задач. Приведу несколько примеров, которые часто встречаются на практике. Итак, очереди способны контролировать скорость передачи пакетов, ограничивая нежелательный сетевой трафик по скорости (позволяет избежать выход из строя сервера или отдельных демонов в результате DoS- и даже DDoS- атак). Позволяет осуществлять распределение нагрузки между несколькими сетевыми интерфейсами. С помощью очередей также можно добиться существенного увеличения производительности сети в целом при помощи разделения различных видов трафика (например, интерактивные данные должны обрабатываться быстрее) на основе поля ToS (type of service — тип услуг). Iproute2 также дает возможность ограничения SYN-flood и ICMP-dDoS атак. Кроме этого можно устанавливать свой предел скорости на основе различных фильтров.
Начну я с некоторых пояснений терминологии. Что есть очередь в данном контексте? Очередь — это механизм, позволяющий управлять приемом и передачей пакетов. Очередь является физическим объектом в памяти и содержит пакеты, поступившие на сетевой интерфейс. Чтобы понять принцип работы очередей (а именно возможности ограничения скорости) рассмотрим такой пример: пакет поступает в пустую очередь и передается на обработку, следующий пакет поступает в очередь и ждет обработки первого пакета (здесь под словом обработка подразумевается доставка пакета ядром на сокет-приемник), если в очередь поступает много пакетов, то они не успевают обрабатываться, и очередь растет. Если размер очереди ограничен (а он ограничен всегда), то следующие пакеты не попадают в очередь, а просто отбрасываются. Таким образом, регулируя длину очереди и зная время обработки одного пакета, можно вычислить наибольшую скорость передачи пакетов. Замечу, что утилита tc пакета iproute2, выполняющая контроль над очередями, осуществляет преобразование скорость->длина очереди автоматически, и вам необходимо указывать не непосредственно длину очереди, а лимит скорости в формате:
- mbit — мегабиты в секунду;
- kbit — килобиты в секунду;
- mbps — мегабайты в секунду;
- kbps — килобиты в секунду.
На деле реализация очередей несколько отличается от вышеприведенной, т.к. отбрасывание пакетов из конца очереди приводит к неприятным последствиям для TCP-протокола: например, когда сообщение потеряно, приложение-отправитель может рассматривать это как сигнал о том, что оно посылает пакеты слишком быстро. TCP реагирует на такой сигнал замедлением отправки сообщений. Но когда очередь полна, то часто несколько сообщений отбрасываются друг за другом — в результате целый ряд приложений решает замедлить передачу. После этого приложения зондируют сеть для определения ее загруженности и буквально через несколько секунд возобновляют передачу с прежним темпом, что опять приводит к перегрузке. Поэтому обычно очередь просто отбрасывает лишние пакеты от различных источников (обычно используется алгоритм генерации случайных чисел), если за определенный промежуток времени ее размер превзошел некий лимит. Такой механизм называется RED (случайное раннее обнаружение — Random Early Detection).
Вторым краеугольным камнем системы управления трафиком iproute2 является многополосность очередей. Представим себе, что у сетевого интерфейса 2 независимые очереди, имеющие различный приоритет. То есть пакеты, поступившие в очередь 2, будут обрабатываться, только если в очереди 1 пакетов, ждущих обработки, нет. Что же дает такая схема? С помощью механизма полос можно выделять трафик, требующий быстрой обработки, и помещать его в 1-ю полосу, в то время как остальной трафик, не требующий особенно высокой скорости (например ftp, smtp, pop), помещается в полосу с меньшим приоритетом (большим порядковым номером). Нечто подобное предлагают ATM или Frame-Relay сети, но iproute2 позволяет организовать подобное поведение на любом интерфейсе. Необходимость в такой системе возникает при передаче интерактивных данных, например, видео или голосовых (ip-телефония). Существует следующий подход к выделению интерактивного трафика: поле ToS — метка, присваиваемая пакету с помощью netfilter (--tos) или присваиваемая непосредственно сетевой службой (ftp, smtp…).
Поле ToS является ранней попыткой реализации сервиса QoS, обеспечивающей достаточно небольшой набор значений, приведу таблицу наиболее популярных протоколов и значения ToS для них:
- минимальная задержка — telnet, ftp (команды), ssh, smtp (команды), DNS (udp);
- максимальная полоса пропускания — ftp (данные), smtp (данные);
- максимальная надежность;
- минимальная стоимость.
В настоящее время основном используются значения «минимальная задержка» и «максимальная полоса пропускания» (хотя я не исключаю других вариантов).
Очереди, имеющие возможность фильтрации трафика по полосам, называют CBQ, (Class-Based Queuing — очередь, классифицирующая пакеты), хотя это и не совсем правильно, т.к. существует отдельный тип очереди cbq, поэтому очереди такого типа я в дальнейшем буду обозначать, как classfull. Команда tc имеет возможность управления множеством типов очередей, поэтому я бы хотел несколько подробнее остановиться на описании синтаксиса этой команды (тем более, что зачастую длинные цепочки команд tc зачастую выглядят весьма устрашающе).
Как и ip, команда tc может управлять несколькими сетевыми объектами. Их собственно три:
- qdisc — собственно управление очередями;
- class — управление определенными частями очереди, например, полосами;
- filters — управление фильтрами — фильтр определяет, в какую полосу очереди попадет тот или иной пакет (на основании определенных параметров пакета, например, протокола, порта, метки, и.т.д.).
Синтаксис команд следующий (нагло содрано мною из мануала :)):
tc qdisc [ add | change | replace | del | link ] dev DEV [ parent qdisc-id | root ] [ handle qdisc-id ] queue-type [ qdisc specific parameters ]
tc class [ add | change | replace | del ] dev DEV parent qdisc-id [ classid class-id ] queue-type [ qdisc specific parameters]
tc filter [ add | change | replace | del ] dev DEV [ parent qdisc-id | root ] protocol protocol prio priority filter-type [ filtertype specific parameters ] flowid flow-id
tc qdisc show [ dev DEV ] — показ очередей интерфейса.
tc class show dev DEV — показ классов сетевого устройства. tc filter show dev DEV — показ фильтров интерфейса DEV.
В основном отметьте последние 3 команды, т.к. назначение первых будет подробно описано далее на конкретных примерах.
Параметров у различных объектов очень много, чтобы описать их все, поэтому сейчас я бы хотел пояснить значение параметров parent qdisc-id и handle(classid) qdisc-id. На самом деле, при добавлении новой очереди к очереди-контейнеру (classfull-очереди, понятие будет рассмотрено далее) производится построение дерева объектов очередей (дерево содержится не в ядре, которое ничего не подозревает о наличии подочередей, а выбор подочереди для извлечения из нее пакета осуществляет очередь-контейнер).
Например:
tc qdisc add dev eth0 root handle 1: classful-queue [parameters]
Дерево выглядит так:
root 1:
Добавляем далее:
tc qdisc add dev eth0 parent 1: handle 10: queue-type [parameters] root 1: | 10:
Для примера добавлю еще одну очередь:
tc qdisc add dev eth0 parent 1: handle 20: queue-type [parameters] root 1:1 | \ 10: 20:
Как происходит извлечение пакета из очереди ядром? Ядро запрашивает очередь самого верхнего уровня (root) о необходимости извлечения пакета. Очередь верхнего уровня проверяет очереди нижнего уровня и выбирает ту, которая подходит по определенным параметрам (это зависит от типа очереди-контейнера и ее настроек), после осуществления выбора очередь верхнего уровня извлекает пакет и передает его выше (очереди, находящуюся выше по иерархии, или ядру, если это очередь root). Таким образом, сама иерархия построена по принципу минимальных знаний о нижних уровнях. Каждый уровень знает о существовании только одного нижнего уровня и осуществляет выбор только в рамках одного уровня. Так примерно можно представить схему уровней очередей:
kernel | root | level 1 / | \ level 2 level 2 level 2 | level 3
По моему мнению, построение дерева очередей — предмет большинства ошибок в скриптах, т.к. очень легко перепутать идентификаторы очередей. Для тех, кто собрался продумывать систему контроля трафика необходимо заранее спланировать (например, нарисовать схему на бумаге) будущее дерево, дабы избежать досадных промахов. Особенно важным становится этот аспект при построении сложных очередей, состоящих из нескольких простых очередей. Замена (удаление) очередей производится командой tc qdisc change(del) с указанием значений parent и handle:
# tc qdisc del dev eth0 root handle 1: queue-type
После некоторого чисто теоретического вступления настало время поговорить о типах очередей и о применении их на практике. Таковых типов существует 6. Среди них 3 типа являются classfull (возможность разделять трафик по полосам): prio, cbq, htb, а 3 являются обычными (classless): tbf, pfifo, sfq. Classless очереди являются более простыми объектами, чем classfull, и способны лишь устанавливать определенные ограничения на передачу трафика. Основное отличие 2 семейств очередей в том, что classfull-очереди содержат внутри себя classless «подочереди», отличающиеся приоритетом. Таким образом, classfull-очереди являются контейнерными объектами и позволяют выполнять разделение трафика по другим очередям (в терминологии traffic control — классами). Для начала я рассмотрю classless-очереди, а потом перейду к более сложным (но, несомненно, более интересным) classfull-очередям. TBF(token bucket filter) — простой тип очереди, не выполняющий разделения пакетов, который удерживает скорость передачи пакетов на примерно постоянном уровне (меньшем, чем реальная скорость интерфейса). При этом, если скорость передачи меньше заданного значения, то пакеты кладутся в очередь, пока не будет достигнут определенный размер очереди, который затем передается на обработку (но происходит некоторая задержка данных, т.к. происходит ожидание необходимого количества пакетов для поддержания постоянной скорости передачи). Если же скорость превышает заданную, то «лишние» пакеты просто отбрасываются. Такой тип очереди нельзя порекомендовать для сетей, где очень резко меняется загруженность, т.к. могут возникнуть неоправданные задержки при слабой загрузке сети и снижение пропускной способности интерфейса при большой загрузке. Основные параметры tbf:
- limit или latency — число байт, которые могут быть помещены в очередь для ожидания, фактически — максимальное время, которое может провести пакет в очереди (чем больше limit, тем сильнее увеличивается задержка данных при низкой загрузке интерфейса).
- burst (buffer или maxburst) — длина буфера очереди в байтах, чем больше заданная скорость передачи, тем больше должен быть буфер данных; например, для скорости 10 mbit необходим буфер не менее 10 кб, а для скорости 256 kbit — не менее 1 кб. В общем, рассчитывается как rate_in_bytes/100. Для 100 мбит — 104857600/800 = 1310172 байт. При указании этого параметра учтите, что все указанные значения — минимальные, ничто не мешает вам увеличить этот параметр для гарантии того, что пакеты не будут отбрасываться очередью по причине переполнения последней. Фактически, размер буфера ограничен только размером физической памяти системы (т.к. буфер очереди обязан находиться в памяти).
mpu — минимальный размер пакета для помещения в очередь; пакеты меньшей длины отбрасываются, для сетей ethernet каждый пакет должен быть больше 64 байт. rate — заданый уровень скорости. peakrate — максимально возможная скорость передачи пакетов из очереди в интерфейс; по умолчанию 1 мегабит в секунду.
С одной стороны tbf снижает в общем пропускную способность интерфейса, но существуют ситуации, когда это оказывается полезным (хотя, честно говоря, я с такими ситуациями не встречался, но, может быть, кому-то это окажется полезным). Например, имеется ADSL-модем, который имеет очень большую очередь на upload. Поэтому при попытке передачи данных в сеть, скорость загрузки данных сильно падает, т.к. на модеме заполняется длинная очередь, контролировать которую не представляется возможным. Одним из вариантов решения проблемы является размещение tbf-очереди на Linux-маршрутизаторе и установке максимального времени ожидания пакета в очереди. Пример конфигурации:
# tc qdisc add dev ppp0 root tbf rate 220kbit latency 50ms burst 1500
Скорость rate выбрана исходя из максимальной скорости интерфейса (в данном примере — 256 кбит/с) минус несколько процентов. Размер очереди burst выбран также соответственно скорости. Особо отметьте параметр latency — чем меньше этот параметр, тем меньше пакеты будут задерживаться в очереди — меньшая задержка увеличивает интерактивность данных. Заметьте, TBF не спасет вас от флуда — см. замечание ниже в описании sfq-очередей.
Следующий тип classless-очереди — sfq (stochastic fairness queueing — очередь равномерного случайного распределения пакетов). Алгоритм работы этого типа таков: данные, поступающие в очередь разделяются на достаточно большое количество «виртуальных» подочередей (виртуальных, т.к. в памяти существует одна очередь, но она представляется совокупностью многих подочередей), из подочередей данные извлекаются по очереди. Т.е. это напоминает Token Ring сети с передачей маркера по кольцу. Подочередь, получившая маркер, передает один пакет данных, а маркер переходит к следующей подочереди. Случайность передачи обеспечивается тем, что размер подочередей обычно не фиксируется жестко и данные могут попадать в различные подочереди. Такая очередь весьма полезна при сильной загрузке сетевого интерфейса многими клиентами. SFQ не позволяет захватить одному клиенту все ресурсы обработки пакетов, поэтому обеспечивается примерно одинаковая скорость поступления данных от различных клиентов. Учтите, это не спасет от DoS или банального флуда, т.к. любая очередь управляет скоростью обработки данных, т.е. фактически скоростью передачи ответных пакетов, а при наводнении пакетами интерфейса, последний не сможет принимать другие пакеты, и очередь в этом случае не помогает, т.к. нарушается работа самого сетевого устройства. Таким образом, sfq-очереди эффективны для регулирования скорости _передачи_ пакетов различным клиентам. Никогда не забывайте об этом факте (хотя далее будет приведен пример, позволяющий «спасти» систему от DoS-атак при помощи очередей, но при этом, если наводняется сам интерфейс, то работать он все равно не будет). Параметры sfq-очередей:
pertrub — число секунд, после которого происходит перерасчет длины подочередей; по умолчанию — 0, т.е. переконфигурирования не происходит, рекомендуется устанавливать этот параметр равный примерно 10 секундам. quantum — число байт, которые может передать подочередь, получившая маркер, значение по умолчанию — MTU интерфейса — подходит для большинства случаев, т.к. это гарантирует, что очередь сможет передать хотя бы один пакет.
SFQ-очереди удобно применять для балансирования нагрузки сервера:
# tc qdisc add dev eth0 root sfq pertrub 10
Наконец, самый простой тип classless-очереди — pfifo (лимит пакетов) и bfifo (лимит байтов). Эти очереди являются простыми очередями определенной длины, не выполняющими никаких действий над поступающими в них пакетами. Единственный параметр таких очередей — limit, означающий размер очереди в пакетах (pfifo) и в байтах (bfifo).
Создает очередь длиной в 500 пакетов:
# tc qdisc add dev eth0 root pfifo limit 500
Прежде чем приступать к чтению следующего раздела, полезно вспомнить о принципах построения дерева объектов очередей, т.к. далее будет рассказываться о classfull-очередях, при создании которых необходимо добавлять очереди (или классы) в очереди-контейнеры.
Итак, classfull-очереди. Как работает такая очередь? Гм… строго говоря, classfull-очередь — это обычно некий контейнер, содержащий другие очереди. Решение, куда отправить тот или иной пакет, принимается на основании фильтров (о них будет рассказано далее) или на основании приоритета очереди-потомка. Решение, откуда отправлять пакет на обработку верхнему уровню, принимается на основании приоритета и настроек очереди-контейнера. Очереди же внутри контейнера не знают о существовании родителя непосредственно, т.е. они продолжают вести себя обычным образом, но все операции осуществляются не с ядром, а с очередью верхнего уровня. Итак, далее я бы хотел рассказать о некоторых типах classfull-очередей, которые являются наиболее полезными на практике.
Первый тип — PRIO-очереди. Очередь такого типа может разделять трафик между тремя полосами, которые являются очередями любого типа. Разделение осуществляется на основании фильтров (см. далее). Каждая подочередь, входящая в PRIO, имеет свой приоритет, определяемый значением handle. При этом больший приоритет имеет та подочередь, которая относится к классу 1:. Т.е. при извлечении пакета из очереди вначале исследуется подочередь с большим приоритетом, если в последней нет пакетов для обработки, то выбирается очередь с более низким приоритетом и.т.д. Каким же образом трафик разделяется между полосами? Есть два пути: использование фильтров и использование поля TOS. На основании поля TOS различный вид трафика помещается в подочереди с разным приоритетом (отметьте, что очереди с меньшим номером обладают большим приоритетом):
TOS полосы Максимальная надежность - 1 Минимальная цена - 2 Максимальная пропускная способность - 2 Минимальная задержка (интерактивный) - 1
Среди параметров prio-очереди можно отметить только 2: bands — число полос, по умолчанию 3, и priomap — карта распределения трафика в зависимости от поля TOS. Эти параметры редко приходится менять, т.к. для сложных ситуаций лучше применять более сложные очереди :). Очередь prio очень полезна, если необходимо повысить общую пропускную способность интерфейса. Разделение на основе TOS особенно выгодно в сетях Unix, т.к. популярные сервера и приложения умеют грамотно устанавливать флаг TOS (например SSH устанавливает TOS «Минимальная задержка», а scp — «Максимальная пропускная способность»). К сожалению, в сетях, где имеются клиенты M$, ситуация усложняется, т.к. по неизвестной причине M$ ставит свои ОС в привилегированные условия, устанавливая TOS «Интерактивный» по умолчанию. PRIO еще полезна тем, что может содержать очереди различного типа, например, tbf или sfq. Это ее основное отличие от очереди pfifo_fast, которая создает три полосы pfifo по умолчанию, и не позволяет менять тип подочередей. Поэтому тип pfifo_fast относится к classless, хотя осуществляет разделение трафика, но подочереди являются неотделимой частью самой pfifo_fast. Приведу пример создания prio очереди, содержащей 2 подочереди sfq и одну tbf:
Псевдо-дерево будет выглядеть так:
root prio 1:1 1:2 1:3 | | | 10: 20: 30: (0)sfq (1)tbf (2)sfq
# tc qdisc add dev eth0 root handle 1: prio
Заметьте, что 1: добавляет автоматически три полосы — 1:1 1:2 1:3, т.к. по умолчанию количество полос равно 3-м.
# tc qdisc add dev eth0 parent 1:1 handle 10: sfq pertrub 10 # tc qdisc add dev eth0 parent 1:2 handle 20: tbf rate 1mbit buffer 15000 latency 10ms # tc qdisc add dev eth0 parent 1:3 handle 30: sfq pertrub 10
Если вы попробуете передавать данные различного типа, например ssh и ftp, то заметите, что интерактивный тарфик поступает в 1-ю и 2-ю полосы, а неинтерактивный — в 3-ю полосу. Если prio является достаточно простым типом очереди, то следующие типы не обладают этим свойством. Прежде всего, это тип cbq (classfull based queueing). Этот тип позволяет добавлять множество классов и осуществлять весьма нетривиальную обработку трафика. Распределение трафика по классам осуществляется посредством фильтров. Но cbq очереди слишком сложны и не всегда делают то, что от них требуется. По этой причине я буду рассматривать не cbq очереди, а их ближайших родственников — htb (hierarchial token bucket), которые имеют те же возможность, что и cbq-очереди, но лишены недостатков последних — излишней громоздкости синтаксиса и непрозрачности (единственным недостатком является то, что для их использования необходимо патчить 2.2 ядро, но 2.4.20 ядро имеет встроенную поддержку этого типа очередей). Итак, позвольте представить, htb-очереди. Для начала убедитесь, что вы имеете ядро версии 2.4.20 и выше. Если такового не имеется, необходимо сходить на сайт проекта htb и скачать пакет htb3, содержащий патчи для ядер 2.2 и 2.4, а также исправленную версию tc. Находится этот пакет по адресу http://luxik.cdi.cz/~devik/qos/htb/v3/htb3.6-020525.tgz.Что же позволяют делать htb-очереди? Они позволяют регулировать полосу пропускания трафика и фактически создавать на базе одного интерфейса несколько более медленных, и разделять трафик по полосам, отличающимся по скорости передачи. На самом деле, htb-очередь — очень полезная вещь. Допустим, нам необходимо повысить полосу пропускания www и smtp трафика, но понизить скорость ftp-трафика. С помощью очереди prio можно лишь менять приоритет разного рода трафика, но если передается только ftp-данные то каким бы ни был приоритет ftp, все равно этот трафик будет передаваться с максимальной скоростью. Единственной возможностью решения такой ситуации для prio-очереди является использование в качестве полос tbf-очередей. В то же время, если необходимо организовать весьма нетривиальное распределение трафика и ограничение полос пропускания, то лучше всего использовать htb-очередь. Для упрощения понимания принципов создания htb-очередей приведу простой пример. Допустим, к серверу подключено 2-е клиентов (А и Б). Эти два клиента пользуются только www- и smtp-сервисами (такое упрощение сделано для наглядности, далее будут приведены примеры реальных скриптов). Имеется 10-и мегабитный интерфейс, полосы пропускания будут выглядеть таким образом:
Клиент Сервис Полоса пропускания A smtp 2mbit A www 3mbit B all 1mbit other all 2mbit
Команды, реализующие эту схему, будут выглядеть так:
# tc qdisc add dev eth0 root handle 1: htb default 13
Думаю, эта команда не вызывает трудностей в понимании. Параметр default 13 означает, что по умолчанию трафик будет направляться на полосу с идентификатором 13. Трафик «по умолчанию» означает тот трафик, который не был распределен при помощи фильтров. Теперь необходимо добавить полосы — классы. Учтите, что классы — это объекты, которые являются составными частями classfull-очередей, но сами по себе не являются очередями. Таким образом, схема составления htb-очереди выглядит примерно так: добавление самой очереди -> добавление классов -> добавление фильтров, распределяющих трафик по классам, -> добавление очередей (обычно classless, по умолчанию pfifo), реализующих классы. Команды, делающие это, представлены ниже. Добавляем классы:
tc class add dev eth0 parent 1: classid 1:1 htb rate 9mbit ceil 9mbit burst 12500 tc class add dev eth0 parent 1:1 classid 1:10 htb rate 2mbit ceil 9mbit burst 12500 tc class add dev eth0 parent 1:1 classid 1:11 htb rate 3mbit ceil 9mbit burst 12500 tc class add dev eth0 parent 1:1 classid 1:12 htb rate 1mbit ceil 9mbit burst 12500 tc class add dev eth0 parent 1:1 classid 1:13 htb rate 3mbit ceil 9mbit burst 12500
Псевдодерево очередей будет выглядеть так:
1:0 root | 1:1 / | | \ 1:10 1:11 1:12 1:13
Сделаю некоторые пояснения относительно параметров классов htb. Параметр rate означает полосу пропускания, ceil означает максимальную скорость обмена класса с родительской очередью (или классом). Также есть возможность указания приоритета каждого класса аналогично prio-очереди при помощи параметра prio (аналогично, меньшее значение параметра означает больший приоритет). Существуют 2 параметра burst и cburst, регулирующие параметры буфера очереди:
- burst — размер в байтах буфера, для Intel-машин вычисляется по формуле (speed_in_bits)/(100*8); для нашего случая минимальное значение — 12500 байт;
- cburst — означает минимальный размер в байтах данных, передаваемых родительской очереди; обычно не меньше mtu интерфейса.
После добавления классов логичным является добавление средств, распределяющих трафик по этим классам. Таким средством являются фильтры. Вернемся к нашему простому примеру. Для него фильтры будут добавляться так:
# tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src A_ip match ip dport 80 0xffff flowid 1:10 # tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src A_ip match ip dport 25 0xfff flowid 1:11 # tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src B_ip flowid 1:12
Поясню все по порядку. Директива protocol означает протокол для фильтрации, parent — очередь, касательно которой работает фильтр (в данном случае корневая htb), prio — приоритет данного фильтра (чем меньше значение, тем больше приоритет), u32 значит, что ведется поиск совпадений в любой части пакета, match … — правила совпадения определенных полей пакета с заданными, если этой директивы нет, то под этот фильтр подходят любые неотфильторованные ранее пакеты (хотя необходимо установить более низкий приоритет для этого фильтра, иначе он будет обрабатывать весь трафик, что не есть хорошо):
# tc filter add dev eth0 protocol ip parent 1:0 prio 2 flowid some_band
Директива flowid обозначает идентификатор класса, куда фильтр будет отправлять трафик. Как правильно строить правила совпадения? При построении учтите несколько общих правил:
- … если в одной команде tc filter add встретилось несколько директив match, то они объединяются операцией «И»;
- … если необходимо создать несколько правил, объединенных «ИЛИ», то необходимо создавать эти правила в различных командах tc filter add.
Составлять правила фильтров совсем несложно:
- match ip src ip_addr — поиск совпадения ip-адреса отправителя;
- match ip dst ip_addr — поиск совпадения ip-адреса назначения (полезно при маршрутизации);
- match ip sport|dport port_num 0xffff — поиск совпадения порта-источника или порта назначения, назначение символов 0xffff — установка маски совпадения.
Существует также возможность фильтрации на основе меток netfilter. Метки ставятся с помощью iptables следующим образом:
# iptables -A FORWARD -t mangle -i eth0 -j MARK --set-mark 6
Где вместо 6-ки может быть любое число до 255.
Установка фильтра выглядит несколько необычно:
# tc filter add dev eth0 ptrotocol ip parent 1:0 prio 1 handle 6 fw classid 1:1
Обратите внимание на отсутствие директивы u32, директивы handle, fw и classid (вместо flowid).
Далее будет показано на примере, как выбирать пакеты с установленными флагами SYN, ASK, выбирать протокол (TCP, UDP, ICMP), что позволяет предотвращать некоторые виды DoS-атак. Для понимания фильтров, которые будут рассмотрены чуть позже, разъясню назначение некоторых дополнительных параметров фильтров:
- а) поиск «сырых» байт — match [u32|u16|u8] 0xXX 0xMASK at where, где маска — 16-иричное значение маски соответствующего формата, например u32 — 32 бита, u16 — 16 бит, u8 8 бит соответственно. Параметр where — обычное число, означающее количество соответствующих элементов (u32, u16, u8), отсчитанных от начала пакета. Например, фильтр match u8 0×10 0чаа at 33 означает выбор поля ToS «Минимальная задержка»;
б) поиск протокола — match ip protocol 6 0xff — поиск пакетов TCP-протокола;
в) непосредственный поиск поля ToS — match ip tos 0×10 0xff;
г) поиск совпадения области роутинга:
# ip route add some_network via some_gate dev eth0 realm 2
Область обозначается директивой realm, и фильтр для пакетов, попадающих в эту область (т.е. тех, что направлены в сеть some_network).
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 route to 2
Если нужно выбирать пакеты, приходящие из области маршрутизации, то нужно просто заменить to на from.
д) отбор трафика, превосходящего определенный лимит скорости (очень полезно): В команде tc filter можно применять те же директивы, что и в tbf-очереди при указании особого параметра — police:
- … buffer
- … mtu
- … mpu
- … rate
- Если трафик превышает установленный лимит, то _сам_ фильтр может выполнять определенные действия:
- … drop — отбрасывание трафика, превысившего лимит;
- … pass — пропускание такого трафика.
е) существует также возможность создания хешей фильтров (т.е. групп фильтров, которые применяются при прохождении определенных других фильтров), но это применяется лишь в случаях тысяч правил, когда обработка всех фильтров занимает значительное процессорное время. Объем этой статьи не позволяет мне рассказать и об этой возможности, поэтому при возникновении этой проблемы нужно почитать HOWTO — www.lartc.org.
После настройки фильтрации необходимо добавить очереди, реализующие классы. Это делается вполне стандартным образом (очереди могут быть любого типа, что позволяет выполнять самые разнообразные задачи). Для нашего примера это будет выглядеть так:
# tc qdisc add dev eth0 parent 1:10 handle 20: pfifo limit 500 # tc qdisc add dev eth0 parent 1:11 handle 30: pfifo limit 500 # tc qdisc add dev eth0 parent 1:12 handle 40: pfifo limit 500 # tc qdisc add dev eth0 parent 1:13 handle 50: sfq perturb 10
Обратите внимание, что родителями этим очередям являются классы, таким образом, псевдодерево будет выглядеть так:
1:0 root | 1:1 / | | \ 1:10 1:11 1:12 1:13 | | | | 20: 30: 40: 50:
Еще хотел бы остановить ваше внимание на классе 1:1. На самом деле, может быть несколько классов верхнего уровня, что позволяет строить весьма сложную иерархию классов. На этом я, пожалуй, закончу теоретическое рассмотрение очередей и приведу реальный пример использования очередей для управления трафиком. В этом примере я покажу основные возможности очередей: разделение трафика и создание полос пропускания, фильтрацию на основе поля ToS, защита сервера от SYN- и ICMP-флуда, разделение трафика между несколькими сетевыми интерфейсами (туннели высокой пропускной способности, аналог port link в Cisco Catalyst). Итак, конфигурация сети:
В нашем случае имеется сервер, соединяющий две локальные сети (Server A). Он соединен через две сетевые карты с 2-м сервером (Server B) для повышения эффективности обмена данными между двумя серверами (например, резервные копии одного сервера на другом). Я не буду подробно останавливаться на механизмах маршрутизации и разграничения доступа, а для начала просто поясню, как при помощи очередей реализовать распределение трафика по двум интерфейсам. Для этого существует специальный тип очереди — teql. В первую очередь, необходимо добавить такую очередь к двум интерфейсам (в нашем случае — eth1 и eth2):
# tc qdisc add dev eth1 root teql0 # tc qdisc add dev eth2 root teql0
После этого появляется виртуальный интерфейс — teql0. Работа с ним не отличается от работы с любым другим интрефейсом:
# ip link set dev teql0 up — включает интерфейс.
Учтите, что подобную операцию нужно проделать на обеих машинах (Server A и Server B), а после этого необходимо настроить первоначально маршрутизацию:
- Server A
-
# ip addr add dev eth1 10.0.0.1/31 # ip addr add dev eth2 10.0.0.3/31
- # ip addr add dev teql0 10.0.0.5/31
Server B
# ip addr add dev eth1 10.0.0.2/31 # ip addr add dev eth2 10.0.0.4/31 # ip addr add dev teql0 10.0.0.6/31
При этом серверы будут общаться через виртуальную подсеть 10.0.0.0/31 и будут видеть друг друга через адреса 10.0.0.5 и 10.0.0.6 соответственно. Необходимо также отключить «отброс» пакетов серверами:
# echo 0 > /proc/sys/net/ipv4/conf/eth1/rp_filter # echo 0 > /proc/sys/net/ipv4/conf/eth2/rp_filter
т.к. это может вызвать нежелательные циклы пакетов между серверами (каждый сервер будет отбрасывать не предназначенные ему пакеты в петлю, и эти пакеты будут гулять, пока не истечет ttl).
На этом будем считать настройку интерфейсов eth1 и eth2 законченной (если Server B настроен правильно, то причин для беспокойства нет, иначе есть смысл настроить firewall и максимальную скорость связи). Главной «оптимизации» подвергнется интерфейс eth0, ведущий в локальную сеть. Eth0 — стамегабитная сетевая карта. В нашей сети имеется 2 «особых» клиента — 192.168.2.2 и 192.168.2.3. Им необходимо выделить отдельные полосы пропускания для ftp- и smb-трафика (по 10 мбит каждому). Кроме этого, сеть состоит из рабочих станций под управлением Unix/Linux, и очень много работают с сервером по ssh (10 мбит дополнительно). Помимо всего прочего в сети необходимо учитывать ToS-флаги и ограничивать ICMP-трафик (защита от любителей делать ping -f). Задачка не из легких, не так ли? ;) Особенно если учесть, что мы будем устанавливать для различных полос различный приоритет… Для начала создаем htb-очередь для выделения полос пропускания:
# tc qdisc add dev eth0 root handle 1: default 14
И сами полосы:
# tc class add dev eth0 parent 1: handle 1:1 rate 10mbit burst 150000 \ ceil 100mbit # tc class add dev eth0 parent 1:1 handle 1:11 rate 10mbit burst 15000 \ ceil 100mbit prio 2 # tc class add dev eth0 parent 1:1 handle 1:12 rate 10mbit burst 15000 \ ceil 100mbit prio 2 # tc class add dev eth0 parent 1:1 handle 1:13 rate 10mbit burst 15000 \ ceil 100mbit prio 2 # tc class add dev eth0 parent 1:1 handle 1:14 rate 69mbit burst 100000 \ ceil 100mbit prio 3
Одна полоса для ICMP-трафика:
# tc class add dev eth0 parent 1:1 handle 1:15 rate 100kbit burst 1000 \ ceil 100mbit prio 4
Еще одна полоса для TCP-AСK пакетов (эти пакеты должны иметь максимальный приоритет, т.к. это существенно повышает скорость загрузки данных с сервера к клиенту):
# tc class add dev eth0 parent 1:1 handle 1:16 rate 900kbit burst 2500 \ ceil 100mbit prio 1
Создаем фильтры:
а) создаем фильтры для особых клиентов:
Для клиента А:
# tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src 192.168.2.2 match ip dport 110 0xffff flowid 1:11 # tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src 192.168.2.2 match ip dport 138 0xfff flowid 1:11
Через 138-й порт в протоколе NetBios передаются данные.
Для клиента Б:
# tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src 192.168.2.3 match ip dport 110 0xffff flowid 1:12 # tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip src 192.168.2.3 match ip dport 138 0xfff flowid 1:12
б) создаем фильтрацию ssh-трафика:
# tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \ match ip dport 22 0xffff flowid 1:13
в) задаем предел для ICMP- и SYN-TCP-трафика:
- SYN:
- Для начала установим правила отбора SYN-пакетов при помощи iptables (это намного проще, чем использовать сырой разбор пакетов и маскирование битов):
# iptables -A PREROUTING -i eth0 -t mangle -p tcp --syn \ -j MARK --set-mark 1
После этого необходимо добавить особый тип очереди — ingress, которая пропускает трафик не выше заданной скорости, при этом заметьте, что очередь не имеет размещения (всегда root), что позволяет добавлять к интерфейсу очередь ingress, не мешая htb (или любой другой очереди), подключенной к этому же интерфейсу:
# tc qdisc add dev eth0 handle ffff: ingress # tc filter add dev eth0 parent ffff: protocol ip prio 50 handle 1 fw \ police rate 100kbit burst 1500 mtu 9k drop flowid :1
Мы установили лимит SYN-пакетов до 320 пакетов в секунду (пакет с SYN-флагом занимает 320 бит, но 320 пакетов в секунду несколько многовато, хотя точно должно обеспечить обработку всех запросов на соединение).
ICMP: Тут мы имеем дело с обычным выделением полосы пропускания и фильтрацией протокола:
# tc filter add dev eth0 parent 10:0 protocol ip prio 100 u32 match ip protocol 1 0xff flowid 1:15
Значение protocol 1 соответствует ICMP.
г) добавляем фильтр TCP-AСK-пакетов:
# tc filter add dev eth0 parent 1: protocol ip prio 10 u32 \ match ip protocol 6 0xff \ match u8 0x05 0x0f at 0 \ match u16 0x0000 0xffc0 at 2 \ match u8 0x10 0xff at 33 \ flowid 1:15
Эта строчка выглядит несколько сложно, но здесь используется сырой разбор заголовков пакетов, поэтому всеьма сложно понять, что к чему.
На этом настройки фильтрации заканчиваются. Приступим к добавлению очередей, реализующих классы:
а) для особых клиентов подойдут обычные pfifo-очереди:
# tc qdisc add dev eth0 parent 1:11 handle 20: pfifo limit 500 # tc qdisc add dev eth0 parent 1:12 handle 30: pfifo limit 500
б) для ssh-полосы лучше всего использовать sfq для равномерности распределения клиентов:
# tc qdisc add dev eth0 parent 1:13 handle 40: sfq pertrub 10
в) для остальной части подсети лучше всего использовать pfifo_fast-очередь, т.к. необходимо выделять приоритетный трафик. pfifo_fast было выбрано вместо prio только по причине простоты использования (думаю, скрипт, реализующий установку всех очередей, и так способен смутить любого):
# tc qdisc add dev eth0 parent 1:14 handle 50: pfifo_fast
г) для AСK-пакетов подойдет также pfifo-очередь:
# tc qdisc add dev eth0 parent 1:15 handle 60: pfifo limit 15
Вот и все, я завершаю свой пример и, собственнно, рассказ о traffic control в GNU/Linux. Надеюсь, что моя статья окажется кому-нибудь полезной. На этом я и прощаюсь с вами. Приведу ряд полезных ссылок:
- www.lartc.org — Linux Advanced Routing and Traffic Control HOWTO;
- http://luxik.cdi.cz/~devik/qos/htb/manual/userg.htm — руководство по работе с htb-очередями (приведены графики производительности и описаны все основные параметры очередей этого типа);
- RFC795 — документ, посвященный использованию поля ToS.
Последние комментарии
- OlegL, 17 декабря в 15:00 → Перекличка 21
- REDkiy, 8 июня 2023 года в 9:09 → Как «замокать» файл для юниттеста в Python? 2
- fhunter, 29 ноября 2022 года в 2:09 → Проблема с NO_PUBKEY: как получить GPG-ключ и добавить его в базу apt? 6
- Иванн, 9 апреля 2022 года в 8:31 → Ассоциация РАСПО провела первое учредительное собрание 1
- Kiri11.ADV1, 7 марта 2021 года в 12:01 → Логи catalina.out в TomCat 9 в формате JSON 1
iproute дал очень широкие возможноступравления трафиком, единственный его минус это сложность в настройке и понимании, что бы облегчить управление маршрутизацией в traffpro был написан специальный модуль по частичному управлению с помощью iprote маршрутами, да он не умеет управлять всеми функциями, зато облегчает настройку :)