Мы — долго запрягаем, быстро ездим, и сильно тормозим.
www.lissyara.su —> статьи —> FreeBSD —> программы —> HotSpot

HotSpot

Автор: levantuev.


В нашем городе очень плохо развита услуга Хот-спот.
Хот-спот (от англ. hot spot — «горячее пятно») — участок местности (например, помещение офиса, кафе, кампуса, станция метро), где при помощи портативного устройства (ноутбука или наладонника), работающего по беспроводному протоколу радиодоступа Wi-Fi, можно получить доступ к интернету.
Но назло развитию я решил внедрить это дело в Грилль-Бар.

Имеем:
1) Точка доступа D-Link 2100AP
2) Тачку с FreeBSD 7.0
3) Канал в внешний мир
4) ip адреса:
re0 (192.168.0.1) - смотрит в точку доступа
fxp0 (192.168.1.2) - смотрит в adsl модем (router)

диапазон локальной сети - 192.168.0.0/24
DNS сервер - 87.103.161.61
IP Точки доступа - 192.168.10.10

Задача:
*Авторизация пользователей должна проходить через веб-интерфейс.
*По истичению времени пользователь отключается автоматически.
*Должна быть админ.панель для генерации логинов и паролей.
*IP адреса должна присваиваться по DHCP.
*Ифнормация о пользователях должна храниться в mysql

В статье подразумевается что у вас -
1) Установлена связка apache20+php5+mysql50
2) Ядро собрано с поддержкой:
options IPFIREWALL
options IPFIREWALL_VERBOSE
options IPFIREWALL_DEFAULT_TO_ACCEPT
options IPFIREWALL_FORWARD
options IPDIVERT
options DUMMYNET

3) Компьютер подключен к интернету и выступает в качетсве шлюза

И так начнем

Авторизация и отключение пользователей осуществляется добавлением/удалением правил в ipfw через php
root# ee /usr/local/www/data/config.php


<?php

define('DEBUG', true);

define('conf_DB_HOST', 'localhost'); 	// хост БД
define('conf_DB_USER', ''); 	// юзер БД
define('conf_DB_PASS', ''); 	// его пароль
define('conf_DB_NAME', 'wifi');	// Имя БД

define('RULE_NUM_MIN', 400);
define('RULE_NUM_MAX', 600);

define('CLIENTS_IP_BEGIN', '192.168.0.2');
define('CLIENTS_IP_COUNT', '253');

define('CLIENTS_TIME', '3600');	// время для клиента (1 час)

define('RULE_ADD_IP', '/usr/local/bin/sudo ipfw add %s allow ip from any to %s');
define('RULE_ADD_IP2', '/usr/local/bin/sudo ipfw add %s allow ip from %s to any');
define('RULE_DEL_IP', '/usr/local/bin/sudo ipfw del %s');
define('RULE_DEL_IP2', '/usr/local/bin/sudo ipfw del %s');

$db_link = mysql_connect(conf_DB_HOST, conf_DB_USER, conf_DB_PASS);

if (!$db_link) return cms_errors('Подключение к базе данных не выполнено!');

if (!mysql_select_db(conf_DB_NAME, $db_link)) 
return cms_errors('Подключение к базе данных не выполнено!!!');

function cms_errors($text)
{
	if (DEBUG) echo $text;
	return false;
}

function dumpVarX(&$Var, $Var_s = null)
{
	echo "<div align='left' class='debug'>";
	dumpVar($Var, 0, $Var_s);
	echo "<div>";
	return true;
}

function dumpVar(&$Var, $Level = 0, $Var_s = null)
{
	if ($Level > 4)
	{
		echo "<b>...</b> LEVEL > 4<br>\n";
		return;
	}
	$is_ob_ar = false;
	$Type = gettype($Var);
	if (is_array($Var)) {$is_ob_ar = true; $Type = "Array[".count($Var)."]";}
	if (is_object($Var)) $is_ob_ar = true;
	if ($Level == 0)
	{
		if ($Var_s) 
        echo "\n<br>\n<b><span style=\"color:#ff0000\">".$Var_s." = {</span></b>";
		if ($is_ob_ar && count($Var)) echo "<pre>\n";
		else echo "\n<tt>";
		$Level_zero = 0;
	}
	if ($is_ob_ar)
	{
		echo "<span style=\"color:#05a209\">$Type</span>\n";
		for (Reset($Var), $Level++; list($k, $v)=each($Var);)
		{
			if (is_array($v) && $k==="GLOBALS") continue;
			for ($i=0; $i<$Level*3; $i++) echo " ";
			echo "<b>".HtmlSpecialChars($k)."</b> => ";
			dumpVar($v, $Level);
		}
	}
	else
	{
		if (is_string($Var) && strlen($Var)>400)
			echo '('.$Type.') <span style="color:#35BBFA">
                        strlen = '.strlen($Var).'</span>'."\n";
		else echo '('.$Type.') "<span style="color:#0000FF">
                ',HtmlSpecialChars($Var),'</span>"'."\n";
	}
	if (isset($Level_zero))
	{
		if ($is_ob_ar && count($Var)) echo "</pre>";
		else echo "</tt>";
		if ($Var_s) echo "<b><span style=\"color:#ff0000\">}</span></b><br>\n";
	}
	return true;
}

?>

Скрипт авторизации пользователя
root# ee /usr/local/www/data/add.php

<?php

require_once('config.php');

$user = get_user($_GET['login'], $_GET['pass']);

if ($user)
{
	switch ($user['status'])
	{
		case 0:
			if (add_rule($user)) echo '<h2>Вы законектились !</h2>';
			else echo 'Ошибка правило не добавлено !';
			break;
		case 1: echo '<h2>Вы уже подклюдчены</h2>'; break;
		case 2: echo '<h2>Логин уже был использован</h2>'; break;
		case 3: echo '<h2>Логин отключен</h2>'; break;
		default: echo 'Error'; break;
	}
} else echo '<h2>Невеный логин/пароль !</h2>';


//dumpVarX($_GET, 'GET');
//dumpVarX($user, 'User');
//dumpVarX($_SERVER['REMOTE_ADDR'], 'REMOTE_ADDR');

// авторизация

function get_user($login, $pass)
{
	$user = null;
	if (!$login || !$pass) return null;
	$login = addslashes($login);
	$sql = 'SELECT * FROM users WHERE username="'.$login.'" 
        AND password="'.$pass.'" LIMIT 1';
	$res = mysql_query($sql);
	if ($res) $user = mysql_fetch_assoc($res);
	return $user;
}

// добавление правила

function add_rule($user)
{
	$user_ip = $_SERVER['REMOTE_ADDR'];
	$current_date = time();

// dumpVarX($user_ip, 'user_ip');

	if (!checkip($user_ip)) return false;
	$temp = 0;
	$sql = 'SELECT rule_num FROM users WHERE status=1 ORDER BY rule_num';

	$res = mysql_query($sql);
	if ($res)
	{
		$t = mysql_fetch_array($res);
		if (!$t) $rule_num = RULE_NUM_MIN;
		else {
//			dumpVarX($t,'t');
			while ($temp = mysql_fetch_array($res))
			{
//				dumpVarX($temp,'temp');
				if (($t[0]+1) < $temp[0]) break;
				$t = $temp;
			}
			if ($t[0] < RULE_NUM_MAX) $rule_num = $t[0]+1; else return false;
		}
	} else return false;

	$command = sprintf(RULE_ADD_IP, $rule_num, $user_ip);
//	dumpVarX($command, 'command');

	exec($command);
	
	$command2 = sprintf(RULE_ADD_IP2, $rule_num+100, $user_ip);
//	dumpVarX($command2, 'command');

	exec($command2);
	
	$sql = 'UPDATE users SET status=1, time_begin=NOW(), 
        rule_num='.$rule_num.' WHERE id='.$user['id'];
//	dumpVarX($sql, 'sql');
	mysql_query($sql);

	return true;
}

function checkip($ip)
{
	if (!$ip) return false;
	$user_ip = explode('.', $ip);
	$check_ip = explode('.', CLIENTS_IP_BEGIN);
	if (($check_ip[0] != $user_ip[0]) && $check_ip[0] != "*") return false;
	if (($check_ip[1] != $user_ip[1]) && $check_ip[1] != "*") return false;
	if (($check_ip[2] != $user_ip[2]) && $check_ip[2] != "*") return false;
	if (!(($check_ip[3] <= $user_ip[3] && ($check_ip[3] 
        + CLIENTS_IP_COUNT) >= $user_ip[3])) 
        && $check_ip[3] != "*") return false;
	return true;
}

?>

Скрипт блокировки пользователя по истичении времени
root# ee /usr/local/www/data/cron.php

<?php

require_once('config.php');

check_users();

function check_users()
{
	$sql = 'SELECT * FROM users WHERE status=1 
        AND time_begin > 0 AND (TIME_TO_SEC(TIMEDIFF(NOW(), time_begin)) > 
        '.CLIENTS_TIME.')';
//	dumpVarX($sql);
	$res = mysql_query($sql);
	if ($res)
	{
		while ($user = mysql_fetch_assoc($res))
		{
			$command = sprintf(RULE_DEL_IP, $user['rule_num']);
//			dumpVarX($command);
			exec($command);
			$command2 = sprintf(RULE_DEL_IP2, $user['rule_num']+100);
//			dumpVarX($command2);
			exec($command2);
			$sql = 'UPDATE users SET status=2, 
                        time_end=NOW() WHERE id='.$user['id'];
//			dumpVarX($sql);
			mysql_query($sql);
		}
	}
	return true;
}

?>

Формы для ввода данных
root# ee /usr/local/www/data/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>Администрирование</title>
	<link rel="stylesheet" type="text/css" href="admin.css" />
</head>
<body>
<div class="login">
<div class="form">
<form method="get" action="add.php">
<p><label>Логин:</label>
<input class="text" name="login" type="text" size="16"/></p>
<p><label>Пароль:</label>
<input class="text" name="pass" type="password" size="16"/></p>
<p><input class="submit" type="submit" value="Все верно!"/>
</form>
</div>
<div class="rules">
	<h1>Правила пользования Wi-Fi</h1>
	<ol>
		<li>Wi-Fi в гриль-баре бесплатный!</li>
		<li>Закажите любую позицию Wi-Fi меню</li>
		<li>Получите логин и пароль</li>
		<li>Пользуйтесь</li>
	</ol>
</div>
</div>

</body>
</html>

Админ.панель будет лежать в папке admin/
Для безопастности можно Защитить папку паролем
root# ee /usr/local/www/data/admin/admin.php

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
	<head>
	<title>Админ.панель</title>
	<link type="text/css" rel="stylesheet" href="style.css">
	</head>
<body>
<form method="post" action="admin.php">
	Количество пользователей: 
        <input type="text" value="" name="num" size=2> шт.<br><br>
	<input type="submit" value="Сгенерировать">
</form><hr>

<?php 
	require_once('config.php');
	$n = (int) $_POST['num'];
	if ($n > 10) { 
  echo 'Количество создаваемых пользователей превышено!<br><br>'; $n=0; } 
  function generate_password($number=10)  
  {  
    $arr = array('1','2','3','4','5','6',  
                 '7','8','9','0');  
    // Генерируем пароль  
    $pass = "";  
    for($i = 0; $i < $number; $i++)  
    {  
      // Вычисляем случайный индекс массива  
      $index = rand(0, count($arr) - 1);  
      $pass .= $arr[$index];  
    }  
    return $pass;  
  }

  	for ($i=0; $i<$n; $i++)
	{
		$login = generate_password(4);
		$pass = generate_password(6);
//		echo 'original: '.$pass.'<br>';  
//		echo 'login: '.$login.'<br>';
//		echo 'pass: '.$pass.'<br>';
		$sql = 'INSERT INTO users (username, password, status, rule_num) 
                VALUES("apt'.$login.'", "'.$pass.'", 0, 0)';
		$res = mysql_query($sql);
		
	}
	if ($res) echo 'Пользователи в количестве <b>'.$n.'
        </b> шт. добавлены.<br><br>';
	
	$sql = 'SELECT * FROM users';
	$res = mysql_query($sql);
	echo '<table width=\'30%\'>
        <td><b>Имя</b></td><td><b>Пароль</b></td>
        <td><b>Статус</b></td><td><b>Правило</b></td>';
	while ($data = mysql_fetch_assoc($res))
	{
		echo '<tr>';
		echo '<td>'.$data['username'].'</td>';
		echo '<td>'.$data['password'].'</td>';
		if ($data['status'] == 0) { 
                echo '<td class=\'blue\'>Неактивен</td>'; }
		if ($data['status'] == 1) { 
                echo '<td class=\'green\'>Используется</td>'; 
		echo '<td>'.$data['rule_num'].'</td>';}
		if ($data['status'] == 2) { 
                echo '<td class=\'reds\'>Использован</td>'; }
		if ($data['status'] == 3) { 
                echo '<td><b>Отключён</b></td>'; }
		echo '</tr>';
	}
	echo '</table>';
?>  

</body>
</html>

Информация о статусе пользователя (Активен, использован, не использован) хранится в mysql
Поэтому создаем базу, пользователя и структуру
CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `username` varchar(50) default NULL,
  `password` varchar(50) default NULL,
  `created` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
  `time_begin` timestamp NOT NULL default '0000-00-00 00:00:00',
  `time_end` timestamp NOT NULL default '0000-00-00 00:00:00',
  `status` tinyint(4) NOT NULL default '0',
  `rule_num` smallint(5) unsigned NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `rule_num` (`rule_num`)
) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ;

Правила ipfw
root# cat /etc/rc.ipfw
#!/bin/sh
ipfw -q -f flush
cmd="ipfw -q add"
ifout="fxp0" #Интерфейс смотрящий в модем
local="re0" #Интерфейс смотрящий в локальную сеть
stburdns="87.103.161.61" #DNS провайдера
localip="192.168.0.0/24"

$cmd 200 divert natd all from any to any via ${ifout}
$cmd 1000 fwd 192.168.0.1,80 tcp from any to any 80 via ${local}
$cmd 1100 allow ip from any to ${stburdns}
$cmd 1200 allow ip from ${stburdns} to any
$cmd 1300 deny ip from ${localip} to any via ${ifout}
$cmd 1400 deny ip from any to ${localip} via ${ifout}

Добавляем в cron задание для проверки пользователя (Истрачен лимит по времени или нет)
*/1    *    *    *    *    root    /usr/local/bin/php /usr/local/www/data/cron.php

Применяем правила
sh /etc/rc.ipfw

Теперь, что бы правила добавлялись через sudo, создаем пользователя (vasya)
и добавляем его в /usr/local/etc/sudoers:
vasya       ALL=(ALL) NOPASSWD: SETENV: ALL

Делаем что бы все скрипты выполнялись от нашего пользователя
/usr/local/etc/apache2/http.conf:
User vasya
Group vasya

все...

Теперь займемся настройкой точки доступа
По-умолчанию ip-адрес у D-Link 2100AP - 192.168.0.50 (Логин: admin)
Заходим на нем, меняем ip-адрес и жмем "Apply"



После того как мы перезагрузили точку доступа и зашли на нее по новому ip адресу (192.168.10.10)
Заходи в вкладку "Advanced" -> "DHCP server"
Делаем все как на рисунке ниже



И снова жмем "Apply" и можем свободно подключаться к точке доступа, дефолтный ssid: dlink.
На компьютере подключенному по wi-fi пробуем зайти на любой сайт xxx.ru, должно выйти окно авторизации, при правильном вводе пароля добавляется правило в firewall, если по конфигам, то через 1 час это правило удаляется.

скрипты
файл скачан размер размещён примечание
hotspot.zip
1377 18kb 2009-08-21 php скрипты hotspot

Документация
http://nigma.ru/index.php?ui=sypperpit&s=Wi-Fi%20hotspot%20on%20FreeBSD



размещено: 2009-08-24,
последнее обновление: 2009-10-20,
автор: levantuev


dimidrol80, 2009-08-24 в 16:08:01

Статя понравилась =) А теперь немного критики
1. Нет статистики по пользователям
2. Правилами запрещено токо 80 порт по остальным все открыто
что не есть хорошо
3. При такой схеме пароль будет запрашывать даже у тех у которых не надо запрашывать

Kolesya, 2009-08-24 в 21:24:20

dimidrol80 прав, но:
1. кому она нужна если "user uknown" в 99.9%
2. убрать "options IPFIREWALL_DEFAULT_TO_ACCEPT", default поставить в DENY
3. исходя из п.1 от этого никуда не денешся :) Если логины/пасы меняются достаточно часто, то нестрашно

levantuev, 2009-08-25 в 4:13:14

1.Статистика о статусе по логинам хоть и маленькая =) но есть в админ.панели.
2.Правилами для не авторизированных пользователей запрещено все кроме 80-го порта, а по 80 порту идет редирект, после авторизации правило allow any to any все меняет.
3.Kolesya ответил...

DyuS, 2009-08-25 в 5:26:39

Уж лучше варианты исполнения своих идей внесли на суд публике, а не "это не так" "это не то", слабо???

levantuev, 2009-08-25 в 5:27:52

Конечно же вынесу, как только появится время, допишу...

morom, 2009-08-25 в 10:27:26

case 2: echo '<h2>Логин уже был использован</h2>'; break;
case 2: echo '<h2>Логин отключен</h2>'; break;

Разве не case 3 должно быть?

light, 2009-08-25 в 10:30:29

с доступом по времени - это разумеется хорошо, но большинство провайдеров все еще берут по трафику

WarWar, 2009-08-25 в 12:53:16

Ну и совсем забыли про нарезку скорости.
Ну зайду я в этот бар, куплю инету и начну жесткий кач тореннтом.
Остальные пользователи где ....
Правильно в Ж....

WarWar, 2009-08-25 в 12:57:11

Если мы юзаем FreeBSD 7 то скорей всего у нас есть возможность использовать nat от ipfw.
Невсегда хорошо едеменутно перегружать правила! Многие играшки к этому относятся отрицательно (например l2.ru).
Из вашего инету поиграть в линейку на офе невыйдет!

Davnozdu, 2009-08-30 в 13:17:46

А как Вам удалось завести DHCP на D-Link 2100AP ?
Проблема в том, что у них DHCP не работает, пробывал на разных версиях прошивок.
На форуме Dlink этот вопрос поднимался, но они этому поводу молчат.

gx, 2009-09-12 в 20:10:35

Davnozdu,
у меня работает DHCP на Dlink 2100AP
прямо так как выше))

playnet, 2009-09-14 в 11:56:07

У меня по офису сеть на 2100 построена, 45 машин, 4 точки. Проблема у 2100 с дхцп реально есть, пока никого нет, оно выдает все нормально, но когда больше ~10 чел на точку, начинается проблема... частенько не может выдать айпи. Так что лучше на той же фре поднять dhcpd и получить более удобную и управляемую систему. Плюс у 2100 штатно больше 20 адресов не выдать, но учитывая выше, это и не надо.
И еще. У 2100 есть режим Multi SSID, его хорошо тоже использовать. Например, основной скрыть, запаролить и входить только админам, а рабочий вывести в отдельный влан.
Плюс есть такая штука, "роуминг точки доступа". Хороша при помещении больше 15х15м и при нескольких комнатах.

gx, 2009-09-20 в 1:56:10

действительно, не проблема и приятнее поднять dhcpd на фре и забить)

o.k., 2009-09-27 в 13:22:30

В конце 2009ого года использовать natd .......... хм

daggerok, 2009-10-03 в 19:58:41

> o.k., 2009-09-27 в 13:22:30
> В конце 2009ого года использовать natd .......... хм

и что в этом такого? или натд стал хуже работать в 2009-ом году?

hranitel_y2k, 2009-11-19 в 9:26:24

Для более полного контроля и удобной настройки можно использовать связку chillispot+freeradius.

DrugsSexAndRockAndRoll, 2009-12-23 в 16:10:41

Статья интересная, но зачем столько ненужной работы?
Авторизация через веб + отключение по неактивности или по по кол-ву трафика, через pf делается в сотни раз проще.

playnet, 2010-01-14 в 5:13:00

наркоману выше:
Напиши такую же статью, но про pf. Будешь популярным и тебе скажут спасибо :)

Bmwden, 2010-11-23 в 18:17:03

Отличное решение!
Поставлю вместо поршивого DSA-3110 HSE
Спасибо!

arh665, 2011-09-23 в 15:29:42

скажите, а переменная %s откуда появляется и куда пропадает в конфигурационном файле и зачем вообще нужна, если используется один раз в правилах ipfw и все?

EvilX, 2012-02-28 в 14:13:22

sql-injection в два счёта. Спасибо за халяву.
А уходя дропну базу. Ибо нехуй.

levantuev, 2012-02-28 в 14:22:56

Леня ты что ле? ))



 

  Этот информационный блок появился по той простой причине, что многие считают нормальным, брать чужую информацию не уведомляя автора (что не так страшно), и не оставляя линк на оригинал и автора — что более существенно. Я не против распространения информации — только за. Только условие простое — извольте подписывать автора, и оставлять линк на оригинальную страницу в виде прямой, активной, нескриптовой, незакрытой от индексирования, и не запрещенной для следования роботов ссылки.
  Если соизволите поставить автора в известность — то вообще почёт вам и уважение.

© lissyara 2006-10-24 08:47 MSK

Время генерации страницы 0.063 секунд
Из них PHP: 54%; SQL: 46%; Число SQL-запросов: 80 шт.
Исходный размер: 70539; Сжатая: 14650