Netflow-искания Часть 3 (изобретаем коллектор)

Блог им. dreamhunter
Итак поигрались с готовыми решениями — красота. Что дальше? Лично меня очень беспокоила информация о потерянных пакетах в логах:
# /var/log/flow-capture.log:
ftpdu_seq_check(): src_ip=xxx.xxx.xxx.xxx dst_ip=0.0.0.0 d_version=5 expecting=1531521097 received=1531521307 lost=210
ftpdu_seq_check(): src_ip=xxx.xxx.xxx.xxx dst_ip=102.0.0.0 d_version=5 expecting=1730244036 received=1730300526 lost=56490
ftpdu_seq_check(): src_ip=xxx.xxx.xxx.xxx dst_ip=99.111.117.110 d_version=5 expecting=1730207586 received=1730300556 lost=92970
ftpdu_seq_check(): src_ip=xxx.xxx.xxx.xxx dst_ip=102.0.0.0 d_version=5 expecting=1730300556 received=1730300586 lost=30

Плюс ко всему какие то нелепые dst_ip.

Что бы не мешать основному процессу, добавляю на сенсоре строчку для дублирования инфы на другой порт:
# conf t
# ip flow-export destination xxx.xxx.xxx.xxx 9997

Циска ругнется, что мы дублируем инфу, но послушно выполнит.

Теперь надо как-то взять информацию:
Пишем на перле (!PhP) скрипт:
#!/usr/bin/perl
use IO::Socket::INET;
$| = 1;
my ($socket,$received_data);
my ($peeraddress,$peerport);
$socket = new IO::Socket::INET (
LocalPort => '9997',
Proto => 'udp',
) or die "ERROR in Socket Creation : $!\n";
while(!$recieved_data)
{
$socket->recv($recieved_data,1464);
$peer_address = $socket->peerhost();
$peer_port = $socket->peerport();
}

open (MYFILE, '>data.txt');
# print MYFILE "$peer_address:$peer_port\n"; # <- строчка просто что бы показать метод.
print MYFILE $recieved_data;
close (MYFILE);

$socket->close();

Запускаем скрипт на сервере и он выхватывает один пакет с информацией по netflow.
Открываем официальную спецификацию по протоколу NetFlow v 5 и анализируем:
00000000 00 05 00 1E │ 64 DB 5B 60 │ 51 23 3F B6 │ 38 E5 5D 5A │ 67 33 DF D6 │ 00 00 00 00  ....dш[`Q#?І8Е]Zg3ъж....
00000018 0A 4F E2 0A │ 0A 01 FF 01 │ C0 A8 22 02 │ 00 14 00 02 │ 00 00 00 02 │ 00 00 01 96  .OБ...Ъ.ю╗".............
00000030 64 DA F4 3C │ 64 DB 11 88 │ E9 96 01 85 │ 00 10 11 00 │ 00 00 00 00 │ 14 10 00 08  dзТ<dш..И...............
00000048 0A 5C 03 04 │ 0A 4F E2 32 │ C0 A8 22 56 │ 00 02 00 14 │ 00 00 00 02 │ 00 00 00 D9  .\...OБ2ю╗"V...........ы
00000060 64 DA F4 40 │ 64 DB 1B 68 │ 00 35 C0 19 │ 00 10 11 24 │ 00 00 00 00 │ 10 14 00 00  dзТ@dш.h.5ю....$........
00000078 0A 49 D0 08 │ 0A 4F E2 0A │ C0 A8 22 56 │ 00 02 00 14 │ 00 00 00 02 │ 00 00 02 8E  .Iп..OБ.ю╗"V............
00000090 64 DA F4 C0 │ 64 DB 14 2C │ 00 8A 00 8A │ 00 10 11 24 │ 00 00 00 00 │ 10 14 00 00  dзТюdш.,.......$........
000000A8 0A 4F A5 0A │ 0A 02 1A 8C │ C0 A8 22 02 │ 00 08 00 02 │ 00 00 00 03 │ 00 00 00 90  .O╔.....ю╗".............
000000C0 64 DA F5 08 │ 64 DB 17 E4 │ 0E 8B 16 13 │ 00 02 06 00 │ 00 00 00 00 │ 14 10 00 08  dзУ.dш.Д................
********************************************************************************************************************
000003D8 0A 4F A1 46 │ 0A 48 38 1B │ C0 A8 22 02 │ 00 08 00 02 │ 00 00 00 0A │ 00 00 04 F2  .O║F.H8.ю╗"............Р
000003F0 64 DB 3E 50 │ 64 DB 48 18 │ 11 9A 01 BB │ 00 1B 06 00 │                            dш>PdшH....╩....

Каждый такой пакет — таблица.
Первые 24 байта (0-23) — это шапка:
00 05 - версия протокола 5
00 1E - в таблице будет 30 записей
64 DB 5B 60 - System uptime в милисекундах (переводить надо?)
51 23 3F B6 - Текущее время в секундах (с начала эпохи)
38 E5 5D 5A - Остаточная часть в НАНОСЕКУНДАХ!!! (с начала эпохи)
67 33 DF D6 - Номер последовательности.
00 - тип движка
00 - идентификатор движка
00 00 - sampling_interval - Если честно я так и не понял что это за фигня.

Остальные данные — записи, которые разбиты по 48 байт.

Здесь я наблюдаю некоторую избыточность:
1) Зачем под версию выделено 2 байта?! То есть планируется больше 255 версий?
2) Очень странно, что под остаточную часть выделяется 4 байта Зачем такая точность?!.. Мне бы хватило 2.
3) Тип движка и его идентификатор. Что то мне подсказывает, что это тоже не очень важные данные.
4) Sampling interval — еще 2 байта, заполненные нулями.
Итого около 7 байт ненужной информации, которую врятли кому понядобятся. Учитывая тот факт, что пятая версия модифицироваться не будет, сомнений о бесполезности не остается.
7 байт тебе жалко что ли? — Умножаем эти 7 байт на миллионы пакетов, которые передаются и получаем ощутимое множество тактов оборудования. NetFlow и так очень прожорливый протокол.
В самих записях есть все необходимые данные по потокам. Вероятно инженеры Cisco разрабатывали такой вид таблиц для какой то своей базы с особой структурой, в которой имеют значения, взятые из заголовков. Но в обычном быту системного администратора (вот только не надо ржать) это не очень нужно. С другой стороны если мы захотим, например следить за устройством, нам все равно придется выкачивать весь объем данных. Вероятно в этом вопросе решили подойти со всех сторон. Вспомним хотя бы тот факт, что маршрутизатор не принимает никаких запросов, а просто шлет накопленную информацию.
Теперь давайте подумаем что мы можем сделать?..
Для начала декодируем пакет в удобоваримый вид:
#!/usr/bin/perl
use IO::Socket::INET;
use POSIX;

$| = 1;

my ($socket,$received_data);
my ($peeraddress,$peerport);

$socket = new IO::Socket::INET (
LocalPort => '9997',
Proto => 'udp',
) or die "ERROR in Socket Creation : $!\n";

while(!$recieved_data) {
    $socket->recv($recieved_data,1464);
    $peer_address = $socket->peerhost();
    $peer_port = $socket->peerport();
}

@header = unpack("nnN4NNNHHH2", substr($recieved_data,0,24));

open (MYFILE, '>data.txt');
print MYFILE "Source: $peer_address:$peer_port\n",
             "Protocol version: $header[0]\n",
             "Records count: $header[1]\n",
             "System uptime: $header[2] ms\n",
             "Timer: " . strftime ("%Y/%m/%d %H:%M:%S", localtime($header[3])) . "\n";
print MYFILE "\nsource_ip\t",
             "dst_ip\t\t",
             "next_hop\t",
             "if_in\t",
             "if_out\t",
             "Pkts\t",
             "Octets\t",
             "Time start\t\t",
             "Time end\t\t",
             "S_port\t",
             "D_port\t",
             "pad1\t",
             "Flag\t",
             "Prot\t",
             "TOS\t",
             "src_as\t",
             "dst_as\t",
             "s_mask\t",
             "d_mask\t",
             "pad2\n\n";
foreach $i (0..$header[1]) {
    if (substr($recieved_data, 24+48*$i, 48)) {
    @substr = unpack("A4 A4 A4 n n N N x8 n n C C C C n n C C n X24 N N", substr($recieved_data, 24+48*$i, 48));
    if ($substr[18] && $substr[19]) {
        @t = localtime($header[3]+floor(($header[4]/1000000+$substr[18]-$header[2])/1000));
        $substr[20] = fmod((floor($header[4]/1000000)+$header[2]-$substr[18]), 1000);
        $start = strftime "%Y-%m-%d.%H:%M:%S", @t;
        $start .= ".$substr[20]";
        @t = localtime($header[3]+floor(($header[4]/1000000+$substr[19]-$header[2])/1000));
        $substr[21] = fmod((floor($header[4]/1000000)+$header[2]-$substr[19]), 1000);
        $stop = strftime "%Y-%m-%d.%H:%M:%S", @t;
        $stop .= ".$substr[21]";
    };
    $fmt = "%vd\t%vd\t%vd\t\%d\t%d\t%d\t%d\t$start\t$stop\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n";
    printf MYFILE ($fmt, @substr);
    }
};

#print MYFILE $recieved_data; #debug info
close (MYFILE);

$socket->close();


Если честно я очень лоханулся, не ознакомившись с работой сокетов Perl. Грабли меня ждали в строчке:
$socket->recv($recieved_data,1464);

У меня было подозрение, что маршрутизатор работает неправильно, говоря в заголовке о том, что передает 30 записей, а передавая на самом деле 20. Дак вот я просто скопировал из примера неправильное число байт, которое будет ожидать коллектор (1024). На самом деле мы должны ожидать 24+48*30=1464 байт. Только постоянные сомнения в себе помогли мне исправиться.

Вдогонку скрипт просмотра потока netflow в реальном внемени. Для него не нужно вообще ничего кроме установленного Perl. Возможно пригодится для диагностики.
#!/usr/bin/perl
#Realtime viewer made by DreamHunter
use IO::Socket::INET;
use POSIX;

$| = 1;

my ($socket,$received_data);
my ($peeraddress,$peerport);

$socket = new IO::Socket::INET (
LocalPort => '9997',
Proto => 'udp',
) or die "ERROR in Socket Creation : $!\n";

while(1) {
    $socket->recv($recieved_data,1464);
    $peer_address = $socket->peerhost();
    $peer_port = $socket->peerport();
    @header = unpack("nnN4NNNHHH2", substr($recieved_data,0,24));
    for ($i=0; substr($recieved_data, 24+48*$i, 48); $i++) {
        @substr = unpack("A4 A4 A4 n n N N x8 n n C C C C n n C C n X24 N N", substr($recieved_data, 24+48*$i, 48));
        if ($substr[18] && $substr[19]) {
            @t = localtime($header[3]+floor(($header[4]/1000000+$substr[18]-$header[2])/1000));
            $substr[20] = fmod((floor($header[4]/1000000)+$header[2]-$substr[18]), 1000);
            $start = strftime "%Y-%m-%d.%H:%M:%S", @t;
            $start .= ".$substr[20]";
            @t = localtime($header[3]+floor(($header[4]/1000000+$substr[19]-$header[2])/1000));
            $substr[21] = fmod((floor($header[4]/1000000)+$header[2]-$substr[19]), 1000);
            $stop = strftime "%Y-%m-%d.%H:%M:%S", @t;
            $stop .= ".$substr[21]";
            $fmt = "%vd\t%vd\t%vd\t\%d\t%d\t%d\t%d\t$start\t$stop\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n";
            printf ($fmt, @substr);
        }
    }
};
$socket->close();


Послесловие: Лично для себя я считаю тему NetFlow версии 5 раскрытой. Предыдущие версии рассматривать нет смысла — там практически тоже самое. То есть работа для спинного мозга (тупо долбить код). Впереди ждет 9-я версия, которая призвана работать с протоколом IPv6.

7 комментариев

avatar
Дописал код программы. Теперь точность соблюдается. Хотя кому нужны эти нано и милисекунды?..
avatar
Пересел с centos 5.8 x32 на 6.3 x64, поставил из реп flow-tools, при импорте в базу выдает: flow-export: Format not supported. Поискал в сети. Типа собрана моя версия без поддержки mysql (в этом я сильно сомневаюсь). Ну не вопрос: ./configure --with-mysql && gmake && gmake install. Результат все тот же. Для тестов пробовал такое:

/usr/local/flow-tools/bin/flow-export -f3 -mSRCADDR,DSTADDR -u "traffic:traffic:localhost:3306:traffic:netflow" < ft-v05.2013-02-28.020001+0400


(это уже собрано из исходников). В ASCII CSV замечательно пишет. В чем может быть проблема??! На ровном месте…

Исходные данные:

mysql> desc netflow;
+-----------+-------------+------+-----+-------------------+-----------------------------+
| Field     | Type        | Null | Key | Default           | Extra                       |
+-----------+-------------+------+-----+-------------------+-----------------------------+
| UNIX_SECS | int(255)    | NO   |     | NULL              |                             |
| DOCTETS   | int(255)    | NO   |     | NULL              |                             |
| SRCADDR   | varchar(45) | NO   |     | NULL              |                             |
| DSTADDR   | varchar(45) | NO   |     | NULL              |                             |
| PROT      | int(11)     | NO   |     | NULL              |                             |
| SRCPORT   | int(5)      | NO   |     | NULL              |                             |
| DSTPORT   | int(5)      | NO   |     | NULL              |                             |
| DPKTS     | int(11)     | NO   |     | NULL              |                             |
| FIRST     | int(255)    | NO   |     | NULL              |                             |
| LAST      | int(255)    | NO   |     | NULL              |                             |
| time      | timestamp   | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-----------+-------------+------+-----+-------------------+-----------------------------+

mysql> show grants for traffic@localhost;
+----------------------------------------------------------------------------------------------------------------+
| Grants for traffic@localhost                                                                                   |
+----------------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'traffic'@'localhost' IDENTIFIED BY PASSWORD '*1FB82F6DFC05F43BC9A0AE4B8CC1D7D6D04E83C9' |
| GRANT ALL PRIVILEGES ON `traffic`.* TO 'traffic'@'localhost'                                                   |
+----------------------------------------------------------------------------------------------------------------+
2 rows in set (0.01 sec)
avatar
Ты не совсем в правильном топике задал вопрос.
По делу отвечу следующее:
Да, у меня тоже не экспортировало в MySQL. И если я указывал ему пересобраться с поддержкой MySQL, он все равно собирался неправильно.
Решил проблему так:
# cd /usr/ports/net-mgmt/flow-tools
# make
# cd work/flow-tools-0.68
# ./configure --with-mysql --without-pgsql --without-openssl
# make
# cd ../../
# make install clean

Черт его знает какие там проблемы с установочными скриптами, но после такой пляски у меня пошло.
avatar
У меня почему-то нет прав даже в свой блог писать. Потому отвечу тут.

Решение для centos 6.3 x64. В файле configure на строках 11789 и 11829 жестко указаны директории, где должны быть библиотеки от mysql. Надо всего на всего поменять пути вот так:

11789: LDFLAGS="$LDFLAGS -L$WHERE_MYSQL/lib64/mysql"
(...)
11829: MYSQLLDFLAGS="-L$WHERE_MYSQL/lib64/mysql"

И заново собрать flow-tools:

% ./configure --with-mysql
% gmake
% gmake install
avatar
защита от спамеров, теперь можно писать
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.