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