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

в 5:41, , рубрики: linux, Сетевые технологии, метки:

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

Пролог

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js