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
Также не забываем настроить задачу планировщика "Активация/деактивация сервисов по периоду" хотя бы раз в сутки в полночь, чтобы корректно обрабатывалось переоформление и перенос сервисов с договора на договор будущим числом.
Типы трафиков и привязки
Типы трафиков
Привязки
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
Типы сервисов
VPN-IPoE
VPN-IPoE (+NAT)
Опции
Заведём опции для соответствующих ISG-сервисов. Опции FLOWON/FLOWOFF нужны для включения/выключения netflow на интерфейсе
Устройства и ресурсы
Группы устройств
Не используются.
Типы устройств
Мы используем 3 типа устройств:
- Группа (ProcessGroup) - пустой тип устройств. Указывается в качестве рута для BGInetAccess и BGInetAccounting (см соответствующий раздел)
- Город - пустой тип устройства, добавлен для разбиения дерева по городам. В будущем, возможно, к нему будут привязываться специфические ProcessHandler-ы, отдельные конфиги или выделяться свои Access и Accounting сервера для каждого города.
- IPoE - тип устройства для цисок с поддержкой ip subscriber interface + ISG
Конфиг типа устройства 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.*
Для 5.2:
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(); } } } } }
С 6.0:
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, Destroyable { 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.getInvDeviceId(), 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); } @Override public void destroy() throws Exception { this.ifaceMap.destroy(); } /** * Кэш соответствий Nas-Port-Id -> id интерфейса в биллинге для устройства * Обновляется при изменении порта или перезагрузке конфигурации */ private class DeviceNasPortMap implements EventListener<DeviceInterfaceModifiedEvent>, Destroyable { /** * Соответствие 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(); } } } @Override public void destroy() throws Exception { if(logger.isDebugEnabled()) { logger.debug("destroy for deviceId = "+this.deviceId); } EventProcessor.getInstance().removeListener(this); } } }
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 ); } } } } }
Устройства
Дерево устройств:
Интерфейсы:
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&characterEncoding=Cp1251&allowUrlInLocalInfile=true&zeroDateTimeBehavior=convertToNull&jdbcCompliantTruncation=false&queryTimeoutKillsConnection=true&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&characterEncoding=Cp1251&allowUrlInLocalInfile=true&zeroDateTimeBehavior=convertToNull&jdbcCompliantTruncation=false&queryTimeoutKillsConnection=true&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Мбит:
Конфигурация на 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
...
Дополнительно
- Мониторинг_Inet-Radius_через_JMX
- Последние версии кода: на github
- Справочник_Cisco-ISG_сервисов
TODO
- Описать настройку [справочника сервисов ISG] (Done)
- Выложить дин. код на github
- Описать подключение netflow для детализации
- Скрипт обновления/проставления ifindex на интерфейсах в BG
- Автоконфигурирование интерфейса по telnet/ssh при создании/удалении
--Cromeshnic 05:11, 25 июля 2013 (UTC)