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

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

(Различия между версиями)
Перейти к: навигация, поиск
м (eg)
 
(4 промежуточные версии не показаны)
Строка 1: Строка 1:
-
== СТАТЬЯ НЕ ЗАКОНЧЕНА! ПРОДОЛЖЕНИЕ СЛЕДУЕТ. ==
+
Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга. В описываемой схеме используется '''FreeBSD 9.1'''
-
 
+
-
 
+
-
Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга.
+
В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 1 справа).
В упрощенном виде сеть представляет из себя стандартную трехуровневую модель. (см. Схему 1 справа).
[[Файл:Fbsd-gw-net-scheme.png|thumb|(Схема 1) Графическое представление сети на основе шлюзов FreeBSD]]
[[Файл:Fbsd-gw-net-scheme.png|thumb|(Схема 1) Графическое представление сети на основе шлюзов FreeBSD]]
 +
Если смотреть в иерархии модуля, то связь такая: Access+Accounting: DHCP -> FreeBSD-шлюз ->Абонентский свитч.
Если смотреть в иерархии модуля, то связь такая: Access+Accounting: DHCP -> FreeBSD-шлюз ->Абонентский свитч.
Схема работает по принципу VLAN на абонента (VLAN-per-user) + IP Unnumbered (SuperVLAN). Сразу оговорюсь, что каждый шлюз предполагает терминирование до 4096 VLAN ID. Т.е. недостатка VLAN ID не предвидится. Скорее у вас закончатся мощности по маршрутизации трафика, чем VLAN ID.
Схема работает по принципу 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, 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.
Минимальный функционал магистрального свитча:802.1q VLAN trunking (т.е. свитч должен уметь через себя пропускать все 4096 VLAN-ов). Собственно от него требуется только собирать гроздь VLAN-ов абонентских свитчей и передавать это все на шлюз. Для IPTV обязательно нужен igmp snooping.
 +
Абонент получает IP-адрес с помощью DHCP Option 82. Идентификатором абонента является либо ID-свитча+VLAN ID, либо ID-свитча+VLAN ID+MAC.
Абонент получает IP-адрес с помощью DHCP Option 82. Идентификатором абонента является либо ID-свитча+VLAN ID, либо ID-свитча+VLAN ID+MAC.
 +
Вся терминация абонентских VLAN-ов на шлюзе сделана в ядре с помощью netgraph (см. Схему 2 справа).
Вся терминация абонентских VLAN-ов на шлюзе сделана в ядре с помощью netgraph (см. Схему 2 справа).
[[Файл:Fbsd-gw-netgraph-scheme.png|thumb|(Схема 2) Визуализация netghraph-нод для шлюза FreeBSD]]
[[Файл:Fbsd-gw-netgraph-scheme.png|thumb|(Схема 2) Визуализация netghraph-нод для шлюза FreeBSD]]
На Схеме 2 и в примерах конфигурации ниже, предполагается, что интерфейс em1 смотрит в сторону абонентов, а интерфейс em2 в сторону ядра сети. Скрипт создает всю цепочку netgraph, создает абонентские VLAN-интерфейсы, переименовывает их более удобоваримый вид. Стоит выделить то, что все, явно несозданные VLAN-ы, прозрачно ходят между em1 и em2. Это полезно, например, если вы в ядре имеете PPPoE-сервер для смешанных сетей (в период модернизации или невыгодно на дом ставить полноценный свитч) или хотите оказать юрику услугу по объединению офисов или опять же пропустить прозрачно VLAN с мультикаст-трафиком.
На Схеме 2 и в примерах конфигурации ниже, предполагается, что интерфейс em1 смотрит в сторону абонентов, а интерфейс em2 в сторону ядра сети. Скрипт создает всю цепочку netgraph, создает абонентские VLAN-интерфейсы, переименовывает их более удобоваримый вид. Стоит выделить то, что все, явно несозданные VLAN-ы, прозрачно ходят между em1 и em2. Это полезно, например, если вы в ядре имеете PPPoE-сервер для смешанных сетей (в период модернизации или невыгодно на дом ставить полноценный свитч) или хотите оказать юрику услугу по объединению офисов или опять же пропустить прозрачно VLAN с мультикаст-трафиком.
 +
Ниже привожу скрипт запуска построения netgraph-нод, кладется в файл '''/usr/local/etc/rc.d/netgraph'''
Ниже привожу скрипт запуска построения netgraph-нод, кладется в файл '''/usr/local/etc/rc.d/netgraph'''
Важно его запускать именно таким способом, это гарантирует, что он выстроит ноды до запуска всех важных сетевых сервисов (например quagga).
Важно его запускать именно таким способом, это гарантирует, что он выстроит ноды до запуска всех важных сетевых сервисов (например quagga).
-
 
'''/usr/local/etc/rc.d/netgraph'''
'''/usr/local/etc/rc.d/netgraph'''
Строка 160: Строка 162:
Для обмена маршрутами между шлюзами и другими маршрутизаторами ядра используется пакет Quagga, в котором мы используем zebra и bgpd/ospfd. Нужно отметить, что для корректной работы конструкции Unnumbered IP (SuperVLAN) нужно в таблицу маршрутизации прописывать маршруты вида:
Для обмена маршрутами между шлюзами и другими маршрутизаторами ядра используется пакет Quagga, в котором мы используем zebra и bgpd/ospfd. Нужно отметить, что для корректной работы конструкции Unnumbered IP (SuperVLAN) нужно в таблицу маршрутизации прописывать маршруты вида:
-
 
<source lang="bash">
<source lang="bash">
route add -host cc.19.64.111 -iface em1.1234
route add -host cc.19.64.111 -iface em1.1234
</source>
</source>
-
 
где cc.19.64.111 IP абонента, а em1.1234 интерфейс (1234 - номер VLAN-интерфейса). Т.к. у нас используется zebra, маршруты будем прописывать в ней, а zebra уже сама их внесет в системную таблицу маршрутизации:
где cc.19.64.111 IP абонента, а em1.1234 интерфейс (1234 - номер VLAN-интерфейса). Т.к. у нас используется zebra, маршруты будем прописывать в ней, а zebra уже сама их внесет в системную таблицу маршрутизации:
<source lang="text">
<source lang="text">
Строка 171: Строка 171:
zebra(conf)# end
zebra(conf)# end
</source>
</source>
-
 
Соответственно и удалять нужно в zebra:
Соответственно и удалять нужно в zebra:
<source lang="text">
<source lang="text">
Строка 179: Строка 178:
</source>
</source>
-
Для автоматизации процесса добавления и удаления маршрутов с FreeBSD-шлюза был переработан скрипт manad, к которому обращается биллинг при инициализации и терминировании сессии, а также для выставления нужных скоростей в соответствии с тарифной политикой.
 
 +
Для автоматизации процесса добавления и удаления маршрутов с FreeBSD-шлюза был переработан скрипт manad, к которому обращается биллинг при инициализации и терминировании сессии, а также для выставления нужных скоростей в соответствии с тарифной политикой.
'''/root/manad.pl'''
'''/root/manad.pl'''
Строка 380: Строка 379:
</source>
</source>
 +
 +
Прослойка между manad и quagga
'''/root/route.sh'''
'''/root/route.sh'''
Строка 410: Строка 411:
fi
fi
</source>
</source>
-
 
+
<s>В ПО Quagga на момент написания этого текста существует [https://bugzilla.quagga.net/show_bug.cgi?id=494 баг под номером 494], из-за которого на FreeBSD не добавляется маршрут в системную таблицу маршрутизации, если в качестве адреса назначения указано имя интерфейса. Баг описан, подтвержден и есть сторонний патч, исправляющий эту проблему, однако разработчики не спешат исправлять проблему и выпускать новый релиз, поэтому порт Quagga придется пропатчить и перекомпилировать самому. Для этого достаточно закинуть патч в каталог files в каталоге с портом quagga, поправить в патче пути и сделать make install clean</s><br>
-
В ПО Quagga на момент написания этого текста существует [https://bugzilla.quagga.net/show_bug.cgi?id=494 баг под номером 494], из-за которого на FreeBSD не добавляется маршрут в системную таблицу маршрутизации, если в качестве адреса назначения указано имя интерфейса. Баг описан, подтвержден и есть сторонний патч, исправляющий эту проблему, однако разработчики не спешат исправлять проблему и выпускать новый релиз, поэтому порт Quagga придется пропатчить и перекомпилировать самому. Для этого достаточно закинуть патч в каталог files в каталоге с портом quagga, поправить в патче пути и сделать make install clean
+
Все, описанное выше, поправлено в последних версиях Quagga (как минимум в 0.99.21).
Строка 431: Строка 432:
options        NETGRAPH_KSOCKET
options        NETGRAPH_KSOCKET
options        NETGRAPH_ETHER
options        NETGRAPH_ETHER
 +
options        NETGRAPH_VLAN
</source>
</source>
-
Обязательно необходимо закомментировать в ядре опцию FLOWTABLE, т.к. ее реализация на данный момент нестабильна
+
 
-
<source lang="text">
+
 
-
#options        FLOWTABLE              # per-cpu routing cache
+
Подсистема 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>
</source>
 +
Строка 442: Строка 473:
net.inet.ip.dummynet.io_fast=1
net.inet.ip.dummynet.io_fast=1
net.inet.ip.fw.one_pass=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.hash_size=65535
net.inet.ip.dummynet.expire=0
net.inet.ip.dummynet.expire=0
net.inet.ip.forwarding=1
net.inet.ip.forwarding=1
-
net.inet.ip.fastforwarding=1
+
net.inet.ip.fastforwarding=0
net.link.ether.inet.proxyall=1
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>
</source>
Строка 457: Строка 516:
firewall_script="/etc/rc.ipfw"
firewall_script="/etc/rc.ipfw"
gateway_enable="YES"
gateway_enable="YES"
 +
defaultrouter="NO"
quagga_enable="YES"
quagga_enable="YES"
quagga_flags="-d -A 127.0.0.1"
quagga_flags="-d -A 127.0.0.1"
Строка 466: Строка 526:
Как вы успели заметить выше, шейпирование скорости реализовано с помощью dummynet. Честно говоря, это спорный выбор, но он отличается элегантностью реализации и простотой интеграции с биллингом, в отличии от ng_car. Реальную разницу нужно смотреть под нагрузкой, однако, как мне кажется, при отсутствии NAT на шлюзе (а NAT-ферма имхо должна стоять где-то поближе к бордеру сети) упереться в производительность dummynet не должны и по факту реальной эксплуатации не уперлись. Единственно, что важно, это приколотить процесс dummynet к конкретному ядру:
Как вы успели заметить выше, шейпирование скорости реализовано с помощью dummynet. Честно говоря, это спорный выбор, но он отличается элегантностью реализации и простотой интеграции с биллингом, в отличии от ng_car. Реальную разницу нужно смотреть под нагрузкой, однако, как мне кажется, при отсутствии NAT на шлюзе (а NAT-ферма имхо должна стоять где-то поближе к бордеру сети) упереться в производительность dummynet не должны и по факту реальной эксплуатации не уперлись. Единственно, что важно, это приколотить процесс dummynet к конкретному ядру:
<source lang="bash">
<source lang="bash">
-
/bin/ps -p 0 -axcH -o lwp,command | /usr/bin/egrep '\/dummynet' | while read lwp cmd; do /usr/bin/cpuset -l 3 -t $lwp; done
+
/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.local'''
+
-
<source lang="bash">
+
-
#!/bin/sh
+
-
 
+
-
# 1. Using delayed interrupts on em(4) ifaces - use carefully
+
-
# This sysctl values are not implemented on startup, so we try to use it here.
+
-
echo "Tuning em(4) interfaces"
+
-
for i in `jot - 0 3 `; do
+
-
    sysctl dev.em.$i.rx_int_delay=600 > /dev/null 2>&1
+
-
    sysctl dev.em.$i.tx_int_delay=600 > /dev/null 2>&1
+
-
    sysctl dev.em.$i.rx_abs_int_delay=1000 > /dev/null 2>&1
+
-
    sysctl dev.em.$i.tx_abs_int_delay=1000 > /dev/null 2>&1
+
-
    sysctl dev.em.$i.rx_kthreads=4 > /dev/null 2>&1
+
-
done
+
</source>
</source>
Строка 590: Строка 633:
$IPFW pipe 1001 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 1002 config mask src-ip 0xffffffff bw 1000Kbit/s buckets 512 queue 100
-
 
-
$IPFW pipe 1500 config mask dst-ip 0xffffffff bw 1500Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 1501 config mask dst-ip 0xffffffff bw 1500Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 1502 config mask src-ip 0xffffffff bw 1500Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 2000 config mask dst-ip 0xffffffff bw 2000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 2001 config mask dst-ip 0xffffffff bw 2000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 2002 config mask src-ip 0xffffffff bw 2000Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 3000 config mask dst-ip 0xffffffff bw 3000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 3001 config mask dst-ip 0xffffffff bw 3000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 3002 config mask src-ip 0xffffffff bw 3000Kbit/s buckets 512 queue 100
 
$IPFW pipe 4000 config mask dst-ip 0xffffffff bw 4000Kbit/s buckets 512 queue 100
$IPFW pipe 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 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 4002 config mask src-ip 0xffffffff bw 4000Kbit/s buckets 512 queue 100
-
 
-
$IPFW pipe 5000 config mask dst-ip 0xffffffff bw 5000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 5001 config mask dst-ip 0xffffffff bw 5000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 5002 config mask src-ip 0xffffffff bw 5000Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 6000 config mask dst-ip 0xffffffff bw 6000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 6001 config mask dst-ip 0xffffffff bw 6000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 6002 config mask src-ip 0xffffffff bw 6000Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 8000 config mask dst-ip 0xffffffff bw 8000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 8001 config mask dst-ip 0xffffffff bw 8000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 8002 config mask src-ip 0xffffffff bw 8000Kbit/s buckets 512 queue 100
 
$IPFW pipe 16000 config mask dst-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100
$IPFW pipe 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 16001 config mask dst-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100
$IPFW pipe 16002 config mask src-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100
$IPFW pipe 16002 config mask src-ip 0xffffffff bw 16000Kbit/s buckets 512 queue 100
-
 
-
$IPFW pipe 20000 config mask dst-ip 0xffffffff bw 20000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 20001 config mask dst-ip 0xffffffff bw 20000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 20002 config mask src-ip 0xffffffff bw 20000Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 2500 config mask dst-ip 0xffffffff bw 2500Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 2501 config mask dst-ip 0xffffffff bw 2500Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 2502 config mask src-ip 0xffffffff bw 2500Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 24000 config mask dst-ip 0xffffffff bw 24000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 24001 config mask dst-ip 0xffffffff bw 24000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 24002 config mask src-ip 0xffffffff bw 24000Kbit/s buckets 512 queue 100
 
-
 
-
$IPFW pipe 12000 config mask dst-ip 0xffffffff bw 12000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 12001 config mask dst-ip 0xffffffff bw 12000Kbit/s buckets 512 queue 100
 
-
$IPFW pipe 12002 config mask src-ip 0xffffffff bw 12000Kbit/s buckets 512 queue 100
 
</source>
</source>
-
 
При необходимости, по аналогии создается больше классов трафиков, которые отдельно нужно зажимать.
При необходимости, по аналогии создается больше классов трафиков, которые отдельно нужно зажимать.
Обращаю внимание на то, что трубы независимы друг от друга как в плане разных абонентов, так и в плане типов трафика ,которые они зажимают. Т.е. например можно одновременно качать с максимальной скоростью трубы из внешнего интернета и тут же качать с максимальной скоростью трубы для локального трафика из локалки.
Обращаю внимание на то, что трубы независимы друг от друга как в плане разных абонентов, так и в плане типов трафика ,которые они зажимают. Т.е. например можно одновременно качать с максимальной скоростью трубы из внешнего интернета и тут же качать с максимальной скоростью трубы для локального трафика из локалки.
-
Скрипт '''/root/restore.sh''', который запускается при загрузке FreeBSD-шлюза, коннектится по ssh к биллингу и забирает актуальные сессии и скорости для данного шлюза и вносит в свои таблицы маршрутизации и ipfw.
+
 
 +
Скрипт '''/root/restore.sh''', который запускается при загрузке FreeBSD-шлюза, подключается к mysql-базе биллинга и забирает актуальные сессии и скорости для данного шлюза и вносит в свои таблицы маршрутизации и ipfw.
<source lang="bash">
<source lang="bash">
#!/bin/sh
#!/bin/sh
 +
 +
DB_HOST=10.254.254.17
 +
DB_USER=user
 +
DB_PASS=pass
# -- Т.к. всю информацию о маршрутах в сети мы получаем от quagga, то ждем до
# -- Т.к. всю информацию о маршрутах в сети мы получаем от quagga, то ждем до
Строка 652: Строка 659:
while [ $RESULT != 0 ]
while [ $RESULT != 0 ]
do
do
-
     ping -c 1 -t 1 aa.200.144.16 >/dev/null 2>&1
+
     ping -c 1 -t 1 $DB_HOST >/dev/null 2>&1
     RESULT=$?
     RESULT=$?
done
done
Строка 658: Строка 665:
# -- Выясняем собственный IP
# -- Выясняем собственный IP
IP_ADDR=`ifconfig vlan11 | grep netmask | awk '{print $2}'`
IP_ADDR=`ifconfig vlan11 | grep netmask | awk '{print $2}'`
-
 
-
# -- Записываем в лог, что собираемся восстановить
 
-
ssh -p 48222 -l gw-worker aa.200.144.16 cat /tmp/bill/sessions/$IP_ADDR > /var/log/restored_sessions.log 2>&1
 
# -- Вносим в таблицу маршрутизации все IP текущих сессий на этом шлюзе
# -- Вносим в таблицу маршрутизации все IP текущих сессий на этом шлюзе
-
ssh -p 48222 -l gw-worker aa.200.144.16 cat /tmp/bill/sessions/$IP_ADDR | while read SESSION
+
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
do
 +
    #echo $SESSION
     RT_IP=`echo $SESSION | awk '{print $1}'`
     RT_IP=`echo $SESSION | awk '{print $1}'`
     RT_VLAN=`echo $SESSION | awk '{print $2}'`
     RT_VLAN=`echo $SESSION | awk '{print $2}'`
 +
    #echo "$RT_IP $RT_VLAN"
     /root/route.sh add $RT_IP $RT_VLAN
     /root/route.sh add $RT_IP $RT_VLAN
done
done
# -- Применяем ограничения по скоростям
# -- Применяем ограничения по скоростям
-
scp -P 48222 gw-worker@aa.200.144.16:/tmp/bill/speeds/$IP_ADDR /tmp/ipfw
+
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
-
. /tmp/ipfw
+
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>
</source>
-
Скрипт на php, который запускается на машине с BGBilling раз в 30 секунд и собирает информацию о текущих сессиях в биллинге и складывает ее в файлы для шлюзов. Решение корявое, но рабочее. В дальнейшем будет переделано.
+
Структура табличек в биллинге, куда будут складываться текущие сессии с привязкой к FreeBSD-шлюзам, а также ограничения по скоростям.
-
 
+
<source lang="mysql">
-
'''/root/sessions.php'''
+
CREATE TABLE `_inet_gw_sessions` (
-
<source lang="php">
+
   `gw_ip` varchar(16),
-
<?
+
  `user_ip` varchar(16),
-
$sess_dir='/tmp/bill/sessions/';
+
  `iface` varchar(10),
-
$speeds_dir='/tmp/bill/speeds/';
+
  `mac` varchar(16),
-
 
+
  `session_start` datetime,
-
$gw_cache_file = '/tmp/bill/gw_map.cache';
+
  `state` tinyint(4),
-
$lock_file = '/tmp/bill/lock';
+
  KEY `gw_ip` (`gw_ip`)
-
$fp_sess   = array();
+
) ENGINE=InnoDB DEFAULT CHARSET=cp1251
-
$fp_speeds = array();
+
-
$gw_map    = array();
+
-
 
+
-
$mysql_master_host = '10.254.254.17';
+
-
$mysql_user = 'bgbilling';
+
-
$mysql_pass = 'pass';
+
-
$mysql_db = 'bgbilling';
+
-
 
+
-
#код модуля inet
+
-
$inet_mid = 1;
+
-
#ID типов абонентских свитчей
+
-
$dev_ids = "3,4,5,8,12";
+
-
 
+
-
if(file_exists($lock_file)) die('LOCK FILE FOUND. EXITING.');
+
-
 
+
-
touch($lock_file);
+
-
 
+
-
mysql_connect($mysql_master_host, $mysql_user,$mysql_pass) or die(mysql_error());
+
-
mysql_select_db($mysql_db) or die(mysql_error());
+
-
 
+
-
function createGWmapCache()
+
-
{
+
-
    global $gw_cache_file;
+
-
    $fp = fopen($gw_cache_file,'w');
+
-
    $r = mysql_query('SELECT COUNT(*) FROM inet_device_'.$inet_mid) or die(mysql_error());
+
-
    $count = mysql_result($r,0,0);
+
-
    fputs($fp,$count."\n");
+
-
    $r = mysql_query('SELECT parentId,id FROM inet_device_'.$inet_mid.' WHERE deviceTypeId IN ('.$dev_ids.')') or die(mysql_error());
+
-
    while($l = mysql_fetch_row($r))
+
-
    {
+
-
        $parentId = $l[0];
+
-
        $typeId = 0;
+
-
        while($typeId!=7 && !empty($parentId))
+
-
        {
+
-
            $r1 = mysql_query('SELECT parentId,deviceTypeId,host,config FROM inet_device_'.$inet_mid.' WHERE id='.$parentId) or die(mysql_error());
+
-
            list($parentId,$typeId,$host,$config) = mysql_fetch_row($r1);
+
-
        }
+
-
        if(!empty($parentId))
+
-
        {
+
-
            $vlan_prefix = 'em1.';
+
-
            if(preg_match('#vlan.prefix=(.+)#',$config,$m))
+
-
            {
+
-
                $vlan_prefix = $m[1];
+
-
            }
+
-
            fputs($fp,$l[1]." $host $vlan_prefix\n");
+
-
        }
+
-
    }
+
-
    fclose($fp);
+
-
}
+
-
 
+
-
function reloadGWmap()
+
-
{
+
-
    global $gw_map,$gw_cache_file;
+
-
 
+
-
    $gw_map = array();
+
-
    $gw_cache = file($gw_cache_file);
+
-
    unset($gw_cache[0]);
+
-
    foreach($gw_cache AS $gw)
+
-
    {
+
-
        $tmp = explode(' ',$gw);
+
-
        $gw_map[$tmp[0]] = array($tmp[1],trim($tmp[2]));
+
-
    }
+
-
}
+
-
 
+
-
if(file_exists($gw_cache_file))
+
-
{
+
-
    $tmp = file($gw_cache_file);
+
-
    $r = mysql_query('SELECT COUNT(*) FROM inet_device_'.$inet_mid) or die(mysql_error());
+
-
    if($tmp[0]!=mysql_result($r,0,0))
+
-
        createGWmapCache();
+
-
    unset($tmp);
+
-
}else{
+
-
    createGWmapCache();
+
-
}
+
-
reloadGWmap();
+
-
 
+
-
 
+
-
$result = mysql_query('SELECT t1.deviceId,t1.callingStationId,t2.vlan,t1.ipAddress,t1.connectionStart,t1.deviceState,t1.deviceOptions FROM inet_connection_'.$inet_mid.' AS t1, inet_serv_'.$inet_mid.' AS t2 WHERE t1.servId=t2.id') or die(mysql_error());
+
-
while($ln = mysql_fetch_row($result))
+
-
{
+
-
    list($deviceId,$mac,$vlan,$ip,$startTime,$state,$options) = $ln;
+
-
    $ip_long = (ord($ip[0]) << 24) | (ord($ip[1]) << 16) | (ord($ip[2]) << 8) | (ord($ip[3]));
+
-
    $ip = long2ip($ip_long);
+
-
    $option = explode(',',$options);
+
-
    $option = intval($option[count($options)-1]);
+
-
    $gw_ip = $gw_map[$deviceId][0];
+
-
    $gw_prefix = $gw_map[$deviceId][1];
+
-
 
+
-
    if(!empty($gw_ip))
+
-
    {
+
-
if(empty($fp_sess[$gw_ip]))
+
-
$fp_sess[$gw_ip] = fopen($sess_dir.$gw_ip.'.new','w');
+
-
 
+
-
fputs($fp_sess[$gw_ip],"$ip\t".$gw_prefix.$vlan."\t$mac\t$startTime\t$state\n");
+
-
 
+
-
 
+
-
if(empty($fp_speeds[$gw_ip]))
+
-
            $fp_speeds[$gw_ip] = fopen($speeds_dir.$gw_ip.'.new','w');
+
-
 
+
-
        if($state==1)
+
-
        {
+
-
            if(!empty($option))
+
-
            {
+
-
                $r = mysql_query("SELECT config FROM inet_option_".$inet_mid." WHERE id=$option");
+
-
                $opt_config = @mysql_result($r,0,0);
+
-
                if(preg_match('#shape-in-ext=(\d+).*shape-in-int=(\d+).*shape-out-ext=(\d+).*shape-out-int=(\d+)#s',$opt_config,$rules))
+
-
                {
+
-
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 1 add $ip ".$rules[1]."\n");
+
-
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 2 add $ip ".$rules[2]."\n");
+
-
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 3 add $ip ".$rules[3]."\n");
+
-
                    fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 4 add $ip ".$rules[4]."\n");
+
-
                }
+
-
            }
+
-
        }else{
+
-
            fputs($fp_speeds[$gw_ip],"/sbin/ipfw table 11 add $ip\n");
+
-
        }
+
-
    }
+
-
}
+
-
foreach($fp_sess AS $ip=>$fp)
+
-
{
+
-
    rename($sess_dir.$ip.'.new',$sess_dir.$ip);
+
-
    fclose($fp);
+
-
}
+
-
foreach($fp_speeds AS $ip=>$fp)
+
-
{
+
-
    rename($speeds_dir.$ip.'.new',$speeds_dir.$ip);
+
-
    fclose($fp);
+
-
}
+
-
unlink($lock_file);
+
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>
</source>
-
Ну и shell-скрипт для его периодического запуска
+
Динамический код для интеграции шлюза в биллинг:
-
'''/root/sessions.sh'''
+
'''ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator'''
-
<source lang="bash">
+
-
#!/bin/bash
+
-
 
+
-
while [ '1'='1' ]
+
-
do
+
-
    /usr/bin/php -f /root/sessions.php
+
-
    sleep 30
+
-
done
+
-
</source>
+
-
 
+
-
Динамический код для интеграции шлюза в биллинг:
+
-
ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator
+
<source lang="java">
<source lang="java">
package ru.bitel.bgbilling.inet.dyn.device.freebsd;
package ru.bitel.bgbilling.inet.dyn.device.freebsd;
Строка 841: Строка 717:
import java.io.*;
import java.io.*;
import java.net.*;
import java.net.*;
-
import java.util.Date;
+
import java.util.*;
-
import java.util.ArrayList;
+
import java.sql.*;
-
import java.util.HashMap;
+
-
import java.util.Iterator;
+
-
import java.util.List;
+
-
import java.util.Map;
+
import org.apache.log4j.*;
import org.apache.log4j.*;
-
import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator;
+
import ru.bitel.bgbilling.modules.inet.access.sa.*;
-
import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorAdapter;
+
import ru.bitel.bgbilling.modules.inet.api.common.bean.*;
-
import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent;
+
import ru.bitel.bgbilling.server.util.*;
-
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;
+
-
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;
+
-
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;
+
-
import ru.bitel.bgbilling.server.util.Setup;
+
import ru.bitel.common.ParameterMap;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.inet.IpAddress;
import ru.bitel.common.inet.IpAddress;
-
import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap;
+
import ru.bitel.bgbilling.modules.inet.runtime.*;
-
import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntime;
+
import ru.bitel.bgbilling.modules.inet.api.common.bean.*;
-
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetOption;
+
-
import ru.bitel.bgbilling.modules.inet.api.server.bean.InetOptionDao;
+
import ru.bitel.common.Utils;
import ru.bitel.common.Utils;
-
 
public class FreeBSDServiceActivator
public class FreeBSDServiceActivator
Строка 869: Строка 734:
{
{
private static final Logger log = Logger.getLogger( FreeBSDServiceActivator.class );
private static final Logger log = Logger.getLogger( FreeBSDServiceActivator.class );
 +
 +
private Connection con = null;
Socket socket;
Socket socket;
PrintWriter out;
PrintWriter out;
Строка 880: Строка 747:
{
{
log.info("FreeBSD CONNECT");
log.info("FreeBSD CONNECT");
 +
con = Setup.getSetup().getDBConnectionFromPool();
socket = new Socket( host, port );
socket = new Socket( host, port );
out = new PrintWriter( socket.getOutputStream(), true );
out = new PrintWriter( socket.getOutputStream(), true );
Строка 897: Строка 765:
int status = InetServ.STATUS_CLOSED;
int status = InetServ.STATUS_CLOSED;
-
 
String ExtIn = "";
String ExtIn = "";
String IntIn = "";
String IntIn = "";
String ExtOut = "";
String ExtOut = "";
String IntOut = "";
String IntOut = "";
 +
if( optId>0 )
if( optId>0 )
{
{
Строка 912: Строка 780:
}
}
-
List<String> commands = new ArrayList<String>();
 
int state = event.getNewState();
int state = event.getNewState();
String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
 +
log.info("optionId="+optId+",state="+state+",ip="+ip);
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 )
if( status == InetServ.STATUS_ACTIVE )
{
{
-
commands.add("set speed "+ip+" 1 "+ExtIn);
+
con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',1,'"+ip+"',"+ExtIn+")").executeUpdate();
-
commands.add("set speed "+ip+" 2 "+IntIn);
+
con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',2,'"+ip+"',"+IntIn+")").executeUpdate();
-
commands.add("set speed "+ip+" 3 "+ExtOut);
+
con.prepareStatement("INSERT INTO _inet_gw_speeds(gw_ip,table_num,user_ip,pipe) VALUES('"+host+"',3,'"+ip+"',"+ExtOut+")").executeUpdate();
-
commands.add("set speed "+ip+" 4 "+IntOut);
+
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{
}else{
-
commands.add("del entry "+ip+" 1");
+
out.println("del entry "+ip+" 1");
-
commands.add("del entry "+ip+" 2");
+
out.println("del entry "+ip+" 2");
-
commands.add("del entry "+ip+" 3");
+
out.println("del entry "+ip+" 3");
-
commands.add("del entry "+ip+" 4");
+
out.println("del entry "+ip+" 4");
}
}
-
if( state == InetServ.STATE_ENABLE )
+
if( state == InetServ.STATE_ENABLE )
-
{
+
-
commands.add("del entry "+ip+" 11");
+
-
}else{
+
-
commands.add("add entry "+ip+" 11 0");
+
-
}
+
-
for( String command : commands )
+
{
{
-
out.println( command );
+
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");
}
}
}
}
Строка 950: Строка 821:
String vlan = String.valueOf( event.getNewInetServ().getVlan() );
String vlan = String.valueOf( event.getNewInetServ().getVlan() );
String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
 +
String mac = event.getConnection().getCallingStationId();
 +
int state = event.getConnection().getConnectionStatus();
 +
log.info( "ip="+ip+",vlan="+vlan );
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 );
out.println( "add route "+ip+" "+vlanPrefix+vlan );
Строка 963: Строка 838:
String vlan = String.valueOf( event.getNewInetServ().getVlan() );
String vlan = String.valueOf( event.getNewInetServ().getVlan() );
String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
String ip = IpAddress.toString( event.getConnection().getInetAddressBytes() );
 +
log.info( "ip="+ip+",vlan="+vlan );
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 );
out.println( "del route "+ip+" "+vlanPrefix+vlan );
Строка 981: Строка 858:
{
{
log.info("FreeBSD DISCONNECT");
log.info("FreeBSD DISCONNECT");
 +
ServerUtils.closeConnection(con);
out.close();
out.close();
socket.close();
socket.close();
Строка 997: Строка 875:
return true;
return true;
}
}
 +
@Override
@Override
Строка 1013: Строка 892:
port = parameterMap.getInt( "manad.port", 4444 );
port = parameterMap.getInt( "manad.port", 4444 );
vlanPrefix = parameterMap.get( "vlan.prefix", "" );
vlanPrefix = parameterMap.get( "vlan.prefix", "" );
 +
log.info( "host="+host+",port="+port+",vlanPrefix="+vlanPrefix );
log.info( "host="+host+",port="+port+",vlanPrefix="+vlanPrefix );
-
+
 
return true;
return true;
}
}
Строка 1023: Строка 903:
[[Файл:Bgb-client-inet-fbsd-conf.png|thumb|Пример дерева для FreeBSD-шлюза]]
[[Файл:Bgb-client-inet-fbsd-conf.png|thumb|Пример дерева для FreeBSD-шлюза]]
-
Настройка Типа устройства
+
 
 +
Настройка '''Типа устройства'''
<source lang="java">
<source lang="java">
# Установить галку "Является источником данных"
# Установить галку "Является источником данных"
Строка 1039: Строка 920:
-
Настройка Устройства
+
Настройка '''Устройства'''
<source lang="java">
<source lang="java">
#IP default-gateway, которое будет отсылаться абоненту в DHCP-ответе
#IP default-gateway, которое будет отсылаться абоненту в DHCP-ответе
Строка 1052: Строка 933:
'''Скорость 1000'''
'''Скорость 1000'''
 +
Конфиг
Конфиг
<source lang="java">
<source lang="java">
Строка 1067: Строка 949:
'''Скорость 1000 + локалка'''
'''Скорость 1000 + локалка'''
 +
Конфиг
Конфиг
<source lang="java">
<source lang="java">

Текущая версия на 07:09, 7 февраля 2013

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

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


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

/root/manad.pl

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


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

/root/route.sh

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

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


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

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


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


/boot/loader.conf

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


/etc/sysctl.conf

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


/etc/rc.conf

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

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

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


/etc/rc.ipfw

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

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


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

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


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

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


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

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

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


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

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


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

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

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

Скорость 1000

Конфиг

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

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

Конфиг

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


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

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