Сетевой инженер
0,0
рейтинг
15 января в 11:29

Администрирование → HotSpot с помощью Cisco WLC5508, FreeRadius, MySQL и Easyhotspot из песочницы

Эта статья о том, как создать и настроить HotSpot. При этом мне хотелось подробно описать, как это выглядит изнутри и сделать упор на работу freeRadius, MySQL и простого биллинга Easyhotspot.

Итак, наша система будет строиться на Cisco WLC5508 и CentOS. Чтобы понять как это все между собой взаимодействует, посмотрим на схему.



Начинается все с сервиса Easyhotspot, по сути это биллинг, который управляется через веб-интерфейс. Это open-source проект и главное его преимущество в том, что он прост для понимания и управления.

С помощью него мы генерируем username и password, которые записываются в базу данных MySQL и передаются для пользования юзеру. Далее пользователь подключается к нашей точке доступа, которая управляется контроллером Cisco WLC. При попытке открыть какую либо страницу в браузере, юзера перекинет на страницу авторизации, где он введет полученные креденциалы.

После этого контроллер их перехватит и в сообщении access-request отправит для проверки серверу авторизации FreeRadius'у. Тот обратится к базе данных, найдет соответствующие логин и пароль, и ответит вай-фай контроллеру, что все ок, при этом выдаст время в течении которого пользователь может быть активен. В терминологии RADIUS, вай-фай контроллер Cisco WLC является для радиус-сервера NAS-клиентом (Network Access Server), в терминологии hotspot этот сервис так же называется captive portal. Задачей этого сервиса является показать пользователю окно авторизации, передать креденциалы радиус-серверу, получить ответ с атрибутами и адекватно их обработать.

И хоть Easyhotspot изначально разрабатывался для работы с captive portal, который называется Chillispot, нам в сущности это неважно.
После того, как пользователь был авторизован, NAS должен в течении пользовательской сессии отправлять радиус-серверу отчеты об использованных юзером ресурсах: количество времени, трафика и тп. Все это дело freeRadius будет логировать в базу данных. А при следующей попытке подключиться, для юзера будет вестись проверка сколько он чего потратил и сколько этого чего ему осталось доступно.

Вспомним, что такое Radius. Это протокол, который условно делистя на две части, это аутентификация/авторизация и аккаунтинг. Первую описывает RFC 2865, вторую RFC 2866. О том, что они работают раздельно говорит и то, что сервер принимает для них запросы на разных портах 1812 и 1813.

Далее мы подробно разберем, как выглядят запросы и ответы, и то, как они обрабатываются FreeRadius'ом, а сейчас приступим к установке. Предположим, что у нас есть сервер CentOS 6.5 с настроенной сетью и доступом в интернет. Установим программу для скачивания файлов из сети:

yum install wget

Загрузим и установим репозиторий:

wget http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
rpm -ivh epel-release-6-8.noarch.rpm

Обновим репозиторий и установленные программы:

yum repolist
yum update

Установим полезные утилиты, MySQL, Apache и php:

yum install mc vim unzip gcc gcc-c++ make git svn nano
yum install mysql-server php httpd php-mysql php-xml php-gd php-pear php-db
yum install patch mod_ssl openssl dnsmasq

В файле /etc/php.ini нужно раскомментировать строку:

short_open_tag = On

Если забыть об этом параметре, то при попытке открыть страничку биллинга будет появляться такое сообщение:

EasyHotSpot config->item('EASYHOTSPOT_VERSION');?>
load->view($this->config->item('FAL_template_dir').'template/menu');?>

EasyHotspot - Hotspot Management System

GNU Public License

Там же в файле /etc/php.ini:

date.timezone = Europe/Moscow

Сделаем так, чтоб MySQL и Apache после загрузки системы запускались автоматически:

chkconfig --level 235 httpd on
chkconfig --level 235 mysqld on
chkconfig --level 235 dnsmasq on

Установим Easyhotspot. Загрузим easyhotspot отсюда github.com/rafeequl:

cd /opt
git clone https://github.com/rafeequl/EasyHotspot
ln -s /opt/EasyHotspot/htdocs /var/www/html/easyhotspot

Создадим базу данных easyhotspot_opensource:

mysql
mysql> create database easyhotspot_opensource;
mysql> CREATE USER 'easyhotspot'@'localhost';
mysql> SET PASSWORD FOR 'easyhotspot'@'localhost' = PASSWORD('easyhotspot');
mysql> GRANT ALL ON easyhotspot_opensource.* to 'easyhotspot'@'localhost';
mysql> quit
mysql -u root easyhotspot_opensource < /opt/EasyHotspot/install/database_with_sample.sql

В EasyHotspot в меню присутствует страничка Chillispot. Когда мы будем нажимать на нее, мы увидим ошибку. Чтобы избежать этого, удалим ее. Для этого нужно удалить или закомментировать следующие строчки в файле /opt/EasyHotspot/htdocs/system/application/views/admin/header.php:

<!--   <li class="chillispot"><?=anchor('admin/chillispot','Chillispot')?></li> -->
<!--   <li class="radius"><?=anchor('admin/freeradius','FreeRadius')?></li> -->

Установим phpMyAdmin, это своего рода GUI для MySQL, поможет нам в изучении таблиц:

wget https://files.phpmyadmin.net/phpMyAdmin/3.5.5/phpMyAdmin-3.5.5-all-languages.zip
unzip phpMyAdmin-3.5.5-all-languages.zip
cp  phpMyAdmin-3.5.5-all-languages EasyHotspot/htdocs/phpmyadmin -rf

Создадим config.inc.php:

vi EasyHotspot/htdocs/phpmyadmin/config.inc.php

<?php
  $i = 0;
  $i++;
  $cfg['ThemeDefault'] = 'original';
  $cfg['Servers'][$i]['host'] = 'localhost';
  $cfg['Servers'][$i]['extension'] = 'mysqli';
  $cfg['Servers'][$i]['connect_type'] = 'tcp';
  $cfg['Servers'][$i]['compress'] = false;
  $cfg['Servers'][$i]['auth_type'] = 'config'; //  - phpmyadmin сам будет логиниться в БД. Если мы хотим, чтоб при входе появлялось окно авторизации, 'config' меняем на 'http' 
  $cfg['Servers'][$i]['user'] = 'easyhotspot';
  $cfg['Servers'][$i]['password'] = 'easyhotspot';  
  
/* End of servers configuration */

$cfg['UploadDir'] = '';
$cfg['SaveDir'] = '';
$cfg['BZipDump'] = false;
$cfg['DefaultLang'] = 'ru';
$cfg['ThemeDefault'] = 'original';
$cfg['ServerDefault'] = 1;
$cfg['CompressOnFly'] = false;
$cfg['UserprefsDeveloperTab'] = true;
$cfg['HideStructureActions'] = false;
$cfg['LoginCookieDeleteAll'] = false;
$cfg['QueryHistoryDB'] = true;
$cfg['RetainQueryBox'] = true;
$cfg['blowfish_secret'] = '51a360783193d3.45092927';
$cfg['LeftDefaultTabTable'] = 'tbl_select.php';
$cfg['MaxTableList'] = 500;

?>

Перезапустим Apache:

service httpd restart

Установка и настройка FreeRadius


Устанавливаем фрирадиус, поддержку mysql и утилиты, которые помогут нам в тестировании системы:

yum install freeradius freeradius-mysql freeradius-utils

Сделаем так, чтоб freeradiu после загрузки системы запускался автоматически:

chkconfig --level 235 radiusd on

В файле /etc/raddb/clients.conf в секции “client localhost“ нужно сделать следующее:

ipaddr = 127.0.0.1
secret = easyhotspot
nastype = other

В файле /etc/raddb/radiusd.conf в секции “module” раскомментировать:

$INCLUDE sql.conf
$INCLUDE sql/mysql/counter.conf

В секции “instantiate“ добавить:

noresetcounter

В файле /etc/raddb/sites-enabled/default в секции “authorize” расскомментировать “sql” и добавить «noresetcounter»:

sql
noresetcounter

Затем /etc/raddb/sites-enabled/default в секции “accounting” расскомментировать “sql”:

sql

Затем /etc/raddb/sites-enabled/default в секции “session” расскомментировать “sql” и закомментировать «radutmp»:

sql
#radutmp

И затем /etc/raddb/sites-enabled/default в секции «post-auth» расскомментировать “sql”:

sql


В файле /etc/raddb/sql/mysql/counter.conf в конце уже определен счетчик «noresetcounter», мы его подредактируем:

sqlcounter noresetcounter {
counter-name = Session-Timeout
check-name = Session-Timeout
reply-name = Session-Timeout
sqlmod-inst = sql
key = User-Name
reset = never
query = "SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='%{${key}}'"
}


В файле /etc/raddb/sql/mysql/dialup.conf для работы с Simultaneous-Use раскомментируем следующее:

simul_count_query = "SELECT COUNT(*) \
   FROM ${acct_table1} \
   WHERE username = '%{SQL-User-Name}' \
   AND acctstoptime IS NULL"

В файле /etc/raddb/sql.conf в секции «sql» сделаем так:

        database = "mysql"
        driver = "rlm_sql_${database}"
        server = "localhost"
        #port = 3306
        login = "easyhotspot"
        password = "easyhotspot"
        radius_db = "easyhotspot_opensource"

Перезапустим freeRadius:

service radiusd start

Настройка Сisco WLC


Подключаем Radius-сервер к WLC: SECURITY->Authentication. SECURITY->Accounting. При настройке нужно ввести IP и Shared Secret.



Настраиваем редирект с помощью которого неавторизованный пользователь при запросе любой страницы будет перенаправлен на страницу авторизации, где нужно ввести логин и пароль.



Настраиваем access-list. Дело в том, что когда пользователь подключается к SSID, всего его запросы из веб-браузера редиректятся на страницу авторизации, но кроме этого его по сути ничего не ограничивает.

Мы должны создать ACL, который разрешит пользователю доступ только к нашей странице авторизации и никуда больше.



Создаем SSID, на вкладке Security->Layer 2 выбираем None. На вкладке Security->Layer 3 выбираем Web Policy — Authentication, в выпадающем списке Preauthentication ACL выбираем наш созданный ранее ACL.



Выбираем из выпадающего списка ранее добавленный сервер радиуса.



Осталось донастроить радиус. Добавим IP-адрес нашего WLC и secret
vi /etc/raddb/clients.conf

client 192.168.0.5 {
#       # secret and password are mapped through the "secrets" file.
        secret      = ololo,karl!
#       shortname   = liv1
#       # the following three fields are optional, but may be used by
#       # checkrad.pl for simultaneous usage checks
        nastype     = cisco
#       login       = !root
#       password    = someadminpas
}

Перезапустим freeRadius:

service radiusd restart 

Теперь по адресу х.х.х.х/easyhotspot доступна страница авторизации:



Логин: admin
Пароль: admin123

На веб-интерфейсе нам доступно два основных меню [ Cashier Menu ] и [ Admin Menu ].



В Admin Menu мы можем создать Billing plan и Account plan, чтобы на основе них генерировать логины/пароли. Создадим биллинг план на час, при этом сгенерированный на его основе ваучер будет действителен до конца следующего дня с момента создания(Valid for = 1), а Idle Timeout сделаем 5 мин.



Перейдем в [ Cashier Menu ] -> Voucher Management и сгенерируем один ваучер.



Протестируем нашу систему. С помощью утилиты radtest мы отправим радиус-серверу запрос на авторизацию таким образом как это делает NAS. Используем сгенерированные логин/пароль, 'easyhotspot' это secret в файле /etc/raddb/clients.conf:

radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot

Здесь у вас может возникнуть ошибка:

[root@FreeRadius ~]# radtest zupvez10 palkipud 127.0.0.1 100 esyhotspot
radclient:: Failed to find IP address for FreeRadius
radclient: Nothing to send.
[root@FreeRadius ~]#

Связана она с тем, что при отправке радиус-пакета утилита пытается резолвить имя сервера, чтобы добавить в NAS-IP-Address. Если имя не localhost, нужно добавить его в /etc/hosts. Итак:

[root@FreeRadius ~]# radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot
Sending Access-Request of id 189 to 127.0.0.1 port 1812
        User-Name = "zupvez10"
        User-Password = "palkipud"
        NAS-IP-Address = 127.0.0.1
        NAS-Port = 100
        Message-Authenticator = 0x00000000000000000000000000000000
rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=189, length=63
        WISPr-Session-Terminate-Time = "2016-1-6T24:00:00"
        Session-Timeout = 3600
        Idle-Timeout = 300
        Acct-Interim-Interval = 120
[root@FreeRadius ~]#

Мы видим, что наша авторизация прошла успешно, при этом мы в ответ получили набор атрибутов:

  • WISPr-Session-Terminate-Time = «2016-1-6T24:00:00» — в полночь следующего дня сессия станет недействительной
  • Session-Timeout = 3600 — означает, что NAS должен будет отключить юзера через час
  • Idle-Timeout = 300 — если в течении 5 мин юзер не проявляет активности, NAS должен скинуть сессию
  • Acct-Interim-Interval = 120 каждые две минуты NAS должен присылать пакет аккаунтинга(interim-update) с отчетом о текущих делах юзера

Отлично, но мы сделали только половину работы NAS, после аутентификации и авторизации должен идти аккаунтинг. Для этого создадим три файла с атрибутами. В поле User-Name = "" не забывайте указывать ваш username.

vi start.txt:

Packet-Type=4
Packet-Dst-Port=1813
Acct-Session-Id = "4D2BB8AC-00000098"
Acct-Status-Type = Start
Acct-Authentic = RADIUS
User-Name = "zupvez10" 
NAS-Port = 0
Called-Station-Id = "00-02-6F-AA-AA-AA:My Wireless"
Calling-Station-Id = "00-1C-B3-AA-AA-AA"
NAS-Port-Type = Wireless-802.11
Connect-Info = "CONNECT 48Mbps 802.11b"

В поле User-Name = "" не забывайте указывать ваш username.

vi interim-update.txt:

Packet-Type=4
Packet-Dst-Port=1813
Acct-Session-Id = "4D2BB8AC-00000098"
Acct-Status-Type = Interim-Update
Acct-Authentic = RADIUS
User-Name = "zupvez10" 
NAS-Port = 0
Called-Station-Id = "00-02-6F-AA-AA-AA:My Wireless"
Calling-Station-Id = "00-1C-B3-AA-AA-AA"
NAS-Port-Type = Wireless-802.11
Connect-Info = "CONNECT 48Mbps 802.11b"
Acct-Session-Time = 11
Acct-Input-Packets = 15
Acct-Output-Packets = 3
Acct-Input-Octets = 1407
Acct-Output-Octets = 467

В поле User-Name = "" не забывайте указывать ваш username.

vi stop.txt:

Packet-Type=4
Packet-Dst-Port=1813
Acct-Session-Id = "4D2BB8AC-00000098"
Acct-Status-Type = Stop
Acct-Authentic = RADIUS
User-Name = "zupvez10" 
NAS-Port = 0
Called-Station-Id = "00-02-6F-AA-AA-AA:My Wireless"
Calling-Station-Id = "00-1C-B3-AA-AA-AA"
NAS-Port-Type = Wireless-802.11
Connect-Info = "CONNECT 48Mbps 802.11b"
Acct-Session-Time = 30
Acct-Input-Packets = 25
Acct-Output-Packets = 7
Acct-Input-Octets = 3407
Acct-Output-Octets = 867
Acct-Terminate-Cause = User-Request

С помощью утилиты radclient мы отправим радиус-серверу пакет аккаунтинга Start.

radclient 127.0.0.1 auto easyhotspot -f start.txt:

[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f start.txt
Received response ID 50, code 5, length = 20

Получен ответ, code 5 означает, что все в порядке. Перейдем в [ Cashier Menu ] -> Online Users.



Видим нашего юзера и время начала сессии. Теперь отправим пакет аккаунтинга interim-update. В этот раз в пакет добавляются новые атрибуты:

  • Acct-Session-Time = 11
  • Acct-Input-Packets = 15
  • Acct-Output-Packets = 3
  • Acct-Input-Octets = 1407
  • Acct-Output-Octets = 467

radclient 127.0.0.1 auto easyhotspot -f interim-update.txt:

[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f interim-update.txt
Received response ID 226, code 5, length = 20

Посмотрим Online Users.



Теперь мы видим, что юзер потратил из отпущенного ему часа 11 секунд и 1874 байта. Отправим пакет аккаунтинга stop.

radclient 127.0.0.1 auto easyhotspot -f stop.txt:

[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f stop.txt
Received response ID 166, code 5, length = 20

Сессия завершена. Перейдем в [ Cashier Menu ] -> Voucher Management.



Видим, что у нашего пользователя осталось 59 мин. Время округлилось, но это только в биллинге, freeRadius считает с точностью до секунды. Проверим:

radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot:

[root@FreeRadius ~]# radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot
Sending Access-Request of id 93 to 127.0.0.1 port 1812
        User-Name = "zupvez10"
        User-Password = "palkipud"
        NAS-IP-Address = 127.0.0.1
        NAS-Port = 100
        Message-Authenticator = 0x00000000000000000000000000000000
rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=93, length=63
        WISPr-Session-Terminate-Time = "2016-1-6T24:00:00"
        Session-Timeout = 3570
        Idle-Timeout = 300
        Acct-Interim-Interval = 120

Как можно догадаться из полученного ответа, freeRadius от изначально заданного значения 3600 отнял потраченные юзером 30 и вернул нам 3570. Давайте теперь пошлем пакет начала сессии аккаунтинга.

radclient 127.0.0.1 auto easyhotspot -f start.txt:

[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f start.txt
Received response ID 15, code 5, length = 20

И заново попробуем авторизоваться, представив что пользователь, получив доступ дал свои креденциалы другу.

radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot:

Sending Access-Request of id 99 to 127.0.0.1 port 1812
        User-Name = "zupvez10"
        User-Password = "palkipud"
        NAS-IP-Address = 127.0.0.1
        NAS-Port = 100
        Message-Authenticator = 0x00000000000000000000000000000000
rad_recv: Access-Reject packet from host 127.0.0.1 port 1812, id=99, length=68
        Reply-Message = "\r\nYou are already logged in - access denied\r\n\n"

Болт! Пока сессия не будет завершена, повторно залогиниться никто не сможет. Завершим сессию.

radclient 127.0.0.1 auto easyhotspot -f stop.txt:

[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f stop.txt
Received response ID 143, code 5, length = 20

Посмотрим, что происходит внутри freeRadius, когда обрабатывается запрос. Остановим freeRadius:

service radiusd stop

Запустим freeRadius в режиме отладки и запишем выдачу в файл log.txt в фоновом режиме.

radiusd -X > log.txt &

Пошлем запрос авторизации.

radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot

Запрос начала и конца сесии:

radclient 127.0.0.1 auto easyhotspot -f start.txt
radclient 127.0.0.1 auto easyhotspot -f interim-update.txt
radclient 127.0.0.1 auto easyhotspot -f stop.txt

Остановим режим отладки.

kill — у вас должно быть свое значение, выдается после команды radiusd -X > log.txt &

[root@FreeRadius ~]# service radiusd stop
Stopping radiusd:                                          [  OK  ]
[root@FreeRadius ~]# radiusd -X > log.txt &
[1] 4215
[root@FreeRadius ~]# radtest zupvez10 palkipud 127.0.0.1 100 easyhotspot
Sending Access-Request of id 200 to 127.0.0.1 port 1812
        User-Name = "zupvez10"
        User-Password = "palkipud"
        NAS-IP-Address = 127.0.0.1
        NAS-Port = 100
        Message-Authenticator = 0x00000000000000000000000000000000
rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=200, length=63
        WISPr-Session-Terminate-Time = "2016-1-6T24:00:00"
        Session-Timeout = 3540
        Idle-Timeout = 300
        Acct-Interim-Interval = 120
[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f start.txt
Received response ID 68, code 5, length = 20
[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f interim-update.txt
Received response ID 142, code 5, length = 20
[root@FreeRadius ~]# radclient 127.0.0.1 auto easyhotspot -f stop.txt
Received response ID 37, code 5, length = 20
[root@FreeRadius ~]# kill 4215
[root@FreeRadius ~]# kill 4215
bash: kill: (4215) - No such process
[1]+  Done                    radiusd -X > log.txt

Посмотрим лог:

less log.txt

debug_Authentication and authorization
rad_recv: Access-Request packet from host 127.0.0.1 port 50024, id=200, length=78
        User-Name = "zupvez10"
        User-Password = "palkipud"
        NAS-IP-Address = 127.0.0.1
        NAS-Port = 100
        Message-Authenticator = 0x9159a59b8e5c58fe44a95a199f84f9cf
# Executing section authorize from file /etc/raddb/sites-enabled/default
+group authorize {
++[preprocess] = ok
++[chap] = noop
++[mschap] = noop
++[digest] = noop
[suffix] No '@' in User-Name = "zupvez10", looking up realm NULL
[suffix] No such realm "NULL"
++[suffix] = noop
[eap] No EAP-Message, not doing EAP
++[eap] = noop
++[files] = noop
[sql]   expand: %{User-Name} -> zupvez10
[sql] sql_set_user escaped user --> 'zupvez10'
rlm_sql (sql): Reserving sql socket id: 31
[sql]   expand: SELECT id, username, attribute, value, op           FROM radcheck           WHERE username = '%{SQL-User-Name}'           ORDER BY id -> SELECT id, username, attribute, value, op
        FROM radcheck           WHERE username = 'zupvez10'           ORDER BY id
[sql] User found in radcheck table
[sql]   expand: SELECT id, username, attribute, value, op           FROM radreply           WHERE username = '%{SQL-User-Name}'           ORDER BY id -> SELECT id, username, attribute, value, op
        FROM radreply           WHERE username = 'zupvez10'           ORDER BY id
[sql]   expand: SELECT groupname           FROM radusergroup           WHERE username = '%{SQL-User-Name}'           ORDER BY priority -> SELECT groupname           FROM radusergroup           WHER
E username = 'zupvez10'           ORDER BY priority
[sql]   expand: SELECT id, groupname, attribute,           Value, op           FROM radgroupcheck           WHERE groupname = '%{Sql-Group}'           ORDER BY id -> SELECT id, groupname, attribute
,           Value, op           FROM radgroupcheck           WHERE groupname = '1hour'           ORDER BY id
[sql] User found in group 1hour
[sql]   expand: SELECT id, groupname, attribute,           value, op           FROM radgroupreply           WHERE groupname = '%{Sql-Group}'           ORDER BY id -> SELECT id, groupname, attribute
,           value, op           FROM radgroupreply           WHERE groupname = '1hour'           ORDER BY id
rlm_sql (sql): Released sql socket id: 31
++[sql] = ok
rlm_sqlcounter: Entering module authorize code
sqlcounter_expand:  'SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='%{User-Name}''
[noresetcounter]        expand: SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='%{User-Name}' -> SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='zupvez10'
WARNING: Please replace '%S' with '${sqlmod-inst}'
sqlcounter_expand:  '%{sql:SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='zupvez10'}'
[noresetcounter] sql_xlat
[noresetcounter]        expand: %{User-Name} -> zupvez10
[noresetcounter] sql_set_user escaped user --> 'zupvez10'
[noresetcounter]        expand: SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='zupvez10' -> SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='zupvez10'
rlm_sql (sql): Reserving sql socket id: 30
[noresetcounter] sql_xlat finished
rlm_sql (sql): Released sql socket id: 30
[noresetcounter]        expand: %{sql:SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='zupvez10'} -> 60
rlm_sqlcounter: Check item is greater than query result
rlm_sqlcounter: Authorized user zupvez10, check_item=3600, counter=60
rlm_sqlcounter: Sent Reply-Item for user zupvez10, Type=Session-Timeout, value=3540
++[noresetcounter] = ok
[expiration] Checking Expiration time: 'January 6 2016 24:00:00'
++[expiration] = ok
++[logintime] = noop
++[pap] = updated
+} # group authorize = updated
Found Auth-Type = PAP
# Executing group from file /etc/raddb/sites-enabled/default
+group PAP {
[pap] login attempt with password "palkipud"
[pap] Using clear text password "palkipud"
[pap] User authenticated successfully
++[pap] = ok
+} # group PAP = ok
# Executing section session from file /etc/raddb/sites-enabled/default
+group session {
[sql]   expand: %{User-Name} -> zupvez10
[sql] sql_set_user escaped user --> 'zupvez10'
[sql]   expand: SELECT COUNT(*)                              FROM radacct                              WHERE username = '%{SQL-User-Name}'                              AND acctstoptime IS NULL -> SELECT COUNT(*)                              FROM radacct                              WHERE username = 'zupvez10'                              AND acctstoptime IS NULL
rlm_sql (sql): Reserving sql socket id: 29
rlm_sql (sql): Released sql socket id: 29
++[sql] = ok
+} # group session = ok
# Executing section post-auth from file /etc/raddb/sites-enabled/default
+group post-auth {
[sql]   expand: %{User-Name} -> zupvez10
[sql] sql_set_user escaped user --> 'zupvez10'
[sql]   expand: %{User-Password} -> palkipud
[sql]   expand: INSERT INTO radpostauth                           (username, pass, reply, authdate)                           VALUES (                           '%{User-Name}',                           '%{%{User-Password}:-%{Chap-Password}}',                           '%{reply:Packet-Type}', '%S') -> INSERT INTO radpostauth                           (username, pass, reply, authdate)                           VALUES (                           'zupvez10',                           'palkipud',                           'Access-Accept', '2016-01-05 21:20:45')
rlm_sql (sql) in sql_postauth: query is INSERT INTO radpostauth                           (username, pass, reply, authdate)                           VALUES (                           'zupvez10',                           'palkipud',                           'Access-Accept', '2016-01-05 21:20:45')
rlm_sql (sql): Reserving sql socket id: 28
rlm_sql (sql): Released sql socket id: 28
++[sql] = ok
++[exec] = noop
+} # group post-auth = ok
Sending Access-Accept of id 200 to 127.0.0.1 port 50024
        WISPr-Session-Terminate-Time := "2016-1-6T24:00:00"
        Session-Timeout := 3540
        Idle-Timeout := 300
        Acct-Interim-Interval := 120
Finished request 0.
Going to the next request


FreeRadius получил наш пакет и первым делом запустил секцию authorize, описанную в файле /etc/raddb/sites-enabled/default
В квадратных скобках показаны модули, которые поочередно обрабатывают наш запрос.

  • [preprocess] — конфигурируется в raddb/huntgroups и raddb/hints, мы его не используем
  • [chap] [mschap] [eap] [pap] — протоколы проверки подлинности, по дефолту freeRadius не знает какой именно мы будем использовать и в секции authorize проверяет все
  • [digest] — используется для Cisco SIP server, нам этот модуль не нужен
  • [suffix] — этот модуль парсит User-Name аттрибут в поисках realm'а, таким образом может быть настроено проксирование
  • [files] — парсит файл /raddb/users в поисках юзеров, мы используем базу данных, поэтому нам он безразличен
  • [sql] — проверяет базу данных в поисках юзера и его атрибутов, сами запросы описаны в файле /etc/raddb/sql/mysql/dialup.conf

Из выдачи дебага видно, что было сделано пять запросов к БД к пяти различным таблицам, рассмотрим их позже, а пока нам понятно, что пользователь был найден и все ок:

  • [noresetcounter] — тот самый модуль, который высчитывает сколько времени у пользователя осталось, описан в файле /etc/raddb/sql/mysql/counter.conf
  • [expiration] — проверяет атрибут expiration, если время в течении которого креденциалы действительны вышло, то неважно сколько из отведенного времени пользователь успел потратить, в доступе будет отказано
  • [logintime] — можно задать время когда креденциалы будут работать, например в обед по четвергам
  • [pap] — Found Auth-Type = PAP протокол проверки подлинности определен

Переходим к секции аутентификации и тут [pap] определяет, что User authenticated successfully. Далее запускается секция section, в ней [sql] определяется, активна ли сессия в данный момент, если да, значит кто-то уже залогинился и в доступе будет отказано. Сам запрос описан в файле /etc/raddb/sql/mysql/dialup.conf. Секция post-auth логирует успешную аутентификацию в базу данных, в таблицу radpostauth. После этого отправляется ответ.

Итак, мы видим, что при обработке пакета авторизации и аутентификации freeRadius работает с шестью таблицами:

  • radcheck
  • radreply
  • radusergroup
  • radgroupcheck
  • radgroupreply
  • radpostauth

Аутентификация и авторизация пройдена, разберемся теперь с аккаунтингом:

debug_Accounting
rad_recv: Accounting-Request packet from host 127.0.0.1 port 58851, id=142, length=177
        Acct-Session-Id = "4D2BB8AC-00000098"
        Acct-Status-Type = Interim-Update
        Acct-Authentic = RADIUS
        User-Name = "zupvez10"
        NAS-Port = 0
        Called-Station-Id = "00-02-6F-AA-AA-AA:My Wireless"
        Calling-Station-Id = "00-1C-B3-AA-AA-AA"
        NAS-Port-Type = Wireless-802.11
        Connect-Info = "CONNECT 48Mbps 802.11b"
        Acct-Session-Time = 11
        Acct-Input-Packets = 15
        Acct-Output-Packets = 3
        Acct-Input-Octets = 1407
        Acct-Output-Octets = 467
# Executing section preacct from file /etc/raddb/sites-enabled/default
+group preacct {
++[preprocess] = ok
[acct_unique] WARNING: Attribute NAS-Identifier was not found in request, unique ID MAY be inconsistent
[acct_unique] Hashing 'NAS-Port = 0,,NAS-IP-Address = 127.0.0.1,Acct-Session-Id = "4D2BB8AC-00000098",User-Name = "zupvez10"'
[acct_unique] Acct-Unique-Session-ID = "55c4c93f54bb88a7".
++[acct_unique] = ok
[suffix] No '@' in User-Name = "zupvez10", looking up realm NULL
[suffix] No such realm "NULL"
++[suffix] = noop
++[files] = noop
+} # group preacct = ok
# Executing section accounting from file /etc/raddb/sites-enabled/default
+group accounting {
[detail]        expand: %{Packet-Src-IP-Address} -> 127.0.0.1
[detail]        expand: /var/log/radius/radacct/%{%{Packet-Src-IP-Address}:-%{Packet-Src-IPv6-Address}}/detail-%Y%m%d -> /var/log/radius/radacct/127.0.0.1/detail-20160105
[detail] /var/log/radius/radacct/%{%{Packet-Src-IP-Address}:-%{Packet-Src-IPv6-Address}}/detail-%Y%m%d expands to /var/log/radius/radacct/127.0.0.1/detail-20160105
[detail]        expand: %t -> Tue Jan  5 21:21:18 2016
++[detail] = ok
[sql]   expand: %{User-Name} -> zupvez10
[sql] sql_set_user escaped user --> 'zupvez10'
[sql]   expand: %{Acct-Session-Time} -> 11
[sql]   expand: %{Acct-Input-Gigawords} ->
[sql]   ... expanding second conditional
[sql]   expand: %{Acct-Input-Octets} -> 1407
[sql]   expand: %{Acct-Output-Gigawords} ->
[sql]   ... expanding second conditional
[sql]   expand: %{Acct-Output-Octets} -> 467
[sql]   expand:            UPDATE radacct           SET              framedipaddress = '%{Framed-IP-Address}',              acctsessiontime     = '%{%{Acct-Session-Time}:-0}',              acctinpu
toctets     = '%{%{Acct-Input-Gigawords}:-0}'  << 32 |                                    '%{%{Acct-Input-Octets}:-0}',              acctoutputoctets    = '%{%{Acct-Output-Gigawords}:-0}' << 32 |
                                  '%{%{Acct-Output-Octets}:-0}'           WHERE acctsessionid = '%{Acct-Session-Id}'           AND username        = '%{SQL-User-Name}'           AND nasipaddress    = '%{NAS-IP-Address}' ->            UPDATE radacct           SET              framedipaddress = '',              acctsessiontime     = '11',              acctinputoctets     = '0'  << 32 |                                    '1407',              acctoutputoctets    = '0' << 32 |                                    '467'           WHERE acctsessionid = '4D2BB8AC-00000098'           AND username        = 'zupvez10'           AND nas
rlm_sql (sql): Reserving sql socket id: 26
rlm_sql (sql): Released sql socket id: 26
++[sql] = ok
++[exec] = noop
[attr_filter.accounting_response]       expand: %{User-Name} -> zupvez10
attr_filter: Matched entry DEFAULT at line 12
++[attr_filter.accounting_response] = updated
+} # group accounting = updated
Sending Accounting-Response of id 142 to 127.0.0.1 port 58851
Finished request 2.
Cleaning up request 2 ID 142 with timestamp +40
Going to the next request


Рассмотрим процесс обработки пакета Interim-Update. Запускается секция preacct, описанная в файле /etc/raddb/sites-enabled/default:

  • [acct_unique] — берет некоторые атрибуты из пакета и высчитывает хэш, далее записывает его в БД в таблицу radacct, как acctuniqueid
  • [suffix] — этот модуль парсит User-Name атрибут в поисках realm'а, таким образом может быть настроено проксирование
  • [files] — позволяет в зависимости от полученных атрибутов по разному обрабатывать пакет, настраивается в файле /etc/raddb/acct_users

Запускается секция accounting:

  • [detail] — логирует пакет в текстовый файл, настраивается в /etc/raddb/modules/detail Посмотреть эти логи мы можем /var/log/radius/radacct/127.0.0.1/
  • [sql] — логирует наш запрос в базу данных в таблицу radacct, при этом мы видим, что используется запрос UPDATE, то есть не создается новая запись, а обновляется существующая, созданная при обработке пакета start. Это важно, тк при таком подходе одна сессия будет иметь одну запись и на этом будут строиться последующие запросы для биллинга и для таких модулей, как [noresetcounter] и section.Но это работает не всегда так, тк в файле /etc/raddb/sql/mysql/dialup.conf кроме запроса accounting_update_query, который обновляет запись, имеется так же accounting_update_query_alt, который в случае с фейлом accounting_update_query создаст новую запись.
  • [exec] — модуль описан в файле /etc/raddb/modules/exec с помощью него можно динамически менять атрибуты с помощью внешней программы
  • [attr_filter.accounting_response] — модуль для настройки проксирования, определяется в файле /etc/raddb/modules/attr_filter

Отправляется ответ. Итак, мы видим, что при обработке пакета аккаунтинга freeRadius работает всего с одной таблицей:

  • radacct

Рассмотрим таблицы в базе данных MySQL


По адресу х.х.х.х/easyhotspot/phpmyadmin мы можем увидеть наши таблицы с помощью GUI. Переходим в БД easyhotspot_opensource. Нас будут интересовать семь вышеперечисленных таблиц с которыми работал freeRadius:



Рассмотрим каждую в отдельности, начнем с radcheck.



Тут мы видим две строки для нашего юзера. В первой с помощью атрибута Cleartext-Password для него задается пароль, а во второй с помощью атрибута Expiration дата по истечению которой креденциалы перестают быть действительными.

Как мы видели в логах дебага атрибут с паролем обрабатывает модуль [pap] в секции authenticate, а атрибут Expiration обрабатывает модуль [expiration] в секции authorize.

Очевидно, что эти строки сгенерировал и записал для нас наш биллинг easyhotspot. Таблица radcheck нужна для записи атрибутов, которые будут проходить проверку, а в таблицу radreply записываются те атрибуты, которые должны передаваться в ответе.



Но здесь мы видим только WISPr-Session-Terminate-Time := «2016-1-6T24:00:00». Все потому, что атрибуты могут быть сгруппированы. Если мы имеем общий биллинг-план с одинаковым набором атрибутов, зачем нам писать их в таблицу radreply для каждого пользователя.
Мы просто в таблице radusergroup указываем username пользователя, в groupname его принадлежность к группе и приоритет.



А дальше в таблице radgroupreply в groupname указываем всю ту же группу и перечисляем все атрибуты, которые должны быть переданы тем пользователям, что в ней состоят.



И тут мы видим оставшиеся три атрибута, что приходили к нам в ответе от freeRadius. Что если мы хотим иметь в группе атрибуты не только для отправки, но и для проверки? Тут нам на помощь приходит таблица radgroupcheck.



В ней мы видим Session-Timeout, который проверяется модулем [noresetcounter] в секции authorize и Simultaneous-Use, который обрабатывается модулем [sql] в секции session. value = 1 как раз таки и говорит радиусу о том, что в один момент времни может быть только одна активная сессия, все последующие попытки авторизоваться будут получать отказ.

И все это за нас делает наш биллинг easyhotspot. Когда мы создали биллинг-план и нажали кнопку 'Generate Voucher', easyhotspot раскидал все необходимые атрибуты по таблицам самым оптимальным образом. В таблицу radpostauth при помощи модуля [sql] в секции post-auth пишутся логи успешных попыток авторизации.



Видим две записи наших успешных попыток. Остается самая объемная таблица radacct, которая создается freeRadius'ом при обработке пакетов аккаунтинга. К ней будет обращаться наш биллинг когда мы будем просить его показать нам кто из юзеров онлайн, кто сколько потратил времени и трафика. Так же к ней будет обращаться и сам freeRadius в тех же целях при обработке запросов от NAS.

Теперь, имея предсталение, как работают в связке NAS-freeRadius-MySQL-Easyhotspot перейдем к практической части. Запустив в продакшн через некоторое время мы увидем следующую картину в меню «Online Users».



Откуда взялись мак-адреса, мы же не генерировали подобных юзернеймов? Все дело в том, что циска в очередной раз нам доказывает, что аутентификация/авторизация и аккаунтинг могут работать независимо друг от друга. У нас открытый SSID, после того, как юзер подцепился и авторизовался, циска в пакетах аккаунтинга для него будет в поле User-Name вписывать тот username под которым он авторизовался.

А если не авторизовался? Тогда циска все равно будет посылать пакеты аккаунтинга и в поле User-Name будет вставлять мак-адрес. Получается, что все устройства, которые подцепились к открытому SSID и висят целыми днями, не подозревая об этом, заполняют нам таблицу radacct. Что, конечно же, нас не устраивает. Для решения этой проблемы воспользуемся встроенным языком freeRadius'а, который называется unlang и регулярными выражениями.

В файле /etc/raddb/sites-enabled/default в секции preacct в самом ее начале запишем следующее выражение:

#  Pre-accounting.  Decide which accounting type to use.
#
preacct {

        if (User-Name=~ /^[A-Fa-f0-9]{12}$/) {
                reject
        }

Мы создаем условие при котором, атрибут User-Name равен:

  • "~" — означает, что мы в своем условии используем регулярное выражение, записывается оно между слешами "//"
  • "^" — означает начало регулярного выражения
  • "[A-Fa-f0-9]" — в квадратных скобках указывается один символ для которого будет происходить сопоставление. Тут мы указываем диапазон значений, которым записывается hex, т.е. мак-адрес
  • "{12}" — в фигурных скобках записываем количество раз, в котором будет повторяться предыдущее выражение, мак-адрес имеет двенадцать символов.
  • "$" — конец выражения

И тогда такой пакет ждет действие «reject», то есть он будет дропнут. Проверим в режиме отладки:

rad_recv: Accounting-Request packet from host 192.168.80.100 port 32770, id=138, length=246
        User-Name = "6c709f251ec4"
        NAS-Port = 13
        NAS-IP-Address = 192.168.0.5
        Framed-IP-Address = 192.168.13.80
        NAS-Identifier = "5508"
        Airespace-Wlan-Id = 2
        Acct-Session-Id = "567be2a8/6c:70:9f:25:1e:c4/217989"
        Acct-Authentic = Remote
        Tunnel-Type:0 = VLAN
        Tunnel-Medium-Type:0 = IEEE-802
        Tunnel-Private-Group-Id:0 = "13"
        Acct-Status-Type = Interim-Update
        Acct-Input-Octets = 3566315
        Acct-Output-Octets = 99562740
        Acct-Input-Packets = 41757
        Acct-Output-Packets = 66012
        Acct-Session-Time = 8345
        Acct-Delay-Time = 0
        Calling-Station-Id = "6c-70-9f-25-2с-b2"
        Called-Station-Id = "28-94-0f-ae-be-13"
        Cisco-AVPair = "nas-update=true"
# Executing section preacct from file /etc/raddb/sites-enabled/default
+- entering group preacct {...}
++? if (User-Name=~ /^[A-Fa-f0-9]{12}$/)
? Evaluating (User-Name=~ /^[A-Fa-f0-9]{12}$/) -> TRUE
++? if (User-Name=~ /^[A-Fa-f0-9]{12}$/) -> TRUE
++- entering if (User-Name=~ /^[A-Fa-f0-9]{12}$/) {...}
+++[reject] returns reject
++- if (User-Name=~ /^[A-Fa-f0-9]{12}$/) returns reject
Finished request 7.

Работает, при этом помним, что при создании креденциалов мы сами не должны попадать под эти условия, иначе пользователь авторизуется, но аккаунтинга мы от него не увидим. Посмотрим, как теперь выглядит наша таблица «Online Users».



Мак-адресов в ней теперь не наблюдается, но видно, что некоторые сессии явно зависли. Проверим таблицу radacct, посмотрим, что случилось с пользователем «detpis7».



А с его сессией как раз таки все впорядке. Мы видим, что она началась 2015-12-24 18:18:00, а завершилась 2015-12-24 19:11:37
Почему же тогда висит у нас в биллинге со временем старта 2015-12-24 18:18:00? Посмотрим как формируется запрос к базе данных. Это происходит в файле /opt/EasyHotspot/htdocs/system/application/models/onlineusermodel.php:

return $this->db->query('select username, MAX(acctstarttime) as start, (acctstoptime) as stop, sum(acctsessiontime) as time,sum(acctoutputoctets)+sum(acctinputoctets) as packet from radacct  where (acctstoptime IS NULL) group by username');

Из запроса видно, что для каждого пользователя записи группируются вместе по максимальному времени начала сессии и выбирается строка где в acctstoptime записано NULL. Для нашего биллинга этот запрос не будет работать правильно по двум причинам.

Во первых вспомним наши тестовые запросы. Мы выяснили, что при нормальных раскладах одна сессия в таблице должна занимать одну строку. Создается она когда прилетает пакет start, а далее только обновляется. За это отвечает запрос accounting_update_query в файле /etc/raddb/sql/mysql/dialup.conf Но если он зафейлится, freeRadius воспользуется запросом accounting_update_query_alt и создаст новую строку, что у нас и произошло.

Во вторых мы видим, что время начала сессии в пределах одной сессии у нас меняется на секунду. Как это получается? NAS присылает только Acct-Session-Time, а вот acctstarttime для таблицы radacct freeRadius считает сам. Посмотрим запрос accounting_update_query_alt в файле /etc/raddb/sql/mysql/dialup.conf:

DATE_SUB('%S', \
                      INTERVAL (%{%{Acct-Session-Time}:-0} + \
                                %{%{Acct-Delay-Time}:-0}) SECOND)

Из него нам становится ясно, что сервер берет текущее время, отнимает от него Acct-Session-Time (Acct-Delay-Time у нас 0) и получает время начала сессии.

Значит весь механизм работает так: допустим пользователь авторизовался в системе в 12.00.00, допустим в 12.00.20 NAS решил послать пакет Interim-update, из 12.00.20 вычел 12.00.00, получил 20 и отправил атрибут Acct-Session-Time = 20 freeRadius'у. Наш сервер, получив это значение, воспользовался функцией MySQL DATE_SUB(date,INTERVAL expr type) из 12.00.20 вычел 20, получил 12.00.00 и записал это значение в таблицу как acctstarttime.

Допустим через 20 сек NAS сделал все тоже самое, и послал Acct-Session-Time = 40 но теперь пакет на секунду где-то в сети задержался и когда прилетел к серверу на часах было уже 12.00.41, 12.00.41 — 40 = 12.00.01 теперь в новой строке таблицы radacct acctstarttime = 12.00.01.

Посмотрите еще раз на предыдущую таблицу. Вот и получается, что запрашивая для биллинга MAX(acctstarttime) где acctstoptime = NULL мы получаем зависшую сессию.

При этом обратите внимание на значение Duration в биллинге и acctsessiontime в таблице, оно так же считается неверно. В запросе эти значения суммируются для каждого юзера, но мы не можем суммировать их, когда сессия имеет больше одной строки в таблице.

Изменим запрос так:

vi /opt/EasyHotspot/htdocs/system/application/models/onlineusermodel.php

return $this->db->query('SELECT username, MAX(radacctid), MAX(acctstarttime) as start, MAX(acctstoptime) as stop, CAST(max(acctsessiontime)/60 AS UNSIGNED) as time,max(acctoutputoctets)+max(acctinputoctets) as packet, framedipaddress FROM radacct GROUP BY acctuniqueid HAVING stop IS NULL');

Во первых сгруппируем запрос не по username, по acctuniqueid. Это уникальное значение в пределах сессии. Раз на MAX(acctstarttime) мы положится не можем, будет в пределах сессии брать максимальное значение radacctid, при этом не сумму, а максимальное значение acctsessiontime в пределах сессии разделим на 60, чтобы сразу получить результат в минутах и сложим между собой в пределах сессии максимальные значения acctoutputoctets и acctinputoctets и теперь из всего этого найдем те строки где stop = NULL.

Это и будут активные сессии с правильными значениями времени и трафика с учетом тех проблем, что мы выявили ранее. При этом добавим в запрос колонку framedipaddress, она показывает IP-адреса наших пользователей. Добавим ее в биллинг.

vi /opt/EasyHotspot/htdocs/system/application/views/onlineusers_view.php

<?php if (!defined('BASEPATH')) exit('No direct script access allowed'); ?>
<?php $this->load->view('header') ?>

<h1><?=$action?></h1>


<table class="stripe">
<tbody>
        <tr>
                <th><?=$this->lang->line('username')?></th>
                <th>Start</th>
                <th>Duration</th>
                <th>Packet</th>
		<th>IP-Address</th>  
                <th>Force Disconnect</th>
        </tr>


        <?php foreach ($onlineusers->result() as $row): ?>
        <tr>
                <td><?=$row->username;?></td>
                <td><?=$row->start;?></td>
                <td><?=$row->time;?></td>
                <td><?=$row->packet;?></td>
		<td><?=$row->framedipaddress;?></td>
                <td><?=anchor('onlineuser/disconnect/'.$row->username,'disconnect','class="disconnect" ')?></td>
        </tr>
        <?php endforeach;?>
</tbody>
</table>




<? $this->load->view('footer'); ?>

Рассмотрим, как запрашиваются данные для странички [ Cashier Menu ] -> Voucher Management.

vi /opt/EasyHotspot/htdocs/system/application/models/vouchermodel.php +22

Идет запрос к таблице voucher_list На самом деле это не таблица, а представление. Его описание, как и для всех созданных таблиц мы можем найти в файле /opt/EasyHotspot/install/database_with_sample.sql.

VIEW `voucher_list` AS select `v`.`id` AS `id`,`v`.`username` AS `username`,`v`.`password` AS `password`,`v`.`billingplan` AS `billingplan`,`v`.`valid_until` AS `valid_until`,`b`.`type` AS `type`,`b`.`amount` AS `amount`,`b`.`valid_for` AS `valid_for`,`b`.`price` AS `price`,(sum(`ra`.`acctsessiontime`) / 60) AS `time_used`,if((`b`.`type` = _latin1'time'),(`b`.`amount` - (sum(`ra`.`acctsessiontime`) / 60)),_latin1'null') AS `time_remain`,((sum(`ra`.`acctoutputoctets`) + sum(`ra`.`acctinputoctets`)) / 1048576) AS `packet_used`,if((`b`.`type` = _latin1'packet'),(`b`.`amount` - (sum((`ra`.`acctoutputoctets` + `ra`.`acctinputoctets`)) / 1048576)),_latin1'null') AS `packet_remain`,`v`.`isexported` AS `isexported`,`v`.`isprinted` AS `isprinted`,if((`b`.`type` = _latin1'time'),if(((sum(`ra`.`acctsessiontime`) / 60) >= `b`.`amount`),_latin1'exp',_latin1'valid'),if((((sum(`ra`.`acctoutputoctets`) + sum(`ra`.`acctinputoctets`)) / 1048576) >= `b`.`amount`),_latin1'exp',_latin1'valid')) AS `valid` from ((`voucher` `v` left join `radacct` `ra` on((`v`.`username` = `ra`.`username`))) join `billingplan` `b` on((`b`.`name` = `v`.`billingplan`))) group by `v`.`username

Смысл в том, что представление собирает данные из других таблиц, как при обычном запросе, а мы обращаемся к представлению, как к обычной таблице.

Если рассматривать наше представление, как обычный запрос, то нам нужно переделать его так:

select id, username, password, billingplan, valid_until, type, amount, valid_for, price, sum(time_used) AS time_used, if((type = _latin1'time'),(amount - sum(time_used)),_latin1'null') AS time_remain, sum(packet_used) AS packet_used, if((type = _latin1'packet'),((amount - sum(packet_used)) / 1048576),_latin1'null') AS packet_remain, isexported, isprinted, if((type = _latin1'time'),if((sum(time_used) >= amount),_latin1'exp',_latin1'valid'),if((sum(packet_used) >= amount),_latin1'exp',_latin1'valid')) AS valid
from (select v.id AS id,v.username AS username,v.password AS password, v.created_by AS created, v.created_time AS created_time, ra.acctuniqueid as acctuniqueid, v.billingplan AS billingplan,v.valid_until AS valid_until,b.type AS type,b.amount AS amount,b.valid_for AS valid_for,b.price AS price,(max(ra.acctsessiontime) / 60) AS time_used,if((b.type = _latin1'time'),(b.amount - (max(ra.acctsessiontime) / 60)),_latin1'null') AS time_remain,((max(ra.acctoutputoctets) + max(ra.acctinputoctets)) / 1048576) AS packet_used,if((b.type = _latin1'packet'),(b.amount - (max((ra.acctoutputoctets + ra.acctinputoctets)) / 1048576)),_latin1'null') AS packet_remain,v.isexported AS isexported,v.isprinted AS isprinted,if((b.type = _latin1'time'),if(((max(ra.acctsessiontime) / 60) >= b.amount),_latin1'exp',_latin1'valid'),if((((max(ra.acctoutputoctets) + max(ra.acctinputoctets)) / 1048576) >= b.amount),_latin1'exp',_latin1'valid')) AS valid from ((voucher v left join radacct ra on((v.username = ra.username))) join billingplan b on((b.name = v.billingplan))) group by v.username, ra.acctuniqueid) as b
group by username

Здесь мы используем запрос с подзапросом, который позволяет из сгруппированных в пределах сессии данных получить суммы максимальных значений acctsessiontime, acctoutputoctets и acctinputoctets по всем сессиям для каждого пользователя.

Логика такая: если пользователь имел три сессии и в таблице radacct имеется пятнадцать строк по пять строк на сессиию, то наш подзапрос выведет три строки с максимальными значениями данных для каждой сессии. А потом основной запрос суммирует эти данные сессий для каждого пользователя. В итоге мы собираем все данные таким образом, что в итоге представление по своей структуре колонок остается таким же, как и оригинальное.

Нюанс здесь в том, что представление не может иметь подзапрос. Но зато одно представление может запрашивать данные у другого. Создадим представление voucher_list_0 на котором будет основываться основное представление voucher_list:

mysql -u easyhotspot -p easyhotspot_opensource
Enter password: easyhotspot
create VIEW voucher_list_0 AS select v.id AS id,v.username AS username,v.password AS password,ra.acctuniqueid as acctuniqueid, v.billingplan AS billingplan, v.valid_until AS valid_until,b.type AS type,b.amount AS amount,b.valid_for AS valid_for,b.price AS price,(max(ra.acctsessiontime) / 60) AS time_used,if((b.type = _latin1'time'),(b.amount - (max(ra.acctsessiontime) / 60)),_latin1'null') AS time_remain,((max(ra.acctoutputoctets) + max(ra.acctinputoctets)) / 1048576) AS packet_used,if((b.type = _latin1'packet'),(b.amount - (max((ra.acctoutputoctets + ra.acctinputoctets)) / 1048576)),_latin1'null') AS packet_remain,v.isexported AS isexported,v.isprinted AS isprinted,if((b.type = _latin1'time'),if(((max(ra.acctsessiontime) / 60) >= b.amount),_latin1'exp',_latin1'valid'),if((((max(ra.acctoutputoctets) + max(ra.acctinputoctets)) / 1048576) >= b.amount),_latin1'exp',_latin1'valid')) AS valid from ((voucher v left join radacct ra on((v.username = ra.username))) join billingplan b on((b.name = v.billingplan))) group by v.username, ra.acctuniqueid;

Удалим старое представление:

drop view voucher_list;

Заменим его новым:

create VIEW voucher_list AS select id, username, password, billingplan, valid_until, type, amount, valid_for, price, sum(time_used) AS time_used, if((type = _latin1'time'),(amount - sum(time_used)),_latin1'null') AS time_remain, sum(packet_used) AS packet_used, if((type = _latin1'packet'),((amount - sum(packet_used)) / 1048576),_latin1'null') AS packet_remain, isexported, isprinted, if((type = _latin1'time'),if((sum(time_used) >= amount),_latin1'exp',_latin1'valid'),if((sum(packet_used) >= amount),_latin1'exp',_latin1'valid')) AS valid from voucher_list_0 group by username; 

Теперь тоже самое нужно сделать для странички [ Cashier Menu ] -> Postpaid. Оригинальное представление выглядит так:

VIEW `postpaid_account_list` AS select `postpaid_account`.`id` AS `id`,`postpaid_account`.`realname` AS `realname`,`postpaid_account`.`username` AS `username`,`postpaid_account`.`password` AS `password`,(sum(`radacct`.`acctsessiontime`) / 60) AS `time_used`,(sum((`radacct`.`acctoutputoctets` + `radacct`.`acctinputoctets`)) / 1048576) AS `packet_used`,`postpaid_account`.`bill_by` AS `bill_by`,(`postplan`.`price` * (sum(`radacct`.`acctsessiontime`) / 60)) AS `time_price`,(`postplan`.`price` * (sum((`radacct`.`acctoutputoctets` + `radacct`.`acctinputoctets`)) / 1048576)) AS `packet_price`,`postpaid_account`.`valid_until` AS `valid_until` from ((`postpaid_account` left join `radacct` on((`postpaid_account`.`username` = `radacct`.`username`))) join `postplan` on((`postplan`.`name` = `postpaid_account`.`bill_by`))) group by `postpaid_account`.`username`


Готовый запрос с подзапросом, который заменит `postpaid_account_list` выглядит так:

select  id, realname, username, password, sum(time_used) AS time_used, sum(packet_used) AS packet_used, bill_by, time_price, packet_price, valid_until
from (select `postpaid_account`.`id` AS `id`,`postpaid_account`.`realname` AS `realname`,`postpaid_account`.`username` AS `username`,`postpaid_account`.`password` AS `password`,(max(`radacct`.`acctsessiontime`)/60 ) AS `time_used`,(max((`radacct`.`acctoutputoctets` + `radacct`.`acctinputoctets`)) / 1048576) AS `packet_used`,`postpaid_account`.`bill_by` AS `bill_by`,(`postplan`.`price` * (max(`radacct`.`acctsessiontime`) / 60)) AS `time_price`,(`postplan`.`price` * (max((`radacct`.`acctoutputoctets` + `radacct`.`acctinputoctets`)) / 1048576)) AS `packet_price`,`postpaid_account`.`valid_until` AS `valid_until` from ((`postpaid_account` left join `radacct` on((`postpaid_account`.`username` = `radacct`.`username`))) join `postplan` on((`postplan`.`name` = `postpaid_account`.`bill_by`))) group by `postpaid_account`.`username`, radacct.acctuniqueid ) list
group by username

Создадим представление postpaid_account_list_0 на котором будет основываться основное представление postpaid_account_list:

create VIEW postpaid_account_list_0 AS select  `postpaid_account`.`id` AS `id`,`postpaid_account`.`realname` AS `realname`,`postpaid_account`.`username` AS `username`,`postpaid_account`.`password` AS `password`,(max(`radacct`.`acctsessiontime`)/60 ) AS `time_used`,(max((`radacct`.`acctoutputoctets` + `radacct`.`acctinputoctets`)) / 1048576) AS `packet_used`,`postpaid_account`.`bill_by` AS `bill_by`,(`postplan`.`price` * (max(`radacct`.`acctsessiontime`) / 60)) AS `time_price`,(`postplan`.`price` * (max((`radacct`.`acctoutputoctets` + `radacct`.`acctinputoctets`)) / 1048576)) AS `packet_price`,`postpaid_account`.`valid_until` AS `valid_until` from ((`postpaid_account` left join `radacct` on((`postpaid_account`.`username` = `radacct`.`username`))) join `postplan` on((`postplan`.`name` = `postpaid_account`.`bill_by`))) group by `postpaid_account`.`username`, radacct.acctuniqueid;

Удалим старое представление:

drop view postpaid_account_list;

И создадим новое:

create VIEW postpaid_account_list AS select  id, realname, username, password, sum(time_used) AS time_used, sum(packet_used) AS packet_used, bill_by, time_price, packet_price, valid_until from postpaid_account_list_0 group by username;


Теперь нужно вспомнить, что freeRadius с помощью своих модулей тоже обращается к radacct. Вспомним про модуль [noresetcounter], описанный в файле /etc/raddb/sql/mysql/counter.conf.

vi /etc/raddb/sql/mysql/counter.conf +110

sqlcounter noresetcounter {
counter-name = Session-Timeout
check-name = Session-Timeout
reply-name = Session-Timeout
sqlmod-inst = sql
key = User-Name
reset = never
query = "SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='%{${key}}'"
}

В этом конфиге мы можем настроить различные счетчики. noresetcounter, как понятно из названия не сбрасывается. То есть, если пользователю дали 3600 секунд, то по истечению их он уже не сможет пользоваться этими креденциалами никогда. Так же можно настроить счетчики, которые скидываются раз в день, неделю, месяц.

  • check-name = Session-Timeout — этот параметр говорит нам о том, что freeRadius в соответствующих таблицах (radcheck и radgroupcheck) будет проверять атрибут Session-Timeout
  • reply-name = Session-Timeout — этот же атрибут будет возвращать, если после вычитания из изначально заданного Session-Timeout того что вернет sql-запрос останется положительное число.

Но самое интересное здесь query. Этот параметр должен вернуть число, которое пользователь уже потратил. Логика нашего запроса будет такая же, как и прежде. В подзапросе мы получим максимальные значения AcctSessionTime во всех сессиях для заданного юзера, а в основном запросе сложим их вместе. Мы применяем встроенную mysql функцию IFNULL, если в таблице ничего не найдется, она вернет 0. Заменим строчку

query = "SELECT IFNULL(SUM(AcctSessionTime),0) FROM radacct WHERE UserName='%{${key}}'" 

на

query = "SELECT SUM(b) FROM (SELECT IFNULL(MAX(AcctSessionTime),0) as b FROM radacct WHERE UserName='%{${key}}'GROUP BY acctuniqueid) as list"

Перейдем к следующему модулю, а вернее к знакомой нам секции session и ее модулю [sql] Запрос этого модуля описан в файле /etc/raddb/sql/mysql/dialup.conf в разделе «Simultaneous Use Checking Queries».

simul_count_query = "SELECT COUNT(*) \
                             FROM ${acct_table1} \
                             WHERE username = '%{SQL-User-Name}' \
                             AND acctstoptime IS NULL"

С помощью этого запроса определяется сколько активных сессий имеется для пользователя. Запрос возвращает количество, а в таблице radgroupcheck у нас имеется атрибут Simultaneous-Use := 1, то есть только один пользователь в один момент времени может использовать выданные ему крединциалы.

Еще раз посмотрим на данные акаунтинга для юзера detpis7:

image

Как вы думаете какое число вернет запрос simul_count_query? Не будем гадать, проверим:

mysql> SELECT COUNT(*)
    -> FROM radacct
    -> WHERE username = 'detpis7'
    -> AND acctstoptime IS NULL;
+----------+
| COUNT(*) |
+----------+
|        3 |
+----------+
1 row in set (0.00 sec)


Хоть активных сессий и нет, при таком запросе detpis7 никогда больше не сможет авторизоваться. Изменим запрос:

vi /etc/raddb/sql/mysql/dialup.conf +300

simul_count_query = "SELECT COUNT(*) FROM (SELECT username, MAX(radacctid), MAX(acctstoptime) as stop, MAX(acctstarttime) FROM ${acct_table1} GROUP BY acctuniqueid HAVING username = '%{SQL-User-Name}' AND stop IS NULL) list"

Проверим в mysql:

mysql> SELECT COUNT(*) FROM (SELECT username, MAX(radacctid), MAX(acctstoptime) as stop, MAX(acctstarttime) FROM radacct GROUP BY acctuniqueid HAVING username = 'detpis7' AND stop IS NULL) list
    -> ;
+----------+
| COUNT(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

Таким образом мы рассмотрели, как установить и настроить hotspot с базовыми функциями биллинга.
Грищенко Алексей @imakabr
карма
8,0
рейтинг 0,0
Сетевой инженер
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Администрирование

Комментарии (6)

  • 0
    Думаю, все подобные рецепты по установке и/или конфигурированию сервисов пора описывать в виде Dockerfile'ов или скриптов с комментариями: а) проще воспроизвести, б) проще поправить
    • 0
      Эта статья не просто рецепт или набор скриптов, здесь я старался показать с помощью режима отладки freeRadius'а, как он обрабатывает пакет изнутри, как с помощью специальных инструментов можно посылать тестовые пакеты с желаемыми атрибутами, как freeRadius работает с таблицами БД, куда какие атрибуты записывает и какие откуда извлекает и сделать все это последовательно. Так, чтобы человек после ее прочтения не привязывался к конкретному NAS'у или биллингу, а понимал, как это работает в общем. Честно говоря, в рунете подобной информации в свое время я не нашел и хотелось бы думать, что эта статья будет полезной именно с точки зрения понимания работы системы изнутри, а не просто, как какой-то пошаговый how to.
      • 0
        К вашему стремлению претензий нет: ) У вас отличная схема, подробные детали.

        Вопрос в том, что, например, я хочу воспроизвести и пощупать ваше предложение, разобраться что к чему, как можно это еще переконфигурировать. И для этого я должен ставить на свою систему весь набор перечисленного софта: wget, mysql, freeradius, easyhotspot и т.д. (оцените примерное время на копирование команд, проделывание это вручную, возможные ошибки в последоватльности действий) Это, в целом, повышает порог для подключения к вашему проекту. Если же бы вы предоставили готовую сборку, например, на основе docker'а, то тогда было бы дело пары команд развернуть копию вашей системы и реально опробовать со своим хотспотом. Плюс из статьи можно было бы исключить некоторые детали типа установки wget, и сосредоточиться на ключевых моментах с точки зрения понимания работы изнутри.

        Правильно ли понимаю, что можно использовать и какие-то другие точки доступа, а не только Cisco WLC5508? И, в целом, здесь две компоненты: биллинг и точка доступа Wi-Fi? Биллинг у нас содержит: radius-сервер + бд + веб-морда к бд (портал управления пользователями). Есть ли другие подобные easyhotspot решения?
        • 0
          Я понял вашу мысль и в будущем постараюсь учитывать такие моменты.

          Да, можно использовать любое устройство, которое может работать как клиент радиус-сервера(NAS) и будет поддерживать эти атрибуты:
          WISPr-Session-Terminate-Time
          Session-Timeout = 3600
          Idle-Timeout = 300
          Acct-Interim-Interval
          Тогда связку из radius-сервер + бд + веб-морда к бд (портал управления пользователями) переделывать вообще не придется.
          При чем вместо точки доступа, которая поддерживает все эти функции можно использовать напримерCoovaChilli. Это опен-сурс проект, потомок Chillispot, который я упоминал в статье. Смысл в том, что этот сервис устанавливается на шлюз между внутренней сетью и интернетом. Пользователь подключается к обычной точки доступа, которая не имеет функций captive portal, но когда пытается попасть в инет, трафик проходит через шлюз, а там Chillispot его редиректит на страницу авторизации и дальше работает с радиус-сервером, как WLC5508 который я описывал в статье. По идее этот сервис можно поднять и на вай-фай роутере с openWRT. Из подобных проектов есть еще PepperSpot, NoCat, WiFiDog.
          А вот подобные easyhotspot'у опен-сурс решения я не находил. Прелесть easyhotspot в том, что он имеет удобный и простой интерфейс заточенный под хотспот. Имеется два вида пользователей биллингом с разными правами. Администратор создает необходимые биллинг-планы, выставляет цену, а так называемый продавец только генерирует необходимое количество ваучеров.
          По сути мы все это можем сделать и через phpmyadmin и есть какой-то проект, который через веб-интерфейс работаес в mysql с таблицами радиуса, предосталяя более удобное управление пользователями, но это все не то.
  • 0
    Интересно есть ли на UniFi от компании Ubiquiti возможность прикрутить freeradius.
    • 0
      Беглый просмотр выдачи гугла показал, что многие подключают UniFi к Windows NPS для авторизации по вай-фай пользователей из базы AD. Значит и к freeraduis подключить можно, он вместо mysql так же поддерживает ldap. Но при этом список радиус атрибутов, который поддерживает UniFi я так и не нашел(

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.