Приветствую, коллеги.
Пролог
Возникла у нас задача – сделать гостевой интернет. Т.е. гость приходит, подключается к сети (WiFi или кабель) и пытается выйти в Интернет. При попытке зайти на сайт у него «должно спросить пароль».
Надо отметить, подобные вопросы о решении такой задачи часто возникают на форумах. Есть куча платного софта (не рассматривался в принципе, ибо задача низкоприоритетная) и какие-то бесплатные решения. Поиск по форумам не дал определиться с выбором, посему было решено в свободные минуты сделать сие самостоятельно.
Основными требования:
Ресурсы на создание – как человеческие (рабочее время), так и материальные – минимальны.
Нагрузка – маленькая. Обычно такой системой будут пользоваться не более 10 человек в день.
Критичность доступности – низкая. Если система сломалась – то ремонтировать будут в последнюю очередь. Потеря нескольких пакетов – не принципиальна.
Изолированность – полная. Если сломают какие-либо части – локальная сеть не должна пострадать.
Платформонезависимость – клиентами будут всякие гаджеты – от телефона до большого компа.
Реализация
Посмотрев на требования и существующие ресурсы, решено было использовать маршрутизатор на линуксе с один имеющимся реальным IP-адресом. Так как расположение гостей точно не оговаривалось, создали отдельный гостевой VLAN в локальной сети с мыслью, что при необходимости любой порт превращается в гостевой. Так как лишних серверов не было, то использовали виртуальную машину на Hyper-V, пробросив к ней два VLAN – гостевой и интернет. В качестве ОС поставили CentOS 6.2 – как первый попавшийся под руку. На нем было штатно настроены DHCP и named. Поднят и настроен httpd для работы только на внутреннем интерфейсе. Ну и защита обычная.
Настройка
Собственно, изначально идея была такая: маршрутизатор (линукс) делает DNAT на свой внутренний интерфейс для всех, кроме IP-адресов, входящих в некий список. А те, кто входит в список – им делаем SNAT наружу. После изучения доков пришлось доставить два пакета – conntrack и ipset. Первый дает возможность, в частности, оборвать все текущие IP-сессии. Это нужно делать после изменения правил трансляции. Второй дает возможность оперировать списками IP-адресов, которые понимает iptables.
Это реализовано командами, помещенными в rc.local:
echo 1 > /proc/sys/net/ipv4/ip_forward
ipset -N good iphash
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
iptables -t nat -A PREROUTING -m set! --set good src -j DNAT --to 192.168.88.1
conntrack -F
Здесь 192.168.88.1 – адрес самого линукса на внутреннем интерфейсе. Eth1 – внешний интерфейс.
Т.е. SNAT мы делаем всем. А вот тем, кто не входит в список good, мы говорим, что на самом деле они хотят не Яндекс, а наш локальный веб-сервер.
Собственно, после этого путем добавления/убирания необходимых адресов в ipset “good” с последующим conntrack –F
мы можем манипулировать разрешениями IP-адресам ходить в интернет.
Аутентификация и авторизация
Теперь было необходимо сделать так, чтобы попасть в Интернет можно было только по паролю. Идея реализации такова:
- Создаем в httpd индексную страницу, которая спрашивает пароль. Если пароль верный – помещает IP-адрес запрашивающего в ipset “good”. Далее, httpd настраивается так, чтобы при 404 ошибке он отображал индексную страницу с запросом пароля. Т.е. при таких условиях внутренний IP-адрес при попытке попасть на любой HTTP-адрес будет отображать индексную страницу локального сервера с запросом пароля.
- Пишем php-скрипт, генерирующий пароль. Этот скрипт должен находиться на отдельном сервере. Доступ к скрипту должен быть ограничен. Например, доступ может быть у секретаря. Как именно генерировать пароль – каждый может придумать сам. Я сделал три вида паролей – пароль, действующий на всех до конца суток, пароль, действующий для всех, но до какого-то конкретного времени (по границе часа, например 14:00, 17:00) и пароль, действующий для конкретного IP-адреса до конкретного времени (по границе часа).
- Пишем php-скрипт, который проверяет пароль и, в случае правильного пароля, добавляет IP-адрес в список разрешенных, а также сбрасывает текущие сессии. Тут есть один нюанс – апач работает от имени специального юзера, а для манипулирования списком адресов и сброса сессий необходимы права суперпользователя. Здесь вариантов несколько – или настройка системы sudo (с чем не очень хотелось разбираться в связи с недостатком времени), или создание трубы (pipe), с одной стороны которой висит скрипт от имени суперпользователя и читает команды, а с другой php-скрипт пишет необходимые команды скрипту. Я для себя выбрал именно такой метод. Для создания файла типа pipe используется команда
mknod pipename p
Мои скрипты
В принципе, дальше уже идет творчество. Каждый может сделать так, как ему хочется/умеет. Я приведу исходники своих скриптов для придания целостности статье.
В моих скриптах, как писалось выше, три типа пароля. Пароли идентифицируются по первому символу. Пароль, который действует для всех до конца суток, начинается со звездочки. Пароль, который для всех, но с ограничением по времени – начинается “#NN-“, где NN – время действия пароля. Пароль для конкретного IP с ограничением по времени начинается “$NN-“, где NN – время действия пароля. В качестве пароля используются отдельные символы md5-хеша строки, получаемой из конкатенации даты, секрета (для всех типов), времени (для 2-го и 3-го типов) и IP-адреса (для третьего типа). Количество, порядок выборки символов из хеша, а также секрет – должны задаваться и, очевидно, совпадать на генераторе паролей и на проверяющем скрипте.
Некоторые нюансы скриптов
- Страница, запрашивающая пароль, выводит некий ID, который клиент должен сообщить секретарю. На самом деле это последний октет IP адреса. Используется только в генерации пароля с привязкой к IP-адресу.
- Массив
$symbols
задает — какие символы из хеша будут использоваться в качестве пароля. Я использую шесть символов, можно использовать любое количество, больше нуля. - Команда
debug
, отправленная в pipe, приводит к выводу в дебаг-файл таблицы текущих IP-адресов со временем действия доступа. Пример:echo debug >pipe
- Для того, чтобы все нормально работало, необходимо раз в час (по крону, желательно в :00 или :01 минуту) в pipe писать слово
update
, а в полночьreload
Генератор паролей (index.php)
<HTML>
<FORM method="get" action=gen.php>
<input name="PwdType" type="radio" value="Common" checked>Common Password</INPUT>
<input name="PwdType" type="radio" value="CommonTill">Common Password till
<select name="Till">
<option value=в__8">8:00</option>
...
<option value=в__23">23:00</option>
</select>
</INPUT>
<input name="PwdType" type="radio" value="Personal">Personal Password</INPUT>
<select name="PersonalTill">
<option value=в__8">8:00</option>
...
<option value=в__23">23:00</option>
</SELECT>
Client ID:<input name="ClientID" value=0>
<INPUT type=submit value="Generate password">
</FORM>
</HTML>
Генератор паролей (gen.php)
<?
$Secret="123"; //Common secret
$d=date("Y-m-d"); //Current Date
$symbols=array(0,4,5,8,1,30); //Symbols in md5 hash for password. Numbers must be in 0..31
$ipnet="192.168.88.";
if ( $PwdType == "Common" )
{
$str=$d."-".$Secret;
$r=md5($str);
$res="*";
foreach ($symbols as &$i) {$res=$res.substr($r,$i,1);};
};
if ( $PwdType == "CommonTill" )
{
$Till=utf8_decode($Till);
$Till=substr($Till,1,strlen($Till)-2);
$str=$d."-".$Till."-".$Secret;
$r=md5($str);
$res="#".$Till."-";
foreach ($symbols as &$i) {$res=$res.substr($r,$i,1);};
};
if ( $PwdType == "Personal" )
{
$ip=$ipnet.$ClientID;
$PersonalTill=utf8_decode($PersonalTill);
$PersonalTill=substr($PersonalTill,1,strlen($PersonalTill)-2);
$str=$d."-".$PersonalTill."-".$ip."-".$Secret;
$r=md5($str);
$res="$".$PersonalTill."-";
foreach ($symbols as &$i) {$res=$res.substr($r,$i,1);};
};
?>
<H2>Password="<?print $res;?>"</H2>
<H1 align=center><a href="index.php">Return</a></H1>
Проверяльщик паролей (index.php)
<HTML>
<!--0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF-->
<?
$addr="192.168.88.1";
$a=getenv("REMOTE_ADDR");
$s=getenv("SERVER_NAME");
$ipnet="192.168.88.";
$p=strpos($a,$ipnet);
if ($p === false)
{
print "<B>Internal error: <I>Wrong network (Network=$ipnet, Address=$a)</I></B>
n";
exit;
};
if ($p > 0 )
{
print "<B>Internal error: <I>Wrong network (Position=$p)</I></B>
n";
exit;
};
?>
Please, call XXXX to get password. Your ID is <STRONG><? print substr($a,strlen($ipnet));?></STRONG>
<FORM action=http://<?print $addr;?>/do.php method=post>
<CENTER>
Password
<INPUT name=pwd type=password>
<INPUT type=submit value="Activate Internet">
</CENTER>
</FORM>
</HTML>
Проверяльщик паролей (do.php)
<?
$Secret="123"; //Common secret
$d=date("Y-m-d"); //Current Date
$symbols=array(0,4,5,8,1,30); //Symbols in md5 hash for password. Numbers must be in 0..31
$ipnet="192.168.88.";
$ip=getenv("REMOTE_ADDR");
$pwd=$_POST["pwd"];
$fc=substr($pwd,0,1); //first charter is code of password type
$PipeFile="./pipe";
if ($pwd == "")
{
print "<H1 align=center>Wrong empty password.
<a href='/'>return</a></H1>";
exit;
};
if ( $fc == "*" )
{
$str=$d."-".$Secret;
$r=md5($str);
$res="*";
foreach ($symbols as &$i) {$res=$res.substr($r,$i,1);};
$Till=25;
};
if ( $fc == "#" )
{
$p=strpos($pwd,"-");
$Till=substr($pwd,1,$p-1);
$str=$d."-".$Till."-".$Secret;
$r=md5($str);
$res="#".$Till."-";
foreach ($symbols as &$i) {$res=$res.substr($r,$i,1);};
};
if ( $fc == "$" )
{
$p=strpos($pwd,"-");
$PersonalTill=substr($pwd,1,$p-1);
$str=$d."-".$PersonalTill."-".$ip."-".$Secret;
$r=md5($str);
$res="$".$PersonalTill."-";
foreach ($symbols as &$i) {$res=$res.substr($r,$i,1);};
$Till=$PersonalTill;
};
if ($pwd != $res)
{
print "<H1 align=center>Wrong password.
<a href='/'>return</a></H1>";
exit;
};
file_put_contents ( $PipeFile, substr($ip,strlen($ipnet))." $Tilln", FILE_APPEND);
?>
<H1 align=center>Access to Internte granted</H1>
Скрипт, мониторящий pipe и делающий изменения.
#!/bin/bash
PipeFile="/var/www/html/pipe"
LogFile="/var/log/script.log"
ErrFile="/var/log/script.err"
DebugFile="/var/log/script.debug"
ipnet="192.168.88."
ipset_prog="/usr/sbin/ipset"
ipset_setname="good"
ctr_prog="/usr/sbin/conntrack"
function debug()
{
local d=`date`
echo "$d Debuging started" >>$DebugFile
for i in `seq 1 254`;
do
if [ ${b[$i]} != "0" ]; then
echo "b[$i] = ${b[$i]}" >>$DebugFile
fi
done;
echo "==================== Debuging finised =================" >>$DebugFile
}
function initfunc()
{
#Init
for i in `seq 1 254`;
do
b[$i]="0"
done;
(
d=`date`
echo "$d Init function called"
$ipset_prog -F $ipsetname
$ctr_prog -F
) &>>$LogFile
};
function update()
{
(
local d1=`date`
local d=`date +%H`
echo "$d1 Update function called"
for i in `seq 1 254`;
do
if [ ${b[$i]} -gt "0" ] && [ ${b[$i]} -le "$d" ]; then
b[$i]=0
ip2="$ipnet$i"
echo "Deleting address $ip2" &>>$LogFile
$ipset_prog -D $ipset_setname $ip2 &>>$LogFile
$ctr_prog -F
fi
done
) &>>$LogFile
} #update
################# Start program #################
initfunc
while true;
do
while read line;
do
a=( $line )
ip=${a[0]}
tm=${a[1]}
if [ "$ip" == "reload" ]; then
initfunc
continue
fi
if [ "$ip" == "update" ]; then
update
continue
fi
if [ "$ip" == "debug" ]; then
debug
continue
fi
b[$ip]=$tm
ip2="$ipnet$ip"
( d=`date`
echo "$d Added address $ip2 with time:$tm"
$ipset_prog -A $ipset_setname $ip2 &>>$LogFile
$ctr_prog -F
) &>>$LogFile
update
done <$PipeFile
done
Автор: umma