šŸ“œ ā¬†ļø ā¬‡ļø

The difficulty of administering guest hotspots. Practice (part 2)

In the previous article, I talked about how to track the status of isc-dhcpd, now about the practical methods of applying this scheme.

When working in highly loaded guest Wi-Fi networks, there is the problem of tracking and adding clients that have expanded access to external services. The best option is to control access by MAC addresses (entering a bunch of addresses in dhcpd.conf), but as practice shows, it is rather inconvenient because You really cannot control the state of the hosts that are already configured and their work.

Also, due to the large number of random devices, it becomes necessary to reduce the time of the lease or expand the range of ip addresses issued, which in turn slows down the search for the desired host. Control over MAC addresses and issuing static addresses can lead to certain problems. Let's say in your company there are 2 Wi-Fi subnets, which are two virtual networks (VLANs) distributed from multiside access points. In this case, one Wi-Fi network is open, and the second is password protected.
For the client to work correctly, you need to add the device’s mac address to 2 pools simultaneously. This leads to confusing configs and problems with administration.

Further description requires from the reader at least a basic understanding of the work of pf, squid, isc-dhcpd and web servers.

')

Which exit ?


The output is a little not obvious, but rather simple. Why dhcpd itself does not control user access to the network?
So, we have isc-dhcpd, we have a list of clients who need to open access to services on the firewall when they appear and disable access when they disappear.
We need a database of MAC addresses of our clients, a convenient web snout for their prompt addition, a firewall that can be managed remotely without manual editing of the config and a little patience.

To begin, we must prepare our system.
I am a FreeBSD supporter and therefore I am doing all the major projects on it, because Many system components have convenient control. As a firewall, we will use a bunch of pf and pftabled . The pf firewall compares favorably with others in that it has dynamic tables that can be changed without touching the basic rules. pftabled is a daemon that can monitor the state of these tables using a specially crafted package.

Approximate config 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 


What gives us the described config?


Why do we only allow port 80.8080? Because by opening other ports, we lose control over the use of our network.
For the correct operation of the scheme, you must configure the local wpad service. To configure it, you need to create an IN A record in the dns server and specify the address of your web server as the IP address. Below is an example config file wpad.dat which should be in the root of your web server.
 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"; } 

File content reads as follows:

How to configure squid in transparent mode is better to read here .

Well, with the preparatory work done. Then you can check the operation of the system kernel.
If dhcpd correctly delivered the settings to the user's host, then with the settings ā€œauto-detect proxy server settingsā€ enabled, the browser will request the wpad.dat file and all its requests will be sent through the proxy server. If you add a host to the androids table, then all traffic will go past the proxy server.

We form a database for work


The easiest and fastest data processing option is to store the current settings of the issued addresses in an accessible place with high-speed access. For me, this place is memcached. You can use databases, file storages, in general everything your heart desires. For the local database, I’m using the standard address bar I use DB_File.

As described in the first post, you need to connect an event handler to dhcpd to create a database of issued addresses. Below is the text of my handler.
 #!/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 } ], }); } 


In the /usr/local/etc/pftabled.key file there should be a key that uses pftabled.
The /var/db/macs.db file should contain the mac base of addresses in the form of DB_File HASH.
When the commit command appears from dhcpd - the received mac address is checked against the base of addresses and if it is there, a packet is added to pftabled that adds the corresponding ike ip address to the androids table. When a release or expire command appears, the ip address from the androids table is automatically deleted.

The lifetime of the data in memcached and pftabled is set to 19,200 seconds (about 5 hours), the same time is set in the dhcpd.conf config in the parameter maximum-lease-time. This is done so that the hosts in memcached and pf are not lost.

pftabled should be run with the following parameters:
pftabled_flags = "- d -a 127.0.0.1 -k /usr/local/etc/pftabled.key -t 19200"
Add the following line to crontab
* / 5 * * * * / sbin / pfctl -t androids -T expire 19200> / dev / null 2> & 1

ATTENTION!!! /usr/local/etc/pftabled.key should have permissions 0444

Well, actually the last part is the web access rule.
The code is simple and unpretentious, I have enough for work. Therefore, without beautiful and frills.

 #!/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; } 


Well, in general, that's all.
I hope the read will be useful to you and guide you to certain thoughts on the possible improvement of the system for your company.

PS: To create hotspots with the ability to open access by the user, you can replace the initial redirection of the proxy with redirection to the web server with the authorization form and from there add the host to pftabled without using isc-dhcpd.
PPS: use "functions.pm" in the last file is a handler for input data of forms and environment variables. The variable check can be rewritten to CGI. For those who wish, I can lay out the functionality and source of the module "functions.pm"

Aborche 2011

Source: https://habr.com/ru/post/134154/


All Articles