Создание гостевого доступа в Интернет с Web-аутентификацией

Приветствую, коллеги.

Пролог

Возникла у нас задача – сделать гостевой интернет. Т.е. гость приходит, подключается к сети (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-адресам ходить в интернет.

Аутентификация и авторизация

Теперь было необходимо сделать так, чтобы попасть в Интернет можно было только по паролю. Идея реализации такова:

  1. Создаем в httpd индексную страницу, которая спрашивает пароль. Если пароль верный – помещает IP-адрес запрашивающего в ipset “good”. Далее, httpd настраивается так, чтобы при 404 ошибке он отображал индексную страницу с запросом пароля. Т.е. при таких условиях внутренний IP-адрес при попытке попасть на любой HTTP-адрес будет отображать индексную страницу локального сервера с запросом пароля.
  2. Пишем php-скрипт, генерирующий пароль. Этот скрипт должен находиться на отдельном сервере. Доступ к скрипту должен быть ограничен. Например, доступ может быть у секретаря. Как именно генерировать пароль – каждый может придумать сам. Я сделал три вида паролей – пароль, действующий на всех до конца суток, пароль, действующий для всех, но до какого-то конкретного времени (по границе часа, например 14:00, 17:00) и пароль, действующий для конкретного IP-адреса до конкретного времени (по границе часа).
  3. Пишем 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))." $Till\n", 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  

ссылка на оригинал статьи http://habrahabr.ru/post/163967/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *