First of all that I want to tell you - this is not for people unfamiliar with Linux, iptables and apache. You really should know exactly what are you doing and this text only needs to give you idea and push you in right direction, because if you're just copying commands, you will not get too far. I'm not trying to scare you, just to point out that I didn't go too far in details. Also, this is a text about Linux as router which utilize iptables and apache, so if you don't have that, you can just read, but not try out. I guess, if you are a guru, you could achieve similar result with other tools besides iptables and apache, but you're on your own then. Let's start.
USAGE
You have Linux router and, while you want to deny your users internet access, you also want to display them page about that. You want to deny your users access to certain sites and, instead of desired site, show them page with warning/notification that they've been naughty little users or that that site is forbidden. Other weird ideas...
BASIC STUFF
Ok, I have small server which (mainly) serves as a router for about 20 users/neighbours and we share internet. Problem is when someone don't want internet for upcoming month or is not there or simple when someone forgets to chip in for internet. Until now, we had simple
iptables solution - REJECT all packets and allow only IP addresses (or MAC) that paid for internet with something like this:
iptables -I FORWARD -i ext0 -o ext1 -j DROP
iptables -I FORWARD -p ALL -s 192.168.0.XX -j ACCEPT
iptables -I FORWARD -p ALL -d 192.168.0.XX -j ACCEPT
Then we got the idea that, when someone who didn't pay for internet visit some site get simple page (from ours Apache) telling him that he didn't paid (until now, he only could see his browser stuck while resolving site's hostname). First idea was to hack our server's DNS service, but it would be hell to do, nonextensible and, well...shitty.
So, here is simple, yet effective solution. General idea is to redirect http request packets to our server and show them simple notification page. So, first we will need is iptables stuff. Simple line that redirect specific user would be:
iptables -t nat -I PREROUTING -s 192.168.0.XX -p tcp --dport 80 -j REDIRECT --to-ports 3001
Quick explanation is that all TCP packets from user with IP 192.168.0.XX destined to port 80 would get redirected to port 3001 of our server. This is where we will be putting our apache server to listen (later on). But, hey...wait...this isn't going to work. Why? Because, when user requests (for example) http://www.mozilla.com/en-US/firefox, hostname should first be resolved, e.g. before any packets on port 80 is sent, packets to DNS server is sent. And, if we have:
iptables -I FORWARD -i ext0 -o ext1 -j DROP
then there would be no resolving, no http packets. So, we must allow DNS packets with something like this:
iptables -I FORWARD -s 192.168.0.XX -p udp --dport 53 -j ACCEPT
iptables -I FORWARD -d 192.168.0.XX -p udp --sport 53 -j ACCEPT
iptables -t nat -I PREROUTING -s 192.168.0.XX -p tcp --dport 80 -j REDIRECT --to-ports 3001
This, in some way is trade-off since our users are now not
completely isolated from internet - they got access to DNS servers. OK, first part with redirecting is over - next to setting up apache.
All you need now is to create new virtual host on Apache. YMMV, but here's all I had to do in
httpd.conf to make it work:
Listen 192.168.0.1:3001
Listen 127.0.0.1:3001
NameVirtualHost *:3001
<VirtualHost *:3001>
ServerName <ours_servers_LAN_name>
DocumentRoot /var/www/no_net/
RewriteEngine On
RewriteRule ^.* /index.html
<VirtualHost>
Append it to end of
httpd.conf, and make new htdocs root folder (here is
/var/www/no_net/) with
index.html which will show notification that user didn't pay for internet. Port for listening must be same as in iptables redirect command (3001) on which this virtual host work. As you can see, listen directive is only for localhost and our LAN, so this virtual host is not visible from internet. If you look carefully in this VirtualHost directive, you could notice two strange lines:
RewriteEngine On
RewriteRule ^.* /index.html
These lines rewrite everything in URL and replaces it with
"/index.html". Why? Because, let's suppose user goes to the site http://www.mozilla.com/en-US/firefox/.
Then it got redirected to 3001 port. From there, user's browser asks for /
en-US/firefox/ page which, of course don't exist. Easiest way to gather all GET request is to rewrite them all with mod_rewrite module. Second line does exactly that - with help of regular expression, it replaces
anything with "
/index.html". All you have to do now is to create
index.html page and that is it.
But, hey - this is too simple to be geeky. Let's spice it with some Bash magic.
ADVANCED STUFF
Since our local network is on 192.168.0.0/24 subnet, we have 253 available IPs and around 20 users only. So, basically you will have to repeat 253-20 times:
iptables -I FORWARD -s 192.168.0.XX -p udp --dport 53 -j ACCEPT
iptables -I FORWARD -d 192.168.0.XX -p udp --sport 53 -j ACCEPT
iptables -t nat -I PREROUTING -s 192.168.0.XX -p tcp --dport 80 -j REDIRECT --to-ports 3001
and 20 times part:
iptables -I FORWARD -p ALL -s 192.168.0.XX -j ACCEPT
iptables -I FORWARD -p ALL -d 192.168.0.XX -j ACCEPT
Huh. Either to have a slave to do work for you, or to learn bash.
So, here's how I did it. This is no extravagant solution, but will give you idea. Create file
/etc/allowed_ips and in it, you put last IP octet of IP's that will have internet - one octet per line. So, if 192.168.0.12 and 192.168.0.34 have internet, this file will look like:
1
12
34
Our subnet is static, and for the sake of simplicity, I ommited whole IP addresses. Also, notice that we need to enable server itself (which is 192.168.0.1). Now, create script ip_allowing (put it wherever you want) with content like this:
#!/bin/bash
FILE=/etc/allowed_ips
ip_listing=`cat $FILE`
function check_exist {
for i in $ip_listing; do
if [ ${i:0:1} != "#" ]; then
if [ "$i" = "$1" ]; then
return 1
fi
fi
done
return 0
}
for ip in `seq 1 254`; do
check_exist $ip
if [ $? -eq 1 ]; then
#ip exist
iptables -I FORWARD -p ALL -s 192.168.0.$ip -j ACCEPT
iptables -I FORWARD -p ALL -d 192.168.0.$ip -j ACCEPT
echo allowing 192.168.0.$ip
else
#ip doesn't exist
iptables -I FORWARD -s 192.168.0.$ip -p udp --dport 53 -j ACCEPT
iptables -I FORWARD -d 192.168.0.$ip -p udp --sport 53 -j ACCEPT iptables -t nat -I PREROUTING -s 192.168.0.$ip -p tcp --dport 80 -j REDIRECT --to-ports 3001
fi
done
No voodoo magic here. Function check_exist takes number (last octet) as argument, and checks whatever that IP is allowed and returns 1 if it is, 0 if it isn't. Main part just goes in loop from 1 to 254, and either allow DNS and redirect or allows all packets. You might have noticed line:
if [ ${i:0:1} != "#" ]; then
This line allows us to add comments to /etc/allowed_ips file, and ignore them, so our file could be like this now:
#server
1
#neighbour 1
12
#neighbour 2
34
Pretty handy, isn't it. All what is left is to call our new script (and give it exec permission, but you all know that - otherwise you wouldn't go this far in reading) from wherever script you setup iptables. Example would be:
iptables -I FORWARD -i ext0 -o ext1 -j DROP
/etc/rc.d/ip_allowing
Where now?
Well, this is simple solution. There are a lot of things that can be changed. For example, you might want to allow and deny internet access by MAC, not by IP addresses. Or, if you have larger subnet, this could be slower, so you would want your iptables commands to work with IP ranges. Or, you could pull your allowed IP addresses not from file, but from LDAP or MySQL with a bit more of parsing. You could also redirect only certain sites by using both source and destination in iptables REDIRECT command. On the other hand, looking at apache part, you see that our virtual host is hungry - it eats all URLs, so you couldn't have two pages in it. If you want that (for example to have
index.html and
contact.html), you would have to play with regular expressions to replace everything except "/contact.html" to /
index.html.
Possibilities are endless, as with everything with Linux, you just have to know what you want and how to do it - simple as that.