Интеграция с FreeBSD-шлюзом по схеме VLAN-per-user и авторизацией через DHCP option 82
Материал из BiTel WiKi
Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга. В описываемой схеме используется FreeBSD 9.1
В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 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"; }
Прослойка между 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; } }
Настройка Типа устройства
# Установить галку "Является источником данных" # Выбрать Обработчик активации серисов (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 свитча и переконфигурировать свитч после перезагрузки. Конкретная реализация зависит от вендора и выходит за рамки данной статьи.