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

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

(Различия между версиями)
Перейти к: навигация, поиск
Строка 1: Строка 1:
-
== СТАТЬЯ НЕ ЗАКОНЧЕНА! ПРОДОЛЖЕНИЕ СЛЕДУЕТ. ==
 
-
 
-
 
Здесь описана схема реально работающей сети на основе FreeBSD-шлюзов. Я опишу логику шлюза, управление которой будет осуществлять новый модуль inet. Интеграция с биллингом реализовывалась самостоятельно при активном содействии разработчиков биллинга.
Здесь описана схема реально работающей сети на основе 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>
-
 
В ПО Quagga на момент написания этого текста существует [https://bugzilla.quagga.net/show_bug.cgi?id=494 баг под номером 494], из-за которого на FreeBSD не добавляется маршрут в системную таблицу маршрутизации, если в качестве адреса назначения указано имя интерфейса. Баг описан, подтвержден и есть сторонний патч, исправляющий эту проблему, однако разработчики не спешат исправлять проблему и выпускать новый релиз, поэтому порт Quagga придется пропатчить и перекомпилировать самому. Для этого достаточно закинуть патч в каталог files в каталоге с портом quagga, поправить в патче пути и сделать make install clean
В ПО Quagga на момент написания этого текста существует [https://bugzilla.quagga.net/show_bug.cgi?id=494 баг под номером 494], из-за которого на FreeBSD не добавляется маршрут в системную таблицу маршрутизации, если в качестве адреса назначения указано имя интерфейса. Баг описан, подтвержден и есть сторонний патч, исправляющий эту проблему, однако разработчики не спешат исправлять проблему и выпускать новый релиз, поэтому порт Quagga придется пропатчить и перекомпилировать самому. Для этого достаточно закинуть патч в каталог files в каталоге с портом quagga, поправить в патче пути и сделать make install clean
Строка 590: Строка 590:
$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-шлюза, подключается к mysql-базе биллинга и забирает актуальные сессии и скорости для данного шлюза и вносит в свои таблицы маршрутизации и ipfw.
Скрипт '''/root/restore.sh''', который запускается при загрузке FreeBSD-шлюза, подключается к mysql-базе биллинга и забирает актуальные сессии и скорости для данного шлюза и вносит в свои таблицы маршрутизации и ipfw.
Строка 686: Строка 646:
Структура табличек в биллинге, куда будут складываться текущие сессии с привязкой к FreeBSD-шлюзам, а также ограничения по скоростям.
Структура табличек в биллинге, куда будут складываться текущие сессии с привязкой к FreeBSD-шлюзам, а также ограничения по скоростям.
<source lang="mysql">
<source lang="mysql">
-
CREATE TABLE `_catv_gw_sessions` (
+
CREATE TABLE `_inet_gw_sessions` (
   `gw_ip` varchar(16),
   `gw_ip` varchar(16),
   `user_ip` varchar(16),
   `user_ip` varchar(16),
Строка 693: Строка 653:
   `session_start` datetime,
   `session_start` datetime,
   `state` tinyint(4),
   `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`)
   KEY `gw_ip` (`gw_ip`)
) ENGINE=InnoDB DEFAULT CHARSET=cp1251
) ENGINE=InnoDB DEFAULT CHARSET=cp1251
</source>
</source>
 +
Динамический код для интеграции шлюза в биллинг:
Динамический код для интеграции шлюза в биллинг:
-
ru.bitel.bgbilling.inet.dyn.device.freebsd.FreeBSDServiceActivator
+
 
 +
'''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;
Строка 704: Строка 674:
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
Строка 732: Строка 691:
{
{
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;
Строка 743: Строка 704:
{
{
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 );
Строка 760: Строка 722:
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 )
{
{
Строка 775: Строка 737:
}
}
-
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");
}
}
}
}
Строка 813: Строка 778:
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 );
Строка 826: Строка 795:
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 );
Строка 844: Строка 815:
{
{
log.info("FreeBSD DISCONNECT");
log.info("FreeBSD DISCONNECT");
 +
ServerUtils.closeConnection(con);
out.close();
out.close();
socket.close();
socket.close();
Строка 860: Строка 832:
return true;
return true;
}
}
 +
@Override
@Override
Строка 876: Строка 849:
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;
}
}
Строка 886: Строка 860:
[[Файл:Bgb-client-inet-fbsd-conf.png|thumb|Пример дерева для FreeBSD-шлюза]]
[[Файл:Bgb-client-inet-fbsd-conf.png|thumb|Пример дерева для FreeBSD-шлюза]]
-
Настройка Типа устройства
+
 
 +
Настройка '''Типа устройства'''
<source lang="java">
<source lang="java">
# Установить галку "Является источником данных"
# Установить галку "Является источником данных"
Строка 902: Строка 877:
-
Настройка Устройства
+
Настройка '''Устройства'''
<source lang="java">
<source lang="java">
#IP default-gateway, которое будет отсылаться абоненту в DHCP-ответе
#IP default-gateway, которое будет отсылаться абоненту в DHCP-ответе
Строка 915: Строка 890:
'''Скорость 1000'''
'''Скорость 1000'''
 +
Конфиг
Конфиг
<source lang="java">
<source lang="java">
Строка 930: Строка 906:
'''Скорость 1000 + локалка'''
'''Скорость 1000 + локалка'''
 +
Конфиг
Конфиг
<source lang="java">
<source lang="java">

Версия 06:47, 29 июня 2012

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

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


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

/root/manad.pl

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


Прослойка между 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


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

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

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

#options        FLOWTABLE               # per-cpu routing cache


/etc/sysctl.conf

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


/etc/rc.conf

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

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

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


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

#!/bin/sh
 
# 1. Using delayed interrupts on em(4) ifaces - use carefully
# This sysctl values are not implemented on startup, so we try to use it here.
echo "Tuning em(4) interfaces"
for i in `jot - 0 3 `; do
    sysctl dev.em.$i.rx_int_delay=600 > /dev/null 2>&1
    sysctl dev.em.$i.tx_int_delay=600 > /dev/null 2>&1
    sysctl dev.em.$i.rx_abs_int_delay=1000 > /dev/null 2>&1
    sysctl dev.em.$i.tx_abs_int_delay=1000 > /dev/null 2>&1
    sysctl dev.em.$i.rx_kthreads=4 > /dev/null 2>&1
done


/etc/rc.ipfw

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

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