Интеграция с FreeBSD-шлюзом по схеме VLAN-per-user и авторизацией через DHCP option 82
Материал из BiTel WiKi
Victor (Обсуждение | вклад) |
Victor (Обсуждение | вклад) м (eg) |
||
(8 промежуточных версий не показаны.) | |||
Строка 1: | Строка 1: | ||
- | + | Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга. В описываемой схеме используется '''FreeBSD 9.1''' | |
- | [[Файл:Fbsd-gw-netgraph-scheme.png|thumb|Визуализация netghraph-нод для шлюза FreeBSD]] | + | |
- | [[ | + | В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 1 справа). |
+ | [[Файл:Fbsd-gw-net-scheme.png|thumb|(Схема 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 справа). | ||
+ | [[Файл:Fbsd-gw-netgraph-scheme.png|thumb|(Схема 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''' | ||
+ | <source lang="bash"> | ||
+ | #!/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" | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Собственно сам скрипт генерации netgraph-нод: | ||
+ | '''/root/ngf.rules''' | ||
+ | <source lang="bash"> | ||
+ | #!/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 | ||
+ | </source> | ||
+ | Вообще, про подсистему netgraph можно почитать например здесь http://habrahabr.ru/blogs/bsdelniki/86553/ | ||
+ | |||
+ | Для обмена маршрутами между шлюзами и другими маршрутизаторами ядра используется пакет Quagga, в котором мы используем zebra и bgpd/ospfd. Нужно отметить, что для корректной работы конструкции Unnumbered IP (SuperVLAN) нужно в таблицу маршрутизации прописывать маршруты вида: | ||
+ | <source lang="bash"> | ||
+ | route add -host cc.19.64.111 -iface em1.1234 | ||
+ | </source> | ||
+ | где cc.19.64.111 IP абонента, а em1.1234 интерфейс (1234 - номер VLAN-интерфейса). Т.к. у нас используется zebra, маршруты будем прописывать в ней, а zebra уже сама их внесет в системную таблицу маршрутизации: | ||
+ | <source lang="text"> | ||
+ | zebra# conf t | ||
+ | zebra(conf)# ip route cc.19.64.111 em1.1234 | ||
+ | zebra(conf)# end | ||
+ | </source> | ||
+ | Соответственно и удалять нужно в zebra: | ||
+ | <source lang="text"> | ||
+ | zebra# conf t | ||
+ | zebra(conf)# no ip route cc.19.64.111 em1.1234 | ||
+ | zebra(conf)# end | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Для автоматизации процесса добавления и удаления маршрутов с FreeBSD-шлюза был переработан скрипт manad, к которому обращается биллинг при инициализации и терминировании сессии, а также для выставления нужных скоростей в соответствии с тарифной политикой. | ||
+ | |||
+ | '''/root/manad.pl''' | ||
+ | <source lang="perl"> | ||
+ | #!/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"; | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Прослойка между manad и quagga | ||
+ | |||
+ | '''/root/route.sh''' | ||
+ | <source lang="bash"> | ||
+ | #!/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 | ||
+ | </source> | ||
+ | <s>В ПО Quagga на момент написания этого текста существует [https://bugzilla.quagga.net/show_bug.cgi?id=494 баг под номером 494], из-за которого на FreeBSD не добавляется маршрут в системную таблицу маршрутизации, если в качестве адреса назначения указано имя интерфейса. Баг описан, подтвержден и есть сторонний патч, исправляющий эту проблему, однако разработчики не спешат исправлять проблему и выпускать новый релиз, поэтому порт Quagga придется пропатчить и перекомпилировать самому. Для этого достаточно закинуть патч в каталог files в каталоге с портом quagga, поправить в патче пути и сделать make install clean</s><br> | ||
+ | Все, описанное выше, поправлено в последних версиях Quagga (как минимум в 0.99.21). | ||
+ | |||
+ | |||
+ | Дополнительный опции, с которыми нужно скомпилировать новое ядро: | ||
+ | <source lang="text"> | ||
+ | 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 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Подсистема net.isr на данном этапе уже достигла стабильности и рекомендована к использованию. Однако, трафик все еще неравномерно распространяется между ядрами и для исправления ситуации есть два патча:<br> | ||
+ | http://www.grosbein.net/freebsd/patches/netisr_ip_flowid.diff<br> | ||
+ | http://www.grosbein.net/freebsd/patches/netisr-aff.diff<br> | ||
+ | которые рекомендуются для высоконагруженных систем. | ||
+ | |||
+ | |||
+ | '''/boot/loader.conf''' | ||
+ | <source lang="bash"> | ||
+ | #Параметр 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 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | |||
+ | '''/etc/sysctl.conf''' | ||
+ | <source lang="bash"> | ||
+ | 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 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | '''/etc/rc.conf''' | ||
+ | <source lang="bash"> | ||
+ | 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" | ||
+ | </source> | ||
+ | Естественно, что указываются только важные опции, остальные по вкусу. | ||
+ | Как вы успели заметить выше, шейпирование скорости реализовано с помощью dummynet. Честно говоря, это спорный выбор, но он отличается элегантностью реализации и простотой интеграции с биллингом, в отличии от ng_car. Реальную разницу нужно смотреть под нагрузкой, однако, как мне кажется, при отсутствии NAT на шлюзе (а NAT-ферма имхо должна стоять где-то поближе к бордеру сети) упереться в производительность dummynet не должны и по факту реальной эксплуатации не уперлись. Единственно, что важно, это приколотить процесс dummynet к конкретному ядру: | ||
+ | <source lang="bash"> | ||
+ | /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 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | '''/etc/rc.ipfw''' | ||
+ | <source lang="bash"> | ||
+ | #!/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 | ||
+ | </source> | ||
+ | При необходимости, по аналогии создается больше классов трафиков, которые отдельно нужно зажимать. | ||
+ | Обращаю внимание на то, что трубы независимы друг от друга как в плане разных абонентов, так и в плане типов трафика ,которые они зажимают. Т.е. например можно одновременно качать с максимальной скоростью трубы из внешнего интернета и тут же качать с максимальной скоростью трубы для локального трафика из локалки. | ||
+ | |||
+ | |||
+ | Скрипт '''/root/restore.sh''', который запускается при загрузке FreeBSD-шлюза, подключается к mysql-базе биллинга и забирает актуальные сессии и скорости для данного шлюза и вносит в свои таблицы маршрутизации и ipfw. | ||
+ | <source lang="bash"> | ||
+ | #!/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 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Структура табличек в биллинге, куда будут складываться текущие сессии с привязкой к FreeBSD-шлюзам, а также ограничения по скоростям. | ||
+ | <source lang="mysql"> | ||
+ | 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 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Динамический код для интеграции шлюза в биллинг: | ||
+ | |||
+ | '''ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator''' | ||
+ | <source lang="java"> | ||
+ | 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; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </source> | ||
+ | |||
+ | [[Файл:Bgb-client-inet-fbsd-conf.png|thumb|Пример дерева для FreeBSD-шлюза]] | ||
+ | |||
+ | |||
+ | Настройка '''Типа устройства''' | ||
+ | <source lang="java"> | ||
+ | # Установить галку "Является источником данных" | ||
+ | # Выбрать Обработчик активации серисов (ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator) | ||
+ | # Добавить интерфейс -1 ANY | ||
+ | |||
+ | manad.port=48444 | ||
+ | vlan.prefix=em1. | ||
+ | |||
+ | #указываем тип flow агента | ||
+ | #в аккаунтинг сервере должен быть добавлен flowListener данного типа | ||
+ | #(а если есть дополнительно фильтр по agentDeviceId - то в фильтре должен быть указан нужный deviceId) | ||
+ | flow.agent.type=netflow | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Настройка '''Устройства''' | ||
+ | <source lang="java"> | ||
+ | #IP default-gateway, которое будет отсылаться абоненту в DHCP-ответе | ||
+ | dhcp.option.gate=cc.19.64.2 | ||
+ | # ID категории VLAN-ресурсов для этого шлюза. На каждый шлюз, своя категория | ||
+ | vlan.resource.category=1 | ||
+ | # Eстройство, с которого получается информация по трафику. В нашем случае это сам шлюз, поэтому указываем собственный ID шлюза. | ||
+ | flow.agent.link=144:-1 | ||
+ | </source> | ||
+ | |||
+ | Примеры опций. | ||
+ | |||
+ | '''Скорость 1000''' | ||
+ | |||
+ | Конфиг | ||
+ | <source lang="java"> | ||
+ | #Цифры - номера pipe на FreeBSD-шлюзе | ||
+ | |||
+ | #Входящий внешний (1000 кбит/сек) | ||
+ | shape-in-ext=1000 | ||
+ | #Входящий локальный (1000 кбит/сек) | ||
+ | shape-in-int=1001 | ||
+ | #Исходящий внешний (1000 кбит/сек) | ||
+ | shape-out-ext=1002 | ||
+ | #Исходящий локальный (не ограничен) | ||
+ | shape-out-int=2 | ||
+ | </source> | ||
+ | |||
+ | '''Скорость 1000 + локалка''' | ||
+ | |||
+ | Конфиг | ||
+ | <source lang="java"> | ||
+ | #Цифры - номера pipe на FreeBSD-шлюзе | ||
+ | |||
+ | #Входящий внешний (1000 кбит/сек) | ||
+ | shape-in-ext=1000 | ||
+ | #Входящий локальный (не ограничен) | ||
+ | shape-in-int=1 | ||
+ | #Исходящий внешний (1000 кбит/сек) | ||
+ | shape-out-ext=1002 | ||
+ | #Исходящий локальный (не ограничен) | ||
+ | shape-out-int=2 | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Управление абонентским свитчом осуществляется через telnet/ssh/snmp, причем биллинг может штатно мониторить uptime свитча и переконфигурировать свитч после перезагрузки. Конкретная реализация зависит от вендора и выходит за рамки данной статьи. |
Текущая версия на 07:09, 7 февраля 2013
Здесь описана схема реально работающей сети на основе 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 свитча и переконфигурировать свитч после перезагрузки. Конкретная реализация зависит от вендора и выходит за рамки данной статьи.