Любой оператор связи вынужден решать задачу управления доступом абонентов. Абоненты получают возможность использовать ресурсы сети оператора связи — обычно для доступа во «внешние сети».
Непосредственно предоставлением доступа занимается NAS (BRAS, BNG) — специализированная форма сетевого маршрутизатора. Большой объём абонентского трафика не позволяет анализировать каждый проходящий пакет на control plane, поэтому на data plane создаётся некое «правило доступа», определяющее параметры пропуска трафика. Абонентские пакеты, не попадающие ни под одно правило, считаются «неклассифицированным трафиком», выборка из которого передаётся на control plane для принятия решения.
NAS не принимает самостоятельного решения о предоставлении доступа. Решение принимает AAA-сервер. Стандартным протоколом взаимодействия NAS и AAA является RADIUS.
В LANBilling используется LBarcd — RADIUS-сервер собственной разработки.
Управление доступом состоит из следующих этапов:
NAS отправляет AAA-серверу RADIUS-запрос, содержащий необходимые идентификаторы абонента и аутентификационную информацию (при наличии). На основании полученного ответа NAS создаёт правило доступа, отвечающее за пропуск абонентского трафика.
Схема доступа — решение задачи управления доступом.
Понятие «схема доступа» можно рассматривать в широком смысле и в узком смысле.
В современных условиях логика управления доступом очень сложна, и не может быть реализована в рамках фиксированной логики.
Схемы управления доступом делятся на два больших класса: PPP и IPoE.
В схеме PPP для управления доступом используется протокол PPP (семейство взаимосвязанных протоколов). У операторов связи используется в виде PPPoE и PPTP.
PPPoE работает на сетевом уровне. PPTP работает на транспортном уровне, поэтому для работы PPTP необходима первичная конфигурация абонентов. Поэтому протокол PPTP используется в современных условиях относительно редко.
«IPoE» — жаргонное название набора технологий управления доступом. В отличие является разрозненным набором технологий, обычно реализуемых на стороне оборудования доступа.
Для конфигурации абонентского оборудования используется протокол DHCP. Что очень занятно, так как DHCP не предназначен для конфигурирования маршрутизаторов.
В схеме PPP используется только один режим создания абонентских сессий. В схеме IPoE возможно несколько вариантов.
1. Старт сессии по первому неклассифицированному IP-пакету
Жаргонное название — «L3».
Если логика data plane не смогла классифицировать первый IP-пакет (в особо запущенных случаях — ARP), то пакет передаётся на control plane для анализа. NAS запрашивает у AAA доступ и по результатам ответа создаёт необходимые правила доступа на data plane и абонентскую сессию. (Обычно сессия живёт на control plane).
Абоненту должен быть присвоен IP-адрес, обычно по DHCP.
2. Старт сессии по первому неклассифицированному ethernet-пакету
Жаргонное название — «L2».
Логика идентична, но работает на сетевом уровне. Абоненту также должен быть присвоен абонентский адрес. Разница только в форме правил на data plane и требованиям к сети [абонентское оборудование должно находиться в том же сегменте ethernet].
3. Старт сессии по созданию виртуального интерфейса
Рассматривается только кейс 1:1 vlan
NAS создаёт виртуальные интерфейсы на основе тегированного ethernet. Пакет с неизвестным тегом передаётся на control plane для анализа. NAS запрашивает у AAA доступ и по результатам создаёт «тегированный» виртуальный интерфейс. В этом схема похожа на PPP.
В общем случае абоненту не требуется присваивать IP-адрес: сессия может создаваться по первом пакету DHCP.
4. DHCP-сервер
NAS выполняет обязанности DHCP-сервера. DHCP-пакеты передаются на control plane для анализа. Обычно в роли инициирующего пакета выступает DHCPDISCOVER. В большинстве случаев NAS не управляет назначением/выдачей IP-адресов самостоятельно, а запрашивает доступ у AAA. Полученный от AAA IP-адрес используется в DHCPOFFER. По завершении DORA-транзакции создаётся абонентская сессия.
5. DHCP-relay
Схема аналогична предыдущей, но NAS не является DHCP сервером а только инспектирует проходящие пакеты.
6. DHCP-proxy
Гибрид двух предыдущих схем. NAS ведёт обмен с абонентом от имени существующего DHCP-сервера. Может самостоятельно отвечать абоненту.
Логин. Используется только в схеме PPP.
Ethernet MAC-адрес (Calling-Station-Id, chaddr). Может использоваться в PPPoE или IPoE. Может выступать в качестве самостоятельного идентификатора абонента [но крайне редко в PPPoE], или в качестве дополнительной информации, используемой логикой авторизации.
Пара vlan. Используется в IPoE [теоретически может использоваться в PPP, но на практике не используется].
IP-адрес. Используется в IPoE для абонентов со статической конфигурацией. Использование DHCP опционально, но не запрещено.
82-я опция (DHCP). Используется исключительно в IPoE. Оборудование доступа (коммутатор, OLT) изменяет проходящие сообщения DHCP, и добавляют дополнительное поле (опцию) с идентификатором абонента (обычно в форме устройство-порт).
82-я опция (PPPoE). Некоторые модели операторского оборудования доступа могут изменять проходящие PADI пакеты, и добавлять уникальный идентификатор абонента. [На практике используется достаточно редко, обычно как дополнительная информация для логики авторизации].
Главная сложность — наличие большого числа слабо связанных механизмов, при минимуме политик. Особенно если рассматривать IPoE.
Любая фиксированная логика будет работать одинаково плохо. Попытки «улучшить» приведут к потере управляемости.
[Одному клиенту «починили» — другому «сломали», и никто не знает как это всё работает. И да, мы это всё проходили на практике.]
Скрипт позволяет уйти от фиксированной логики, но не препятствует созданию произвольного количества примитивов — кусков логики управления доступом.
При решении задачи управления доступом, автор скрипта необходимым образом комбинирует примитивы. В результате, с одной стороны, для каждого клиента (и/или установки) разрабатывается индивидуальное решение; с другой стороны используются «штатные» [т.е. понятные, описанные, протестированные] механизмы.
В перспективе скрипт должен управлять всеми аспектами логики управления доступом абонентов. Сейчас возможно решить только задачу идентификации/аутентификации абонентов.
Скрипт выполняется при обработке RADIUS сообщения Access-Request.
Скрипт выполняется в два этапа.
На этапе конфигурации происходит передача управления в скрипт, так как будто бы он является неявной функцией. [В терминах языка lua это называется chunk]. На данном этапе происходит создание и конфигурация модели управления доступом.
Например:
-- Сюда передано управление
-- На этапе конфигурации разрешено исполнять любую логику
print("Configuration phase")
-- В журнал агента будет зафиксировано сообщение «Configuration phase»
-- Создание модели
local lb = require "lb"
local plugin = lb.require("20250701")
local function access_control(ctx)
-- Здесь размещается логика управления доступом
...
end
plugin:connect('access-control', access_control)
-- Регистрация модели
plugin:register("Demo")
-- Модель зарегистрирована, на следующем этапе будет вызван метод
-- `access_control`, в качестве параметра будет передана сущность
-- «Контекст аутентификации»
На этапе исполнения управление передаётся зарегистрированной модели, которая и определяет дальнейшую логику управления доступом.
Опция radius-external-auth-script задаёт путь к файлу скрипта.
Включение опции отключает все остальные методы аутентификации.
При внесении изменений в скрипт перезагружать агент не требуется.
os и io. [Запрос через require
завершится с ошибкой.]base.
Все остальные стандартные библиотеки необходимо запросить явным образом через
require.print (base.print) перенаправлен в журнал агента.Для решения задач управления доступом используется «версионный» API. В перспективе будет существовать несколько несовместимых (!!!) версий, но в настоящий момент существует только одна.
Для получения «точки входа» в API используется метод require.
local lb = require "lb"
local plugin = lb.require("20250701").
...
Точка входа хранится в переменной plugin.
[lb и plugin — обычные переменные языка lua. При желании можно использовать
любой идентификатор.]
plugin:connectМетод plugin:connect отвечает за подключение «нефиксированных» элементов
модели.
В настоящий момент возможен только один вариант использования — подключение логики идентификации абонента.
local lb = require "lb"
local plugin = lb.require("20250701").
local function access_control(ctx)
-- Здесь размещается логика управления доступом
...
end
plugin:connect('access-control', access_control)
plugin:register("Demo")
Метод access_control принимает сущность «Контекст аутентификации» и должен
вернуть либо вернуть сущность «Решение» [положительное или отрицательное],
либо инициировать ошибку lua [обычно при помощи assert].
plugin:registerМетод plugin:register регистрирует модель управления доступом. В качестве
параметра передаётся любой строковый объект, который используется в качестве
«человекочитаемого идентификатора».
Логика управления доступом осуществляется через сущность «Контекст аутентификации».
Контекст аутентификации — гибридная сущность в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Попытки использовать контекст аутентификации вне предписанного интерфейса приведут к ошибке времени исполнения.
С контекстом аутентификации ассоциирован набор т.н. понятий предметной области (ППО).
Метод ctx:get возвращает строковое представление ППО по имени. [Т.е. копию
значения.]
...
local function access_control(ctx)
local framed_ip_address = assert(ctx:get('Framed-IP-Address'), "Error")
...
end
...
[ctx:get возвращает nil, если ППО не существует или не найдено]
В настоящий момент определены следующие ППО
User-Name
Содержимое атрибута User-Name сообщения Access-Request.Framed-IP-Address
Содержимое атрибута Framed-IP-Address сообщения Access-Request.
Семантика зависит о контекста.NAS-Port-Id
Содержимое атрибута NAS-Port-Id сообщения Access-Request.Calling-Station-Id
нормализованное значение ethernet MAC-адреса абонентского устройства.
Формируется из атрибута Calling-Station-Id сообщения Access-Request.Agent-Circuit-Id, Agent-Circuit-Id нормализованное значение
соответствующих субопций 82-й опции.
Формируется из поддерживаемых вендорских атрибутов сообщения Access-Request.
Если значение субопции содержит хотя бы один непечатный символ, то всё
значение кодируется hex.ctx:getМетод ctx:get возвращает строковое представление ППО по имени или nil, если
ППО не найдено или не существует.
ctx:get возвращает нормализованное значение, например
local chaddr = assert(ctx:get('Calling-Station-Id'), "Error")
всегда возвращает ethernet MAC-адрес в формате XX:XX:XX:XX:XX:XX.
Значение субопций 82-й опции нормализуются по правилу:
Логика управления доступом должна либо вернуть сущность «Результат»
(положительный либо отрицательный), либо инициировать ошибку lua, например,
при помощи assert.
Сущность «Результат» — гибридная сущность, в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Создать сущность «Результат» возможно только при помощи специальных
конструкторов ctx:accept или ctx:reject.
Созданная сущность неизменяема, попытка взаимодействия через интерфейс lua приведёт к ошибке уровня исполнения.
Метод ctx:reject используется относительно редко, более эффективно
инициировать ошибку lua.
local function access_control(ctx)
local subscriber = ... -- получи абонента
-- Если абонент не найден (nil), то инициируй ошибку
assert(subscriber, "Reason")
-- Положительный результат
return ctx:accept { subscriber = subscriber }
end
ctx:acceptМетод ctx:accept является конструктором сущности «Результат».
На вход должна быть передана lua-таблица с заполненным ключом subscriber
(абонент).
ctx:rejectМетод ctx:reject является конструктором сущности «Результат».
На вход должна быть передана lua-таблица с заполненным ключам reason.
Значение reason будет зафиксировано в журнале агента и в журнале
аутентификаций агента.
N.B. return ctx:reject {reason = "Reason"} и assert(nul, "Reason")
эквиваленты.
ctx:check_passwordМетод ctx:check_password проверяет значение пароле в атрибуте User-Password
сообщения Access-Reject.
Пароль никогда не передаётся на сторону скрипта.
В случае IPoE в Access-Request передаётся фиксированные значения User-Name
и User-Password. Проверка фиксированного User-Password применяется для
проверки разделяемого секрета в случаях, когда не передан атрибут
Message-Authenticator.
[Всегда необходимо передавать Message-Authenticator]
Так как для в IPoE не используется User-Password для аутентификации абонента, в случае некорректно заданного разделяемого секрета Access-Request будет обработан без ошибок на стороне AAA, но ответ не примет NAS из-за несовпадения секретов.
Явная проверка фиксированного пароля позволяет избежать подобной ситуации.
Например,
User-Name = "cisco"
User-Password = "cisco"
local string = require "string"
local function access_control(ctx)
user_name = ctx:get("User-Name") or "<None>".
assert("cisco" == user_name,
string.format("Unexpected User-Name: %s", user_name))
assert(ctx:check_password("cisco"),
"Unexpected fixed password. Use 'cisco'")
...
end
Сущность абонент содержит информацию об идентифицированном абоненте.
Сущность «Абонент» — гибридная сущность, в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Создать сущность можно только при помощи специальных конструкторов контекста аутентификации.
Попытки использовать сущность «Абонент» вне предписанного интерфейса приведут к ошибке времени исполнения.
Доступные методы:
subscriber:id — идентификатор в БД;subscriber:name — строковый идентификатор абонента (логин).ctx:subscriber_by_idМетод subscriber_by_id ищет абонента по идентификатору учётной записи абонента
в БД.
Метод создаёт и возвращает сущность «Абонент» или nil, если не найден.
local function access_control(ctx)
local subscriber_id = ... -- Идентификатор абонента
local subscriber = ctx:subscriber_by_id(subscriber_id)
assert(subscriber,
string.format("Unexpected subscriber_id: %d", subscriber_id))
print(string.format("Found subscriber id: %d, name: %s",
subscriber:id(), subscriber:name()))
return ctx:accept { subscriber = subscriber }
end
ctx:subscriber_by_nameМетод ctx:subscriber_by_name ищет абонента по имени учётной записи
абонента.
Метод создаёт и возвращает сущность «Абонент» или nil, если не найден.
local function access_control(ctx)
local subscriber_name = ... -- Имя абонента
local subscriber = ctx:subscriber_by_id(subscriber_name)
assert(subscriber,
string.format("Unexpected subscriber name: %s", subscriber_name))
print(string.format("Found subscriber id: %d, name: %s",
subscriber:id(), subscriber:name()))
return ctx:accept { subscriber = subscriber }
end
Сущность «Операторское оборудование» содержит информацию об операторском оборудовании доступа на котором терминируется абонентская линия абонента.
Сущность «Операторское оборудование» — гибридная сущность, в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Создать сущность можно только при помощи специальных конструкторов контекста аутентификации.
Попытки использовать сущность «Операторское оборудование» вне предписанного интерфейса приведут к ошибке времени исполнения.
Доступные методы
device:id — идентификатор в БД;device:label — человекочитаемое имя;device:type — тип (“switch” | “GPON” | nil);device:ip — строковое представление IP-адреса;device:mac — нормализованное представление ethernet MAC-адреса;device:opt82 — текстовое представление 82-й опции (машиночитаемый
идентификатор).[MAC-адрес всегда в формате XX:XX:XX:XX:XX.]
local access_control(ctx)
local remote_id = assert(ctx:get("Agent-Remote-Id"), "No Remote-Id")
local device = ... -- Операторское оборудование
print(string.format(
"Found device id: %d, label: %s, type: %s, ip: %s, mac: %s, opt82: %s",
device:id(), device:label(), device:type(), device:ip(), device:mac(),
device:opt82()))
...
end
device_by_id, device_by_opt82, device_by_mac, device_by_ipМетод device_by_X ищет операторское оборудование по одному из
идентификаторов.
Метод создаёт и возвращает сушность «Абонентское устройство» или nil, если устройство не найдено.
local access_control(ctx)
local remote_id = assert(ctx:get("Agent-Remote-Id"), "No Remote-Id")
local device = assert(ctx:device_by_opt82(remote_id), "No device found");
print(string.format(
"Found device id: %d, label: %s, type: %s, ip: %s, mac: %s, opt82: %s",
device:id(), device:label(), device:type(), device:ip(), device:mac(),
device:opt82()))
...
end
Сущность «Порт» содержит информацию «физическом» порте операторского оборудования доступа на котором терминируется абонентская линия абонента.
Сущность «Порт» — гибридная сущность в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Создать сущность можно только при помощи специальных конструкторов контекста аутентификации.
Попытки использовать сущность «Порт» вне предписанного интерфейса приведут к ошибке времени исполнения.
Доступные методы:
port:id — идентификатор в БД;port:name — имя (машиночитаемое);port:vlan — текстовое представление vlan;subscriber_id — идентификатор абонента в БД.local access_control(ctx)
local port = ... -- Порт операторского оборудования
print(string.format(
"Found port port_id: %d, name: %s, vlan: %s, subscriber_id: %s",
port:id(), port:name(), port:vlan(), port:subscriber_id() or "<None>"))
...
end
ctx:port_by_idМетод ctx:port_by_id ищет порт операторского оборудования по идентификатору
в БД.
Метод создаёт и возвращает сущность «Порт» или nil, если не найден.
local access_control(ctx)
local port_id = ... -- Идентификатор порта
local port = assert(ctx:port_by_id(port_id), "Port not found")
print(string.format(
"Found port port_id: %d, name: %s, vlan: %s, subscriber_id: %s",
port:id(), port:name(), port:vlan(), port:subscriber_id() or "<None>"))
...
end
ctx:port_by_nameМетод ctx:port_by_name ищет порт операторского оборудования по сущности
«Операторское оборудование» и (машиночитаемому) имени порта.
Метод создаёт и возвращает сущность «Порт» или nil, если не найден.
[Сущность «Операторское оборудование» должна быть должным образом создана одним из методов-конструкторов контекста аутентификации.]
local access_control(ctx)
local circuit_id = ctx:get("Agent-Circuit-Id") -- Машиночитаемое имя порта
local remote_id = assert(ctx:get("Agent-Remote-Id"), "No Remote-Id")
local device = assert(ctx:device_by_opt82(remote_id), "No device found");
print(string.format(
"Found device id: %d, label: %s, type: %s, ip: %s, mac: %s, opt82: %s",
device:id(), device:label(), device:type(), device:ip(), device:mac(),
device:opt82()))
local port = assert(ctx:port_by_name(device, circuit_id), "Port not found")
print(string.format(
"Found port port_id: %d, name: %s, vlan: %s, subscriber_id: %s",
port:id(), port:name(), port:vlan(), port:subscriber_id() or "<None>"))
...
end
Сущность «Виртуальный порт» содержит информацию о виртуальном порте операторского оборудования доступа на котором терминируется абонентская линия абонента.
Сущность «Виртуальный порт» — гибридная сущность в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Создать сущность можно только при помощи специальных конструкторов контекста аутентификации.
Попытки использовать сущность «Виртуальный порт» вне предписанного интерфейса приведут к ошибке времени исполнения.
Доступные методы:
vport:id — идентификатор в БД;vport:name — имя (машиночитаемое);vport:subscriber_id — идентификатор абонента в БД.[Сущность «Абонент» можно получить по идентификатору БД при помощи
ctx:subscriber_by_id.]
ctx:vport_by_idМетод ctx:vport_by_id ищет виртуальный порт операторского оборудования по
идентификатору в БД.
Метод создаёт и возвращает сущность «Виртуальный порт» или nil, если не найден.
local function access_control(ctx)
local vport_id = ... -- Идентификатор виртуального порта
local vport = assert(ctx:vport_by_id(vport_id), "No vport found")
print(string.format("Found vport vport_id: %d, name: %s, subscriber_id: %s",
vport:id(), vport:name(), vport:subscriber_id() or "<None>"))
end
ctx:vport_by_nameМетод ctx:vport_by_name ищет виртуальный порт операторского оборудования по
сущности «Порт» и (машиночитаемому идентификатору порта).
Метод создаёт и возвращает сущность «Виртуальный порт» или nil, если не найден.
[Сущность «Порт» должна быть должным образом создана одним из методов-конструкторов контекста аутентификации.]
local access_control(ctx)
local circuit_id = ctx:get("Agent-Circuit-Id")
local remote_id = assert(ctx:get("Agent-Remote-Id"), "No Remote-Id")
local device = assert(ctx:device_by_opt82(remote_id), "No device found");
print(string.format(
"Found device id: %d, label: %s, type: %s, ip: %s, mac: %s, opt82: %s",
device:id(), device:label(), device:type(), device:ip(), device:mac(),
device:opt82()))
local port = assert(ctx:port_by_name(device, circuit_id), "Port not found")
print(string.format(
"Found port port_id: %d, name: %s, vlan: %s, subscriber_id: %s",
port:id(), port:name(), port:vlan(), port:subscriber_id() or "<None>"))
local vport = assert(ctx:vport_by_name(port, vport_name), "No vport found")
print(string.format("Found vport vport_id: %d, name: %s, subscriber_id: %s",
vport:id(), vport:name(), vport:subscriber_id() or "<None>"))
...
end
Сущность «Vlan» содержит информацию о ethernet vlan.
Сущность «Vlan» — гибридная сущность в равной степени существующая как в среде исполнения lua, так и в среде исполнения LBarcd.
Создать сущность можно только при помощи специальных конструкторов контекста аутентификации.
Доступные методы:
vlan:id — идентификатор в БД;vlan:svlan — svlan (outer vlan);vlan:cvlan — cvlan (inner vlan);__string — допустимо использовать в стоковом контексте.local access_control(ctx)
local svlan, cvlan = ... -- Теги: svlan и cvlan
local vlan = assert(ctx:vlan(svlan, cvlan), "Vlan not found")
print(string.format("Found vlan vlan_id: %d, %s", vlan:id(), vlan))
...
end
ctx:vlanМетод ctx:vlan ищет vlan по паре svlan:cvlan.
Метод принимает два параметра:
Метод создаёт и возвращает сущность «Vlan» или nil, если не найдена.
[Vlan должен быть создан на стороне LB.]
local access_control(ctx)
local svlan, cvlan = ... -- Теги: svlan и cvlan
local vlan = assert(ctx:vlan(svlan, cvlan), "Vlan not found")
print(string.format("Found vlan vlan_id: %d, %s", vlan:id(), vlan))
...
end
ctx:port_by_vlanМетод ctx:port_by_vlan ищет порт операторского оборудования по сущности
«Vlan».
Метод создаёт и возвращает сущность «Порт» или nil, если не найдена.
[Сущность «Vlan» должна быть создана при помощи методов-конструкторов контекста аутентификации.]
local access_control(ctx)
local svlan, cvlan = ... -- Идентификаторы svlan:cvlan
local vlan = assert(ctx:vlan(svlan, cvlan), "Vlan not found")
print(string.format("Found vlan vlan_id: %d, %s", vlan:id(), vlan))
local port = assert(ctx:port_by_vlan(vlan), "Port not found")
print(string.format(
"Found port port_id: %d, name: %s, vlan: %s, subscriber_id: %s",
port:id(), port:name(), port:vlan(), port:subscriber_id() or "<None>"))
...
end
Есть вопросы по документации? Пожалуйста, напишите их