Simult Chek: различия между версиями
Sirmax (обсуждение | вклад) |
Sirmax (обсуждение | вклад) |
||
(не показано 35 промежуточных версий 2 участников) | |||
Строка 1: | Строка 1: | ||
+ | [[Категория:Linux]] |
||
+ | [[Категория:FreeRadius]] |
||
=Описание проблемы= |
=Описание проблемы= |
||
В сети используется VPN (accel-pptp) с авторизацией на центральном radius-сервере (freeradius). |
В сети используется VPN (accel-pptp) с авторизацией на центральном radius-сервере (freeradius). |
||
Строка 5: | Строка 7: | ||
Это связано с особенностью провеки одновременности подключений - запрос |
Это связано с особенностью провеки одновременности подключений - запрос |
||
<PRE>simul_count_query = "SELECT COUNT(*) FROM ${acct_table1} WHERE UserName=\'%{SQL-User-Name}\' AND AcctStopTime = 0"</PRE> |
<PRE>simul_count_query = "SELECT COUNT(*) FROM ${acct_table1} WHERE UserName=\'%{SQL-User-Name}\' AND AcctStopTime = 0"</PRE> |
||
− | проверяет |
+ | проверяет только наличие сессий в таблице acct_table1 (обычно radacct). Сессии в таблице acct_table1 создаются только при получении Acct-пакета от NAS-а (VPN-серверов в моем случае ). |
− | В результате, возможна ситуация когда из-за различных причин, как например, нагрузка NAS-a или потерь в сети, |
+ | В результате, возможна ситуация когда из-за различных причин, как например, нагрузка NAS-a или потерь в сети, возникает некоторый промежмежуток времени, в течении которого возможно авторизоваться повторно. |
− | Следующяя схема илюстрирует эту ситуацию |
||
+ | Следующяя схема илюстрирует эту ситуацию: |
||
+ | С UserName=test пробуют соединиться 2 различных клиента. |
||
+ | <PRE> |
||
+ | mysql> select * from radcheck where username='test'; |
||
+ | +-------+-----------+------------------+----+-----------+ |
||
+ | | id | UserName | Attribute | op | Value | |
||
+ | +-------+-----------+------------------+----+-----------+ |
||
+ | | 9295 | test | Pool-Name | := | ippool_1 | |
||
+ | | 9294 | test | Password | == | password | |
||
+ | | 9293 | test | Auth-Type | := | MS-CHAP | |
||
+ | | 9296 | test | Simultaneous-Use | := | 1 | |
||
+ | +-------+-----------+------------------+----+-----------+ |
||
+ | 4 rows in set (0.00 sec) |
||
+ | </PRE> |
||
<TABLE border=1> |
<TABLE border=1> |
||
<TR> |
<TR> |
||
− | <TD>Время ("кванты")</TD><TD>Первый клиент</TD><TD>Второй клиент</TD><TD>Radius-Сервер</TD> |
+ | <TD>Время ("кванты")</TD><TD>Первый клиент (UserName=test)</TD><TD>Второй клиент (UserName=test)</TD><TD>Radius-Сервер</TD> |
</TR> |
</TR> |
||
<TR> |
<TR> |
||
− | <TD>1</TD><TD>Установка соединения (ppp)</TD><TD> |
+ | <TD>1</TD><TD>Установка соединения (ppp)</TD><TD></TD><TD></TD> |
</TR> |
</TR> |
||
<TR> |
<TR> |
||
− | <TD>2</TD><TD>Запрос к радиусу Auth-Request</TD><TD>Установка соединения (ppp)</TD> |
+ | <TD>2</TD><TD>Запрос к радиусу Auth-Request</TD><TD>Установка соединения (ppp)</TD><TD>Получение запроса от Клиента 1, проверка атрибутов, Вычисление значения Simultaneous-Use (=1 т.к. пользователь не был подключен, отправка Access-Accept на NAS)</TD> |
</TR> |
</TR> |
||
<TR> |
<TR> |
||
+ | <TD>3</TD><TD>Получение Access-Accept, авторизация клиента, создание интерфейса, и т.п.</TD><TD>Запрос к радиусу Auth-Request</TD><TD>Получение запроса от Клиента 2, проверка атрибутов, Вычисление значения Simultaneous-Use (=1 т.к. сессия первого клиента еще не попала в acct_table1)</TD> |
||
− | <TD>2</TD><TD></TD><TD>Запрос к радиусу Auth-Request</TD> |
||
</TR> |
</TR> |
||
+ | <TR> |
||
− | |||
+ | <TD>4</TD><TD>NAS формирует пакет Acct-Start, задержка с отправкой из-за нагрузки на CPU</TD><TD>Получение Access-Accept, авторизация клиента, создание интерфейса, и т.п.</TD><TD>Ожидание Acct-Start от NAS</TD> |
||
+ | </TR> |
||
+ | <TR> |
||
+ | <TD>5</TD><TD>Отправка Acct-Start</TD><TD>Отправка Acct-Start</TD><TD>Занесенеее 2 сессий в acct_table1</TD> |
||
+ | </TR> |
||
</TABLE> |
</TABLE> |
||
+ | |||
+ | Несмотря на то, что на первый взгляд такая ситуация кажется маловероятной, это совсем не так. Я столкнулся в своей сети с тем, что абоненты согласовывая (вероятно, по телефону) время включения, использовали 1 аккаунт 2 раза. В тестовых условиях при подключении нескольких компьютеров в одной комнате удавалось подключить 4 одновременных сессии с одним UserName. |
||
+ | |||
+ | =Варианты решения= |
||
+ | ==Простой== |
||
+ | Наиболее простое решение - это внести задержку, для того что бы к моменту проверки simul_count_query сессия от одного из других пытающихся авторизоваться клиентов уже попала в acct_table1. |
||
+ | |||
+ | Для внесения задержки можно модифицировать запрос |
||
+ | <PRE> |
||
+ | authorize_check_query = "SELECT id, UserName, Attribute, Value, op \ |
||
+ | FROM ${authcheck_table} \ |
||
+ | WHERE Username = '%{SQL-User-Name}' \ |
||
+ | ORDER BY id" |
||
+ | </PRE> |
||
+ | следующим образом |
||
+ | <PRE> |
||
+ | authorize_reply_query = "SELECT id+sleep(FLOOR(0 + (RAND() * 10))), UserName, Attribute, Value, op \ |
||
+ | FROM ${authreply_table} \ |
||
+ | WHERE Username = '%{SQL-User-Name}' \ |
||
+ | ORDER BY id" |
||
+ | </PRE> |
||
+ | Этот запрос будет выполняться с задержкой 0-10 секунд. |
||
+ | |||
+ | Данное решение оказалось полностью функциональным, и после внесения модификации добиться множественных подключений не удавалось. |
||
+ | |||
+ | Однако, присутвуют следующие недостатки: |
||
+ | # Существует вероятность отличная от нуля что функция RAND() вернет 2 раза одинаковые значения и запросы все же остануться "одновременными". |
||
+ | # Внесение задержки создает неудобства для абонентов (были жалобы). |
||
+ | # Слишком большое значение множителя ( более 10 секуд) может приводить к ошибкам авторизации по таймауту. |
||
+ | Под "одновременными" запросами авторизации подразумевается что времени между запросами окажется недостаточно для внесения данных в acct_table1 по той или инной причине. |
||
+ | |||
+ | ==Вариант решения основанный на транзакциях== |
||
+ | Автор решения - [http://vorona.com.ua voron], хотя нельзя сказать что я совсем не принимал участия в разработке ) |
||
+ | Основная идея этого решения - при авторизации создавать в дополнительной таблице записи о "текущих сессиях авторизации", и использовать транзакции для блокировок строк таблицы. |
||
+ | |||
+ | ===Дополнительный процедуры и таблицы=== |
||
+ | |||
+ | |||
+ | Таблица для хранения текущих сессий авторизации |
||
+ | <PRE> |
||
+ | create table auth_sessions( |
||
+ | username varchar(64), |
||
+ | time integer not null, |
||
+ | key username(`username`(64)), |
||
+ | key time(`time`) |
||
+ | )engine=InnoDB; |
||
+ | </PRE> |
||
+ | |||
+ | Таблиа для хранения блокировок авторизующихся пользователей, единственное назначение - блокировка строк таблицы auth_sessions для нужного пользователя |
||
+ | <PRE> |
||
+ | create table auth_sessions_users( |
||
+ | username varchar(64), |
||
+ | ulock integer not null, |
||
+ | primary key username(`username`(64)) |
||
+ | )engine=InnoDB; |
||
+ | </PRE> |
||
+ | |||
+ | Таблица для логгирования (используется только при отладке) |
||
+ | <PRE> |
||
+ | CREATE table check_simul_log |
||
+ | ( |
||
+ | UserName varchar(64) NOT NULL, |
||
+ | SimulCheck integer NOT NULL, |
||
+ | Time integer NOT NULL |
||
+ | )engine=InnoDB; |
||
+ | </PRE> |
||
+ | |||
+ | |||
+ | Процедура check_simul предназначена для того что бы одновременно проверять кроме уже существующих сессий еще и текущие сессии авторизации. |
||
+ | Процедура блокирует таблицу auth_sessions_users.<BR> |
||
+ | AuthLifeTime - время жизни сесии авторизации, по истечении которого незавершенная сессия авторизации независимо от результата удалиться, ascount - auth session count, кол-во сессий сессий авторизации (активных попыток авторизоваться в текущий момент). Закомментарены запросы которые использовались для отладки. |
||
+ | |||
+ | <PRE> |
||
+ | delimiter // |
||
+ | create procedure check_simul(in SQLUserName varchar(64), in AuthLifeTime int) |
||
+ | begin |
||
+ | declare ascount integer; |
||
+ | DECLARE radacct_count INTEGER; |
||
+ | set autocommit=0; |
||
+ | start transaction; |
||
+ | INSERT INTO auth_sessions_users values(SQLUserName,1) on duplicate key update ulock=1; -- lock |
||
+ | DELETE FROM auth_sessions WHERE username=SQLUserName and time<(UNIX_TIMESTAMP()-AuthLifeTime); -- delete too old auth sessions |
||
+ | SELECT COUNT(*) INTO ascount from auth_sessions WHERE username=SQLUserName; -- Count active auth sessions (0 if no other auth sessions ) |
||
+ | INSERT INTO auth_sessions values(SQLUserName,UNIX_TIMESTAMP()); -- Add current auth session |
||
+ | UPDATE auth_sessions_users SET ulock=0 where username=SQLUserName; |
||
+ | -- INSERT into check_simul_log values(SQLUserName,ascount,UNIX_TIMESTAMP(),-1); |
||
+ | commit; |
||
+ | set autocommit=1; |
||
+ | SELECT COUNT(*) INTO radacct_count FROM radacct WHERE UserName=SQLUserName AND AcctStopTime = 0; |
||
+ | -- INSERT into check_simul_log values(SQLUserName,radacct_count+ascount,UNIX_TIMESTAMP(),radacct_count); |
||
+ | SELECT (radacct_count+ascount); |
||
+ | end; |
||
+ | // |
||
+ | delimiter ; |
||
+ | </PRE> |
||
+ | |||
+ | Удалении сессии авторизации после неуспешной по причине too many connections авторизации.<BR> |
||
+ | Т.е. если соединение отброшено по-тому что в acct_table1 присутвует активная сессия то добавлять попытку авторизации не нужно. |
||
+ | <PRE> |
||
+ | delimiter // |
||
+ | CREATE PROCEDURE postauth(in SQLUserName varchar(64),in PacketType varchar(64),in ReplyMessage varchar(255) ) |
||
+ | begin |
||
+ | INSERT into radpostauth(user, pass, reply, date, ReplyMessage) values (SQLUserName, 'Chap-Password', PacketType, NOW(), ReplyMessage); |
||
+ | if ((strcmp(lower(PacketType),"access-reject")=0) and ((lower(ReplyMessage) REGEXP lower("You are already logged in - access denied"))=1)) then |
||
+ | SET autocommit=0; |
||
+ | start transaction; |
||
+ | INSERT INTO auth_sessions_users values(SQLUserName,1) on duplicate key update ulock=1; |
||
+ | DELETE FROM auth_sessions WHERE username=SQLUserName order by time desc limit 1; |
||
+ | UPDATE auth_sessions_users SET ulock=0 WHERE username=SQLUserName; |
||
+ | commit; |
||
+ | set autocommit=1; |
||
+ | end if; |
||
+ | end; |
||
+ | // |
||
+ | delimiter ; |
||
+ | </PRE> |
||
+ | |||
+ | ===Изменения в sql.conf=== |
||
+ | <PRE>authorize_check_query = "CALL radius_authorize_check_query('%{SQL-User-Name}')"</PRE> |
||
+ | (Соответсующяя часть запроса тоже вынесена в процедуру) |
||
+ | <PRE> |
||
+ | delimiter // |
||
+ | CREATE PROCEDURE radius_authorize_check_query(IN SQLUserName varchar(255)) |
||
+ | BEGIN |
||
+ | SELECT id,UserName,Attribute,Value,op FROM radcheck WHERE Username = SQLUserName; |
||
+ | END |
||
+ | // |
||
+ | delimiter ; |
||
+ | </PRE> |
||
+ | |||
+ | |||
+ | |||
+ | <PRE>simul_count_query = "CALL check_simul('%{SQL-User-Name}',60)"</PRE> |
||
+ | <PRE>postauth_query = "CALL postauth('%{User-Name}', '%{reply:Packet-Type}', '%{reply:Reply-Message:-EmptyReplyMessage}')"</PRE> |
||
+ | |||
+ | ===Add=== |
||
+ | - показать нужный код sql.conf |
||
+ | - убрать SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; и autocommit в 4-х местах |
||
+ | - назначение postauth - уделение сессий авторизации при "долбёжке", это мона указать |
||
+ | - check_simul_log энжин иннодб |
||
+ | - неплохо бы поформатать sql, единственное что я нашёл это http://www.sqlinform.com/, java нужна |
Текущая версия на 10:33, 24 июня 2010
Описание проблемы
В сети используется VPN (accel-pptp) с авторизацией на центральном radius-сервере (freeradius). При попытке авторизации 2 и более клиентов с одинаковыми UserName может возникнуть ситуация когда пытающиеся авторизоваться (или часть из них) пройдут авторизацию успешно.
Это связано с особенностью провеки одновременности подключений - запрос
simul_count_query = "SELECT COUNT(*) FROM ${acct_table1} WHERE UserName=\'%{SQL-User-Name}\' AND AcctStopTime = 0"
проверяет только наличие сессий в таблице acct_table1 (обычно radacct). Сессии в таблице acct_table1 создаются только при получении Acct-пакета от NAS-а (VPN-серверов в моем случае ). В результате, возможна ситуация когда из-за различных причин, как например, нагрузка NAS-a или потерь в сети, возникает некоторый промежмежуток времени, в течении которого возможно авторизоваться повторно.
Следующяя схема илюстрирует эту ситуацию:
С UserName=test пробуют соединиться 2 различных клиента.
mysql> select * from radcheck where username='test'; +-------+-----------+------------------+----+-----------+ | id | UserName | Attribute | op | Value | +-------+-----------+------------------+----+-----------+ | 9295 | test | Pool-Name | := | ippool_1 | | 9294 | test | Password | == | password | | 9293 | test | Auth-Type | := | MS-CHAP | | 9296 | test | Simultaneous-Use | := | 1 | +-------+-----------+------------------+----+-----------+ 4 rows in set (0.00 sec)
Время ("кванты") | Первый клиент (UserName=test) | Второй клиент (UserName=test) | Radius-Сервер |
1 | Установка соединения (ppp) | ||
2 | Запрос к радиусу Auth-Request | Установка соединения (ppp) | Получение запроса от Клиента 1, проверка атрибутов, Вычисление значения Simultaneous-Use (=1 т.к. пользователь не был подключен, отправка Access-Accept на NAS) |
3 | Получение Access-Accept, авторизация клиента, создание интерфейса, и т.п. | Запрос к радиусу Auth-Request | Получение запроса от Клиента 2, проверка атрибутов, Вычисление значения Simultaneous-Use (=1 т.к. сессия первого клиента еще не попала в acct_table1) |
4 | NAS формирует пакет Acct-Start, задержка с отправкой из-за нагрузки на CPU | Получение Access-Accept, авторизация клиента, создание интерфейса, и т.п. | Ожидание Acct-Start от NAS |
5 | Отправка Acct-Start | Отправка Acct-Start | Занесенеее 2 сессий в acct_table1 |
Несмотря на то, что на первый взгляд такая ситуация кажется маловероятной, это совсем не так. Я столкнулся в своей сети с тем, что абоненты согласовывая (вероятно, по телефону) время включения, использовали 1 аккаунт 2 раза. В тестовых условиях при подключении нескольких компьютеров в одной комнате удавалось подключить 4 одновременных сессии с одним UserName.
Варианты решения
Простой
Наиболее простое решение - это внести задержку, для того что бы к моменту проверки simul_count_query сессия от одного из других пытающихся авторизоваться клиентов уже попала в acct_table1.
Для внесения задержки можно модифицировать запрос
authorize_check_query = "SELECT id, UserName, Attribute, Value, op \ FROM ${authcheck_table} \ WHERE Username = '%{SQL-User-Name}' \ ORDER BY id"
следующим образом
authorize_reply_query = "SELECT id+sleep(FLOOR(0 + (RAND() * 10))), UserName, Attribute, Value, op \ FROM ${authreply_table} \ WHERE Username = '%{SQL-User-Name}' \ ORDER BY id"
Этот запрос будет выполняться с задержкой 0-10 секунд.
Данное решение оказалось полностью функциональным, и после внесения модификации добиться множественных подключений не удавалось.
Однако, присутвуют следующие недостатки:
- Существует вероятность отличная от нуля что функция RAND() вернет 2 раза одинаковые значения и запросы все же остануться "одновременными".
- Внесение задержки создает неудобства для абонентов (были жалобы).
- Слишком большое значение множителя ( более 10 секуд) может приводить к ошибкам авторизации по таймауту.
Под "одновременными" запросами авторизации подразумевается что времени между запросами окажется недостаточно для внесения данных в acct_table1 по той или инной причине.
Вариант решения основанный на транзакциях
Автор решения - voron, хотя нельзя сказать что я совсем не принимал участия в разработке ) Основная идея этого решения - при авторизации создавать в дополнительной таблице записи о "текущих сессиях авторизации", и использовать транзакции для блокировок строк таблицы.
Дополнительный процедуры и таблицы
Таблица для хранения текущих сессий авторизации
create table auth_sessions( username varchar(64), time integer not null, key username(`username`(64)), key time(`time`) )engine=InnoDB;
Таблиа для хранения блокировок авторизующихся пользователей, единственное назначение - блокировка строк таблицы auth_sessions для нужного пользователя
create table auth_sessions_users( username varchar(64), ulock integer not null, primary key username(`username`(64)) )engine=InnoDB;
Таблица для логгирования (используется только при отладке)
CREATE table check_simul_log ( UserName varchar(64) NOT NULL, SimulCheck integer NOT NULL, Time integer NOT NULL )engine=InnoDB;
Процедура check_simul предназначена для того что бы одновременно проверять кроме уже существующих сессий еще и текущие сессии авторизации.
Процедура блокирует таблицу auth_sessions_users.
AuthLifeTime - время жизни сесии авторизации, по истечении которого незавершенная сессия авторизации независимо от результата удалиться, ascount - auth session count, кол-во сессий сессий авторизации (активных попыток авторизоваться в текущий момент). Закомментарены запросы которые использовались для отладки.
delimiter // create procedure check_simul(in SQLUserName varchar(64), in AuthLifeTime int) begin declare ascount integer; DECLARE radacct_count INTEGER; set autocommit=0; start transaction; INSERT INTO auth_sessions_users values(SQLUserName,1) on duplicate key update ulock=1; -- lock DELETE FROM auth_sessions WHERE username=SQLUserName and time<(UNIX_TIMESTAMP()-AuthLifeTime); -- delete too old auth sessions SELECT COUNT(*) INTO ascount from auth_sessions WHERE username=SQLUserName; -- Count active auth sessions (0 if no other auth sessions ) INSERT INTO auth_sessions values(SQLUserName,UNIX_TIMESTAMP()); -- Add current auth session UPDATE auth_sessions_users SET ulock=0 where username=SQLUserName; -- INSERT into check_simul_log values(SQLUserName,ascount,UNIX_TIMESTAMP(),-1); commit; set autocommit=1; SELECT COUNT(*) INTO radacct_count FROM radacct WHERE UserName=SQLUserName AND AcctStopTime = 0; -- INSERT into check_simul_log values(SQLUserName,radacct_count+ascount,UNIX_TIMESTAMP(),radacct_count); SELECT (radacct_count+ascount); end; // delimiter ;
Удалении сессии авторизации после неуспешной по причине too many connections авторизации.
Т.е. если соединение отброшено по-тому что в acct_table1 присутвует активная сессия то добавлять попытку авторизации не нужно.
delimiter // CREATE PROCEDURE postauth(in SQLUserName varchar(64),in PacketType varchar(64),in ReplyMessage varchar(255) ) begin INSERT into radpostauth(user, pass, reply, date, ReplyMessage) values (SQLUserName, 'Chap-Password', PacketType, NOW(), ReplyMessage); if ((strcmp(lower(PacketType),"access-reject")=0) and ((lower(ReplyMessage) REGEXP lower("You are already logged in - access denied"))=1)) then SET autocommit=0; start transaction; INSERT INTO auth_sessions_users values(SQLUserName,1) on duplicate key update ulock=1; DELETE FROM auth_sessions WHERE username=SQLUserName order by time desc limit 1; UPDATE auth_sessions_users SET ulock=0 WHERE username=SQLUserName; commit; set autocommit=1; end if; end; // delimiter ;
Изменения в sql.conf
authorize_check_query = "CALL radius_authorize_check_query('%{SQL-User-Name}')"
(Соответсующяя часть запроса тоже вынесена в процедуру)
delimiter // CREATE PROCEDURE radius_authorize_check_query(IN SQLUserName varchar(255)) BEGIN SELECT id,UserName,Attribute,Value,op FROM radcheck WHERE Username = SQLUserName; END // delimiter ;
simul_count_query = "CALL check_simul('%{SQL-User-Name}',60)"
postauth_query = "CALL postauth('%{User-Name}', '%{reply:Packet-Type}', '%{reply:Reply-Message:-EmptyReplyMessage}')"
Add
- показать нужный код sql.conf - убрать SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; и autocommit в 4-х местах - назначение postauth - уделение сессий авторизации при "долбёжке", это мона указать - check_simul_log энжин иннодб - неплохо бы поформатать sql, единственное что я нашёл это http://www.sqlinform.com/, java нужна