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

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

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

СТАТЬЯ НЕ ЗАКОНЧЕНА! ПРОДОЛЖЕНИЕ СЛЕДУЕТ.

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

В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 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";
}


/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


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

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

Обязательно необходимо закомментировать в ядре опцию FLOWTABLE, т.к. ее реализация на данный момент нестабильна

#options        FLOWTABLE               # per-cpu routing cache


/etc/sysctl.conf

net.inet.ip.dummynet.io_fast=1
net.inet.ip.fw.one_pass=1
net.inet.ip.dummynet.hash_size=65535
net.inet.ip.dummynet.expire=0
net.inet.ip.forwarding=1
net.inet.ip.fastforwarding=1
net.link.ether.inet.proxyall=1


/etc/rc.conf

ifconfig_em1="up"
ifconfig_em2="up"
firewall_enable="YES"
firewall_script="/etc/rc.ipfw"
gateway_enable="YES"
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 3 -t $lwp; done


Немного тюнинга в /etc/rc.local

#!/bin/sh
 
# 1. Using delayed interrupts on em(4) ifaces - use carefully
# This sysctl values are not implemented on startup, so we try to use it here.
echo "Tuning em(4) interfaces"
for i in `jot - 0 3 `; do
    sysctl dev.em.$i.rx_int_delay=600 > /dev/null 2>&1
    sysctl dev.em.$i.tx_int_delay=600 > /dev/null 2>&1
    sysctl dev.em.$i.rx_abs_int_delay=1000 > /dev/null 2>&1
    sysctl dev.em.$i.tx_abs_int_delay=1000 > /dev/null 2>&1
    sysctl dev.em.$i.rx_kthreads=4 > /dev/null 2>&1
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 1500 config mask dst-ip 0xffffffff bw 1500Kbit/s buckets 512 queue 100
$IPFW pipe 1501 config mask dst-ip 0xffffffff bw 1500Kbit/s buckets 512 queue 100
$IPFW pipe 1502 config mask src-ip 0xffffffff bw 1500Kbit/s buckets 512 queue 100
 
$IPFW pipe 2000 config mask dst-ip 0xffffffff bw 2000Kbit/s buckets 512 queue 100
$IPFW pipe 2001 config mask dst-ip 0xffffffff bw 2000Kbit/s buckets 512 queue 100
$IPFW pipe 2002 config mask src-ip 0xffffffff bw 2000Kbit/s buckets 512 queue 100
 
$IPFW pipe 3000 config mask dst-ip 0xffffffff bw 3000Kbit/s buckets 512 queue 100
$IPFW pipe 3001 config mask dst-ip 0xffffffff bw 3000Kbit/s buckets 512 queue 100
$IPFW pipe 3002 config mask src-ip 0xffffffff bw 3000Kbit/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 5000 config mask dst-ip 0xffffffff bw 5000Kbit/s buckets 512 queue 100
$IPFW pipe 5001 config mask dst-ip 0xffffffff bw 5000Kbit/s buckets 512 queue 100
$IPFW pipe 5002 config mask src-ip 0xffffffff bw 5000Kbit/s buckets 512 queue 100
 
$IPFW pipe 6000 config mask dst-ip 0xffffffff bw 6000Kbit/s buckets 512 queue 100
$IPFW pipe 6001 config mask dst-ip 0xffffffff bw 6000Kbit/s buckets 512 queue 100
$IPFW pipe 6002 config mask src-ip 0xffffffff bw 6000Kbit/s buckets 512 queue 100
 
$IPFW pipe 8000 config mask dst-ip 0xffffffff bw 8000Kbit/s buckets 512 queue 100
$IPFW pipe 8001 config mask dst-ip 0xffffffff bw 8000Kbit/s buckets 512 queue 100
$IPFW pipe 8002 config mask src-ip 0xffffffff bw 8000Kbit/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
 
$IPFW pipe 20000 config mask dst-ip 0xffffffff bw 20000Kbit/s buckets 512 queue 100
$IPFW pipe 20001 config mask dst-ip 0xffffffff bw 20000Kbit/s buckets 512 queue 100
$IPFW pipe 20002 config mask src-ip 0xffffffff bw 20000Kbit/s buckets 512 queue 100
 
$IPFW pipe 2500 config mask dst-ip 0xffffffff bw 2500Kbit/s buckets 512 queue 100
$IPFW pipe 2501 config mask dst-ip 0xffffffff bw 2500Kbit/s buckets 512 queue 100
$IPFW pipe 2502 config mask src-ip 0xffffffff bw 2500Kbit/s buckets 512 queue 100
 
$IPFW pipe 24000 config mask dst-ip 0xffffffff bw 24000Kbit/s buckets 512 queue 100
$IPFW pipe 24001 config mask dst-ip 0xffffffff bw 24000Kbit/s buckets 512 queue 100
$IPFW pipe 24002 config mask src-ip 0xffffffff bw 24000Kbit/s buckets 512 queue 100
 
$IPFW pipe 12000 config mask dst-ip 0xffffffff bw 12000Kbit/s buckets 512 queue 100
$IPFW pipe 12001 config mask dst-ip 0xffffffff bw 12000Kbit/s buckets 512 queue 100
$IPFW pipe 12002 config mask src-ip 0xffffffff bw 12000Kbit/s buckets 512 queue 100

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

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

#!/bin/sh
 
# -- Т.к. всю информацию о маршрутах в сети мы получаем от quagga, то ждем до
# -- тех пор, пока quagga не получит маршрут до биллинга
RESULT=1
while [ $RESULT != 0 ]
do
    ping -c 1 -t 1 aa.200.144.16 >/dev/null 2>&1
    RESULT=$?
done
 
# -- Выясняем собственный IP
IP_ADDR=`ifconfig vlan11 | grep netmask | awk '{print $2}'`
 
# -- Записываем в лог, что собираемся восстановить
ssh -p 48222 -l gw-worker aa.200.144.16 cat /tmp/bill/sessions/$IP_ADDR > /var/log/restored_sessions.log 2>&1
 
# -- Вносим в таблицу маршрутизации все IP текущих сессий на этом шлюзе
ssh -p 48222 -l gw-worker aa.200.144.16 cat /tmp/bill/sessions/$IP_ADDR | while read SESSION
do
    RT_IP=`echo $SESSION | awk '{print $1}'`
    RT_VLAN=`echo $SESSION | awk '{print $2}'`
    /root/route.sh add $RT_IP $RT_VLAN
done
 
# -- Применяем ограничения по скоростям
scp -P 48222 gw-worker@aa.200.144.16:/tmp/bill/speeds/$IP_ADDR /tmp/ipfw
. /tmp/ipfw


Скрипт на php, который запускается на машине с BGBilling раз в 30 секунд и собирает информацию о текущих сессиях в биллинге и складывает ее в файлы для шлюзов. Решение корявое, но рабочее. В дальнейшем будет переделано.

/root/sessions.php

<?
$sess_dir='/tmp/bill/sessions/';
$speeds_dir='/tmp/bill/speeds/';
 
$gw_cache_file = '/tmp/bill/gw_map.cache';
$lock_file = '/tmp/bill/lock';
$fp_sess   = array();
$fp_speeds = array();
$gw_map    = array();
 
$mysql_master_host = '10.254.254.17';
$mysql_user = 'bgbilling';
$mysql_pass = 'pass';
$mysql_db = 'bgbilling';
 
$inet_mid = 1;
 
if(file_exists($lock_file)) die('LOCK FILE FOUND. EXITING.');
 
touch($lock_file);
 
mysql_connect($mysql_master_host, $mysql_user,$mysql_pass) or die(mysql_error());
mysql_select_db($mysql_db) or die(mysql_error());
 
function createGWmapCache()
{
    global $gw_cache_file;
    $fp = fopen($gw_cache_file,'w');
    $r = mysql_query('SELECT COUNT(*) FROM inet_device_'.$inet_mid) or die(mysql_error());
    $count = mysql_result($r,0,0);
    fputs($fp,$count."\n");
    $r = mysql_query('SELECT parentId,id FROM inet_device_'.$inet_mid.' WHERE deviceTypeId IN (3,4,5,8,12)') or die(mysql_error());
    while($l = mysql_fetch_row($r))
    {
        $parentId = $l[0];
        $typeId = 0;
        while($typeId!=7 && !empty($parentId))
        {
            $r1 = mysql_query('SELECT parentId,deviceTypeId,host,config FROM inet_device_'.$inet_mid.' WHERE id='.$parentId) or die(mysql_error());
            list($parentId,$typeId,$host,$config) = mysql_fetch_row($r1);
        }
        if(!empty($parentId))
        {
            $vlan_prefix = 'em1.';
            if(preg_match('#vlan.prefix=(.+)#',$config,$m))
            {
                $vlan_prefix = $m[1];
            }
            fputs($fp,$l[1]." $host $vlan_prefix\n");
        }
    }
    fclose($fp);
}
 
function reloadGWmap()
{
    global $gw_map,$gw_cache_file;
 
    $gw_map = array();
    $gw_cache = file($gw_cache_file);
    unset($gw_cache[0]);
    foreach($gw_cache AS $gw)
    {
        $tmp = explode(' ',$gw);
        $gw_map[$tmp[0]] = array($tmp[1],trim($tmp[2]));
    }
}
 
if(file_exists($gw_cache_file))
{
    $tmp = file($gw_cache_file);
    $r = mysql_query('SELECT COUNT(*) FROM inet_device_'.$inet_mid) or die(mysql_error());
    if($tmp[0]!=mysql_result($r,0,0))
        createGWmapCache();
    unset($tmp);
}else{
    createGWmapCache();
}
reloadGWmap();
 
 
$result = mysql_query('SELECT t1.deviceId,t1.callingStationId,t2.vlan,t1.ipAddress,t1.connectionStart,t1.deviceState,t1.deviceOptions FROM inet_connection_'.$inet_mid.' AS t1, inet_serv_'.$inet_mid.' AS t2 WHERE t1.servId=t2.id') or die(mysql_error());
while($ln = mysql_fetch_row($result))
{
    list($deviceId,$mac,$vlan,$ip,$startTime,$state,$options) = $ln;
    $ip_long = (ord($ip[0]) << 24) | (ord($ip[1]) << 16) | (ord($ip[2]) << 8) | (ord($ip[3]));
    $ip = long2ip($ip_long);
    $option = explode(',',$options);
    $option = intval($option[count($options)-1]);
    $gw_ip = $gw_map[$deviceId][0];
    $gw_prefix = $gw_map[$deviceId][1];
 
    if(!empty($gw_ip))
    {
		if(empty($fp_sess[$gw_ip]))
			$fp_sess[$gw_ip] = fopen($sess_dir.$gw_ip.'.new','w');
 
		fputs($fp_sess[$gw_ip],"$ip\t".$gw_prefix.$vlan."\t$mac\t$startTime\t$state\n");
 
 
		if(empty($fp_speeds[$gw_ip]))
            $fp_speeds[$gw_ip] = fopen($speeds_dir.$gw_ip.'.new','w');
 
        if($state==1)
        {
            if(!empty($option))
            {
                $r = mysql_query("SELECT config FROM inet_option_".$inet_mid." WHERE id=$option");
                $opt_config = @mysql_result($r,0,0);
                if(preg_match('#shape-in-ext=(\d+).*shape-in-int=(\d+).*shape-out-ext=(\d+).*shape-out-int=(\d+)#s',$opt_config,$rules))
                {
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 1 add $ip ".$rules[1]."\n");
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 2 add $ip ".$rules[2]."\n");
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 3 add $ip ".$rules[3]."\n");
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 4 add $ip ".$rules[4]."\n");
                }
            }
        }else{
            fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 11 add $ip\n");
        }
    }
}
foreach($fp_sess AS $ip=>$fp)
{
    rename($sess_dir.$ip.'.new',$sess_dir.$ip);
    fclose($fp);
}
foreach($fp_speeds AS $ip=>$fp)
{
    rename($speeds_dir.$ip.'.new',$speeds_dir.$ip);
    fclose($fp);
}
 
unlink($lock_file);
?>


Ну и shell-скрипт для его периодического запуска

/root/sessions.sh

#!/bin/bash
 
while [ '1'='1' ]
do
    /usr/bin/php -f /root/sessions.php
    sleep 30
done

Управление абонентским свитчом осуществляется через telnet\ssh, причем биллинг производит мониторинг uptime свитча и переконфигурирует свитч после перезагрузки.

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