Интеграция с FreeBSD-шлюзом по схеме VLAN-per-user и авторизацией через DHCP option 82
Материал из BiTel WiKi
Victor (Обсуждение | вклад) |
Victor (Обсуждение | вклад) |
||
Строка 1021: | Строка 1021: | ||
</source> | </source> | ||
- | [[Файл:Bgb-client-inet-fbsd-conf.png]] | + | [[Файл:Bgb-client-inet-fbsd-conf.png|thumb|Пример дерева для FreeBSD-шлюза]] |
Настройка Типа устройства | Настройка Типа устройства |
Версия 03:50, 29 июня 2012
СТАТЬЯ НЕ ЗАКОНЧЕНА! ПРОДОЛЖЕНИЕ СЛЕДУЕТ.
Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга.
В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 1 справа).
Если смотреть в иерархии модуля, то связь такая: 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 и в примерах конфигурации ниже, предполагается, что интерфейс 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 $inet_mid = 1; #ID типов абонентских свитчей $dev_ids = "3,4,5,8,12"; 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 ('.$dev_ids.')') 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
Динамический код для интеграции шлюза в биллинг: 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.Date; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.log4j.*; import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator; import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorAdapter; import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ; import ru.bitel.bgbilling.server.util.Setup; import ru.bitel.common.ParameterMap; import ru.bitel.common.inet.IpAddress; import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap; import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntime; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetOption; import ru.bitel.bgbilling.modules.inet.api.server.bean.InetOptionDao; import ru.bitel.common.Utils; public class FreeBSDServiceActivator extends ServiceActivatorAdapter implements ServiceActivator { private static final Logger log = Logger.getLogger( FreeBSDServiceActivator.class ); Socket socket; PrintWriter out; String host; int port; String vlanPrefix; @Override public Object connect() throws Exception { log.info("FreeBSD CONNECT"); 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(); } List<String> commands = new ArrayList<String>(); int state = event.getNewState(); String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() ); log.info("optionId="+optId+",state="+state+",ip="+ip); if( status == InetServ.STATUS_ACTIVE ) { commands.add("set speed "+ip+" 1 "+ExtIn); commands.add("set speed "+ip+" 2 "+IntIn); commands.add("set speed "+ip+" 3 "+ExtOut); commands.add("set speed "+ip+" 4 "+IntOut); }else{ commands.add("del entry "+ip+" 1"); commands.add("del entry "+ip+" 2"); commands.add("del entry "+ip+" 3"); commands.add("del entry "+ip+" 4"); } if( state == InetServ.STATE_ENABLE ) { commands.add("del entry "+ip+" 11"); }else{ commands.add("add entry "+ip+" 11 0"); } for( String command : commands ) { out.println( command ); } } @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() ); log.info( "ip="+ip+",vlan="+vlan ); 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 ); 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"); 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; } }
Настройка Типа устройства
# Установить галку "Является источником данных" # Выбрать Обработчик активации серисов (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 свитча и переконфигурировать свитч после перезагрузки. Конкретная реализация зависит от вендора и выходит за рамки данной статьи.