NETGRAPH имеет ряд преимуществ. Наверняка, многие, замечали, как отличается объем трафика, который считаешь при помощи ipfw, и тот, который прислал провайдер со счетом. Объясняется все достаточно просто, ipfw отрабатывает не все пакеты, поступившие в bpf - пакетный фильтр системы. NETGRAPH выступает в данном случае как промежуточное звено, как маленькое кольцо, через которое проходят пакеты, считаются и перенаправляются дальше. Одно из его преимуществ - он работает на уровне ядра, используя минимум времени процессора и памяти. Тонкости работы и возможности его описаны в статье "Все о NETGRAPH" Арчи Коббса (перевод статьи на русский язык можно посмотреть на
http://www.opennet.ru/docs/RUS/netgraph_freebsd/index.html).
Мы же разберем, как установить ng_ipacct и сам NETGRAPH.
Перед тем, как делать какие либо шаги, скажу что все это протестировано на FreeBSD 5.2.1-RELEASE-p10, 5.3-RELEASE-p4, 4.10-RELEASE-p3, 4.11-RELEASE. Стоит обратить внимание, что с переходом на 5.3 и выше потребуется заново откомпилировать и собрать ng_ipacct. Так же это потребуется и при каждой новой компиляции ядра(на 5-й ветке).
Таким образом, исходные данные есть. Возьмемся за netgraph. Загружать в память его можно используя два метода: запускать нужные модули при старте либо вкомпилировать (нет имено так) сразу же в ядро. Мне предпочтителен последний вариант.
Делается все достаточно просто. Рассмотрим на примере для FreeBSD-4.10.
Первым делом идем в /usr/src/sys/i386/conf/ и смотрим LINT-файл:
root@ostwest :cd /usr/src/sys/i386/conf/
root@ostwest :less LINT
.............
options NETGRAPH #netgraph(4) system
options NETGRAPH_ASYNC
options NETGRAPH_BPF
options NETGRAPH_CISCO
options NETGRAPH_ECHO
options NETGRAPH_ETHER
options NETGRAPH_FRAME_RELAY
options NETGRAPH_HOLE
options NETGRAPH_IFACE
options NETGRAPH_KSOCKET
options NETGRAPH_L2TP
options NETGRAPH_LMI
# MPPC compression requires proprietary files (not included)
#options NETGRAPH_MPPC_COMPRESSION
options NETGRAPH_MPPC_ENCRYPTION
options NETGRAPH_ONE2MANY
options NETGRAPH_PPP
options NETGRAPH_PPPOE
options NETGRAPH_PPTPGRE
options NETGRAPH_RFC1490
options NETGRAPH_SOCKET
options NETGRAPH_TEE
options NETGRAPH_TTY
options NETGRAPH_UI
options NETGRAPH_VJC
.............
То есть опций достаточно много и есть из чего выбрать. Для избежания проблем с разного рода устройствами можно их все включить в наше ядро, но в самом простом случае (считаем только с (нет это обозначает что ?с такого типа? то есть предлог уместен) ethernet устройства) нам потребуются только такие опции в ядре:
options NETGRAPH
options NETGRAPH_ETHER
options NETGRAPH_SOCKET
options NETGRAPH_TEE
Дальнейшие наши действия заключаются в компиляции ядра:
root@ostwest :config SKIF
Don't forget to do a ``make depend''
Kernel build directory is ../../compile/SKIF
root@ostwest : cd ../../compile/SKIF && make depend && make && make install && make clean && rehash
Объясним, что же тут сделано. Первая команда:
config SKIF - конфигурирование файла ядра, в моем случае это SKIF Если ошибок в файле не было выявлено, то она выдаст такое:
Don't forget to do a ``make depend''
Kernel build directory is ../../compile/SKIF
Это маленькое напоминание о том, что необходимо сделать make depend и где это сделать.
cd ../../compile/SKIF && make depend && make && make install && make clean && rehash
- это полный список команд, необходимый для того, что бы перейти и скомпилировать наше ядро. Достаточно удобный, если никаких ошибок не ожидается, но, если возникнут, то выяснить на каком этапе они произошли будет проблематично. Посему, команды лучше выполнять по отдельности.
После всех этих манипуляций перезагрузим сервер.
root@ostwest : shutdown -r now
В принципе эту команду можно было добавить сразу же в верхнюю строку.
После перезагрузки мы получаем чистое ядро с поддержкой NETGRAPH.
Что ж, часть работы выполнена. Устанавливаем ng_ipacct. Первым делом смотрим порты, имеющиеся в системе. Там присутствует только ipacct:
root@ostwest :cd /usr/ports/
root@ostwest :make search key=ipacct
Port: ipacctd-1.46
Path: /usr/ports/net-mgmt/ipacctd
Info: IP accounting using divert socket
Maint: skv@FreeBSD.org
B-deps:
R-deps:
root@ostwest :
Сам же ng_ipacct можно найти здесь:
ftp://ftp.wuppy.net.ru/pub/FreeBSD/local/kernel/ng_ipacct/
На сервере присутствуют версии как для четвертой ветки FreeBSD, так и для пятой. Они неидентичны, так как реализация NETGRAPH в этих версиях FreeBSD заметно отличается. Основное отличие - синхронизация. В RELENG_4 она осуществляется через уровни прерываний, о которых можно почитать в man 9 spl. Весь код netgraph должен выполняться на уровне splnet.
Все граничные ноды, осуществляющие связь между netgraph и другой подсистемой, например ng_ether ,переходят в уровень splnet перед тем как отправить данные в граф. Если это невозможно, то данные ставятся в очередь и позже раздаются в нужной последовательности. Любые внешние вызовы, которые работают с netgraph, тоже должны первым делом вызывать splnet(). Таким образом, в одну единицу времени может существовать только один контекст выполнения netgraph и конфликтовать ему не с кем.
В RELENG_5 ядро многонитевое(multithreads) и синхронизация netgraph осуществляется с помощью мьютексов (блокировок, используемых для реализации гарантированной исключительности) и атомарных операций. Ноды передают друг другу объекты (items) различных типов: данные (mbufs), сообщения (ng_mesg), ссылки на функции. У объекта есть атрибут - reader или writer.
Нода может одновременно обрабатывать сколько угодно reader items или только одну writer item. По умолчанию объекты с данными - readers, а все остальные writers. Однако это можно указать как на уровне конкретных объектов, так и на уровне хуков(hooks).
Важным является то, что в момент, когда выполняется код внутри ноды, тред не держит ни одного мьютекса, что позволяет граничным нодам вызывать методы других подсистем избегая LOR(Lock order reversal ? блокирования устанавливаемых изменений).
То есть, это грозит нам как минимум тем, что один и тот же ng_ipacct не будет работать на разных ветках FreeBSD.
Что ж, скачиваем и распаковываем.
root@ostwest : tar xfvz ng_ipacct-20040109.tar.gz
root@ostwest :cd ng_ipacct/
root@ostwest :make && make install && make clean && rehash
Ничего особо сложного здесь нет и программа без особых проблем проинсталируется. В принципе это и все, что было необходимо для установки ng_ipacct. В комплекте к ней идут четыре скрипта, которые объясняют, как запустить программу для подсчета трафика и какостановить. Готовый скрипт для запуска и остановки: ng_ipacct_init.sh, он находиться в распакованной папке ng_ipacct/script. Этот скрипт, слегка подкорректировав, можно смело поместить /usr/local/etc/rc.d/
Все что нужно в нем прописать это:
прослушиваемые интерфейсы INTERFACES="ed0" - здесь это будет ed0. Для того, что бы указать более одного интерфейса ? перечислите их через запятую.
VERBOSE=1 - уровень расширенного вывода статистики, по умолчанию в скрипте 1, которая выведет нам дополнительно кроме IP-адреса источника и назначения количества пакетов и байт, еще и порты и протоколы, которые использовались. Стоит обратить внимание, что названия протоколов, если указан расширенный вывод(VERBOSE=1), будут отображены в числовом, а не буквенном виде. Что значит каждый номер, можно посмотреть в /etc/protocols
THRESHOLD=50000 - количество записей, которые будут храниться программой в памяти. На этот параметр стоит обратить особое внимание, так как неправильно подобранный размер threshold может привести к потери части данных или даже к панике ядра. Это возможно по той причине, что ng_ipacct работает на уровне ядра и ей не будет доступна полностью вся память, имеющаяся на машине, а только малая часть, зарезервированная непосредственно под ядро. В результате переполнения памяти выделенной системе на ядро может произойти паника со всеми вытекающими последствиями, как-то, в лучшем случае, остановка сервера и потеря записей, относительно трафика прошедшего через него. Поэтому если у вас менее 128 Mb памяти стоит себя ограничить на уровне менее 4000-5000 записей и чаще снимать статистику, чтобы не потерять нужные данные.
Для снятия статистики в ng_ipacct необходимо проделать следующее:
Передать данные в checkpoint (контрольную точку), вывести ее при помощи show из контрольной точки и очистить контрольную точкку.
Вот так это делается для интерфейса rl0:
root@ostwest : ipacctctl rl0_ip_acct:rl0 checkpoint
root@ostwest : ipacctctl rl0_ip_acct:rl0 show
root@ostwest : ipacctctl rl0_ip_acct:rl0 clear
После show вы увидите все пакеты, которые проходили через интерфейс.
Статистика выводиться в достаточно удобном CISCO формате:
ip_источника port_источника ip_назначения port_назначения протокол пакетов байт
Обычный режим имеет несколько другой формат вывода:
ip_источника ip_назначения пакетов байт
Стоит отметить, что имеется проблема с кодировками в man ipacctctl, просмотреть его удастся разве что в браузере. Ноэтолегко вылечить:
root@ostwest : zcat /usr/share/man/man8/ipacctctl.8.gz | nroff -man | gzip > /usr/share/man/cat8/ipacctctl.8.gz
В принципе, если вас интересует исключительно возможность поднять ng_ipacct, то на этом можно остановиться.
Мы же проследуем дальше, ибо этого мне было мало. Мне требовалось, чтобы все данные хранились в базе MySQL для каждого хоста и интерфейса, разнесенные по дате и времени.
Вот теперь опишем основные требования, которые были предъявлены биллингу:
Первое:
Система должна хранить данные не только по-интерфейсно, но и по хостам. Объясню для чего это нужно - что бы быстро разделить трафик между разными хостами/роутерами с которых считывается статистика. При этом количество интерфейсов различно и их наименование может совпадать (почти везде есть rl0 или fxp0).
Второе:
База должна разделять трафик за текущий и предыдущий месяцы самостоятельно и иметь возможность предоставить пользователю отчет за каждый из них. Для чего это нужно? Что бы таблицы бессмысленно не росли. Гораздо проще обработать одну маленькую за месяц, чем одну большую за год с выборкой за месяц. Просмотр статистики за предыдущие месяцы, может быть необходим дляотчета перед начальством или выставления счета клиенту, если такой имеется.
Третье:
В случае недоступности MySQL-сервера необходимо хранить полученные данные локально до тех пор, пока не будет устранена причина недоступности сервера базы данных. После чего данные автоматически должны быть перенесены в базу при следующем сеансе.
Четвертое:
Единый конфигурационный файл с удобным и интуитивно понятным содержанием.
Пятое:
Графический или web-интерфейс, для удобоваримого отображения статистики.
Шестое:
Неплохо было, что бы система, где необходимо, отличала локальный трафик от внешнего.
В принципе этот список можно продолжить, но, как по мне, выше приведенные требования являются ключевыми.
Итак, требования перечислены. Создадим, исходя из этого, наш конфигурационный файл. Все свои скрипты и программы я размещаю в папки расположенные в /usr/local/script . Если у вас такой нет, рекомендую создать. Если у вас путь будет отличен от моего, тогда внесите необходимые коррективы.
Итак, создаем рабочую папку со скриптами:
root@ostwest : mkdir -p /usr/local/script/ng_stat
root@ostwest : chown skif:wheel /usr/local/script/ng_stat
Смена владельца выполняется с целью защитить систему, в случае если наши скромные потуги в области программирования окажутся небезопасны. По крайней мере, никто не увидит что написано внутри скрипта, а значит, ломать его будет труднее.
skif@ostwest : mkdir /usr/local/script/ng_stat/etc
skif@ostwest : mkdir /usr/local/script/ng_stat/bin
Этим мы создали папки, где будут лежать наши конфигурационные и исполняемые файлы.
Что ж создадим конфигурационный файл и внесем первые параметры. По мере продвижения мы будем дополнять его нужными параметрами.
skif@ostwest : cd /usr/local/script/ng_stat/etc
Здесь мы создадим файл настройки ng_stat.conf и внесем следующие строки.
# Имя сервера, где находиться база данных статистики
server_db = freebsd
# Имя базы данных, где будет сохраняться статистика
db_name = ng_stat
# Имя пользователи для доступа к базе
db_user = nguser
# Пароль для доступа к базе
db_pass = rfn.if
# Имя хоста с которого снимается статистика
listen_host = freebsd2
# Имена интерфейсов, которые прослушиваются на компьютере.
# Указывать через запятую
listen_interfaces = rl0
Думаю пояснений к строкам приведенного конфигурационного файла не нужно.
Итак, первым делом откажемся от поставляемого в комплекте с ng_ipacct скрипта для его старта и остановки. Лучшенапишемсвой
skif@ostwest : cd /usr/local/script/ng_stat/bin
skif@ostwest : touch ng_stat_start.pl
Данный скрипт будет служить нам скелетом для последующих, и мы будем частенько от него отталкиваться.
Итак первое что мы сделаем это объявим основной набор переменных:
#!/usr/bin/perl -w
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
Все переменные созвучны описанным в конфиге и являются глобальными для данного файла. Внеся заранее значение "test" в них, мы избежали проблемы получить в самом не подходящем месте undef. Но обратите внимание что, прослушиваемые интерфейсы обозначены не переменной, а массивом. Сделано это потому, что интерфейсов может быть несколько, а не один. Вот мы и используем массив.
Почему были внесены такие непонятные значения переменных? Объясняется все достаточно просто. Во-первых, сюда можно внести значения реальных данных по умолчанию, которые будут считываться. Во вторых, если на этапе отладки будут проблемы ? изменив значения, вы сможете выяснить, с какой переменной у вас непорядок и где.
Теперь откроем конфигурационный файл и прочитаем значения наших
переменных:
open (CONFIG, "/usr/local/script/ng_stat/etc/ng_stat.conf");
while () {
}
close (CONFIG);
Этими строками открывается конфигурационный файл и, при помощи while, полностью считывается и закрывается. Обратите внимание, что в данном случае используется полное указание пути к файлу в явном виде, а в последствии будем указывать его неявно, через переменные.
Что ж первое, что нам нужно сделать, это разобрать строки, которые поочередно считывает while до тех пор, пока не дойдет до конца файла. Но среди полезной информации конфигурационный файл несет в себе комментарии. От них нужно избавиться. Для этого в perl имеется мощнейшие инструменты поиска в строках/словах. Один из них - конструкция вида m/шаблон/ограничитель, им и воспользуемся, условившись, что комментарием будет символ # :
$comment = '#';
if(/^$comment/) {
print "Коментарий\n";
}
else {
# разбор строк не ограниченных коментарием
}
Объясним конструкцию if ... else : если вначале строки присутствует символ комментария, то на экран будет выведено сообщение "Комментарий", в противном случае строка пойдет по else. Вывод сообщений о наличии комментариев нам необходим только на этапе отладки. Кстати, можете проверить, как скрипт работает, в последствии он будет закомментирован.
Но этого мало, необходимо разобрать и полезную строку.
($param,$arg) = split("=",$_);
chomp $param;
chomp$arg;
$param =~ s/\s//g;
$arg =~ s/\s//g;
Для разбора использовалась функция split, которая на основе разделителя ?=?, заданного еще в конфигурационном файле, разбила все полезные строки на две части: параметр и аргумент.Что бы избавиться от пробельных символов используется оператор замены s/шаблон/замена/ограничитель.Так как необходимо избавиться от пробельных символов, а не поменять их на что-то другое, мы не используем параметр ?замена?, оставляя его пустым.
Модификатор \s означает любой пробельный символ.
Перед этим были убраны из обоих переменных символы перевода строки при помощи chomp.
Если в строке присутствуют не только символы пробела, но и табуляции или если их несколько, то придется прибегнуть к следующей конструкции:
$param =~ s/[\s\t]+//g;
$arg =~ s/[\s\t]+//g;
Теперь необходимо присвоит каждой объявленной переменной ее истинное значение, находящееся в конфигурационном файле. В этом нам поможет конструкция следующего вида:
if ($param eq "server_db"){
$serverdb = $arg;
}
Объясним. Если левая часть полученной из файла строки соответствует server_db (смотрим наш конфигурационный файл), то правая часть присвоится необходимой переменной.
Но у нас же есть еще несколько значений параметра в одной из строк. Их мы должны, предварительно разобрав, занести в массив.
Листинг приведен ниже:
#!/usr/bin/perl -w
use DBI;
use POSIX ":sys_wait_h";
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
my $iface_set = "no";
my @ng_modules;
my $ng_modules_def = "netgraph,ng_ether,ng_socket,ng_tee,ng_ipacct";
my$threshold = 5000;
#########################
# Читаем конфиг. файл.
#########################
open (CONFIG, "/usr/local/script/ng_stat/etc/ng_stat.conf");
while () {
$comment = '#';
if(/^$comment/) {
# print "Коментарий\n";
}
else {
($param,$arg) = split("=",$_);
chomp $param;
chomp $arg;
my $razdel = "";
$param =~ s/[\s\t]+/$razdel/g;
$arg =~ s/[\s\t]+/$razdel/g;
if ($param eq "server_db"){
$serverdb = $arg;
}
if ($param eq "db_name"){
$dbname = $arg;
}
if ($param eq "db_user") {
$dbuser = $arg;
}
if ($param eq "db_pass") {
$dbpass = $arg;
}
if ($param eq "table_auth") {
$table_auth = $arg;
}
if ($param eq "table_protocols") {
$table_proto = $arg;
}
if ($param eq "listen_host") {
$listen_host = $arg;
}
if ($param eq "listen_interfaces") {
my $coma = ',';
if (defined $arg) {
$iface_set = "ok";
if ($arg ne ""){
if ($arg =~ m/$coma/ ) {
@listen_interf=split($coma,$arg);
}
else {
@listen_interf = $arg;
}
}
}
}
if ($param eq "ng_modules") {
my $coma = ',';
if ($arg =~ m/$coma/ ){
@ng_modules = split($coma,$arg);
}
else {
@ng_modules = split ($coma,$ng_modules_def);
}
}
}
}
close (CONFIG);
if (!defined $listen_interf[0]) {
print "Установите пожалуйста в режим прослушивания хотя бы один интерфейс.\n";
}
else {
&check_kld_modules;
&listening;
}
Как видите, мы считали все параметры, и в случае, если интерфейс по какой либо причине установлен не будет, то на экран будет выдано сообщение об этом. А если все нормально, то в массив будут внесены необходимые имена интерфейсов (например, rl0, rl1,rl2,fxp0) и, после проверки массива @listen_interf на наличие в нем не пустых значений, будут выполнены подпрограммы: &check_kld_modules и &listening.
Первая проверяет, какие из обязательных модулей загружены. При необходимости, будет проведена их загрузка.
Вторая включает режим прослушивания интерфейсов.
Рассмотрим первую.
subcheck_kld_modules {
my @modules;
my $pid;
my $ng_module_cfg;
my $chk_ng_file = "/tmp/ng_file";
my $check_ng = 'kldstat -v | grep ng';
$check_ng = "$check_ng";# " > $chk_ng_file";
my $check_netgraph = 'kldstat -v | grep netgraph';
$check_netgraph = "$check_netgraph";#" >> $chk_ng_file";
# $pid = fork;
@modules =split ("\n", `$check_ng && $check_netgraph`);
my $mod;
if (defined $modules[0]) {
foreach my $modules (@modules) {
$modules=~ s/\d+//g;
if ($modules =~ s/.ko//g) {
#
}
else {
$modules =~ s/[\s\t]+//g;
$mod = "$mod $modules ";
}
}
chop $mod;
foreach my $ng_modules (@ng_modules) {
if ($mod=~m/$ng_modules/g){
# print "$mod содержит $ng_modules\n";
}
else {
my ($pid,$kid);
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"\n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1"
or die "Ошибка загрузки модуля $ng_modules !\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы не возможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
}
}
}
else {
foreach my $ng_modules (@ng_modules) {
my ($pid,$kid);
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"\n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы не возможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
}
}
}
Итак, первым делом объявляются действующие только в переделах этого модуля массивы и переменные. В нашем случае это @modules, куда будут заноситься все модули netgraph присутствующие в ядре или загруженные на данный момент. $check_netgraph и $check_ng переменные, в которых записаны команды, проверки загруженных модулей ядра.
Команда эта достаточно проста и имеет вид:
root@ostwest : kldstat -v
...............
234 dummynet
235 if_gif
236 ipfw
237 if_loop
238 ng_async
239 ng_bpf
4 1 0xc272d000 4000 ng_ipacct.ko
Containsmodules:
Name
246 ng_ipacct
....................
Как вы можете заметить вывод не маленький, поэтому пришлось его урезать. Нам нужны не все модули, а только те, которые имеют отношение к netgraph. Этим и займутся переменные, когда их используют как значения для оператораexec.
Что бы получить список загруженных модулей используется split и обратные кавычки, в качестве разделителя выступает символ переноса строки:
@modules =split ("\n", `$check_ng && $check_netgraph`);
Дальше пойдем по проторенному пути, а именно ? выясним, имеется в массиве хоть какие то данные. Если полученный массив не пустой, то мы выполним проверку, какие модули нам необходимо подгрузить для работы.
В данном случае информация о том из какого файла был загружен модуль(linux.ko, logo_server.ko или что-то другое) не нужна. Так же не нужны ID загруженных модулей. Для их удаления используется все тот же m//:
$modules=~ s/\d+//g;
?\d? означает любой цифровой символ.
После удаления ID проверяется, что присутствует в выводе, информация о том из какого модуля загрузился файл или сам модуль. Однозначно на файл указывает присутствие расширения ?.ko? в строке. А потому все полученные строки, где присутствует ?.ko? подлежат удалению. В листинге вы видите, что на месте совпадения if с ".ko" стоит комментарий. Если хотите, можете провести синтаксически разбор и вывести на экран имя того модуля, который был загружен вручную.