Самописный парсер netflow

Опубликовано admin в Чт, 24/02/2011 - 11:02

Долгое время для сбора статистики и отображения ее на графиках использовал cuflow+flowtools. Штука полезная, но мне не требовалось строить графики по протоколам, портам и так далее. Причем в конфигурации Cuflow задавалась только сеть которая является "нашим источником" трафика и никаких разговоров о том, чтобы рисовать график допустим из "Сегмента А" в "Сегмент Б", да еще для каждого IP-адреса из "Сегмента А" стоить свой график. Готового бесплатного продукта для своих нужд не нашел, пришлось писать ручками. Для начала записал на бумажку несколько пунктов этакого технического задания для себя :

  1. Простая конфигурация в XML файле
  2. Каждый график конфигурируется отдельно
  3. Составление графиков по определенным спискам сетей
  4. Исключение сетей из графика

Так далее по бумажке и пошел. Сразу же на клочке разрисовал как будет выглядеть мой конфигурационный файл, куда какие настройки можно засунуть. Получилось что-то вроде этого :

<collector scanperiod="10" 
           rrddir="/usr/local/www/mon/htdocs/nflow2/" 
           flowdir="/var/db/flows/" 
           flowpattern="ft-v05.*"> 
    <subnets> 
        <subnet name='internet'>0.0.0.0/0</subnet> 
        <subnet name='nat1'>5.5.5.5/32</subnet> 
    </subnets> 
 
    <sections> 
        <section name='nat1_traffic' sourcenet='nat1'> 
            <graph name='NAT1'> 
                <subnet>internet</subnet> 
            </graph> 
        </section> 
    </sections> 
</collector>

Тоесть тут я для себя изобразил примерный конфигурационный файл для следующей задачи - рисовать график всего трафика с именем "NAT1" для сервера "nat1". Конфиг визуально понравился и получился довольно гибким для моих нужд (далее станет заметно, сейчас ничем от cuflow не отличается).

А вот уже конфигурация, приближенная к "боевой". Рисуется 3 графика для сервера NAT1 : общий входящий/исходящий, входящий/исходящий в Российские сети (RU_IX), входящий/исходящий в Украинские сети (UA_IX).

<collector scanperiod="10" 
           rrddir="/usr/local/www/mon/htdocs/nflow2/" 
           flowdir="/var/db/flows/" 
           flowpattern="ft-v05.*"> 
    <subnets> 
        <subnet name='internet'>0.0.0.0/0</subnet> 
        <subnet name='UA_IX'>4.4.4.4/30,8.8.0.0/19</subnet> 
        <subnet name='RU_IX'>2.2.2.2/8, 3.3.3.3/8</subnet> 
        <subnet name='nat1'>5.5.5.5/32</subnet> 
    </subnets> 
 
    <sections> 
        <section name='nat1_traffic' sourcenet='nat1'> 
            <graph name='NAT1'> 
                <subnet>internet</subnet> 
            </graph> 
            <graph name='NAT1_2_UA'> 
                <subnet>UA_IX</subnet> 
            <graph> 
            <graph name='NAT1_2_RU'> 
                <subnet>RU_IX</subnet> 
            </graph> 
        </section> 
    </sections> 
</collector>

Писать парсер бинарных файлов netflow смысла не было, подглядел какие библиотеки использовались в cuflow и нужные подключил в свой код. Заголовок получился следующим :

#!/usr/bin/perl 
 
use Cflow qw(:flowvars 1.017); #Стянул у cuflow 
use FlowScan;                  #Стянул у cuflow 
use XML::Simple;               #XML Для чтения конфигурации 
use Net::Patricia;             #Бинарные дервья для IP адресов 
use Benchmark; 
use strict; 
use warnings;

Далее надо было разбирать наш конфигурационный файл, сразу же вынес все это в процедуру

sub parse_config{ 
    $mCfg   = $xml->XMLin($xmlConfig);     #Получаем XML нашего конфига 
    $rrddir = $mCfg->{'rrddir'};           #Узнаем куда надо складывать 
                                           # RRD файлы 
    $flowdir = $mCfg->{'flowdir'};         #Узнаем откуда брать flow файлы 
    $scanperiod = $mCfg->{'scanperiod'};   #Период сканирования 
    $flowpattern = $mCfg->{'flowpattern'}; #Паттерн flow файлов 
 
    #У XML::Simple есть такая особенность : если в секциях содержится 
    #  всего одна секция, то получаем массив будет одного вида, иначе совсем другого 
    my $sectionHR = $mCfg->{'sections'}->{'section'}; 
    if (exists($sectionHR->{'name'})){ 
        $sections->{ ($sectionHR->{'name'}) } = $sectionHR; 
    }else{ 
        $sections = $sectionHR; 
    } 
    #Заполняем списки наших сетей 
    foreach my $network_name (keys %{ $mCfg->{'subnets'}->{'subnet'} }){ 
        $subnets->{$network_name} = subnet2tree($network_name);#subnet2tree объявим ниже 
    } 
}

Функция subnet2tree просто создает хеш объектов Net::Patricia в которые добавлены сети из нашего конфига :

sub subnet2tree{ 
    my $subnet_name = shift; 
    my $tree = shift || new Net::Patricia; 
    my $nets = $mCfg->{'subnets'}->{'subnet'}->{$subnet_name}->{'content'}; 
    my @net; 
    foreach (split(/,/, $nets)){ 
        s/[\s]+//g; 
        s/\n//g; 
        $tree->add_string($_); #Добавляем ИП адресс в бинарное дерево 
    } 
    return $tree; 
}

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

sub wanted{ 
    #$sections  у нас глобальная переменная (отражает в себе <sections> 
    # из конфигурационного файла) 
    foreach my $section (keys %{ $sections }){ 
        # 
        my $sectionName = $section; 
        #Проверяем XML::Simple 
        $section = ( exists($sections->{'name'}) ) ? $sections : 
                                                     $sections->{$sectionName} ; 
        #Определим сеть-источник для этой секции 
        my $sourcenet = $sebnets->{ ($section->{'sourcenet'}) } || 
              die("Undefined sourcenet in <section name='". $sectionName ."'>"); 
        #Если в настройках секции указан параметр explode=1, то будут строиться 
        # графики по каждому ИП-адресу из этих сетей. 
        my $exp_condition = ($section->{explode}) ? 1 : 0 ; 
        #Начинаем "потрошить" графики, которые относятся к этой секции 
        foreach my $graph (keys %{ ($section->{'graph'}) }){ 
            my $graphName = $graph; 
            #Опять проверяем поведение XML::Simple 
            $graph = (exists($section->{'graph'}->{'name'})) ? $section->{'graph'} : 
                                                        $section->{'graph'}->{$graph}; 
            #Проверяем присутствуют ли сеть/хосты которые надо исключить из 
            # графика (эта часть больше относится к конфигу, @TODO: 
            # перенести туда, где ей место) 
            my $exc = $exclude->{$sectionName}->{$graphName}; 
            if (exists($graph->{'excludenet'}) && 
               (!exists($exc->{'v'}) || $exc->{'v'} !=1)){ 
                my $tree = new Net::Patricia; 
                foreach my $net (split(/,/,$graph->{'excludenet'})){ 
                    $tree = subnet2tree($net, $tree); 
                } 
                $exclude->{$sectionName}->{$graphName}->{'net'} = $tree; 
                $exclude->{$sectionName}->{$graphName}->{'v'}   = 1; 
            } 
 
            #Проверяем присутствуют ли в конфигурации сети, которые требуется 
            #исключить 
            my $netHR = $exclude->{$sectionName}->{$graphName}; 
            $exc = (exists($netHr->{'net'})) ? $netHR->{'net'} : new Net::Patricia; 
 
            #Собственно начинаем считать трафик 
            #Входящий 
            my $src_match = $sourcenet->match_string($srcip); #$srcip у нас из Cuflow 
            #Если $srcip нашлось в бин.дереве сетей и должны строиться графики 
            # для каждого хоста соурс сети (explode) 
            # @TODO : Этот кусок нужно переписать по человечески 
            if ($src_match && $exp_condition){ 
                foreach my $net (split(/,/, $graph->{'subnet'})){ 
                    #Если ИП совпал с указанным списком сетей и его не нужно исключать 
                    if (($subnets->{$net}->match_string($dstip) || $subnets->{$net}->match_string("0.0.0.0/0")) && !$exc->match_string($dstip){ 
                        $cGraphs->{$sectionName}->{$graphName}->{'_explode_'} 
                                ->{$srcip}->{'out'}->{'bytes'} += $bytes; 
                        $cGraphs->{$sectionName}->{$graphName}->{'_explode_'} 
                                ->{$srcip}->{'out'}->{'pkts'} += $pkts; 
                    } 
                } 
            }elseif($src_match){ 
                foreach my $net (split(/,/, $graph->{'subnet'})){ 
                    if (($subnets->{$net}->match_string($dstip) || $subnets->{$net}->match_string('0.0.0.0/0')) && !$exc->match_string($dstip)){ 
                        $cGraphs->{$sectionName}->{$graphName}->{'out'}->{'bytes'} += $bytes; 
                        $cGraphs->{$sectionName}->{$graphName}->{'out'}->{'pkts'} += $pkts; 
                    } 
                } 
            } 
            #Output 
            my $dst_match = $sourcenet->match_string($dstip); 
            #........... 
            #Блок волностью идентичный как и для входящего трафика, 
            # только вместо $src_mathc у нас $dst_match, вместо 
            # $srcip у нас $dstip; 
            #........... 
            last if $exp_condition 
        } 
    } 
}

В принципе все подготовлено, осталось описать цикл который будет обрабатывать flow файлы и функции для создания RRD графиков. Все до боли просто и в посте даже описывать не стоит. Работала данная конструкция довольно шустро за 4,35 минуты, при “боевом” конфиге укладывала flow файл размером 56 мегабайт. Мне большего не требовалось, потому и не стал оптимизировать и облагораживать код.

Скачать исходники




Источник: http://alienstudio.ru/samopisnyjj-parser-netflow/