Создание гостевого доступа в Интернет с 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

Поделиться публикацией
Комментарии 26
    0
    Где же вы были пол года назад? Искренне благодарю за мануал.
      +3
      всегда хорошо получить опыт и сделать все самому, но не стоит забывать, что технология captive portal реализована во многих продуктах, в том числе и бесплатных, к примеру pfsense.
        0
        pfSense is a free, open source customized distribution of FreeBSD

        Против FreBSD ничего не имею, но ни разу с ней не работал. А с RedHat-клонами общаюсь давно и регулярно.
          0
          В случае с pfSense — вам бы даже консоль открывать не пришлось — все штато через веб-интерфейс.
            0
            И была бы непонятная мне коробка, которую ни настроить, ни защитить толком не смогу.
            К тому же, ЕМНИП, под фряху нет Integration Services для Hyper-V. А сие не есть хорошо.
        +1
        Да уж проще mikrotik + daloRadius…
          0
          Возможно и проще. Но пришлось бы прикручивать разовые пароли к радиусу. Стоит ли оно того?
            +1
            Зачем daloRadius, там же есть втроенный Hotspot + User Manager. Там настраивается доступ по паролю, либо без пароля по нажатию кнопки.
          0
          Сколько времени у Вас заняло создание этой «системы» с нуля до рабочего состояния?
            0
            Рабочая неделя на все. Начиная с установки Линукса и создания VLAN на коммутаторах до запуска.
            В день уходило от получаса до двух часов активной траты времени. Т.е. я не считаю, например, времени, которое ставилась ОС. Большая часть времени ушла на скрипт, ибо на bash программил давно и очень мало и возникла там пара проблем.
            +2
            VLAN, Hyper-V, CentOS с httpd и named, conntrack и ipset, программная часть на php и bash… По-моему проще и выгодней потратить 1000 руб. и поставить на него тот же DD-WRT. Там несколько hotspot-ов, есть из чего выбрать и настройка займет всего полчаса.
              0
              Проще взять ненужный ATX-ящик, накормить отдельной сетевой картой, в неё воткнуть роутер\точку доступа, а на сам ящик навернуть ZeroShell или m0n0wall + dalo.
                +3
                Ненужный ATX-ящик — это лишних 150-200W круглосуточно и несколько децибел. Плюс меньшая аппаратная надежность, т.к. в ненужном ATX-ящике, как правило, стоят достаточно старые компоненты.
                  0
                  Ну, если он стоит в серверной — сотня лишних ватт и децибел погоды не сделают.
                  По поводу компонентов — я особо сказал «ненужный», а не старый.

                  В любом случае, решение, приведённое ОПом, имеет право на жизнь, если оно ему нравится и работает безбажно.
                  Я лишь предложил более простой и скоростной в реализации вариант.
                  0
                  Не, не, не, Давид Блейн. (с)

                  У нас ынжынэры избавляются в серверной от обычных компов, серверов-тауэров, рековых серверов старых. А я тут с системником. Виртуалка рулит — благо ресурсы на севрерах виртуализации есть.
                  Если кто понял из моего поста, что я специально поднимал Hyper-V — то это не так. Нашел на одном из серверов лишний запасной гиг памяти и поднял еще одну виртуальную машину.
                    0
                    Ну, когда ресурсы есть — сам Б-г велел. Когда же виртуалить нечего — то и системник сойдёт.
                    В любом случае, подумайте о том, чтобы выкинуть велосипед, и заменить его на pfSense или ZeroShell. Оба умеют в радиус из коробки.
                      0
                      Зачем мне радиус? Это ГОСТЕВОЙ интернет. Тут нужны разовые пароли без логинов. Пришел человек с ифоном/нокией/самсунгом/etc. Попросил интернет, пока ждет в приемной. Ему и сказали пароль. Ввел и счастлив целый час.
                        0
                        ZeroShell умеет генерить разовые ваучеры, т.е. как раз-таки гостевые аккаунты.
                        pfSense, если я не ошибаюсь, тоже это умел год назад.
                          0
                          Простите за некропостинг.
                          Скажите, а где именно в ZeroShell вы видели это функцию?
                          Второй день копаюсь с ним, так и не нашел, каким образом можно генерировать ваучеры и настроить гостевой доступ, без необходимости накатывать сертификаты и вводить логины/пароли при соединении к Wi-Fi.
                  +2
                  Как писал Лукьяненко:
                  — Данька, ты когда-нибудь ездил на велосипеде без рук или с закрытыми глазами?
                  — Ездил, — с заметной гордостью ответил мальчишка.
                  — А зачем? Проще было бы держаться за руль и смотреть на дорогу.


                  Т.е. и сделать захотелось, и развивать, ежели потребуется, проще.
                  P.S. «поставить на него тот же DD-WRT» — это речь о чем именно?
                    0
                    О сторонней прошивке DD-WRT для роутеров.
                      0
                      dd-wrt, наверно хорошо, но как-то мутно. Видимо, сказывается печальный опыт в попытках поставить и настроить сие на рутерах asus. Уж не помню что было не так, но работало оно не так, как заявлялось. К тому же, кто знает, какая моча завтра в голову стукнет? Может, придется добавлять какую-нить хитрую аутентификацию, например MAC+пароль, действующий месяц.
                  +2
                  А почему не взяли тот же chillispot?
                  Делал аналогичное решение, и выбрал chillispot+easyhotspot.
                  Если все поднимать не спеша то пара дней уйдет.
                    0
                    Изначально решил делать на виртуальной машине. Список дистрибутивов, которые поддерживаются Hyper-V (используется в продакшене на предприятии) — ограничен. Посему, особо не стал ковырять и сделал свое.
                      +2
                      Мы для клиентов поставили RB/751U-2HnD, установили туда Yota LTE модем и активировали Hotspot службу. Плюс активировали Virtual AP для сотрудников компании с шифрованием. Настройка заняла 15 минут.
                        0
                        Что-ли заморочиться и попробовать сделать такое у меня на .Net + cisco…

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

                        Самое читаемое