Pull to refresh

Трудности администрирования гостевых хотспотов. Практика (часть 2)

Reading time 11 min
Views 2.4K
В предыдущей статье я рассказал о том, как отследить состояние isc-dhcpd, теперь о практических методах применения данной схемы.

При работе в высоко нагруженных гостевых Wi-Fi сетях возникает проблема отслеживания и добавления клиентов, которые имеют расширенный доступ к внешним сервисам. Самый лучший вариант это контроль доступа по MAC адресам (занесение связки адресов в dhcpd.conf), но как показывает практика он достаточно неудобен, т.к. Вы реально не можете контролировать состояние уже занесенных в конфиг хостов и их работу.

Также из-за большого количества случайных устройств возникает необходимость уменьшения времени действия lease или расширение диапазона выдаваемых ip адресов, что в свою очередь приводит к замедлению поиска нужного хоста. Контроль по MAC адресам и выдача статических адресов может привести к определённым проблемам. Скажем у Вас в компании есть 2 Wi-Fi подсети, представляющие собой две виртуальных сети(VLAN) раздаваемые с мультисидовых точек доступа. При этом одна Wi-Fi сеть является открытой, а вторая запаролена.
Для корректной работы клиента Вам необходимо добавить mac адрес устройства в 2 пула одновременно. Это ведет к запутыванию конфига и проблемам с администрированием.

Дальнейшее описание требует от читателя хотя бы базового представления о работе pf, squid, isc-dhcpd и веб серверов.


Какой выход ?


Выход немного не очевидный, но достаточно простой. Почему бы самому dhcpd не контролировать доступ пользователей к сеть?
Итак у нас есть isc-dhcpd, есть список клиентов которым необходимо открывать на firewall доступ к сервисам при их появлении и отключать доступ при их исчезновении.
Нам понадобится база данных MAC адресов наших клиентов, удобная веб морда для их оперативного добавления, firewall которым можно управлять удалённо без ручной правки конфига и немного терпения.

Для начала мы должны подготовить нашу систему.
Я сторонник FreeBSD и поэтому все основные проекты делаю на ней, т.к. многие компоненты системы имеют удобное управление. В качестве firewall мы будем использовать связку pf и pftabled. Межсетевой экран pf выгодно отличается от других тем, что он имеет динамические таблицы, которые можно изменять не трогая основные правила. pftabled это демон умеющий при помощи специально сформированного пакета контролировать состояние этих таблиц.

Примерный конфиг pf.conf

int_if="em0"
ext_if="em1"

table <androids>
table <private_nets> persist { 10.0.0.0/8, 172.16.0.0/16, 192.168.0.0/16 }

# nat должен работать только для внешних хостов!
nat on $ext_if from { <private_nets> } to { !<private_nets> } -> ($ext_if)

# мы создаём правила для таблиц. Запросы от пользователей не входящих с таблицу <androids> будут перенаправляться на squid в transparent порт.
no rdr on $int_if from <androids> to any
# редирект не должен работать для локального apache на котором будет расположен файл wpad.dat
rdr on $int_if from ($int_if:network) to { any, !($int_if) } port { 80,8080 } -> 127.0.0.1 port 3129

# Allow DHCP
pass in on $int_if proto udp from any port bootpc to any port bootps
pass out on $int_if proto udp from any port bootps to any port bootpc

# Allow DNS
pass in on $int_if proto udp from ($int_if:network) to ($int_if) port domain keep state
pass out on $int_if proto udp from ($int_if) to ($int_if:network) port domain

# Allow Proxy access
pass in on $int_if proto tcp from ($int_if:network) to ($int_if) port 3128 flags S/SA keep state
pass in on $int_if proto tcp from ($int_if:network) to 127.0.0.1 port 3129 flags S/SA keep state

# Allow access to local HTTP Server
pass in on $int_if proto tcp from ($int_if:network) to ($int_if) port 80 flags S/SA keep state

# Allow access from <androids> to any services
pass in from <androids> to any keep state
pass out on $ext_if from ($ext_if) to any keep state
pass out on $ext_if proto icmp from any to any keep state


Что нам даёт описанный конфиг?

  • Любые запросы направленные из гостевой сети к любому хосту на порт 80,8080 будут перенаправлены в proxy сервер в порт 3129
  • Любые запросы от хостов входящих в таблицу androids будут пробрасываться во внешнюю сеть напрямую
  • Разрешены запросы к dns, dhcpd, локальному web серверу и на proxy

Почему мы разрешаем только порт 80,8080? Потому что открыв другие порты, мы теряем контроль над использованием нашей сети.
Для корректной работы схемы необходимо настроить локальный wpad сервис. Для его настройки необходимо создать IN A запись в dns сервере и указать в качестве IP адреса — адрес вашего веб сервера. Ниже примерный конфиг файла wpad.dat который должен лежать в корне вашего веб сервера.
function FindProxyForURL(url, host)
{
        if(
                isPlainHostName(host) ||
                dnsDomainIs(host, ".conf.local") ||
                localHostOrDomainIs(host, "127.0.0.1")
        ){
                return "DIRECT";
        }

        if (
                isInNet(host, "172.16.0.0", "255.255.0.0") ||
                isInNet(host, "192.168.0.0", "255.255.0.0") ||
                isInNet(host, "10.0.0.0", "255.0.0.0") ||
                isInNet(host, "127.0.0.0", "255.255.255.0")
        ){
                return "DIRECT";
        }
         if ( isInNet(myIpAddress(), "192.168.0.0", "255.255.0.0")) return "PROXY 192.168.0.1:3128";
         else return "DIRECT";
}

Содержимое файла гласит:
  • Если запрос содержит имя хоста без dns суффикса, или домен запрашиваемого хоста содержит ".conf.local", или хост резолвится в 127.0.0.1, то запросы отправлять напрямую на запрошенный хост.
  • Если запрошенный хост резолвится в адреса локальной подсети, то отправлять запрос напрямую
  • Всё остальное отправлять в proxy сервер

Как настроить squid в transparent режиме лучше почитать вот тут.

Ну-с подготовительные работы сделаны. Далее можно проверить работу ядра системы.
Если dhcpd корректно отдал настройки хосту пользователя, то при включенных настройках «автоматическое определение настроек прокси сервера», браузер запросит файл wpad.dat и будет все свои запросы отправлять через proxy сервер. Если же добавить хост в таблицу androids, то весь трафик пойдёт мимо прокси сервера.

Формируем базу данных для работы


Самый простой и быстрый вариант обработки данных это хранение текущих настроек выданных адресов в доступном месте с высокоскоростным доступом. Для меня таким местом является memcached. Вы можете использовать базы данных, файловые хранилища, в общем всё что душа пожелает. Для локальной базы мак адресов я использую штатную перловую DB_File.

Как описано в первом посте для создания базы выданных адресов нужно подключить к dhcpd обработчик событий. Ниже текст моего обработчика.
#!/usr/bin/perl
use strict;
use warnings;

use DB_File;
use IO::Socket;
use Digest::HMAC_SHA1 qw(hmac_sha1);
use Cache::Memcached::Fast;
use Sys::Syslog;

use vars qw/ $keyfile $key $lease_base $pftabled %macs/;

use constant PFTBLPORT => 56789;
use constant pfip => "127.0.0.1";

use constant PFTBLVERSION => 2;
use constant PFTABLED_CMD_ADD => 1;
use constant PFTABLED_CMD_DEL => 2;
use constant PFTABLED_CMD_FLUSH => 3;
use constant PFTBLCOMMAND => 1;
use constant PFTBLMASK => 32;
use constant SHA1_DIGEST_LENGTH => 20;
use constant PFTBLNAME => "androids";

my $command = shift;
my $ip = shift;
my $mac = shift;

my $pid = fork();

if($pid == 0) {

$pftabled = IO::Socket::INET->new(Proto     => 'udp',
  PeerPort  => PFTBLPORT,
  PeerAddr  => pfip)
  or die "Creating socket: $!\n";

openlog("publish-ip-mac","ndelay");
&load_key;
&open_memcached;
if($command eq 'commit') { &commit_address; }
elsif($command eq 'release') { &release_address; }
elsif($command eq 'expiry') { &release_address; }
elsif($command eq 'hostname') { &commit_hostname; }
else {
  syslog("info|local6","Unknown operation $command for $ip and $mac");
#print STDERR "Unknown operation $command for $ip and $mac\n";
}
closelog;
}

exit(0);

sub commit_address
{
$mac = join(":",map { sprintf("%02s",$_); } split(":",$mac));
#print STDERR "Host ".$ip." with MAC ".$mac." is alive\n";
syslog("info|local6","Host ".$ip." with MAC ".$mac." is alive");
$lease_base->set('ip/'.$mac,$ip,19200);
$lease_base->set('mac/'.$ip,$mac,19200);
$lease_base->set('lease_start/'.$ip,time(),19200);
$lease_base->set('lease_end/'.$ip,time()+19200,19200);
tie %macs, 'DB_File', '/var/db/macs.db', O_RDONLY, 0666, $DB_HASH or die "Cannot open file '/var/db/macs.db': $!\n";
syslog("info|local6","Mac: $mac Table Mac: $macs{$mac}");
if(exists $macs{$mac})
{
   syslog("info|local6","IP $ip put to pftable");
   &pftabled_operations(PFTABLED_CMD_ADD,$ip);
}
untie %macs;
}


sub commit_hostname
{
$lease_base->set('name/'.$ip,$mac,19200);
}

sub release_address
{
if($mac = $lease_base->get('mac/'.$ip))
  {
     syslog("info|local6","Host ".$ip." with MAC ".$mac." released or expired");
#     print STDERR "Host ".$ip." with MAC ".$mac." released or expired\n";
     $lease_base->delete('ip/'.$mac);
     $lease_base->delete('mac/'.$ip);
     $lease_base->delete('name/'.$ip);
     $lease_base->delete('lease_start/'.$ip);
     $lease_base->delete('lease_end/'.$ip);
  }
else
  {
#     print STDERR "Host ".$ip." without MAC info released or expired\n";
     syslog("info|local6","Host ".$ip." without MAC info released or expired");
  }
  &pftabled_operations(PFTABLED_CMD_DEL,$ip);
}


sub check_access_table
{
}

sub pftabled_operations
{
my $command = shift;
my $iparray = shift;
#print @iparray;
foreach my $addip (split("\0",$iparray))
{
  my $addr = inet_aton($addip);
  my $time = time();
  my $block = pack("C1 S1 C1",PFTBLVERSION,$command,PFTBLMASK).$addr.pack("a32 N*",PFTBLNAME,$time);
  my $digest = hmac_sha1($block, $key);
  $block .= $digest;
  $pftabled->send($block);
}
}

sub load_key
{
my $keyfile = "/usr/local/etc/pftabled.key";

if (! -r $keyfile) {
  print STDERR "Cannot Read KeyFile $keyfile\n";
  exit 1;
  }
open(KEY, "<$keyfile");
  sysread KEY, $key, SHA1_DIGEST_LENGTH;
close KEY;
}

sub open_memcached
{
$lease_base = new Cache::Memcached::Fast({
     servers => [ { address => 'localhost:11211', noreply => 1 } ],
     });
}



В файле /usr/local/etc/pftabled.key должен лежать ключ который использует pftabled.
В файле /var/db/macs.db должна лежать база mac адресов в виде DB_File HASH.
При появлении команды commit от dhcpd — полученный mac адрес проверяется по базе адресов и если он там присутствует, в pftabled отправляется пакет добавляющий в таблицу androids соответствующий маку ip адрес. При появлении команды release или expire — ip адрес из таблицы androids удаляется автоматически.

Время жизни данных в memcached и pftabled установлено в 19200 секунд(около 5 часов), такое же время устанавливается в конфиге dhcpd.conf в параметре maximum-lease-time. Это сделано для того, чтобы хосты в memcached и pf не терялись.

pftabled должен быть запущен со следующими параметрами:
pftabled_flags="-d -a 127.0.0.1 -k /usr/local/etc/pftabled.key -t 19200"
В crontab нужно добавить следующую строку
*/5 * * * * /sbin/pfctl -t androids -T expire 19200 > /dev/null 2>&1

ВНИМАНИЕ!!! /usr/local/etc/pftabled.key должен иметь права 0444

Ну и собственно последняя часть это веб рулилка доступом.
Код простой и неприхотливый, мне для работы хватает. Посему без красивостей и наворотов.

#!/usr/bin/perl

use POSIX qw(strftime);
use DB_File;
use strict;
use IO::Socket;
use Digest::HMAC_SHA1 qw(hmac_sha1);
use Cache::Memcached::Fast;

use vars qw/%sv %form %cookie %macs $lease_base $pftabled $key/;

use constant PFTBLPORT => 56789;
use constant pfip => "127.0.0.1";

use constant PFTBLVERSION => 2;
use constant PFTABLED_CMD_ADD => 1;
use constant PFTABLED_CMD_DEL => 2;
use constant PFTABLED_CMD_FLUSH => 3;
use constant PFTBLCOMMAND => 1;
use constant PFTBLMASK => 32;
use constant SHA1_DIGEST_LENGTH => 20;
use constant PFTBLNAME => "androids";

require "functions.pm";

#BEGIN { Net::ISC::DHCPd::OMAPI::_DEBUG = sub { 1 } }

$lease_base = new Cache::Memcached::Fast({
     servers => [ { address => 'localhost:11211', noreply => 1 } ],
     });

&systeminit;

tie %macs, 'DB_File', '/var/db/macs.db', O_CREAT|O_RDWR, 0666, $DB_HASH or die "Cannot open file '/var/db/macs.db': $!\n";

print "Content-Type: text/html;\r\n\r\n";
print << "[end]";
<HTML>
<HEAD>
<meta http-equiv="Content-Type" content="text/html;">
<TITLE>DHCP State</TITLE>
<STYLE>
 TD { font:14px Courier; border-left:0px; border-top:0px;  border-right:1px; border-bottom:1px; border-style: dashed; text-align: center;}
 BODY { font:14px Courier; }
 INPUT[type=button] { width: 100px; font: Verdana, Tahoma; }
 TR.red { background-color: #A0A0A0; }
 TR.head { background-color: #808080; }
 TR.green { background-color: #00C000; }
 TABLE {  }
</STYLE>
</HEAD>
<BODY>
<script>
function subm(id)
{
if(confirm("Really toggle access type for " + id))
{
 document.toggle.mac.value=id;
 document.toggle.submit();
}
}
</script>
[end]
#print "Macs list: ",join (",",keys %macs),"\n";
if($form{"mac"})
{
  &load_key;
  $pftabled = IO::Socket::INET->new(Proto     => 'udp',
        PeerPort  => PFTBLPORT,
        PeerAddr  => pfip)
        or die "Creating socket: $!\n";

  my $ip = $lease_base->get("ip/".$form{"mac"});
#  print "Form list: ",join (",",keys %form),"\n";
  if(defined($macs{$form{"mac"}}))
  {
    delete($macs{$form{"mac"}});
    &pftabled_operations(PFTABLED_CMD_DEL,$ip);
    print STDERR "MAC Address ".$form{"mac"}." with IP $ip removed from full access\n";
  }
  else
  {
    print STDERR "MAC Address ".$form{"mac"}." with IP $ip added to full access\n";
    &pftabled_operations(PFTABLED_CMD_ADD,$ip);
    $macs{$form{"mac"}} = 'full';
  }
}
print << "[end]";
<table width=100% border=0 cellspacing=0 cellpadding=1>
<form name="toggle" method=POST>
<input type=hidden name="mac" value="">
</form>
[end]
for(my $network=0;$network<255;$network++)
{
print << "[end]";
<tr><th colspan=6>Network $network</th></tr>
<tr class="head"><td>IP</td><td>MAC</td><td>Access</td><td>Hostname</td><td>Last Seen/Lease Start</td><td>Planned Expire</td></tr>
[end]

for(my $i=0;$i<256;$i++)
{
  my $ip_address = "192.168.$network.$i";
  my $mac_address = $lease_base->get('mac/'.$ip_address) || next;
  #print Dumper($lease);
  my $hostname = $lease_base->get('name/'.$ip_address);
  my $checkboxvalue = ($macs{$mac_address}) ? "Back to normal" : "Switch to full";
  my $style = ($macs{$mac_address}) ? "green" : "red";
  my $checkboxfield = ($mac_address) ? "<input type=button name=\"".$mac_address."\" value=\"$checkboxvalue\" onclick=\"subm(this.name)\">" : " ";
  print "<tr class=\"$style\"><td>";
  print join ("</td><td>",$ip_address,$mac_address||" ",$checkboxfield,$hostname||" ",time_expand($lease_base->get('lease_start/'.$ip_address)),time_expand($lease_base->get('lease_end/'.$ip_address)));
  print "</tr>\n";
}
print "<tr><td colspan=6>
</td></tr>\n";
}

print << "[end]";
<tr><th colspan=6>Registered Mac Addresses</td></tr>
[end]
foreach my $mac (keys %macs)
{
print << "[end]";
<tr><td class="red" colspan=2>$mac -> $macs{$mac}</td><td colspan=4> </td></tr>
[end]
}

print "</table></html></body>";
untie %macs;
exit(0);


sub time_expand
{
my $time = shift;
#($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time);
my $now = strftime "%a %b %e %H:%M:%S %Y", localtime($time);
return($now);
}

sub pftabled_operations
{
my $command = shift;
my $iparray = shift;
#print @iparray;
foreach my $addip (split("\0",$iparray))
{
  my $addr = inet_aton($addip);
  my $time = time();
  my $block = pack("C1 S1 C1",PFTBLVERSION,$command,PFTBLMASK).$addr.pack("a32 N*",PFTBLNAME,$time);
  my $digest = hmac_sha1($block, $key);
  $block .= $digest;
  $pftabled->send($block);
}
}

sub load_key
{
my $keyfile = "/usr/local/etc/pftabled.key";

if (! -r $keyfile) {
  print STDERR "Cannot Read KeyFile $keyfile\n";
  exit 1;
  }
open(KEY, "<$keyfile");
  sysread KEY, $key, SHA1_DIGEST_LENGTH;
close KEY;
}



Ну вот в общем то и всё.
Надеюсь прочтёное будет Вам полезным и наведёт на определённые мысли по возможному усовершенствованию системы для Вашей компании.

PS: Для создания хотспотов с возможностью открытия доступа самим пользователем, можно начальное перенаправление прокси заменить на перенаправление на веб сервер с формой авторизации и уже оттуда добавлять хост в pftabled без использования isc-dhcpd.
PPS: use «functions.pm» в последнем файле это обработчик входных данных форм и переменных окружений. Проверку переменных можно переписать на CGI. Для желающих могу выложить функционал и исходник модуля «functions.pm»

Aborche 2011
Tags:
Hubs:
+4
Comments 1
Comments Comments 1

Articles