Интеграция с FreeBSD-шлюзом по схеме VLAN-per-user и авторизацией через DHCP option 82

Материал из BiTel WiKi

Перейти к: навигация, поиск

Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга. В описываемой схеме используется FreeBSD 9.1

В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 1 справа).

(Схема 1) Графическое представление сети на основе шлюзов FreeBSD

Если смотреть в иерархии модуля, то связь такая: Access+Accounting: DHCP -> FreeBSD-шлюз ->Абонентский свитч. Схема работает по принципу VLAN на абонента (VLAN-per-user) + IP Unnumbered (SuperVLAN). Сразу оговорюсь, что каждый шлюз предполагает терминирование до 4096 VLAN ID. Т.е. недостатка VLAN ID не предвидится. Скорее у вас закончатся мощности по маршрутизации трафика, чем VLAN ID.

Минимальный функционал абонентского свитча: 802.1q VLAN, DHCP Relay с option 82, ssh\telnet управление. Естественно нелишним будет как минимум loop guard, RSTP, acl, port isolation (private vlan). Для IPTV - igmp snooping и MVR.

Минимальный функционал магистрального свитча:802.1q VLAN trunking (т.е. свитч должен уметь через себя пропускать все 4096 VLAN-ов). Собственно от него требуется только собирать гроздь VLAN-ов абонентских свитчей и передавать это все на шлюз. Для IPTV обязательно нужен igmp snooping.

Абонент получает IP-адрес с помощью DHCP Option 82. Идентификатором абонента является либо ID-свитча+VLAN ID, либо ID-свитча+VLAN ID+MAC.

Вся терминация абонентских VLAN-ов на шлюзе сделана в ядре с помощью netgraph (см. Схему 2 справа).

(Схема 2) Визуализация netghraph-нод для шлюза FreeBSD

На Схеме 2 и в примерах конфигурации ниже, предполагается, что интерфейс em1 смотрит в сторону абонентов, а интерфейс em2 в сторону ядра сети. Скрипт создает всю цепочку netgraph, создает абонентские VLAN-интерфейсы, переименовывает их более удобоваримый вид. Стоит выделить то, что все, явно несозданные VLAN-ы, прозрачно ходят между em1 и em2. Это полезно, например, если вы в ядре имеете PPPoE-сервер для смешанных сетей (в период модернизации или невыгодно на дом ставить полноценный свитч) или хотите оказать юрику услугу по объединению офисов или опять же пропустить прозрачно VLAN с мультикаст-трафиком.


Ниже привожу скрипт запуска построения netgraph-нод, кладется в файл /usr/local/etc/rc.d/netgraph Важно его запускать именно таким способом, это гарантирует, что он выстроит ноды до запуска всех важных сетевых сервисов (например quagga).

/usr/local/etc/rc.d/netgraph

#!/bin/sh
 
# PROVIDE: netgraph
# REQUIRE: FILESYSTEMS netif routing nfsclient
# BEFORE: quagga
# KEYWORD: nojail
 
. /etc/rc.subr
 
name="netgraph"
start_cmd="/root/ngf.rules"
stop_cmd=":"
 
load_rc_config $name
run_rc_command "$1"


Собственно сам скрипт генерации netgraph-нод: /root/ngf.rules

#!/bin/sh
 
# GLOBAL VARIABLES
# -- Первый пользовательский VLAN
FIRST_VLAN="1001"
# -- Последний пользовательский VLAN
LAST_VLAN="3000"
# -- Собственный адрес шлюза, через который будет осуществляться маршрутизация
GATEWAY_ADDR="cc.19.64.2"
# -- Адрес для управления шлюза
MANAGMENT_ADDR="192.168.129.40"
# -- Адрес коллектора netflow-потока
NETFLOW_COLLECTOR="aa.200.144.16:2004"
# -- Номер ngeth-интерфейса, с которого начинается пользовательские VLAN'ы
# -- Нумерация с нуля. Ниже до основного цикла создаются два VLAN-а для
# -- собственных нужд, соответственно нумерация клиентских пойдет с 2.
FIRST_NGETH_TO_RENAME="2"
 
# PROGRAMS PATH
NGCTL=/usr/sbin/ngctl
IFCONFIG=/sbin/ifconfig
ROUTE=/sbin/route
EXPR=/bin/expr
GREP=/usr/bin/grep
AWK=/usr/bin/awk
 
MAC_ADDR=`$IFCONFIG em1 | $GREP ether | $AWK '{print $2}'`
 
# -- Создание netflow ноды, а также двух служебных VLAN. Через VLAN ID 1
# -- осуществляется управление шлюзом и всеми свитчами за ним. Через
# -- VLAN ID 11 осуществляется маршрутизация всего абонентского трафика.
$NGCTL -f - << EOF
mkpeer em1: netflow lower iface0
name em1:lower netflow
 
mkpeer netflow: vlan out0 downstream
name netflow:out0 vlan_em1
 
msg netflow: setconfig { iface=0 conf=11 }
msg netflow: settimeouts { inactive=15 active=60 }
 
mkpeer em2: vlan lower downstream
name em2:lower vlan_em2
 
connect vlan_em1: vlan_em2: nomatch nomatch
 
mkpeer vlan_em2: eiface vlan11 ether
name vlan_em2:vlan11 vlan11
msg vlan_em2: addfilter { vlan=11 hook="vlan11" }
 
mkpeer vlan_em1: bridge vlan1 link1
name vlan_em1:vlan1 bridge0
 
connect vlan_em2: bridge0: vlan1 link2
 
mkpeer bridge0: eiface link0 ether
name bridge0:link0 vlan1
 
msg vlan_em1: addfilter { vlan=1 hook="vlan1" }
msg vlan_em2: addfilter { vlan=1 hook="vlan1" }
 
msg em1: setpromisc 1
msg em2: setpromisc 1
msg em1: setautosrc 0
msg em2: setautosrc 0
 
EOF
 
# -- Создаем абонентские VLAN-интерфейсы
VLAN=$FIRST_VLAN
while [ "$VLAN" -le "$LAST_VLAN" ]
do
    $NGCTL mkpeer vlan_em1: eiface vlan$VLAN ether
    $NGCTL name vlan_em1:vlan$VLAN em1_$VLAN
    $NGCTL msg vlan_em1: addfilter \{ vlan=$VLAN hook=\"vlan$VLAN\" \}
    VLAN=`$EXPR $VLAN + 1`
done
 
# -- Назначаем имя и IP для 11-го VLAN-интерфейса
$IFCONFIG ngeth0 name vlan11
$IFCONFIG vlan11 down
$IFCONFIG vlan11 ether `$IFCONFIG em2 | $GREP ether | $AWK '{print $2}'`
$IFCONFIG vlan11 inet $GATEWAY_ADDR netmask 255.255.255.0 up
 
# -- Назначаем имя и IP для 1-го VLAN-интерфейса
$IFCONFIG ngeth1 name vlan1
$IFCONFIG vlan1 down
$IFCONFIG vlan1 ether $MAC_ADDR
$IFCONFIG vlan1 inet $MANAGMENT_ADDR netmask 255.255.240.0 up
 
# -- Выставляем параметры netflow-ноды
$NGCTL mkpeer netflow: ksocket export inet/dgram/udp
$NGCTL msg netflow:export bind inet/$GATEWAY_ADDR
$NGCTL msg netflow:export connect inet/$NETFLOW_COLLECTOR
 
# -- Приводим имена интерфейсов абонентских VLAN-интерфейсов к удобному
# -- виду и назначаем им IP-адрес шлюза
VLAN=$FIRST_VLAN
NGETH=$FIRST_NGETH_TO_RENAME
while [ "$VLAN" -le "$LAST_VLAN" ]
do
    $IFCONFIG ngeth$NGETH name em1.$VLAN
    $IFCONFIG em1.$VLAN ether $MAC_ADDR
    $IFCONFIG em1.$VLAN inet $GATEWAY_ADDR netmask 255.255.255.255 up
    VLAN=`$EXPR $VLAN + 1`
    NGETH=`$EXPR $NGETH + 1`
done
 
# -- Удаляем ненужный маршрут
$ROUTE del $GATEWAY_ADDR/32
 
#Применяем любые другие настройки, специфичные для конкретного шлюза, если есть
. /root/other_conf.sh

Вообще, про подсистему netgraph можно почитать например здесь http://habrahabr.ru/blogs/bsdelniki/86553/

Для обмена маршрутами между шлюзами и другими маршрутизаторами ядра используется пакет Quagga, в котором мы используем zebra и bgpd/ospfd. Нужно отметить, что для корректной работы конструкции Unnumbered IP (SuperVLAN) нужно в таблицу маршрутизации прописывать маршруты вида:

route add -host cc.19.64.111 -iface em1.1234

где cc.19.64.111 IP абонента, а em1.1234 интерфейс (1234 - номер VLAN-интерфейса). Т.к. у нас используется zebra, маршруты будем прописывать в ней, а zebra уже сама их внесет в системную таблицу маршрутизации:

zebra# conf t
zebra(conf)# ip route cc.19.64.111 em1.1234
zebra(conf)# end

Соответственно и удалять нужно в zebra:

zebra# conf t
zebra(conf)# no ip route cc.19.64.111 em1.1234
zebra(conf)# end


Для автоматизации процесса добавления и удаления маршрутов с FreeBSD-шлюза был переработан скрипт manad, к которому обращается биллинг при инициализации и терминировании сессии, а также для выставления нужных скоростей в соответствии с тарифной политикой.

/root/manad.pl

#!/usr/bin/perl
 
use POSIX;
use IO::Socket;
use IO::Select;
use Socket;
use Fcntl;
use Tie::RefHash;
use Time::HiRes qw( usleep );
 
$debug = 1;
$port = 48444;
$ipfw = "/sbin/ipfw";
$route = "/root/route.sh";
 
# Начать с пустыми буферами
%inbuffer       = ();
%outbuffer      = ();
%ready          = ();
 
tie %ready, 'Tie::RefHash';
 
# Прослушивать порт
$server = IO::Socket::INET->new( LocalPort => $port, Listen => 10 )
        or die "Can`t make server socket: $@\n";
 
nonblock( $server );
 
$SIG{INT} = sub { $server->close(); exit( 0 ); };
 
$select = IO::Select->new( $server );
 
$pid = getpid();
 
open(FILE, ">/var/run/manad.pid");
print FILE $pid;
close(FILE);
 
# Устанавливаем новый root каталог для процесса
# chroot( $homedir ) or die "Couldn`t chroot to $homedir: $!\n";
 
# Главный цикл: проверка чтения/принятия, проверка записи,
# проверка готовности к работе
 
while( 1 )
{
        my $client;
        my $rv;
        my $data;
 
        # Проверить наличие новой информации на имеющихся подключениях
 
        # Есть ли что-нибудь для чтения или подтверждения?
        foreach $client ( $select->can_read( 1 ) )
        {
                if ( $client == $server )
                {
                        # Принять новое подключение
                        $client = $server->accept();
                        $select->add( $client );
                        nonblock( $client );
                }
                else
                {
                        # Прочитать данные
                        $data = '';
                        $rv = $client->recv( $data, POSIX::BUFSIZ, 0 );
 
                        unless( defined( $rv ) && length $data )
                        {
                                # Это должен быть конец файла, поэтому закрываем клиента
                                delete $inbuffer{$client};
                                delete $outbuffer{$client};
                                delete $ready{$client};
 
                                $select->remove( $client );
                                close $client;
                                next;
                        }
 
                        $inbuffer{$client} .= $data;
 
                        # Проверить, говорят ли данные в буфере или только что прочитанные
                        # данные о наличии полного запроса, ожидающего выполнения. Если да -
                        # заполнить $ready{$client} запросами, ожидающими обработки.
                        while( $inbuffer{$client} =~ s/(.*\n)// ) { push( @{$ready{$client}}, $1 ) }
                }
        }
 
        # Есть ли полные запросы для обработки?
        foreach $client ( keys %ready ) { handle( $client ); }
 
        # Сбрасываем буферы?
        foreach $client ( $select->can_write( 1 ) )
        {
                # Пропустить этого слиента, если нам нечего сказать
                next unless $outbuffer{$client};
                block( $client );
                $rv = $client->send( $outbuffer{$client}, 0 );
                nonblock( $client );
                unless( defined $rv )
                {
                        # Пожаловаться, но следовать дальше
                        warn "I was told I could write? but I can`t.\n";
                        next;
                }
                if ( $rv == length $outbuffer{$client} || $! == POSIX::EWOULDBLOCK )
                {
                        substr( $outbuffer{$client}, 0, $rv ) = '';
                        delete $outbuffer{$client} unless length $outbuffer{$client};
                }
                else
                {
                        # Не удалось записать все данные и не из-за блокировки.
                        # Очистить буферы и следовать дальше.
                        delete $inbuffer{$client};
                        delete $outbuffer{$client};
                        delete $ready{$client};
 
                        $select->remove($client);
                        close($client);
                        next;
                }
        }
}
 
# handle( $socket ) обрабатывает все необработанные запросы
# для клиента $client
sub handle
{
        # Запрос находится в $ready{$client}
        # Отправить вывод в $outbuffer{$client}
        my $client = shift;
        my $request;
 
        foreach $request ( @{$ready{$client}} )
        {
                print "\nrequest=".$request if ( $debug == 1 );
 
                if ( $request =~ /^set speed\t(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\t(\d+)\t(\d+)/ )
                {
                        my ($ip, $table, $pipe) = ($1, $2, $3);
                        print "\nip=".$ip.",table=".$table.",pipe=".$pipe if ( $debug == 1 );
                        $err = `$ipfw table $table delete $ip`;
                        $err = `$ipfw table $table add $ip $pipe`;
                }
                elsif ( $request =~ /^add entry\t(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\t(\d+)/ )
                {
                        my ($ip, $table, $pipe) = ($1, $2, $3);
 
                        $err = `$ipfw table $table add $ip $pipe`;
                }
                elsif ( $request =~ /^del entry\t(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\t(\d+)/ )
                {
                        my ($ip, $table) = ($1, $2);
                        $err = `$ipfw table $table delete $ip`;
                }
                elsif ( $request =~ /^add route\t(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\t(.+)/ )
                {
                        my ($ip, $iface) = ($1, $2);
                        $err = `$route add $ip $iface`;
                }
                elsif ( $request =~ /^del route\t(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\t(.+)/ )
                {
                        my ($ip, $iface) = ($1, $2);
                        $err = `$route del $ip $iface`;
                }
        }
 
        delete $ready{$client};
}
 
# nonblock( $socket ) переводит сокет в неблокирующий режим
sub nonblock
{
        my $socket = shift;
        my $flags;
 
        $flags = fcntl( $socket, F_GETFL, 0 )
                or die "Can`t get flags for socket: $!\n";
        fcntl( $socket, F_SETFL, $flags | O_NONBLOCK )
                or die "Can`t make socket nonblocking: $!\n";
}
 
sub block
{
        my $socket = shift;
        my $flags;
 
        $flags = fcntl( $socket, F_GETFL, 0 )
                or die "Can`t get flags for socket: $!\n";
        fcntl( $socket, F_SETFL, $flags ^ O_NONBLOCK )
                or die "Can`t make socket nonblocking: $!\n";
}


Прослойка между manad и quagga

/root/route.sh

#!/bin/sh
 
if [ "$1" = "add" ]
then
    /usr/local/bin/vtysh -d zebra -c 'conf t' -c "ip route $2/32 $3" -c 'end'
fi
if [ "$1" = "del" ]
then
    /usr/local/bin/vtysh -d zebra -c 'conf t' -c "no ip route $2/32 $3" -c 'end'
fi
if [ "$1" = "flush" ]
then
    IP=""
    /usr/local/bin/vtysh -d zebra  -c 'sh ip route static' | grep 'is directly connected' | while read ROUTE
    do
        ADDR=`echo $ROUTE | awk '{print $2}'`
        if [ "$ADDR" = "directly" ]
        then
            PORT=`echo $ROUTE | awk '{print $4}'`
        else
            IP=$ADDR
            PORT=`echo $ROUTE | awk '{print $7}'`
        fi
        /usr/local/bin/vtysh -d zebra -c 'conf t' -c "no ip route $IP $PORT" -c 'end'
    done
fi

В ПО Quagga на момент написания этого текста существует баг под номером 494, из-за которого на FreeBSD не добавляется маршрут в системную таблицу маршрутизации, если в качестве адреса назначения указано имя интерфейса. Баг описан, подтвержден и есть сторонний патч, исправляющий эту проблему, однако разработчики не спешат исправлять проблему и выпускать новый релиз, поэтому порт Quagga придется пропатчить и перекомпилировать самому. Для этого достаточно закинуть патч в каталог files в каталоге с портом quagga, поправить в патче пути и сделать make install clean
Все, описанное выше, поправлено в последних версиях Quagga (как минимум в 0.99.21).


Дополнительный опции, с которыми нужно скомпилировать новое ядро:

options         IPFIREWALL
options         IPFIREWALL_VERBOSE
options         IPFIREWALL_VERBOSE_LIMIT=100
options         IPFIREWALL_FORWARD
options         HZ=3000
options         IPFIREWALL_DEFAULT_TO_ACCEPT
options         DUMMYNET
#
options         NETGRAPH
options         NETGRAPH_BRIDGE
options         NETGRAPH_EIFACE
options         NETGRAPH_SOCKET
options         NETGRAPH_NETFLOW
options         NETGRAPH_KSOCKET
options         NETGRAPH_ETHER
options         NETGRAPH_VLAN


Подсистема net.isr на данном этапе уже достигла стабильности и рекомендована к использованию. Однако, трафик все еще неравномерно распространяется между ядрами и для исправления ситуации есть два патча:
http://www.grosbein.net/freebsd/patches/netisr_ip_flowid.diff
http://www.grosbein.net/freebsd/patches/netisr-aff.diff
которые рекомендуются для высоконагруженных систем.


/boot/loader.conf

#Параметр net.isr.maxthreads обычно выставляется равным количеству ядер.
net.isr.maxthreads=4
net.isr.dispatch=hybrid
net.isr.bindthreads=1
 
net.graph.maxalloc=65536
net.graph.maxdata=65536
 
#Специфичные настройки для Intel-овского драйвера em(4)
#Есть подобные настройки для igb(4), по другим смотрите документацию к драйверу Вашей сетевой.
#------------ em ---------------
hw.em.rxd=4096
hw.em.txd=4096
hw.em.rx_int_delay=200
hw.em.tx_int_delay=200
hw.em.rx_abs_int_delay=4000
hw.em.tx_abs_int_delay=4000
hw.em.rx_process_limit=4096
#------------ em ---------------
 
net.link.ifqmaxlen=10240


/etc/sysctl.conf

net.inet.ip.dummynet.io_fast=1
net.inet.ip.fw.one_pass=1
net.inet.ip.dummynet.pipe_slot_limit=1000
net.inet.icmp.icmplim=20000
net.inet.ip.dummynet.hash_size=65535
net.inet.ip.dummynet.expire=0
net.inet.ip.forwarding=1
net.inet.ip.fastforwarding=0
net.link.ether.inet.proxyall=1
 
#Специфичные настройки для Intel-овского драйвера em(4)
#Есть подобные настройки для igb(4), по другим смотрите документацию к драйверу Вашей сетевой.
#------------ em ---------------
dev.em.1.rx_int_delay=200
dev.em.1.tx_int_delay=200
dev.em.1.rx_abs_int_delay=4000
dev.em.1.tx_abs_int_delay=4000
dev.em.1.rx_processing_limit=4096
 
dev.em.2.rx_int_delay=200
dev.em.2.tx_int_delay=200
dev.em.2.rx_abs_int_delay=4000
dev.em.2.tx_abs_int_delay=4000
dev.em.2.rx_processing_limit=4096
#------------ em ---------------
 
kern.ipc.nmbclusters=128000
kern.ipc.maxsockbuf=16000000
net.graph.maxdgram=8388608
net.graph.recvspace=8388608
 
net.inet.ip.redirect=0
kern.random.sys.harvest.ethernet=0
kern.random.sys.harvest.point_to_point=0
kern.random.sys.harvest.interrupt=0


/etc/rc.conf

ifconfig_em1="up"
ifconfig_em2="up"
firewall_enable="YES"
firewall_script="/etc/rc.ipfw"
gateway_enable="YES"
defaultrouter="NO"
quagga_enable="YES"
quagga_flags="-d -A 127.0.0.1"
quagga_daemons="zebra bgpd"
watchquagga_enable="YES"
watchquagga_flags="-dz -R '/usr/local/etc/rc.d/quagga restart' zebra bgpd"

Естественно, что указываются только важные опции, остальные по вкусу. Как вы успели заметить выше, шейпирование скорости реализовано с помощью dummynet. Честно говоря, это спорный выбор, но он отличается элегантностью реализации и простотой интеграции с биллингом, в отличии от ng_car. Реальную разницу нужно смотреть под нагрузкой, однако, как мне кажется, при отсутствии NAT на шлюзе (а NAT-ферма имхо должна стоять где-то поближе к бордеру сети) упереться в производительность dummynet не должны и по факту реальной эксплуатации не уперлись. Единственно, что важно, это приколотить процесс dummynet к конкретному ядру:

/bin/ps -p 0 -axcH -o lwp,command | /usr/bin/egrep '\/dummynet' | while read lwp cmd; do /usr/bin/cpuset -l 0 -t $lwp; done


/etc/rc.ipfw

#!/bin/sh
 
IPFW=/sbin/ipfw
 
$IPFW -q flush
# -- Запрет любых DHCP-запросов не прошедших через обработку абонентскими свитчами
$IPFW -q add 00020 deny udp from not 192.168.128.0/20 to aa.200.144.16 67
# -- Закрываем управляющий интерфейс manad от всех, кроме биллинга
$IPFW -q add 00025 deny ip from not aa.200.144.16 to me 48444
 
# -- table 11: ip-адреса абонентов, которым запрещено все, кроме доступа на страничку
# -- своего счета. При попытке загрузить любую страничку в интернете выдается
# -- страница с ошибкой и просьбой поплнить счет и т.п. Аргумент игнорируется
# -- 
# -- Даем пинговать основные DNS-сервера и биллинг для диагностики
$IPFW -q add 00030 allow icmp from table\(11\) to aa.200.144.3 icmptype 0,8
$IPFW -q add 00040 allow icmp from table\(11\) to aa.200.149.3 icmptype 0,8
$IPFW -q add 00050 allow icmp from table\(11\) to aa.200.144.16 icmptype 0,8
# -- Даем доступ к DNS-серверам для резолвинга
$IPFW -q add 00070 allow tcp from table\(11\) to aa.200.144.3 53
$IPFW -q add 00080 allow udp from table\(11\) to aa.200.144.3 53
$IPFW -q add 00090 allow tcp from table\(11\) to aa.200.149.3 53
$IPFW -q add 00100 allow udp from table\(11\) to aa.200.149.3 53
# -- Даем доступ к абонентскому отделу биллинга
$IPFW -q add 00105 allow tcp from table\(11\) to aa.200.144.3 443
# -- Заворачиваем все остальные http-запросы на машину с nginx, который выдает
# -- информацию о блокировке доступа при попытке зайти на любую страницу
# -- интернета
$IPFW -q add 00130 fwd 192.168.128.3 ip from table\(11\) to any 80
# -- Запрещаем все остальное
$IPFW -q add 00140 unreach net-prohib ip from table\(11\) to any
 
# -- Разрешаем избранным хостам в соответствии с правилами выше, отсылать
# -- что-либо заблокированному абоненту
$IPFW -q add 00510 allow ip from aa.200.144.3 to table\(11\)
$IPFW -q add 00520 allow ip from aa.200.149.3 to table\(11\)
$IPFW -q add 00530 allow ip from aa.200.144.16 to table\(11\)
$IPFW -q add 00550 allow tcp from any 80 to table\(11\)
# -- Запрещаем все остальное
$IPFW -q add 00560 unreach host-prohib ip from any to table\(11\)
 
# -- table 2: ip-адреса абонентов, у которых нужно шейпить входящий локальный
# -- трафик. В качестве аргумента указывается номер pipe с нужной скоростью.
# -- Используются pipe с mask dst-ip.
$IPFW -q add 01010 pipe tablearg ip from aa.200.144.0/20 to table\(2\) out
$IPFW -q add 01020 pipe tablearg ip from bb.198.216.0/21 to table\(2\) out
$IPFW -q add 01030 pipe tablearg ip from cc.19.64.0/21 to table\(2\) out
$IPFW -q add 01040 pipe tablearg ip from 172.16.0.0/12 to table\(2\) out
$IPFW -q add 01050 pipe tablearg ip from 10.16.0.0/12 to table\(2\) out
$IPFW -q add 01060 pipe tablearg ip from dd.154.64.0/18 to table\(2\) out
 
# -- table 1: ip-адреса абонентов, у которых нужно шейпить входящий внешний
# -- трафик. В качестве аргумента указывается номер pipe с нужной скоростью.
# -- Используются pipe с mask dst-ip.
$IPFW -q add 01070 pipe tablearg ip from any to table\(1\) out
 
# -- table 4: ip-адреса абонентов, у которых нужно шейпить исходящий локальный
# -- трафик. В качестве аргумента указывается номер pipe с нужной скоростью.
# -- Используются pipe с mask src-ip.
$IPFW -q add 02010 pipe tablearg ip from table\(4\) to aa.200.144.0/20 in
$IPFW -q add 02020 pipe tablearg ip from table\(4\) to bb.198.216.0/21 in
$IPFW -q add 02030 pipe tablearg ip from table\(4\) to cc.19.64.0/21 in
$IPFW -q add 02040 pipe tablearg ip from table\(4\) to 172.16.0.0/12 in
$IPFW -q add 02050 pipe tablearg ip from table\(4\) to 10.16.0.0/12 in
$IPFW -q add 02060 pipe tablearg ip from table\(4\) to dd.154.64.0/18 in
 
# -- table 3: ip-адреса абонентов, у которых нужно шейпить исходящий внешний
# -- трафик. В качестве аргумента указывается номер pipe с нужной скоростью.
# -- Используются pipe с mask src-ip.
$IPFW -q add 02070 pipe tablearg ip from table\(3\) to any in
 
$IPFW -q pipe flush
# -- pipe 1 и 2 созданы как заглушка для биллинга, который должен устанавливать
# -- хоть какое-то правило, даже при отсутствии ограничения на скорость.
$IPFW pipe 1 config mask dst-ip 0xffffffff buckets 512 queue 100
$IPFW pipe 2 config mask src-ip 0xffffffff buckets 512 queue 100
 
# -- Собственно сами трубы для разных скоростей
$IPFW pipe 200 config mask dst-ip 0xffffffff bw 200Kbit/s buckets 512 queue 100
$IPFW pipe 201 config mask dst-ip 0xffffffff bw 400Kbit/s buckets 512 queue 100
$IPFW pipe 202 config mask src-ip 0xffffffff bw 200Kbit/s buckets 512 queue 100
 
$IPFW pipe 500 config mask dst-ip 0xffffffff bw 500Kbit/s buckets 512 queue 100
$IPFW pipe 501 config mask dst-ip 0xffffffff bw 1000Kbit/s buckets 512 queue 100
$IPFW pipe 502 config mask src-ip 0xffffffff bw 500Kbit/s buckets 512 queue 100
 
$IPFW pipe 700 config mask dst-ip 0xffffffff bw 700Kbit/s buckets 512 queue 100
$IPFW pipe 701 config mask dst-ip 0xffffffff bw 1400Kbit/s buckets 512 queue 100
$IPFW pipe 702 config mask src-ip 0xffffffff bw 700Kbit/s buckets 512 queue 100
 
$IPFW pipe 650 config mask dst-ip 0xffffffff bw 650Kbit/s buckets 512 queue 100
$IPFW pipe 651 config mask dst-ip 0xffffffff bw 1300Kbit/s buckets 512 queue 100
$IPFW pipe 652 config mask src-ip 0xffffffff bw 650Kbit/s buckets 512 queue 100
 
$IPFW pipe 800 config mask dst-ip 0xffffffff bw 800Kbit/s buckets 512 queue 100
$IPFW pipe 801 config mask dst-ip 0xffffffff bw 1600Kbit/s buckets 512 queue 100
$IPFW pipe 802 config mask src-ip 0xffffffff bw 800Kbit/s buckets 512 queue 100
 
$IPFW pipe 1000 config mask dst-ip 0xffffffff bw 1000Kbit/s buckets 512 queue 100
$IPFW pipe 1001 config mask dst-ip 0xffffffff bw 1000Kbit/s buckets 512 queue 100
$IPFW pipe 1002 config mask src-ip 0xffffffff bw 1000Kbit/s buckets 512 queue 100
 
$IPFW pipe 4000 config mask dst-ip 0xffffffff bw 4000Kbit/s buckets 512 queue 100
$IPFW pipe 4001 config mask dst-ip 0xffffffff bw 4000Kbit/s buckets 512 queue 100
$IPFW pipe 4002 config mask src-ip 0xffffffff bw 4000Kbit/s buckets 512 queue 100
 
$IPFW pipe 16000 config mask dst-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100
$IPFW pipe 16001 config mask dst-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100
$IPFW pipe 16002 config mask src-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100

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


Скрипт /root/restore.sh, который запускается при загрузке FreeBSD-шлюза, подключается к mysql-базе биллинга и забирает актуальные сессии и скорости для данного шлюза и вносит в свои таблицы маршрутизации и ipfw.

#!/bin/sh
 
DB_HOST=10.254.254.17
DB_USER=user
DB_PASS=pass
 
# -- Т.к. всю информацию о маршрутах в сети мы получаем от quagga, то ждем до
# -- тех пор, пока quagga не получит маршрут до биллинга
RESULT=1
while [ $RESULT != 0 ]
do
    ping -c 1 -t 1 $DB_HOST >/dev/null 2>&1
    RESULT=$?
done
 
# -- Выясняем собственный IP
IP_ADDR=`ifconfig vlan11 | grep netmask | awk '{print $2}'`
 
# -- Вносим в таблицу маршрутизации все IP текущих сессий на этом шлюзе
echo "SELECT user_ip,iface FROM _inet_gw_sessions WHERE gw_ip='$IP_ADDR';" | /usr/local/bin/mysql -N -h$DB_HOST -u$DB_USER -p$DB_PASS bgbilling | while read SESSION
do
    #echo $SESSION
    RT_IP=`echo $SESSION | awk '{print $1}'`
    RT_VLAN=`echo $SESSION | awk '{print $2}'`
    #echo "$RT_IP $RT_VLAN"
    /root/route.sh add $RT_IP $RT_VLAN
done
 
# -- Применяем ограничения по скоростям
echo "SELECT table_num,user_ip,pipe FROM _inet_gw_speeds WHERE gw_ip='$IP_ADDR';" | /usr/local/bin/mysql -N -h$DB_HOST -u$DB_USER -p$DB_PASS bgbilling | while read SPEED
do
    TABLE_NUM=`echo $SPEED | awk '{print $1}'`
    USER_IP=`echo $SPEED | awk '{print $2}'`
    PIPE=`echo $SPEED | awk '{print $3}'`
    /sbin/ipfw table $TABLE_NUM add $USER_IP $PIPE
done


Структура табличек в биллинге, куда будут складываться текущие сессии с привязкой к FreeBSD-шлюзам, а также ограничения по скоростям.

CREATE TABLE `_inet_gw_sessions` (
  `gw_ip` VARCHAR(16),
  `user_ip` VARCHAR(16),
  `iface` VARCHAR(10),
  `mac` VARCHAR(16),
  `session_start` DATETIME,
  `state` TINYINT(4),
  KEY `gw_ip` (`gw_ip`)
) ENGINE=INNODB DEFAULT CHARSET=cp1251
 
CREATE TABLE `_inet_gw_speeds` (
  `gw_ip` VARCHAR(16),
  `table_num` INT(4),
  `user_ip` VARCHAR(16),
  `pipe` INT(11),
  KEY `gw_ip` (`gw_ip`)
) ENGINE=INNODB DEFAULT CHARSET=cp1251


Динамический код для интеграции шлюза в биллинг:

ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator

package ru.bitel.bgbilling.inet.dyn.device.freebsd;
 
import java.io.*;
import java.net.*;
import java.util.*;
import java.sql.*;
import org.apache.log4j.*;
import ru.bitel.bgbilling.modules.inet.access.sa.*;
import ru.bitel.bgbilling.modules.inet.api.common.bean.*;
import ru.bitel.bgbilling.server.util.*;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.inet.IpAddress;
import ru.bitel.bgbilling.modules.inet.runtime.*;
import ru.bitel.bgbilling.modules.inet.api.common.bean.*;
import ru.bitel.common.Utils;
 
public class FreeBSDServiceActivator
	extends ServiceActivatorAdapter
	implements ServiceActivator
{
	private static final Logger log = Logger.getLogger( FreeBSDServiceActivator.class );
 
	private Connection con = null;
	Socket socket;
	PrintWriter out;
	String host;
	int port;
	String vlanPrefix;
 
	@Override
	public Object connect()
		throws Exception
	{
		log.info("FreeBSD CONNECT");
		con = Setup.getSetup().getDBConnectionFromPool();
		socket = new Socket( host, port );
		out = new PrintWriter( socket.getOutputStream(), true );
 
		return true;
	}
 
	private void setSpeed( ServiceActivatorEvent event )
		throws Exception
	{
		log.info("FreeBSD SET SPEED");
		log.info(event.getNewInetServ());
		int optId = 0;
		Iterator<Integer> it = event.getNewOptions().iterator();
		while(it.hasNext())
			optId = it.next();
 
		int status = InetServ.STATUS_CLOSED;
		String ExtIn = "";
		String IntIn = "";
		String ExtOut = "";
		String IntOut = "";
 
		if( optId>0 )
		{
			InetOptionRuntime option = InetOptionRuntimeMap.getInstance().get( optId );
			ExtIn = option.config.get( "shape-in-ext", "" );
			IntIn = option.config.get( "shape-in-int", "" );
			ExtOut = option.config.get( "shape-out-ext", "" );
			IntOut = option.config.get( "shape-out-int", "" );
			status = event.getNewInetServ().getStatus();
		}
 
		int state = event.getNewState();
		String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
 
		log.info("optionId="+optId+",state="+state+",ip="+ip);
 
		con.prepareStatement("DELETE FROM _inet_gw_speeds WHERE user_ip='"+ip+"'").executeUpdate();
		if( status == InetServ.STATUS_ACTIVE )
		{
			con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',1,'"+ip+"',"+ExtIn+")").executeUpdate();
			con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',2,'"+ip+"',"+IntIn+")").executeUpdate();
			con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',3,'"+ip+"',"+ExtOut+")").executeUpdate();
			con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',4,'"+ip+"',"+IntOut+")").executeUpdate();
 
			out.println("set speed	"+ip+"	1	"+ExtIn);
			out.println("set speed	"+ip+"	2	"+IntIn);
			out.println("set speed	"+ip+"	3	"+ExtOut);
			out.println("set speed	"+ip+"	4	"+IntOut);
		}else{
			out.println("del entry	"+ip+"	1");
			out.println("del entry	"+ip+"	2");
			out.println("del entry	"+ip+"	3");
			out.println("del entry	"+ip+"	4");
		}
 
		if( state == InetServ.STATE_ENABLE )
		{
			out.println("del entry	"+ip+"	11");
		}else{
			con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',11,'"+ip+"',0)").executeUpdate();
			out.println("add entry	"+ip+"	11	0");
		}
	}
 
	@Override
	public Object onAccountingStart( ServiceActivatorEvent event )
		throws Exception
	{
		log.info("FreeBSD ACC START");
		setSpeed( event );
		String vlan = String.valueOf( event.getNewInetServ().getVlan() );
		String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
		String mac = event.getConnection().getCallingStationId();
		int state = event.getConnection().getConnectionStatus();
 
		log.info( "ip="+ip+",vlan="+vlan );
		con.prepareStatement("INSERT INTO _inet_gw_sessions(gw_ip,user_ip,iface,mac,session_start,state) VALUES('"+host+"','"+ip+"','"+vlanPrefix+vlan+"','"+mac+"',NOW(),"+state+")").executeUpdate();
		out.println( "add route	"+ip+"	"+vlanPrefix+vlan );
 
		return true;
	}
 
	@Override
	public Object onAccountingStop( ServiceActivatorEvent event )
		throws Exception
	{
		log.info("FreeBSD ACC STOP");
		String vlan = String.valueOf( event.getNewInetServ().getVlan() );
		String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
 
		log.info( "ip="+ip+",vlan="+vlan );
		con.prepareStatement("DELETE FROM _inet_gw_sessions WHERE gw_ip='"+host+"' AND user_ip='"+ip+"' AND iface='"+vlanPrefix+vlan+"'").executeUpdate();
		out.println( "del route	"+ip+"	"+vlanPrefix+vlan );
 
		return true;
	}
 
	@Override
	public Object connectionClose( ServiceActivatorEvent serviceActivatorEvent1 )
		throws Exception
	{
		return null;
	}
 
	@Override
	public Object disconnect()
		throws Exception
	{
		log.info("FreeBSD DISCONNECT");
		ServerUtils.closeConnection(con);
		out.close();
		socket.close();
 
		return true;
	}
 
	@Override
	public Object connectionModify( ServiceActivatorEvent event )
		throws Exception
	{
		log.info("FreeBSD Connection MODIFY");
		setSpeed( event );
		event.setConnectionStateModified( true );
 
		return true;
	}
 
 
	@Override
	public Object serviceCancel( ServiceActivatorEvent event )
		throws Exception
	{
		return null;
	}
 
	@Override
	public Object init( Setup setup, int mid, InetDevice device, InetDeviceType deviceType, ParameterMap parameterMap )
		throws Exception
	{
		log.info("FreeBSD INIT");
		host = device.getHost();
		port = parameterMap.getInt( "manad.port", 4444 );
		vlanPrefix = parameterMap.get( "vlan.prefix", "" );
 
		log.info( "host="+host+",port="+port+",vlanPrefix="+vlanPrefix );
 
		return true;
	}
 
}
Пример дерева для FreeBSD-шлюза


Настройка Типа устройства

# Установить галку "Является источником данных"
# Выбрать Обработчик активации серисов (ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator)
# Добавить интерфейс -1 ANY
 
manad.port=48444
vlan.prefix=em1.
 
#указываем тип flow агента
#в аккаунтинг сервере должен быть добавлен flowListener данного типа
#(а если есть дополнительно фильтр по agentDeviceId - то в фильтре должен быть указан нужный deviceId)
flow.agent.type=netflow


Настройка Устройства

#IP default-gateway, которое будет отсылаться абоненту в DHCP-ответе
dhcp.option.gate=cc.19.64.2
# ID категории VLAN-ресурсов для этого шлюза. На каждый шлюз, своя категория
vlan.resource.category=1
# Eстройство, с которого получается информация по трафику. В нашем случае это сам шлюз, поэтому указываем собственный ID шлюза.
flow.agent.link=144:-1

Примеры опций.

Скорость 1000

Конфиг

#Цифры - номера pipe на FreeBSD-шлюзе
 
#Входящий внешний (1000 кбит/сек)
shape-in-ext=1000
#Входящий локальный (1000 кбит/сек)
shape-in-int=1001
#Исходящий внешний (1000 кбит/сек)
shape-out-ext=1002
#Исходящий локальный (не ограничен)
shape-out-int=2

Скорость 1000 + локалка

Конфиг

#Цифры - номера pipe на FreeBSD-шлюзе
 
#Входящий внешний (1000 кбит/сек)
shape-in-ext=1000
#Входящий локальный (не ограничен)
shape-in-int=1
#Исходящий внешний (1000 кбит/сек)
shape-out-ext=1002
#Исходящий локальный (не ограничен)
shape-out-int=2


Управление абонентским свитчом осуществляется через telnet/ssh/snmp, причем биллинг может штатно мониторить uptime свитча и переконфигурировать свитч после перезагрузки. Конкретная реализация зависит от вендора и выходит за рамки данной статьи.

Личные инструменты