от Vladsun(19-10-2006)

рейтинг (20)   [ добре ]  [ зле ]

Printer Friendly Вариант за отпечатване

Оптимизация на iptables и tc правила.

Целта на статията е да покаже начини за подобряване ефективността и работата на рутери, които извършват и трафик контрол върху потребителите. Това е постигнато чрез оптимизиране на правилата използвани в iptables и tc инструментите в рутера. Като допълнително четиво към тази статия е статията на Uvigii "Разпределяне на трафик от два (или повече) интернет доставчика".

1. Предварителна подготовка
Статията НЕ е предназначена за начинаещи в тази област и за тях е наложително да разгледат някои документации:



2. Системни изисквания
Предварително трябва да е инсталиран (пачнат) следния софтуер:
  • в ядрото - QoS (htb, sfq);
  • iptables;
  • iptables - IPMARK;
  • tc;
  • Perl (за скриптовете използвани в тази статия).


3. Проблемът
Най-често срещания алгоритъм за разпределяне на трафик между потребители е следния:
  1. разделяне на трафика на входящ и изходящ;
  2. разделяне на трафика на български и международен чрез подходящо маркиране на пакетите (вж. "Разпределяне на трафик от два (или повече) интернет доставчика");
  3. разделяне на трафика за всеки потребител чрез повторно маркиране на пакетите за всеки вид трафик;
Освен това обикновено се налага да се прави контрол на достъпа на потребителите - чрез използването на iptables правила в PREROUTING или FORWARD веригите.
Проблемът при повечето от така реализирани алгоритми е значителното натоварване на сървъра поради ниската ефективност на набора от използваните правила в iptables и tc. За по-детайлно обяснение ще покажа примерен набор от правила използвани в iptables. Предполага се, че разделянето на трафика на български и международен, съответно входящ/изходящ вече е направен и са получени следните потребителски дефинирани вериги - BG_IN, BG_OUT, INT_IN, INT_OUT. В тези вериги трябва да се извърши маркирането за всеки потребител, т.е. добавяме по 4 правила на потребител:
Примерен код
iptables -A BG_OUT -s IP1 -j MARK --set-mark MARK_IP1_BG_OUT
 iptables -A INT_IN -d IP1 -j MARK --set-mark MARK_IP1_INT_IN
 iptables -A INT_OUT -s IP1 -j MARK --set-mark MARK_IP1_INT_OUT
Малката ефективност на този метод е породена от линейното (последователното) обхождане на тези правила за всеки пакет. По този начин всеки пакет минава през около 260 правила, при това само в тези вериги! Още по-лошият вариант, при който няма разделян на изходящ/входящ трафик, броят на правилата обходени от всеки пакет е над 510! При този вариант горните правила изглеждат по този начин:
Примерен код
iptables -A BG -s IP1 -j MARK --set-mark MARK_IP1_BG_OUT
 iptables -A INT -d IP1 -j MARK --set-mark MARK_IP1_INT_IN
 iptables -A INT -s IP1 -j MARK --set-mark MARK_IP1_INT_OUT
След маркирането на пакетите е необходимо също така да се създадат tc правилата, които ще определят минималната и максималната скорост за всеки един потребител в зависимост от вида на трафика. Това означава, че за всеки потребител ще имаме по 4 класа, 4 qdisc (б.а. - дайте свестен превод) и 4 филтъра - по 2 за всеки интерфейс. Прякото следствие от този начин на построяване на tc правилата е отново линейното обхождане на tc филтрите. В случая не знам дали при достигане на съответния филтър за маркирания пакет проследяването на пакета в следващите филтри продължава или не. Но дори и в най-добрия случай имаме средностатистически брой на обходените филтри - 1/2 от всички филтри, или 254 филтъра. Това също е свързано с допълнително (при това излишно) натоварване на рутера.

4. Решението
Ще покажа решение (не твърдя, че е единственото или най-доброто), при което проблемът с линейното и излишно обхождане на правила в iptables и съответните филтри в tc е избягнат напълно. Всъщност самото решение е реализирано отдавна, но за съжаление не е добре документирано (да не казвам изобщо). Нека разгледаме man-а на iptables и по-специално IPMARK:
  IPMARK
        Allows you to mark a received packet basing on its IP address. This can replace many mangle/mark entries with only one,
        if you use firewall based classifier.
 
        This target is to be used inside the mangle table, in the PREROUTING, POSTROUTING or FORWARD hooks.
 
        --addr src/dst
               Use source or destination IP address.
 
        --and-mask mask
               Perform bitwise `and' on the IP address and this mask.
 
        --or-mask mask
               Perform bitwise `or' on the IP address and this mask.
 
        The order of IP address bytes is reversed to meet "human order of bytes": 192.168.0.1 is 0xc0a80001. At first the `and'
        operation is performed, then `or'.
 
        Examples:
 
        We create a queue for each user, the queue number is adequate to the IP address of the user, e.g.:  all  packets  going
        to/from 192.168.5.2 are directed to 1:0502 queue, 192.168.5.12 -> 1:050c etc.
 
        We have one classifier rule:
 
               tc filter add dev eth3 parent 1:0 protocol ip fw
 
        Earlier we had many rules just like below:
 
               iptables -t mangle -A POSTROUTING -o eth3 -d 192.168.5.2 -j MARK --set-mark 0x10502
 
               iptables -t mangle -A POSTROUTING -o eth3 -d 192.168.5.3 -j MARK --set-mark 0x10503
 
        Using IPMARK target we can replace all the mangle/mark rules with only one:
 
               iptables -t mangle -A POSTROUTING -o eth3 -j IPMARK --addr=dst --and-mask=0xffff --or-mask=0x10000
 
        On the routers with hundreds of users there should be significant load decrease (e.g. twice).
 
Като 'жокер' ви давам и допълнителна информация, която открих в Интернет (случайно):
 Something I've only just noticed from a comment in the code - htb can use mark without the need for lots of filters. 
 You only need one empty filter on the root (maybe you can still nest) like -
 
 tc filter add dev eth0 parent 1:0 protocol ip prio 1 fw
 
 and then if you arrange for your classes to be the same minor numbers as the marks it will behave like using classify. 
 You need to set the major number of your htb (1 in example above) in the top 16 bits of the mark. 
 There is also a netfilter pom-ng patch IPMARK that will set marks based on ipaddress. 
 
 Andy.
 
Нека сега преведа на български и малко по-подробно всичко това:
  • IPMARK: маркира пакетите, като за стойността на маркера използва стойността на самото IP;
  • IPMARK: приема като параметри 3 неща - IP-то по което ще маркираме и 2 параметъра за управление на получения MARK;
  • IPMARK: IP-то по което ше маркираме се взима директно от пакета и в зависимост от това дали --addr параметъра е src или dst, се взима съответно src IP-то или dst IP-то;
  • IPMARK: 2-та параметъра за управление на получения MARK реализират побитово И и ИЛИ върху стойността на IP адреса;
Пример: искаме да маркираме всички IP-та от мрежа 192.168.0.0/24, така че маркерът да е последната група от IP-то сумирано с 512. При това положение AND-маската е 0x00ff - взимаме последните 2 шестнайсетични числа и чрез OR-маска 0x0200 (512 dec.) сумираме с 512. Така получените пакети ще имат следните маркери:
 	192.168.0.1   - МАРК = 0x201;
 	192.168.0.2   - МАРК = 0x202;
 	192.168.0.3   - МАРК = 0x203;
 	...........
 	...........
 	192.168.0.253 - МАРК = 0x2FD;
 	192.168.0.254 - МАРК = 0x2FE;
 
Във втората дадена по-горе информация се обяснява, че ако оставим tc филтър без параметри за fw полето, то тогава този филтър действа директно по MARK полето на пакета. Действието на филтъра е следното:
  • разделя МАРК полето на 2 част - младша тетрада и старша тетрада;
  • младшата тетрада определя minor номера на класа, а старшата - major номера на класа.
Ако за пример вземем пакет маркиран с 0x0001 0005, този пакет се насочва към клас със classid = 1:5.

Нека сега след тези разяснения да получим решение за разпределяне на трафика на една С-клас мрежа (прим. 192.168.0.0):
Примерен код
iptables -A BG_IN -d 192.168.0.0/24 -j IPMARK --addr=dst --and-mask=0xff --or-mask=0x10100
 iptables -A BG_OUT -s 192.168.0.0/24 -j IPMARK --addr=dst --and-mask=0xff --or-mask=0x10200
 iptables -A INT_IN -d 192.168.0.0/24 -j IPMARK --addr=dst --and-mask=0xff --or-mask=0x10300
 iptables -A INT_OUT -s 192.168.0.0/24 -j IPMARK --addr=dst --and-mask=0xff --or-mask=0x10400
Добавяме общите филтри (дефинирането на tc правилата за общия, българския и международния трафик, съответно входящ/изходящ няма да бъде разглеждан в тази статия):
Примерен код
tc filter add dev eth0 parent 1:0 protocol ip prio 1 fw
 tc filter add dev eth1 parent 1:0 protocol ip prio 1 fw
и един Perl скрипт за добавяне на едно ИП (последните 3 цифри):
ip.add
#!/usr/bin/perl
 ($ip, $bgmin, $bgmax, $intmin, $intmax) = @ARGV;
 $id = sprintf("%X", $ip + 0x200);
 $class_bg_ul = "tc class add dev eth0 parent 1:15 classid 1:0".$id." htb rate ".$bgmin."Kbit ceil ".$bgmax."Kbit prio 5";
 $qdisc_bg_ul = "tc qdisc add dev eth0 parent 1:0".$id." handle 0".$id." sfq perturb 10 ";
 
 $id = sprintf("%X", $ip + 0x100);
 $class_bg_dl = "tc class add dev eth1 parent 1:10 classid 1:0".$id." htb rate ".$bgmin."Kbit ceil ".$bgmax."Kbit prio 5";
 $qdisc_bg_dl = "tc qdisc add dev eth1 parent 1:0".$id." handle ".$id." sfq perturb 10 ";
 
 $id = sprintf("%X", $ip + 0x400);
 $class_int_ul = "tc class add dev eth0 parent 1:25 classid 1:0".$id." htb rate ".$intmin."Kbit ceil ".$intmax."Kbit prio 4";
 $qdisc_int_ul = "tc qdisc add dev eth0 parent 1:0".$id." handle ".$id." sfq perturb 10 ";
 
 $id = sprintf("%X", $ip + 0x300);
 $class_int_dl = "tc class add dev eth1 parent 1:20 classid 1:0".$id." htb rate ".$intmin."Kbit ceil ".$intmax."Kbit prio 4";
 $qdisc_int_dl = "tc qdisc add dev eth1 parent 1:0".$id." handle ".$id." sfq perturb 10 ";
 
 
 `$class_bg_ul`;
 `$qdisc_bg_ul`;
 
 `$class_int_ul`;
 `$qdisc_int_ul`;
 
 `$class_bg_dl`;
 `$qdisc_bg_dl`;
 
 `$class_int_dl`;
 `$qdisc_int_dl`;
По този начин съкратихме 4*254=1016 правила в iptables na 4!, и още 1016 филтъра в tc също на 4!!! :)

5. Допълнителна оптимизация
По-горе беше отбелязано, че често се налага използването на iptables правила за реализирането на контрол върху дсотъпа на потребителите. Най-често реализацията е по следния начин:
Примерен код
 iptables -A FORWARD -s IP1 -j ACCEPT
 iptables -A FORWARD -d IP1 -j ACCEPT
 ....................................
 ....................................
 iptables -A FORWARD -s IPn -j ACCEPT
 iptables -A FORWARD -d IPn -j ACCEPT
Или казано по-друг начин: всички потребители, които имат разрешение за достъп до Интернет изрично се добавят във FORWARD веригата с ACCEPT. На останалите им се отказва достъп заради избраната политика на тази верига - DROP. Отново имаме линейно обхождане на много правила, което обаче изглежда абсолютно наложително. Този път ще приложа друга стратегия - ще трансформирам тези линейно разположени правила в дървовидна структура, чрез последователно разделяна на 2. Така при обхождането на правилата за един пакет ще се реализира двоично търсене, което както се знае е най-ефективното за наредени данни (поправете ме, ако греша). За да се изясня ще покажа нагледно наборът от правила в полученото дърво за входящ български трафик при мрежа 192.168.2.0/24 :
Примерен код
Първа итерация:
 iptables -t filter -N BG_IN_192_168_2_0-255
 iptables -t filter -A FORWARD_IN -d 192.168.2.0/24 -j BG_IN_192_168_2_0-255
 iptables -t filter -N BG_IN_192_168_2_0-127
 iptables -t filter -A BG_IN_192_168_2_0-255 -d 192.168.2.0/25 -j BG_IN_192_168_2_0-127
 iptables -t filter -N BG_IN_192_168_2_128-255
 iptables -t filter -A BG_IN_192_168_2_0-255 -d 192.168.2.128/25 -j BG_IN_192_168_2_128-255
 
 Втора итерация:
 iptables -t filter -N BG_IN_192_168_2_0-255
 iptables -t filter -A FORWARD_IN -d 192.168.2.0/24 -j BG_IN_192_168_2_0-255
 iptables -t filter -N BG_IN_192_168_2_0-127
 iptables -t filter -A BG_IN_192_168_2_0-255 -d 192.168.2.0/25 -j BG_IN_192_168_2_0-127
 iptables -t filter -N BG_IN_192_168_2_0-63
 iptables -t filter -A BG_IN_192_168_2_0-127 -d 192.168.2.0/26 -j BG_IN_192_168_2_0-63
 iptables -t filter -N BG_IN_192_168_2_64-127
 iptables -t filter -A BG_IN_192_168_2_0-127 -d 192.168.2.64/26 -j BG_IN_192_168_2_64-127
 iptables -t filter -N BG_IN_192_168_2_128-255
 iptables -t filter -A BG_IN_192_168_2_0-255 -d 192.168.2.128/25 -j BG_IN_192_168_2_128-255
 iptables -t filter -N BG_IN_192_168_2_128-191
 iptables -t filter -A BG_IN_192_168_2_128-255 -d 192.168.2.128/26 -j BG_IN_192_168_2_128-191
 iptables -t filter -N BG_IN_192_168_2_192-255
 iptables -t filter -A BG_IN_192_168_2_128-255 -d 192.168.2.192/26 -j BG_IN_192_168_2_192-255
 
 ...................
и така докато получим интервал съдържащ избрания от нас брой IP-та, които ще бъдат обходени последователно (прим. 2 или 4). По този начин средностатичстическия брой на правилата, които трябва да премине всеки пакет е брой_деления пъти по-малък от иначе необходимия (127).
Тъй като тази задача е доста трудоемка прилагам Perl скрипт (може би не най-добрия) за решаването й:
rc.netsubdiv
#!/usr/bin/perl
 
 # Мрежа/24
 $net            = "192.168.2.0";
 
 # Таблицата в iptables
 $table          = "filter";
 
 # Веригата, от която тръгваме
 $start_chain    = "FORWARD";
 
 # Префикса на веригите, които получаваме в дървото
 $chain          = "BG";
 
 # Да се слагат ли правила за src/dst
 $src            = 1;
 $dst            = 1;
 
 # До кога да се дели (мин=1, макс=256) (винаги степен на 2)
 $subdiv         = 128;
 
 # Ясно ;)
 $ipt           = "/usr/local/sbin/iptables";
 
 sub divide_net
 {
         my($net,$maxdiv, $div, $subnet, $prev_chain, $int_min) = @_;
         my $subint = 256/$div;
         my $ip;
         
         if ($div > $maxdiv)
         {
                 return;
         }
         
         $net2 = $net;
         $net2 =~ s/\./_/g;
         
         for ($ip=0; $ip<$div and $ip<2 ; $ip++)
         {
                 $min = $ip*$subint+$int_min;
                 $max = $min + $subint - 1;
                 
                 print $ipt." -t ".$table." -N "
                 .$chain."_IN_".$net2."_".$min."-"
                 .$max."\n";
                 
                 if ($div > 1)
                 {
                         if ($dst == 1)
                         {
                                 print $ipt." -t ".$table." -A "
                                 .$chain."_IN_".$prev_chain." –d "
                                 .$net.".".$min."/".$subnet." -j "           
                                 .$chain."_IN_".$net2."_".$min."-".$max."\n";
                         }
                         if ($src == 1)
                         {
                                 print $ipt." -t ".$table." -A "
                                 .$chain."_OUT_".$prev_chain." -s "
                                 .$net.".".$min."/".$subnet." -j "
                                 .$chain."_OUT_".$net2."_".$min."-".$max."\n\n";
                         }
                 }
                 else
                 {
                         if ($dst == 1)
                         {
                                 print $ipt." -t ".$table." -A ".$start_chain."_IN -d "
                                 .$net.".0/".$subnet." -j ".$chain."_IN_"
                                 .$net2."_".$min."-".$max."\n";
                         }
                         if ($src == 1)
                         {
                                 print $ipt." -t ".$table." -A ".$start_chain."_OUT -s "
                                 .$net.".0/".$subnet." -j ".$chain."_OUT_"
                                 .$net2."_".$min."-".$max."\n\n";
                         }
                 }
                 
                 $prev_chain2 = $net2."_".$min."-".$max;
                 divide_net($net, $maxdiv, $div*2, $subnet+1, $prev_chain2, $min);
                 
         }
 }
 
 $net =~ /^((\d+)\.(\d+)\.(\d+))/;
 $net = $1;
 divide_net($net, $subdiv, 1, 24, '', 0);
Променливите в началато на скрипта се използват конфигуриране. Естествено за специфични нужди този скрипт ще се наложи да се модифицира. Забележете, че самия скрипт не изпълнява iptables правилата. Необходимо е изходът от този скрипт да се пренасочи към файл и вече от този файл да се изпълнят iptables командите.
След като сме построили дървото е нужно да добавим правилата за контрол на достъпа за всяко IP. Отново на помощ идва Perl:
Примерен код
sub get_subnet_interval()
 {
         ($ip, $subdiv) = $_;
         
         my $subint = 256/$subdiv;
         my $net = $ip;
         
         $net =~ /^((\d+).(\d+).(\d+))/;
         $net = $1;
         $ip  =~ /^(\d+).(\d+).(\d+).(\d+)/;
         $ip = $5;
         
         $min = (int($ip/$subint))*$subint;
         $max = $min + $subint - 1;
         
         return ($min."-".$max);
 }
Чрез тази функция получавате веригата, в която се намира даденото от Вас IP. Функцията връща стринг, който е от вида ползван в предишния скрипт за генериране на дървото, но без префиксите на веригата. Това е направено с цел по-универсалната употреба на функцията.
Е, това е :) Очаквам коментари!


<< Делегиране на права чрез /etc/sudoers | Плавно преминаване от една ОС/Дистрибуция на друга >>