Simult Chek: различия между версиями

Материал из noname.com.ua
Перейти к навигацииПерейти к поиску
 
(не показаны 33 промежуточные версии 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-серверов в моем случае ).
+
проверяет только наличие сессий в таблице acct_table1 (обычно radacct). Сессии в таблице acct_table1 создаются только при получении Acct-пакета от NAS-а (VPN-серверов в моем случае ).
В результате, возможна ситуация когда из-за различных причин, как например, нагрузка NAS-a или потерь в сети, в результате возникает некоторый промежмежуток времени, в течении которого возможно авторизоваться повторно.
+
В результате, возможна ситуация когда из-за различных причин, как например, нагрузка NAS-a или потерь в сети, возникает некоторый промежмежуток времени, в течении которого возможно авторизоваться повторно.
   
   
Строка 30: Строка 32:
 
</TR>
 
</TR>
 
<TR>
 
<TR>
<TD>1</TD><TD>Установка соединения (ppp)</TD><TD> </TD><TD></TD>
+
<TD>1</TD><TD>Установка соединения (ppp)</TD><TD></TD><TD></TD>
 
</TR>
 
</TR>
 
<TR>
 
<TR>
Строка 36: Строка 38:
 
</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)
4NAS формирует пакет 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 секунд.

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

Однако, присутвуют следующие недостатки:

  1. Существует вероятность отличная от нуля что функция RAND() вернет 2 раза одинаковые значения и запросы все же остануться "одновременными".
  2. Внесение задержки создает неудобства для абонентов (были жалобы).
  3. Слишком большое значение множителя ( более 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 нужна