Vlan per user + Cisco IP subscriber interface + ISG

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

Перейти к: навигация, поиск

Содержание

Описание задачи

Клиентам предоставляется доступ по схеме vlan per user на роутерах cisco. Необходимо реализовать учёт и управление услугами в модуле Inet.

Решение

Сразу оговоримся, что в нашем случае речь идёт об услуге MPLS IP VPN. Для доступа интернет, возможно, появятся какие-то нюансы, но в целом механизм тот же.

Схема решения такая:

  • на клиентском интерфейсе настраивается ip subscriber interface + isg
  • интерфейсы авторизуются в BGInetAccess по паре "устройство+интерфейс"
  • Настройки скорости выдаются через ISG-сервисы, смена тарифа происходит через CoA
  • Трафик собираем по радиус-счётчикам ISG-сервисов и родительской сессии, либо через Netflow
  • В сервисе модуля Inet указываем только устройство и интерфейс

В нашем случае в биллинге не задаются ни сеть IP клиента на интерфейсе, ни его VLAN, т.к. для тарификации они не играют роли. Но это можно реализовать для учёта ресурсов, а также для автоматического конфигурирования.

Дополнительно усложним задачу: В некоторых случаях клиенту предоставляется VPN + доступ в интернет через NAT на нашей циске. Доступ в интернет считается в другом модуле, но такая схема несколько усложняет задачу, поскольку тогда в каждой точке VPN-а нужно разделять трафик на собственно VPN и интернет, чтобы не тарифицировать последний 2 раза. Эту схему мы реализуем отдельным типом сервиса с отдельной привязкой трафика и отдельными сервисами ISG.

Настройка биллинга

Настройка модуля

Для услуг VPN было решено завести отдельный экземпляр модуля Inet с названием 'VPN'.

Плюсы:

  • Можно учитывать услуги интернета и vpn на одном договоре без проблем с пересечением тарифных планов (основная причина, т.к. в наследство осталось много таких договоров)
  • Учёт услуг, визуальное разделение услуг на договоре и в отчётах.

Минусы:

  • Дублирование ресурсов между несколькими экземплярами модулей Inet: одни и те же устройства, vlan, интерфейсы используются в разных местах. Могут быть проблемы с учётом.

Конфигурация модуля:

# Активные и приостановленные статусы договора
contract.status.active.codes=0
contract.status.suspend.codes=3,4
# Проверка цены в тарифе: 0 - проверка отсутсвует, 1 - ошибка, только если у сессии есть трафик определенного типа,
# но для него нет цены, 2 - ошибка, если хотя бы для одного типа трафика в привязке типа сервиса нет цены (по умолчанию - 1)
#http://forum.bgbilling.ru/viewtopic.php?p=65629#p65629
accounting.tariffication.checkPrice=0
# Режим активации учетного периода, если не используется скрипт на событие активации,
# 0 (по умолчанию) - активация со дня подключения (старта сессии), 1 - активация с начала месяца.
# Следует учитывать, что учетный период является второй величиной при вычислении пропорциональности
# в тарифной ветке "Диапазон трафика"
#accounting.period.activation.mode=0
 
# Нужно ли отключать сервис с типом инициации "по трафику", если тариф не найден
#serv.disableOnTariffError=0
 
#Пункты Web - меню
#web.menuItem1=Отчет по сессиям Inet
#web.menuItem2=Смена пароля на логины Inet
#web.menuItem3=none
#web.menuItem3=Отчет по трафикам Inet
 
# Параметры автоматической генерации логина для сервиса.
# Минимальное значение логина при генерации логина
#serv.login.min=1
# Максимальное значение логина при генерации логина (т.е. если в базе присутствуют логины 1,2,3 и 10000000,
# то при генерации создастся логин 4, а не 10000001)
#serv.login.max=9999999
 
# Парамерты автоматической генерации пароля для сервиса. Можно указать в конфигурации модуля, конфигурации устройства, конфигурации типа сервиса
# (в последнем случае значения будут главнее):
# Минимальная длина пароля
serv.password.length.min=5
# Максимальная длина пароля
serv.password.length.max=16
# Разрешенные символы (используются также при генерации пароля)
serv.password.chars=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
# Описание разрешенных символов, если пользователь ввел другие
serv.password.chars.description=В пароле допустимы только цифры и латинские буквы.
# Длина для автоматически генерируемого пароля
serv.password.length.auto=6
# Используемые символы для автоматически генерируемого пароля (по умолчанию значение берется из параметра serv.password.chars)
#serv.password.chars.auto=
 
# Параметры активации карточек модуля card при использовании InetRadiusProcessor,
# данные параметры можно указать как в конфигурации модуля, так и в конфигурации устройства.
# Код модуля card
#card.moduleId=
# id услуг активации
#card.activate.serviceIds=
# Минимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;
# если 0, то ограничение не действует.
#card.login.min=0
# Максимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;
# если 0, то ограничение не действует.
#card.login.max=0

Также не забываем настроить задачу планировщика "Активация/деактивация сервисов по периоду" хотя бы раз в сутки в полночь, чтобы корректно обрабатывалось переоформление и перенос сервисов с договора на договор будущим числом.

Типы трафиков и привязки

Типы трафиков

Файл:traffic_types.png

Привязки

Radius - full

Простая привязка с 2 типами трафика: входящий/исходящий, которые берутся из стандартных счетчиков радиус-пакетов родительской сессии. Для входящего: вендор = -2, атрибут = 1 Для исходящего: вендор = -2, атрибут = 2

Radius - NAT

Привязка для VPN + NAT. Для родительской сессии будет 2 дочерних сессии ISG: IPVPN-NAT-INET и IPVPN-NAT-VPN-xxx, где xxx - скорость. Будем брать трафики из них. Для входящего: вендор = -2, атрибут = 1 Для исходящего: вендор = -2, атрибут = 2 Файл:radius-nat.png

Типы сервисов

VPN-IPoE

Файл:serv-type-ipoe.png

VPN-IPoE (+NAT)

Файл:serv-type-ipoe-nat.png

Опции

Заведём опции для соответствующих ISG-сервисов. Опции FLOWON/FLOWOFF нужны для включения/выключения netflow на интерфейсе Файл:options.png

Устройства и ресурсы

Группы устройств

Не используются.

Типы устройств

Мы используем 3 типа устройств:

  • Группа (ProcessGroup) - пустой тип устройств. Указывается в качестве рута для BGInetAccess и BGInetAccounting (см соответствующий раздел)
  • Город - пустой тип устройства, добавлен для разбиения дерева по городам. В будущем, возможно, к нему будут привязываться специфические ProcessHandler-ы, отдельные конфиги или выделяться свои Access и Accounting сервера для каждого города.
  • IPoE - тип устройства для цисок с поддержкой ip subscriber interface + ISG

Файл:devicetype-ipoe.png

Конфиг типа устройства IPoE:

# Realm default атрибуты
radius.realm.default.attributes=cisco-SSG-Account-Info=ADEFAULT;cisco-avpair=subscriber:accounting-list=BG-DSI-IPVRF
#коды ошибок, которые обрабатываются системой Reject-To-Accept (то же самое, что и realm.reject.error)
#http://bgbilling.ru/v5.2/doc/ch18s20.html
#-------reject-to-accept отсутствует
#radius.disable.accessCodes=4,10,11,12,44
# Какие адреса выдавать при ответе Access-Accept в состоянии disable: 
# 0 (по умолчанию) - из radius.disable.ipCategories, 1 - так же, как если бы не было ошибки (в том числе привязанные к сервису в договоре)
#radius.disable.mode=0
# код категории ресурсов Fake пула
#radius.disable.ipCategories=7
# радиус атрибуты, отправляемые в режиме Reject-To-Accept
#radius.disable.attributes=
# Id фиктивного сервиса, к которому будут привязываться сессии, по которым нормальный сервис не был найден (код ошибки: 1, логин не найден).
# Необходим, если в radius.disable.accessCodes присутствует код 1
#radius.disable.servId=
# Атрибуты, при наличии которых соединение должно считаться в состоянии DISABLE (т.е. с ограниченным доступом)
#radius.disable.pattern.attributes=
# Вендор атрибута, где хранится MAC-адрес
# Берём стандартный NAS-Port-Id
radius.macAddress.vendor=-1
# Код атрибута, где хранится MAC-адрес
# Берём NAS-Port-Id
radius.macAddress.type=87
# Префикс атрибута (если есть), где хранится MAC-адрес. Например, для cisco avpair 
#radius.macAddress.prefix=
#Порт для отправки PoD и CoA запросов (по умолчанию - порт, заданный в параметрах устройства Хост/порт)
radius.port=1700
#
# Режим поиска сервиса: 0 (по умолчанию) - по логину, 1 - по интерфейсу на устройстве (в предобработке должны быть
# проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или INTERFACE_ID), 2 - по VLAN на устройстве (в предобработке
# должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID), 4 - по VLAN на устройстве или
# дочернем устройстве (в предобработке должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID),
# 5 - по MAC-адресу на устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS), 6 - по MAC-адресу на
# устройстве или дочернем устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS).
radius.servSearchMode=1,0
#
# Нужно ли проверять пароль: 0 - нет, 1 (по умолчанию) - да.
radius.password.verification=0
#
# При выдаче access-accept добавлять запись в базу
# необходимо, если используется reject-to-accept и по старт пакету нельзя определить в каком состоянии соединение
#чтобы Access при Access-Accept добавлял соединение в базе со статусом WAIT и указанием выданного состояния и опций
connection.start.fromAccept=1
# Бывают ситуации, когда start-пакет не дошел до Accounting-сервера. В этом случае, при
# 1 (значение по умолчанию) - сессия создастся от текущего момента,
# 2 - Accounting проверит, что время сессии из update/stop пакета не больше, чем значение connection.close.timeout и создаст сессию от ее начала, иначе,
# если время сессии больше чем connection.close.timeout, сессия создастся от текущего момента,
# 0 - сессия без старт-пакета создана не будет.
connection.start.fromUpdate=1
# таймаут перевода соединения в статус suspended при остутствии радиус пакетов
connection.suspend.timeout=1200
# таймаут закрытия соединения при остутствии радиус пакетов (не складывается с connection.suspend.timeout)
connection.close.timeout=1260
#При завершении соединения по сигналу Stop-пакетом (RADIUS-Stop) оно фактически завершается через количество секунд, определяемое переменной connection.finish.timeout. Это позволяет, в частности, реализовать сбор "запоздалой" информации о трафике, которая может прийти после Stop-пакета.
connection.finish.timeout=2
# Проверка на повторную аутентификацию при Access-Request. Бывает нужна в случаях, когда NAS сбрасывает (теряет) сессию, но
# Stop-пакет не присылает и клиент пытается подключиться повторно, но у него стоит ограничение на максимум одну сессию. При совпадении
# callingStationId с одной из активных сессий и установленным параметром: 1 - осуществляется попытка закрытия старой сессии (connectionClose),
# 2 - попытка закрытия сессии (connectionClose) и завершение ее в базе, не дожидаясь стоп пакета, 3 - завершение в базе.
#radius.connection.checkDuplicate=0
#
# Нужно ли убирать домен перед поиском сервиса по логину из поля User-Name. По умолчанию - да (1).
# Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.
#для IPoE какой пришёл, такой и берём, там не должно быть лишнего
radius.username.removeDomain=0
#
# Нужно ли убирать пробелы из поля User-Name перед поиском логина. По умолчанию - нет (0).
# Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.
radius.username.removeWhitespace=0
#
# Шаблон вывода ошибки в мониторе с использованием атрибутов из RADIUS-пакета
#radius.accessError.infoPattern=LOGIN:$User-Name
#
# Параметры активации сервисов
# длина паузы, если возникла ошибка
#sa.error.pause=60
# количество заданий за раз
#sa.batch.size=20
# время (сек) ожидания завершения всех заданий (при асинхронной работе)
#sa.batch.wait=5
# пауза (сек) после обработки заданий
#sa.batch.pause=0
# время (сек) ожидания новой задачи перед вызовом disconnect.
#sa.batch.waitNext=5
#
#----------------------------------------
#параметры обработчика активации сервисов
#----------------------------------------
# откуда при отправке CoA брать атрибуты опций (по умолчанию - те же атрибуты, что выдаются при удачной авторизации)
#sa.radius.option.attributesPrefix=radius.inetOption.
#sa.radius.connection.attributes=NAS-Port, Acct-Session-Id, User-Name, Framed-IP-Address, NAS-IP-Address, NAS-Identifier
sa.radius.connection.attributes=Acct-Session-Id, User-Name
#режим отправки CoA. 0 - команды 0xc и 0xb в одном пакете для всех сервисов, 1 - команды 0xc и 0xb в отдельном пакете для каждого сервиса, 2 - атрибуты subscriber:command= в раздельных пакетах для каждого сервиса
sa.radius.connection.coa.mode=1
#Что делать для закрытия соединения:
# 0 (default) - ничего
# 2 - шлём PoD
# 3 - шлём subscriber:command=account-logoff
sa.radius.connection.close.mode=3
#если dhcp lease time большой, а при положительном балансе доступ нужно дать (даже если адрес сейчас выдан серый), нужно установить 1
sa.radius.connection.coa.onEnable=0
#атрибуты CoA запроса для прекращения доступа (используется при sa.radius.connection.withoutBreak=1)
#sa.radius.disable.attributes={@radius.disable.attributes}
#фиксированные атрибуты, добавляемые в запрос перед отправкой CoA
#sa.radius.coa.attributes=
#добавлять ли при отправке CoA атрибуты реалма (для default - из radius.realm.default.attributes)
#sa.radius.realm.addAttributes=0
#фиксированные атрибуты, добавляемые в запрос перед отправкой PoD
#sa.radius.pod.attributes=
#
#
###### VPN services ######
radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle
#Сопоставление nas-port-id из запроса с id порта в биллинге по имени интерфейса (см ru.dsi.bgbilling.modules.inet.dyn.device.cisco.ISGIPoEProtocolHandler)
radius.ipoe.nas_port_id.pattern.1.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{1,4}$)0{0,3}([1-9]\d{0,3})$
radius.ipoe.nas_port_id.pattern.1.replacement=$1/0/$2/$3
radius.ipoe.nas_port_id.pattern.2.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{8}$)0{0,3}([1-9]\d{0,3})(?=\d{4}$)0{0,3}([1-9]\d{0,3})$
radius.ipoe.nas_port_id.pattern.2.replacement=$1/0/$2/$4.$3
radius.ipoe.nas_port_id.pattern.3.pattern=^BD(\d+)$
radius.ipoe.nas_port_id.pattern.3.replacement=255/0/$1

Обратите внимание на строчку:

radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle

Здесь мы говорим, что все опции модуля, находящиеся в ветке с id=1 ("Группа: VPN"), следует трактовать как сервисы ISG с соответствующим названием.

Параметры radius.ipoe.nas_port_id.pattern.* нужны для самописного предобработчика радиус-пакетов ISGIPoEProtocolHandler. В них определяются regexp-шаблоны, по которым сопоставляется значение радиус-атрибута Nas-Port-Id (например, 0/0/0/1112) и название интерфейса в биллинге (Gi0/0.1112). Подробнее см. описание класса.

ServciceActivator

Для нашей схемы используется модифицированный ISGServciceActivator.

Отличия от стандартного Бителовского: - соответствие "опция - сервис ISG" берётся из optionRadiusAttributesMap, чтобы работали новые шаблоны атрибутов (radius.inetOption.1.template) - в соответствие "опция - сервис ISG" добавлена зависимость от realm-а - убрано всё, что касается DHCP

package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;
 
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;
import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;
import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator;
import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent;
import ru.bitel.bgbilling.modules.inet.api.common.bean.InetConnection;
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.modules.inet.dyn.device.radius.AbstractRadiusServiceActivator;
import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
 
import java.util.*;
 
/**
 * Конфигурация устройства:
 * sa.radius.connection.coa.mode = 1
 *      режим отправки CoA:
 *          0 - отправка в атрибуте cisco-SSG-Command-Code в одном пакете
 *          1 - (default) отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис
 *          2 - отправка в атрибуте cisco-avpair="subscriber:command=deactivate-service"
 *
 * sa.radius.service.disable =
 *      имена сервисов, при котором доступ отключен
 *      отправляются в режиме Reject-To-Accept
 *      по-умолчанию не указано
 *
 * sa.radius.connection.close.mode = 2
 *      что делать для закрытия соединения:
 *          1 - ничего не делать
 *          2 - (default) посылаем PoD
 *          3 - посылаем subscriber:command=account-logoff
 *
 * sa.radius.connection.close.disableServices = 0
 *      отключать ли сервисы ISG при закрытии
 *          0 - (default) не отключать
 *          1 - отключать (посылаем CoA на отключение всех сервисов перед тем как закрыть соединение по sa.radius.connection.close.mode)
 */
public class ISGServiceActivator
        extends AbstractRadiusServiceActivator
        implements ServiceActivator
{
    private static final Logger logger = Logger.getLogger( ISGServiceActivator.class );
 
    /**
     * per-realm:
     *      код опции -> набор сервисов ISG
     */
    protected Map<String,Map<Integer, Set<String>>> optionISGServiceMap = new HashMap<String,Map<Integer, Set<String>>>();
 
    /**
     * имя(имена) сервиса, при котором доступ отключен.
     */
    protected Set<String> disableServiceNames;
 
    /**
     * Отправка в атрибуте cisco-SSG-Command-Code в одном пакете
     */
    protected static final int COA_MODE_SSG_COMMAND_PACKET = 0;
 
    /**
     * Отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис
     */
    protected static final int COA_MODE_SSG_COMMAND = 1;
 
    /**
     * Отправка в атрибуте cisco-avpair="subscriber:command=deactivate-service"
     */
    protected static final int COA_MODE_SUBSCR_COMMAND = 2;
 
    /**
     * Режим отправки команд
     */
    protected int coaMode;
 
    @Deprecated
    protected static final int CLOSE_MODE_POD_DEPRECATED = 0;
 
    protected static final int CLOSE_MODE_NONE = 1;
    protected static final int CLOSE_MODE_POD = 2;
    protected static final int CLOSE_MODE_SUBSCR_COMMAND = 3;
 
    protected int closeMode;
    protected boolean disableServicesOnClose;
 
    public ISGServiceActivator()
    {
        super( null, false, "Acct-Session-Id", false );
    }
 
    /**
     * {@inheritDoc}
     */
    @Override
    public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap deviceConfig )
            throws Exception
    {
        super.init( setup, moduleId, device, deviceType, deviceConfig );
 
        this.coaMode = deviceConfig.getInt( "sa.radius.connection.coa.mode", deviceConfig.getInt( "radius.coa.mode", deviceConfig.getInt( "coa.mode", COA_MODE_SSG_COMMAND ) ) );
 
        //вендор атрибута cisco-SSG-Account-Info (9)
        int ciscoSSGAccountInfo_attribute_vendor=9;
        //id атрибута cisco-SSG-Account-Info (250)
        int ciscoSSGAccountInfo_attribute_id=250;
 
        Map<Integer, Set<String>> map;
        Set<String> set;
        List<RadiusAttribute<?>> raList;
        InetOptionRuntimeMap inetOptionRuntimeMap = InetOptionRuntimeMap.getInstance(moduleId);
        // определение сервисов на каждой из опций
        for(Map.Entry<String, Map<Integer, RadiusAttributeSet>> e_realm : this.optionRadiusAttributesMap.getRealmMap().entrySet()){
 
            map = this.optionISGServiceMap.get(e_realm.getKey());
            if(null==map){
                map = new HashMap<Integer, Set<String>>();
                this.optionISGServiceMap.put(e_realm.getKey(), map);
            }
            //Перебираем опции в realm-е
            for(Map.Entry<Integer, RadiusAttributeSet> e_option : e_realm.getValue().entrySet()){
                logger.info("option = "+inetOptionRuntimeMap.get(e_option.getKey()).title+"("+e_option.getKey()+"), realm = "+e_realm.getKey()+", ra = "+e_option.getValue());
                set = null;
                raList = e_option.getValue().getAttributes(ciscoSSGAccountInfo_attribute_vendor, ciscoSSGAccountInfo_attribute_id);
                if(raList!=null){
                    for(RadiusAttribute<?> attr : raList){
                        if(null==set){
                            set = new HashSet<String>();
                        }
                        //вырезаем из атрибута cisco-SSG-Account-Info=ASERVICENAME имя сервиса SERVICENAME
                        set.add(attr.getValue().toString().substring(1));
                    }
                    if(set!=null && set.size()>0){
                        map.put(e_option.getKey(), set);
                    }
                }
            }
        }
 
        // сервис(ы), отправляемый в режиме Reject-To-Accept
        List<String> disableServiceNames = Utils.toList( deviceConfig.get( "sa.radius.service.disable", deviceConfig.get( "radius.serviceName.disable", "" ) ) );// INET_FAKE
        if( disableServiceNames.size() > 0 )
        {
            this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap<String, Boolean>() );
            this.disableServiceNames.addAll( disableServiceNames );
        }
        else
        {
            this.disableServiceNames = null;
        }
 
        logger.info( "Disable services: " + disableServiceNames );
 
        this.closeMode = deviceConfig.getInt( "sa.radius.connection.close.mode", CLOSE_MODE_POD );
        this.disableServicesOnClose = deviceConfig.getInt( "sa.radius.connection.close.disableServices", 0 ) > 0;
 
        return null;
    }
 
    /**
     *
     * {@inheritDoc}
     */
    @Override
    public Object connectionModify( ServiceActivatorEvent e )//TODO добавить timeout, чтобы не отправлять слишком быстро. Дожидаться ответов например.
            throws Exception
    {
        logger.info( "Connection modify: oldState: " + e.getOldState() + "; newState: " + e.getNewState() + "; oldOptionSet: " + e.getOldOptions() + "; newOptionSet: " + e.getNewOptions() );
 
        final InetConnection connection = e.getConnection();
 
        if( e.getNewState() == InetServ.STATE_DISABLE )
        {
            if( !withoutBreak )
            {
                return connectionClose( e );
            }
 
            // устанавливаем флаг, что нужно будет поменять состояние соединения в базе
            if( needConnectionStateModify )
            {
                e.setConnectionStateModified( true );
            }
 
            return sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );
        }
 
        if( e.getOldState() == InetServ.STATE_DISABLE )
        {
            if( !withoutBreak )
            {
                return connectionClose( e );
            }
 
            // устанавливаем флаг, что нужно будет поменять состояние соединения в базе
            if( needConnectionStateModify )
            {
                e.setConnectionStateModified( true );
            }
 
            // отключаем disable сервис и включаем активные опции
            return sendCommands( connection, disableServiceNames, optionsToServiceNames(e.getRealm(), e.getNewOptions()) );
        }
 
        Collection<Integer> removeOptions = e.getOptionsToRemove();
        Collection<Integer> addOptions = e.getOptionsToAdd();
 
        return sendCommands( connection, optionsToServiceNames(e.getRealm(), removeOptions), optionsToServiceNames(e.getRealm(), addOptions ) );
    }
 
    /**
     * {@inheritDoc}
     */
    @Override
    public Object connectionClose( ServiceActivatorEvent e )
            throws Exception
    {
        logger.info( "Connection close" );
 
        Object result;
 
        final InetConnection connection = e.getConnection();
 
        if( disableServicesOnClose )
        {
            result = sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );
        }
        else
        {
            result = null;
        }
 
        switch( closeMode )
        {
            default:
            case CLOSE_MODE_NONE:
            {
                break;
            }
 
            case CLOSE_MODE_POD_DEPRECATED:
            case CLOSE_MODE_POD:
            {
                RadiusPacket request = radiusClient.createDisconnectRequest();
                prepareRequest( request, connection );
 
                logger.info( "Send PoD: \n" + request );
                result = radiusClient.sendAsync( request );
 
                break;
            }
 
            case CLOSE_MODE_SUBSCR_COMMAND:
            {
                logger.info( "Connection close (logoff)" );
 
                RadiusPacket packet = radiusClient.createModifyRequest();
                prepareRequest( packet, connection );
 
                packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=account-logoff" ) );
 
                logger.info( "Send logoff CoA:\n" + packet );
                result = radiusClient.sendAsync( packet );
 
                break;
            }
        }
 
        return result;
    }
 
    protected Collection<String> optionsToServiceNames(String realm, final Collection<Integer> options)//, final Collection<String> serviceNames )
    {
        if( options == null || options.size() == 0 )
        {
            return null;
        }
 
        if(null==realm || "".equals(realm)){
            realm = "default";
        }
 
        final Set<String> result = Collections.newSetFromMap( new LinkedHashMap<String, Boolean>( options.size() + 2 ) );
 
        for( Integer option : options )
        {
            Set<String> serviceNames = this.optionISGServiceMap.get(realm).get(option);
            if( serviceNames == null ){
                serviceNames = this.optionISGServiceMap.get("default").get(option);
            }
            if( serviceNames != null )
            {
                result.addAll( serviceNames );
            }
        }
 
        return result;
    }
 
    /**
     * Отправка команд на деактивацию и активацию сервисов
     * @param connection - InetConnection
     * @param serviceNamesDeactivate - список сервисов, которые нужно деактивировать
     * @param serviceNamesActivate - список сервисов, которые нужно активировать
     * @return
     * @throws Exception
     */
    protected Object sendCommands( final InetConnection connection, final Collection<String> serviceNamesDeactivate, final Collection<String> serviceNamesActivate )
            throws Exception
    {
        Object result = null;
 
        if(logger.isInfoEnabled()){
            logger.info("Sending commands to deactivate services (mode="+this.coaMode+"): ["+Utils.toString(serviceNamesDeactivate)+"]");
            logger.info("Sending commands to activate services (mode="+this.coaMode+"): ["+Utils.toString(serviceNamesActivate)+"]");
        }
 
        switch( coaMode )
        {
            case COA_MODE_SSG_COMMAND_PACKET:
            {
                if( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
                {
                    RadiusPacket packet = radiusClient.createModifyRequest();
                    prepareRequest( packet, connection );
 
                    for( String serviceName : serviceNamesDeactivate )
                    {
                        String value = "\\0xc" + serviceName;
                        // добавление cisco-SSG-Command-Code
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
                    }
 
                    result = radiusClient.sendAsync( packet );
                    //logger.info( "Send deactivate services CoA:\n" + packet );
                }
 
                if( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
                {
                    RadiusPacket packet = radiusClient.createModifyRequest();
                    prepareRequest( packet, connection );
 
                    for( String serviceName : serviceNamesActivate )
                    {
                        String value = "\\0xb" + serviceName;
                        // добавление cisco-SSG-Command-Code
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
                    }
 
                    result = radiusClient.sendAsync( packet );
                    //logger.info( "Send activate services CoA:\n" + packet );
                }
 
                break;
            }
 
            case COA_MODE_SSG_COMMAND:
            {
                if( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
                {
                    for( String serviceName : serviceNamesDeactivate )
                    {
                        RadiusPacket packet = radiusClient.createModifyRequest();
                        prepareRequest( packet, connection );
 
                        String value = "\\0xc" + serviceName;
                        // добавление cisco-SSG-Command-Code
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
 
                        //logger.info( "Send deactivate service CoA:\n" + packet );
                        result = radiusClient.sendAsync( packet );
                    }
                }
 
                if( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
                {
                    for( String serviceName : serviceNamesActivate )
                    {
                        RadiusPacket packet = radiusClient.createModifyRequest();
                        prepareRequest( packet, connection );
 
                        String value = "\\0xb" + serviceName;
                        // добавление cisco-SSG-Command-Code
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );
 
                        //logger.info( "Send activate service CoA:\n" + packet );
                        result = radiusClient.sendAsync( packet );
                    }
                }
 
                break;
            }
 
            case COA_MODE_SUBSCR_COMMAND:
            default:
            {
                if( serviceNamesDeactivate != null && serviceNamesDeactivate.size() > 0 )
                {
                    for( String serviceName : serviceNamesDeactivate )
                    {
                        RadiusPacket packet = radiusClient.createModifyRequest();
                        prepareRequest( packet, connection );
 
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:service-name=" + serviceName ) );
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=deactivate-service" ) );
 
                        //logger.info( "Send deactivate service CoA:\n" + packet );
                        result = radiusClient.sendAsync( packet );
                    }
                }
 
                if( serviceNamesActivate != null && serviceNamesActivate.size() > 0 )
                {
                    for( String serviceName : serviceNamesActivate )
                    {
                        RadiusPacket packet = radiusClient.createModifyRequest();
                        prepareRequest( packet, connection );
 
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:service-name=" + serviceName ) );
                        packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, "subscriber:command=activate-service" ) );
 
                        //logger.info( "Send activate service CoA:\n" + packet );
                        result = radiusClient.sendAsync( packet );
                    }
                }
 
                break;
            }
        }
 
        return result;
    }
}

ProtocolHandler

Используется собственный ProtocolHandler, необходимый для поиска сервисов Inet по порту на основе Nas-Port-Id. Задача в том, чтобы по данным из радиус-пакета авторизации найти и авторизовать сервис в биллинге. В пакете приходят:

User-Name=nas-port:XXX.XXX.XXX.XXX:0/0/0/1112
NAS-Port-Id=0/0/0/1112

где XXX.XXX.XXX.XXX - ip-адрес NAS-а 0/0/0/1112 - слот/карта/интерфейс/инкапсуляция Было решено использовать NAS-Port-Id

Схема следующая:

  • При подключении клиента конфигурируем имя интерфейса в соответствии с инкапсуляцией: для 'encapsulation dot1Q XXX' интерфейс будет 'interface GigabitEthernet0/0.XXX'
  • Заводим интерфейс на устройстве в биллинге: имя=Gi0/0.XXX. При необходимости проставляем нужный ifIndex в поле 'интерфейс', если хотим собирать netflow
  • При загрузке (или перечитывании конфигурации) наш ISGIPoEProtocolHandler парсит параметры конфигурации radius.ipoe.nas_port_id.pattern.*, по которым создаёт в памяти соответствие 'NAS-Port-Id'->'ifaceId' для каждого заведённого в биллинге интерфейса устройства. Например, '0/0/0/1112'->'id интерфейса с именем Gi0/0.1112 в биллинге'
  • При авторизации ISGIPoEProtocolHandler ищет интерфейс по NAS-Port-Id из пакета и проставляет опцию INTERFACE_ID, по которой стандартный радиус-процессор будет затем искать сервис Inet.
  • При изменении интерфейсов устройства в биллинге кэш 'NAS-Port-Id'->'ifaceId' в ISGIPoEProtocolHandler-е автоматически обновляется

Примечание: Если используется QinQ, то NAS-Port-Id будет вида 0/0/0/105.2015 для: 'encapsulation dot1Q 2015 second-dot1q 105', а имя интерфейса: 'Gi0/0.20150105'. Последнее настраивается в radius.ipoe.nas_port_id.pattern.*

package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;
 
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.event.EventListener;
import ru.bitel.bgbilling.kernel.event.EventListenerContext;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;
import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;
import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;
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.radius.InetRadiusProcessor;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.sql.ConnectionSet;
import ru.bitel.common.worker.ThreadContext;
import ru.bitel.oss.systems.inventory.resource.common.DeviceInterfaceService;
import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;
import ru.bitel.oss.systems.inventory.resource.common.event.DeviceInterfaceModifiedEvent;
 
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * @author cromeshnic@gmail.com
 * ProtocolHandler для работы с cisco ip subscriber interface + cisco ISG
 * В предобработке устанавливается опция пакета InetRadiusProcessor.INTERFACE_ID, где указывается номер интерфейса в биллинге,
 * соответствующий атрибуту Nas-Port-Id из пакета
 * Соответствие определяется по шаблонам, заданным в конфигурации, и имени интерфейса сервиса в биллинге
 * Предполагается, что клиент авторизуется именно на том устройстве, на котором указан этот ProtocolHandler (не на дочерних)
 *
 * Параметры конфигурации:
 * radius.ipoe.nas_port_id.pattern.[i].pattern - регэксп-шаблон для имени интерфейса в биллинге
 * radius.ipoe.nas_port_id.pattern.[i].replacement - выражение для построения nas_port_id по шаблону
 *
 * Пример:
 * Gi0/0.1112 -> 0/0/0/1112
 * Gi0/0.20150105 -> 0/0/0/105.2015
 *
 */
public class ISGIPoEProtocolHandler extends ISGProtocolHandler implements RadiusProtocolHandler {
 
    private static final Logger logger = Logger.getLogger( ISGIPoEProtocolHandler.class );
 
    /**
     * Кэш интерфейсов устройства
     */
    private volatile DeviceNasPortMap ifaceMap;
 
    @Override
    public void init(Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig) throws Exception {
        super.init(setup, moduleId, inetDevice, inetDeviceType, deviceConfig);
        this.ifaceMap = new DeviceNasPortMap(moduleId, inetDevice.getId(), deviceConfig.subIndexed("radius.ipoe.nas_port_id.pattern."));
    }
 
    @Override
    public void preprocessAccountingRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {
        super.preprocessAccountingRequest(request, response, connectionSet);
        //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета
        setBGIfaceId( request);
    }
 
    /**
     * {@inheritDoc}
     */
    @Override
    public void preprocessAccessRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {
        super.preprocessAccessRequest(request, response, connectionSet);
        //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета
        setBGIfaceId( request);
    }
 
    /**
     * по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета InetRadiusProcessor.INTERFACE_ID
     * @param request    радиус-пакет
     */
    private void setBGIfaceId(RadiusPacket request) {
        String nas_port_id = request.getStringAttribute(-1, RadiusDictionary.NAS_Port_Id, null);
        Integer port=-1;
        if(nas_port_id!=null){
            port = this.ifaceMap.getIfacePort(nas_port_id);
        }
        if(null==port)
        {
            port=-1;//Насчёт port=0 и port=-1 - см http://forum.bgbilling.ru/viewtopic.php?f=44&t=7694&p=64541#p64541
        }
        request.setOption(InetRadiusProcessor.INTERFACE_ID, port);
    }
 
    /**
     * Кэш соответствий Nas-Port-Id -> id интерфейса в биллинге для устройства
     * Обновляется при изменении порта или перезагрузке конфигурации
     */
    private class DeviceNasPortMap implements EventListener<DeviceInterfaceModifiedEvent> {
        /**
         * Соответствие cisco Nas-Port-Id -> id интерфейса в биллинге
         */
        private volatile Map<String, Integer> nasPortIdToBGPortIdMap;
        private final int moduleId;
        private final int deviceId;
        /**
         * Список шаблонов-регулярных выражений, по которым будем получать Nas-Port-Id по названию интерфейса
         * Список паттернов не обновляется, т.к. берётся из конфига.
         * При перезагрузке конфига в любом случае ISGIPoEProtocolHandler будет переинициализирован целиком
         */
        private final SortedMap<Integer, ParameterMap> patternMap;
 
        public DeviceNasPortMap(int moduleId, int deviceId, SortedMap<Integer, ParameterMap> patternMap) throws BGException {
            this.moduleId = moduleId;
            this.deviceId = deviceId;
            this.patternMap = patternMap;
            EventProcessor.getInstance().addListener(this, DeviceInterfaceModifiedEvent.class);
            this.load();
        }
 
        private synchronized void load(){
            this.nasPortIdToBGPortIdMap = new HashMap<String, Integer>();
            //Перебираем порты устройств
            logger.info("(Re)loading DeviceNasPortMap for device "+this.deviceId);
            ServerContext ctx = (ServerContext) ThreadContext.get();
            try {
                DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, moduleId);
                List<DeviceInterface> deviceIfaceList = devicePortService.devicePortList(this.deviceId);
                String nasPortId;
                if(deviceIfaceList!=null){
                    for(DeviceInterface iface : deviceIfaceList){
                        nasPortId = nasPortIdByIfaceTitle(iface.getTitle());
                        if(null!=nasPortId){
                            nasPortIdToBGPortIdMap.put(nasPortId, iface.getPort());
                            logger.debug("[device id=" + this.deviceId + "]: nas-port-id='" + nasPortId + "' -> " + iface.getPort());
                        }
                    }
                }
            } catch (BGException e) {
                logger.error("Error (re)loading DeviceNasPortMap", e);
            }
        }
 
        /**
         * Возвращает Nas-Port-Id по имени интерфейса на основе регекспов из patternMap
         * @param ifaceTitle имя инерфейса (ex Gi0/0.123)
         * @return Nas-Port-Id (ex 0/0/0/123)
         */
        protected String nasPortIdByIfaceTitle(String ifaceTitle){
            if(null==ifaceTitle){
                return null;
            }
            Pattern p;
            Matcher m;
            String pattern;
            String replacement;
            String nasPortId;
            for(Map.Entry<Integer, ParameterMap> patternMapEntry : patternMap.entrySet()){
                pattern = patternMapEntry.getValue().get("pattern", null);
                replacement = patternMapEntry.getValue().get("replacement", null);
                if(pattern!=null && replacement!=null){
                    p = Pattern.compile(pattern);
                    m = p.matcher(ifaceTitle);
                    if (m.find()) {
                        //Получаем логин путём подстановки найденных capturing groups в $1, $2 и т.д. шаблона
                        nasPortId = m.replaceFirst(replacement);
                        return nasPortId;
                    }
                }
            }
            return null;
        }
 
        /**
         * Получаем id порта в биллинге по nas_port_id из кэша
         */
        public Integer getIfacePort(String nas_port_id) {
            return this.nasPortIdToBGPortIdMap.get(nas_port_id);
        }
 
        /**
         * Обновляем кэш при изменении интерфейса
         * @throws BGException
         */
        @Override
        public void notify(DeviceInterfaceModifiedEvent event, EventListenerContext eventListenerContext) throws BGException {
            DeviceInterface deviceIface = event.getNewItem();
            if(deviceIface==null){
                deviceIface = event.getOldItem();
            }
            if(deviceIface!=null){
                if(deviceIface.getDeviceId()==this.deviceId){
                    this.load();
                }
            }
        }
    }
}
package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;
 
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.kernel.network.dhcp.DhcpProtocolHandler;
import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;
import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;
import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;
import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;
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.modules.inet.dyn.device.radius.AbstractRadiusProtocolHandler;
import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.common.sql.ConnectionSet;
 
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
 
/**
 * Базовый класс для Cisco ISG
 * Копипаста бителовского, без лишней потехи с option 82
 */
public class ISGProtocolHandler
    extends AbstractRadiusProtocolHandler
    implements RadiusProtocolHandler, DhcpProtocolHandler
{
	private static final Logger logger = Logger.getLogger( ISGProtocolHandler.class );
 
	/**
	 * Код атрибута - id родительского аккаунтинга
	 */
	protected int parentAcctSessionIdType;
 
	/**
	 * Префикс id родительского аккаунтинга
	 */
	protected String parentAcctSessionIdPrefix;
 
	/**
	 * Код атрибута - имя сервиса (для cisco-avpair)
	 */
	protected int serviceNameType;
 
	/**
	 * Префикс имени сервиса (для cisco-avpair)
	 */
	protected String serviceNamePrefix;
 
	/**
	 * Имя сервиса, при котором доступ отключен.
	 */
	protected Set<String> disableServiceNames;
 
	public ISGProtocolHandler()
	{
		super( 9 ); // Cisco
	}
 
	@Override
	public void init( Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig )
	    throws Exception
	{
		super.init( setup, moduleId, inetDevice, inetDeviceType, deviceConfig );
 
		parentAcctSessionIdType = deviceConfig.getInt( "radius.parentAcctSessionId.type", 1 ); // cisco-avpair
		parentAcctSessionIdPrefix = deviceConfig.get( "radius.parentAcctSessionId.prefix", "parent-session-id=" );
		serviceNameType = deviceConfig.getInt( "radius.serviceName.type", 251 ); // cisco-SSG-Service-Info
		serviceNamePrefix = deviceConfig.get( "radius.serviceName.prefix", "" );
 
		List<String> disableServiceNames = Utils.toList( deviceConfig.get( "radius.serviceName.disable", "" ) );// INET_FAKE
		if( disableServiceNames.size() > 0 )
		{
			this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap<String, Boolean>() );
			this.disableServiceNames.addAll( disableServiceNames );
		}
		else
		{
			this.disableServiceNames = null;
		}
	}
 
	@Override
	public void preprocessAccountingRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )
	    throws Exception
	{
		int acctStatusType = request.getIntAttribute( -1, RadiusDictionary.Acct_Status_Type, -1 );
        super.preprocessAccountingRequest(request, response, connectionSet);
 
        // извлекаем parentAcctSessionId
        String parentAcctSessionId;
        // если parentAcctSessionId находится в cisco-avpair - то нужно искать по префиксу
        if( parentAcctSessionIdType == 1 )
        {
            parentAcctSessionId = null;
            // смотрим во всех cisco-avpair атрибутах
            List<RadiusAttribute<?>> attributes = request.getAttributes( radiusVendor, parentAcctSessionIdType );
            if( attributes != null )
            {
                for( RadiusAttribute<?> attr : attributes )
                {
                    @SuppressWarnings("unchecked")
                    String value = ((RadiusAttribute<String>)attr).getValue();
                    if( value.startsWith( parentAcctSessionIdPrefix ) )
                    {
                        parentAcctSessionId = value.substring( parentAcctSessionIdPrefix.length() );
                        break;
                    }
                }
            }
        }
        else
        {
            parentAcctSessionId = request.getStringAttribute( radiusVendor, parentAcctSessionIdType, null );
        }
 
        // если это аккаунтинг сервисной сессии
        if( parentAcctSessionId != null )
        {
            // извлекаем serviceName
            String serviceName;
            // если serviceName находится в cisco-avpair - то нужно искать по префиксу
            if( serviceNameType == 1 )
            {
                serviceName = null;
                // смотрим во всех cisco-avpair атрибутах
                final List<RadiusAttribute<?>> ras = request.getAttributes( radiusVendor, serviceNameType );
                if( ras != null )
                {
                    for( RadiusAttribute<?> ra : ras )
                    {
                        @SuppressWarnings("unchecked")
                        final String value = ((RadiusAttribute<String>)ra).getValue();
                        if( value.startsWith( serviceNamePrefix ) )
                        {
                            serviceName = value.substring( serviceNamePrefix.length() );
                            break;
                        }
                    }
                }
            }
            else
            {
                serviceName = request.getStringAttribute( radiusVendor, serviceNameType, null );
            }
 
            if( serviceName == null || !serviceName.startsWith( "N" ) )
            {
                logger.error( "Parent acctSessionId found, but ServiceName is not" );
            }
            else
            {
                serviceName = serviceName.substring( 1 );
            }
 
            // устанавливаем id родительской сессии
            request.setOption( InetRadiusProcessor.PARENT_ACCT_SESSION_ID, parentAcctSessionId );
            // устанавливаем имя сервиса текущего аккаунтинга
            request.setOption( InetRadiusProcessor.SERVICE_NAME, serviceName );
 
            // если указан сервис, при котором доступ ограничен - проверяем, не его ли это аккаунтинг,
            // и, если это так, переключаем состояние соединения
            if( disableServiceNames != null && disableServiceNames.contains( serviceName ) )
            {
                // start или update
                if( acctStatusType == 1 || acctStatusType == 3 )
                {
                    logger.debug( "State is disable (from start disable service)" );
                    request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_DISABLE );
                }
                else
                {
                    logger.debug( "State is enable (from stop disable service)" );
                    request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_ENABLE );
                }
            }
        }
	}
 
}

Устройства

Дерево устройств:

Файл:devices.png

Интерфейсы:

Файл:device-ifaces.png

IP-ресурсы

Не используются.

VLAN-ресурсы

Не используются.

Настройка BGInetAccess и BGInetAccounting

BGInetAccess-VPN

access.sh:

#!/bin/sh
 
cd ${0%${0##*/}}.
 
. ./setenv.sh
 
APP_HOME=.
CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar
COMMON_PARAMS="-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed"
LOG_PARAMS="-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml"
NAME=inet-access
NAME_SHORT=access
ADMIN_PORT=3851
MEMORY=-Xmx512m
 
if [ "$1" = "start" ]; then
        nohup  ${JAVA_HOME}|>/bin/java ${COMMON_PARAMS}|> ${LOG_PARAMS}|> ${MEMORY}|> -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH}|> ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME}|> > ./log/${NAME_SHORT}|>.out 2>&1 & echo $! > .run/${NAME_SHORT}|>.pid &
else
        if [ "$1" = "debug" ]; then
                #starting in debug mode
            nohup  ${JAVA_HOME}|>/bin/java ${COMMON_PARAMS}|> ${MEMORY}|> -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH}|> -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME}|> > ./log/${NAME_SHORT}|>.out 2>&1 & echo $! > .run/${NAME_SHORT}|>.pid
        else
                #execute command
                ${JAVA_HOME}|>/bin/java ${COMMON_PARAMS}|> -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH}|> ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME}|> $1 $2 $3 $4 $5 $6
        fi
fi

inet-access.xml:

<?xml version="1.0" encoding="UTF-8"?>
<application context="access">
        <!-- Уникальное имя приложения -->
        <param name="app.name" value="BGInetAccess-VPN"/>
        <!-- Уникальный числовой id приложения -->
        <param name="app.id" value="4"/>
 
        <!-- Параметры подключения к БД -->
        <param name="db.driver" value="com.mysql.jdbc.Driver"/>
        <param name="db.url" value="jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;characterEncoding=Cp1251&amp;allowUrlInLocalInfile=true&amp;zeroDateTimeBehavior=convertToNull&amp;jdbcCompliantTruncation=false&amp;queryTimeoutKillsConnection=true&amp;connectTimeout=1000"/>
        <param name="db.user" value="bill"/>
        <param name="db.pswd" value="bgbilling"/>
        <param name="db.maxIdle" value="10"/>
        <param name="db.validationTimeout" value="10"/>
 
        <!-- Параметры подключения к MQ -->
        <param name="mq.url" value="failover:(tcp://localhost:61616)"/>
        <param name="mq.user" value="bill"/>
        <param name="mq.pswd" value="bgbilling"/>
 
        <!-- id модуля -->
        <param name="moduleId" value="30"/>
        <!-- id корневого устройства -->
        <param name="rootDeviceId" value="1"/>
        <!-- Типы фейковых устройств, являющихся аккаунтинг серверами -->
        <param name="accounting.deviceTypeIds" value="3"/>
        <!-- Внутренняя переменная приложения, не изменять -->
        <param name="commonIdentifierName" value="rootDeviceId"/>
 
        <!-- Параметры сохранения логов данных -->
        <!-- Директория, в которую сохранять radius логи -->
        <param name="datalog.radius.dir" value="data/radius" />
        <!-- Размер блока данных в файле лога, также размер буфера на лог файл -->
        <param name="datalog.radius.chunk.size" value="262144" />
        <!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib -->
        <param name="datalog.radius.compression.type" value="1" />
        <!-- Директория, в которую сохранять flow логи -->
        <param name="datalog.dhcp.dir" value="data/dhcp" />
        <!-- Размер блока данных в файле лога, также размер буфера на лог файл -->
        <param name="datalog.dhcp.chunk.size" value="131072" />
        <!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib -->
        <param name="datalog.dhcp.compression.type" value="1" />
 
 
        <!-- Создание Access -->
        <bean name="access" class="ru.bitel.bgbilling.modules.inet.access.Access" />
 
        <context name="radius">
                <!-- Cоздание процессора radius-пакетов -->
                <bean name="radiusProcessor" class="ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor"/>
 
                <!-- Служебный ScheduledExecutorService, необходимый для dataLogger -->
                <scheduledExecutorService name="hrlydtlggr" corePoolSize="1" />
 
                <!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) -->
                <bean name="radiusDataLogger" class="ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger">
                        <param name="scheduledExecutor">hrlydtlggr</param>
                </bean>
 
                <!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger -->
                <bean name="radiusListener" class="ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener">
                        <constructor>
                                <!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех -->
                                <param name="host" value=""/>
                                <!-- Порт, на котором будет открыт сокет -->
                                <param name="port" value="3812"/>
                                <!-- Размер буфера приема слушателя -->
                                <param name="recvBufferSize">512 * 1024</param>
                                <!-- Рекомендуемый SO_RCVBUF сокета -->
                                <param name="soRCVBUF"></param>
                                <!-- Количество потоков-обработчиков -->
                                <param name="threadCount">10</param>
                                <!-- Максимальное количество пакетов в очереди на обработку -->
                                <param name="maxQueueSize">200</param>
                                <!-- Передача процессора -->
                                <param name="processor">radiusProcessor</param>
                                <!-- Режим работы, RadiusListener.Mode.authentication -->
                                <param name="mode">RadiusListener.Mode.authentication</param>
                                <!-- Передача dataLogger -->
                                <param name="dataLogger">radiusDataLogger</param>
                        </constructor>
                </bean>
        </context>
</application>

BGInetAccess-ISG

TODO: описать настройку справочника сервисов ISG.

У нас для этих целей сейчас используется модуль Dialup - так исторически сложилось. Но по-хорошему, нужно настроить отдельный BGInetAccess, повешать его на отдельную ProcessGroup и авторизовать сервисы из него со специального служебного договора. Пример есть в статье ISG,_схема_со_стартом_сессии_и_ее_авторизацией_по_IP,_выдача_адресов_на_основе_option82_(Конфигурация_BGBilling'а) (см. 'ASR ISG Service')

BGInetAccounting-VPN

accounting.sh:

#!/bin/sh
 
cd ${0%${0##*/}}.
 
. ./setenv.sh
 
APP_HOME=.
CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar
COMMON_PARAMS="-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccounting-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed"
LOG_PARAMS="-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-accounting.xml"
NAME=inet-accounting
NAME_SHORT=accounting
ADMIN_PORT=3852
MEMORY=-Xmx1024m
 
if [ "$1" = "start" ]; then
        nohup  ${JAVA_HOME}|>/bin/java ${COMMON_PARAMS}|> ${LOG_PARAMS}|> ${MEMORY}|> -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH}|> ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME}|> > ./log/${NAME_SHORT}|>.out 2>&1 & echo $! > .run/${NAME_SHORT}|>.pid &
else
        if [ "$1" = "debug" ]; then
                #starting in debug mode
            nohup  ${JAVA_HOME}|>/bin/java ${COMMON_PARAMS}|> ${MEMORY}|> -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH}|> -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME}|> > ./log/${NAME_SHORT}|>.out 2>&1 & echo $! > .run/${NAME_SHORT}|>.pid
        else
                #execute command
                ${JAVA_HOME}|>/bin/java ${COMMON_PARAMS}|> -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH}|> ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME}|> "$@"
        fi
fi

inet-accounting.xml:

<?xml version="1.0" encoding="UTF-8"?>
<application context="accounting">
        <!-- Уникальное имя приложения -->
        <param name="app.name" value="BGInetAccounting-VPN"/>
        <!-- Уникальный числовой id приложения -->
        <param name="app.id" value="5"/>
 
        <!-- Параметры подключения к БД -->
        <param name="db.driver" value="com.mysql.jdbc.Driver"/>
        <param name="db.url" value="jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;characterEncoding=Cp1251&amp;allowUrlInLocalInfile=true&amp;zeroDateTimeBehavior=convertToNull&amp;jdbcCompliantTruncation=false&amp;queryTimeoutKillsConnection=true&amp;connectTimeout=1000"/>
        <param name="db.user" value="bill"/>
        <param name="db.pswd" value="bgbilling"/>
        <param name="db.maxIdle" value="10"/>
        <param name="db.validationTimeout" value="10"/>
 
        <!-- Параметры подключения к MQ -->
        <param name="mq.url" value="failover:(tcp://localhost:61616)"/>
        <param name="mq.user" value="bill"/>
        <param name="mq.pswd" value="bgbilling"/>
 
        <!-- id модуля -->
        <param name="moduleId" value="30"/>
        <!-- id корневого устройства -->
        <param name="rootDeviceId" value="1"/>
 
        <!-- Внутренняя переменная приложения, не изменять -->
        <param name="commonIdentifierName" value="rootDeviceId"/>
 
        <!-- Параметры сохранения radius-пакетов в файлы логов -->
        <!-- Директория, в которую сохранять radius логи -->
        <param name="datalog.radius.dir" value="data/radius" />
        <!-- Размер блока данных в файле лога, также размер буфера на лог файл -->
        <param name="datalog.radius.chunk.size" value="524288" />
        <!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib -->
        <param name="datalog.radius.compression.type" value="1" />
        <!-- Параметры сохранения flow-пакетов в файлы логов -->
        <!-- Директория, в которую сохранять flow логи -->
        <param name="datalog.flow.dir" value="data/flow" />
        <!-- Размер блока данных в файле лога, также размер буфера на лог файл и поток слушателя -->
        <param name="datalog.flow.chunk.size" value="524288" />
        <!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib -->
        <param name="datalog.flow.compression.type" value="1" />
 
        <!-- Создание Accounting -->
        <bean name="accounting" class="ru.bitel.bgbilling.modules.inet.accounting.Accounting"/>
 
        <context name="radius">
                <!-- Cоздание процессора radius-пакетов -->
                <bean name="radiusProcessor" class="ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor"/>
 
                <!-- Служебный ScheduledExecutorService, необходимый для dataLogger -->
                <scheduledExecutorService name="hrlydtlggr" corePoolSize="1"/>
 
                <!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) -->
                <bean name="radiusDataLogger" class="ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger">
                        <param name="scheduledExecutor">hrlydtlggr</param>
                </bean>
 
                <!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger -->
                <bean name="radiusListener" class="ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener">
                        <constructor>
                                <!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех -->
                                <param name="host" value=""/>
                                <!-- Порт, на котором будет открыт сокет -->
                                <param name="port" value="3813"/>
                                <!-- Размер буфера приема слушателя -->
                                <param name="recvBufferSize">1 * 1024 * 1024</param>
                                <!-- Рекомендуемый SO_RCVBUF сокета -->
                                <param name="soRCVBUF"></param>
                                <!-- Количество потоков-обработчиков -->
                                <param name="threadCount">30</param>
                                <!-- Максимальное количество пакетов в очереди на обработку -->
                                <param name="maxQueueSize">500</param>
                                <!-- Передача процессора -->
                                <param name="processor">radiusProcessor</param>
                                <!-- Режим работы, RadiusListener.Mode.accounting -->
                                <param name="mode">RadiusListener.Mode.accounting</param>
                                <!-- Передача setup -->
                                <param name="setup">setup</param>
                                <!-- Передача dataLogger -->
                                <param name="dataLogger">radiusDataLogger</param>
                        </constructor>
                </bean>
        </context>
</application>

Тарифы

Безлимит 1Мбит:

Файл:vpn-unlim.png

Конфигурация на cisco

Пример конфига интерфейса клиента:

interface GigabitEthernet0/0.127
 description ---- TEST VPN ----
 encapsulation dot1Q 127
 ip vrf forwarding vpn-test
 ip address IP.IP.IP.IP MASK.MASK.MASK.MASK
 no ip redirects
 no ip proxy-arp
 ip verify unicast source reachable-via rx
 no cdp enable
 service-policy type control IP-ISG-VRFSUB
 ip subscriber interface
end

...

Дополнительно

TODO

  • Описать настройку [справочника сервисов ISG] (Done)
  • Выложить дин. код на github
  • Описать подключение netflow для детализации
  • Скрипт обновления/проставления ifindex на интерфейсах в BG
  • Автоконфигурирование интерфейса по telnet/ssh при создании/удалении

--Cromeshnic 05:11, 25 июля 2013 (UTC)

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