Permalink
Browse files

merge master 040ee81 changes into ipv6 branch

  • Loading branch information...
2 parents d472994 + e7fedad commit c2ec707b2e960fca349b603b1db0e7f5d19240d6 @micolous committed Aug 22, 2011
Showing with 191 additions and 23 deletions.
  1. +28 −1 HACKING.md
  2. +5 −0 backend/iptables-clean
  3. +77 −22 backend/iptables.py
  4. +81 −0 captive_landing/tproxy.py
View
@@ -6,7 +6,8 @@ Tasks that I've got in mind at the moment:
I've started implementing IPv6 support in the backend of tollgate in a seperate branch. At the moment it's got some basic backend support but I haven't implemented the frontend code just yet (or the scanning module for ipv6). There's some stuff to consider though:
-* `-t nat -j REDIRECT` doesn't work in ip6tables at all. They have `-t mangle -j TPROXY` instead, which requires setting some "interesting" socket options (ie: I don't think Apache will work any more for the first stage of the captivity landing.
+
+* **Done, implemented in IPv4** `-t nat -j REDIRECT` doesn't work in ip6tables at all. They have `-t mangle -j TPROXY` instead, which requires setting some "interesting" socket options (ie: I don't think Apache will work any more for the first stage of the captivity landing.
* IPv6 privacy extensions will mean you have a lot of constantly changing IPs. We can handle this pretty easily though (at least for the outgoing connections) by having a browser window open that updates the IP address periodically (treating it similar to an IPv4 change).
* Incoming connections I want to have a flag on to say whether a host may be accessible from the outside world, defaulting to no. As part of this getting the "permanent" IPv6 RA address may be important. This can be done by a multicasted ping for most operating systems, or sniffing router advertisements.
@@ -28,3 +29,29 @@ There's no test cases, we probably should have some.
Started an `eo` (Esperanto) translation of the project a while ago, haven't really touched it since. Many strings aren't in the translation files, and the setup of where the strings actually are could be improved a lot, because it's a mess.
+## Porting to non-Linux systems ##
+
+I'd like to see this software ported to non-Linux systems. I've done some preliminary research that's come up empty as yet as to operating systems with firewalls that meet all of the following requirements for tollgate's backend to work properly.
+
+The majority of the work you'd need to do in porting the software is in the backend. There's a little bit of Linux-specific frontend code to print out the contents of the ARP table. All the backend is setup in a way that it calls from the frontend are abstracted away from iptables.
+
+Before you start work porting it to a new operating system, please consider the following list of requirements. If you can't get it to do **everything** in this list, then tollgate won't work.
+
+* Ability to filter traffic by IP and MAC address.
+* Port redirection, so captivity can work. (ie: When you have no quota it redirects HTTP requests to a web server, resets all connections)
+* Ability to filter based on quota remaining.
+* Ability to do both positive and negative accounting. Positive accounting is where you count upwards continuously the amount you've used, and negative accounting is where you decrement the amount you've used. Many firewalls I've looked at only do positive accounting.
+* Ability to have shared/named accounting labels. So I can say "decrement counter X", and multiple rules can share that counter.
+* Expose the counters via some `procfs` or `sysfs`-like interface. For example, `xt_quota2` (the module we use in `iptables` for quota) exposes it's counters in `/proc/net/xt_quota/LABEL_NAME`. With that you can read and write counters like files.
+* Ability to access the ARP table.
+* Doing all of the above stuff in kernel space. Running an extra daemon isn't really an acceptable solution to me.
+
+
+## Flow diagram (Linux) ##
+
+TODO: finish this
+
+This is how a packet is handled inside tollgate when running on Linux.
+
+1. If it's a new connection, it hits the NAT table rules.
+
View
@@ -3,6 +3,11 @@
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
+*mangle
+:PREROUTING ACCEPT [0:0]
+:POSTROUTING ACCEPT [0:0]
+:OUTPUT ACCEPT [0:0]
+COMMIT
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
View
@@ -66,8 +66,13 @@ def ipt_dual(*args):
def create_nat():
# enable forwarding
write_file('/proc/sys/net/ipv4/ip_forward', 1)
-
- #$ IPv4 rules (NAT)
+
+ # enable tproxy captivity routing
+ system('ip rule add fwmark 0x1/0x1 lookup 100')
+ system('ip route add local 0.0.0.0/0 dev lo table 100')
+
+ ## IPv4 rules (NAT)
+
# define NAT rule
run((IPTABLES,'-t','nat','-F','POSTROUTING'))
run((IPTABLES,'-t','nat','-A','POSTROUTING','-o',EXTERN_IFACE,'-j','MASQUERADE'))
@@ -93,39 +98,54 @@ def create_nat():
# create rejection rule
run((IPTABLES,'-D','FORWARD','-p','tcp','-j','REJECT','--reject-with','tcp-reset'))
run((IPTABLES,'-D','FORWARD','-j','REJECT','--reject-with',REJECT_MODE))
+
+ # handle captivity properly with tproxy
+ run((IPTABLES,'-D','FORWARD','--mark','0x1','-i',INTERN_IFACE,'-p','tcp','--dport','80','-j','ACCEPT'))
+ run((IPTABLES,'-D','FORWARD','--mark','0x1','-o',INTERN_IFACE,'-p','tcp','--sport','80','-j','ACCEPT'))
+ run((IPTABLES,'-A','FORWARD','--mark','0x1','-i',INTERN_IFACE,'-p','tcp','--dport','80','-j','ACCEPT'))
+ run((IPTABLES,'-A','FORWARD','--mark','0x1','-o',INTERN_IFACE,'-p','tcp','--sport','80','-j','ACCEPT'))
+
if REJECT_TCP_RESET:
run((IPTABLES,'-A','FORWARD','-p','tcp','-j','REJECT','--reject-with','tcp-reset'))
run((IPTABLES,'-A','FORWARD','-j','REJECT','--reject-with',REJECT_MODE))
+
run((IPTABLES,'-P','FORWARD','DROP'))
# captivity related entries
# define port forwarding chain
- run((IPTABLES,'-t','nat','-D','PREROUTING','-j',IP4PF_RULE))
- run((IPTABLES,'-t','nat','-N',IP4PF_RULE))
- run((IPTABLES,'-t','nat','-F',IP4PF_RULE))
- run((IPTABLES,'-t','nat','-I','PREROUTING','1','-j',IP4PF_RULE))
+ run((IPTABLES,'-t','mangle','-D','PREROUTING','-j',IP4PF_RULE))
+ run((IPTABLES,'-t','mangle','-N',IP4PF_RULE))
+ run((IPTABLES,'-t','mangle','-F',IP4PF_RULE))
+ run((IPTABLES,'-t','mangle','-I','PREROUTING','1','-j',IP4PF_RULE))
run((IPTABLES,'-t','filter','-D','FORWARD','-j',IP4PF_RULE))
run((IPTABLES,'-t','filter','-N',IP4PF_RULE))
run((IPTABLES,'-t','filter','-F',IP4PF_RULE))
run((IPTABLES,'-t','filter','-I','FORWARD','1','-j',IP4PF_RULE))
# define unmetered chain
- run((IPTABLES,'-t','nat','-D','PREROUTING','-j',UNMETERED_RULE))
- run((IPTABLES,'-t','nat','-N',UNMETERED_RULE))
- run((IPTABLES,'-t','nat','-F',UNMETERED_RULE))
- run((IPTABLES,'-t','nat','-I','PREROUTING','2','-j',UNMETERED_RULE))
+ run((IPTABLES,'-t','mangle','-D','PREROUTING','-j',UNMETERED_RULE))
+ run((IPTABLES,'-t','mangle','-N',UNMETERED_RULE))
+ run((IPTABLES,'-t','mangle','-F',UNMETERED_RULE))
+ run((IPTABLES,'-t','mangle','-I','PREROUTING','2','-j',UNMETERED_RULE))
# define blacklist chain
- run((IPTABLES,'-t','nat','-D','PREROUTING','-j',BLACKLIST_RULE))
- run((IPTABLES,'-t','nat','-N',BLACKLIST_RULE))
- run((IPTABLES,'-t','nat','-F',BLACKLIST_RULE))
- run((IPTABLES,'-t','nat','-I','PREROUTING','3','-j',BLACKLIST_RULE))
+ run((IPTABLES,'-t','mangle','-D','PREROUTING','-j',BLACKLIST_RULE))
+ run((IPTABLES,'-t','mangle','-N',BLACKLIST_RULE))
+ run((IPTABLES,'-t','mangle','-F',BLACKLIST_RULE))
+ run((IPTABLES,'-t','mangle','-I','PREROUTING','3','-j',BLACKLIST_RULE))
# define "captive" rule
- run((IPTABLES,'-t','nat','-N',CAPTIVE_RULE))
- run((IPTABLES,'-t','nat','-F',CAPTIVE_RULE))
- run((IPTABLES,'-t','nat','-A',CAPTIVE_RULE,'-i',INTERN_IFACE,'-p','tcp','--dport','80','-j','REDIRECT','--to-port',str(CAPTIVE_PORT)))
+ run((IPTABLES,'-t','mangle','-N',CAPTIVE_RULE))
+ run((IPTABLES,'-t','mangle','-F',CAPTIVE_RULE))
+ run((IPTABLES,'-t','mangle','-A',CAPTIVE_RULE,'-j','MARK','--set-mark','1'))
+ run((IPTABLES,'-t','mangle','-A',CAPTIVE_RULE,'-m','socket','-j','ACCEPT'))
+
+ # define the catch-all
+ run((IPTABLES,'-t','mangle','-D','PREROUTING','-i',INTERN_IFACE,'-p','tcp','-j',CAPTIVE_RULE))
+ run((IPTABLES,'-t','mangle','-A','PREROUTING','-i',INTERN_IFACE,'-p','tcp','-j',CAPTIVE_RULE))
+ run((IPTABLES,'-t','mangle','-D','PREROUTING','-i',INTERN_IFACE,'-m','mark','--mark','1','-p','tcp','--dport','80','-j','TPROXY','--tproxy-mark','0x1/0x1','--on-port',str(CAPTIVE_PORT)))
+ run((IPTABLES,'-t','mangle','-A','PREROUTING','-i',INTERN_IFACE,'-m','mark','--mark','1','-p','tcp','--dport','80','-j','TPROXY','--tproxy-mark','0x1/0x1','--on-port',str(CAPTIVE_PORT)))
run((IPTABLES,'-t','nat','-A','PREROUTING','-j',CAPTIVE_RULE))
@@ -173,7 +193,7 @@ def create_ipv6_router():
def add_unmetered(ip,proto=None,port=None):
cmd1 = [IPTABLES,'-A',UNMETERED_RULE,'-i',INTERN_IFACE,'-d',ip,'-j','ACCEPT']
- cmd2 = [IPTABLES,'-t','nat','-A',UNMETERED_RULE,'-i',INTERN_IFACE,'-d',ip,'-j','ACCEPT']
+ cmd2 = [IPTABLES,'-t','mangle','-A',UNMETERED_RULE,'-i',INTERN_IFACE,'-d',ip,'-j','ACCEPT']
arg = []
if proto != None and port == None:
arg = ['-p', proto]
@@ -197,7 +217,7 @@ def add_blacklist(ip,proto=None,port=None):
if proto == 'tcp' and REJECT_TCP_RESET:
r = 'tcp-reset'
cmd1 = [IPTABLES,'-A',BLACKLIST_RULE,'-i',INTERN_IFACE,'-d',ip,'-j','REJECT','--reject-with',r]
- cmd2 = [IPTABLES,'-t','nat','-A',BLACKLIST_RULE,'-i',INTERN_IFACE,'-d',ip,'-j',CAPTIVE_RULE]
+ cmd2 = [IPTABLES,'-t','mangle','-A',BLACKLIST_RULE,'-i',INTERN_IFACE,'-d',ip,'-j',CAPTIVE_RULE]
arg = []
if proto != None and port == None:
arg = ['-p', proto]
@@ -308,7 +328,7 @@ def add_host(self, uid, mac, ip):
# take the host out of captivity
start_at = '4'
- run((IPTABLES,'-t','nat','-I','PREROUTING',start_at,'-i',INTERN_IFACE,'-s',ip,'-m','mac','--mac-source',mac,'-m','quota2','--name',limit_rule(uid),'--no-change','-j','ACCEPT'))
+ run((IPTABLES,'-t','mangle','-I','PREROUTING',start_at,'-i',INTERN_IFACE,'-s',ip,'-m','mac','--mac-source',mac,'-m','quota2','--name',limit_rule(uid),'--no-change','-j','ACCEPT'))
@dbus.service.method(dbus_interface=DBUS_INTERFACE, in_signature='sss', out_signature='')
def add_host6(self, uid, mac, ip):
@@ -326,7 +346,7 @@ def add_host6(self, uid, mac, ip):
# captivity doesn't work on ipv6
#start_at = '4'
#run((IPT6ABLES,'-t','nat','-I','PREROUTING',start_at,'-i',INTERN_IFACE,'-s',ip,'-m','mac','--mac-source',mac,'-m','quota2','--name',limit_rule(uid),'--no-change','-j','ACCEPT'))
-
+
@dbus.service.method(dbus_interface=DBUS_INTERFACE, in_signature='s', out_signature='')
def flush_hosts(self, uid):
@@ -373,7 +393,42 @@ def flush_hosts(self, uid):
# doesn't work as you have to provide exact rule
#run((IPTABLES,'-D','FORWARD','-j',user_rule(uid)))
-
+
+ # TODO: This is somewhat dangerous, really should prevent multiple actions occuring while this one is.
+ d = run_capture_output((IPTABLES,'-L','FORWARD','-n','--line-numbers'))
+ a = d.split('\n')
+
+ rules_to_remove = []
+ for line in a:
+ r = PARSE_RULE.search(line)
+ # debugging
+ #if r != None:
+ # print "Parsed with user-rule %s and line %s" % (r.group('user'), r.group('rule_num'))
+ #else:
+ # print "no match: %s" % (line,)
+
+ if r != None and r.group('user') == user_rule(uid):
+ try:
+ rules_to_remove.append(long(r.group('rule_num')))
+
+ if r.group('mac') != None:
+ # take the host out of captive-exempt mode, we know it's mac address.
+ run((IPTABLES,'-t','mangle','-D','PREROUTING','-i',INTERN_IFACE,'-s',r.group('ip'),'-m','mac','--mac-source',r.group('mac'),'-m','quota2','--name',limit_rule(uid),'--no-change','-j','ACCEPT'))
+ except:
+ # Non-match, we don't care.
+ pass
+
+ # sort out the list of rules, and put it in reverse order as otherwise the numbering shuffles
+ rules_to_remove.sort()
+ rules_to_remove.reverse()
+
+ #print "Will delete: %s" % (rules_to_remove,)
+ # now remove those rules
+ for ln in rules_to_remove:
+ run((IPTABLES,'-D','FORWARD',str(ln)))
+
+ # doesn't work as you have to provide exact rule
+ #run((IPTABLES,'-D','FORWARD','-j',user_rule(uid)))
@dbus.service.method(dbus_interface=DBUS_INTERFACE, in_signature='s', out_signature='bt')
def get_quota(self, uid):
View
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+
+from socket import SOL_IP
+from warnings import warn
+
+try:
+ # py3
+ from urllib.parse import quote
+except ImportError:
+ # py2
+ from urllib import quote
+
+try:
+ # py3
+ from http.server import HTTPServer, BaseHTTPRequestHandler
+except ImportError:
+ # py2
+ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+try:
+ from socket import IP_TRANSPARENT
+except ImportError:
+ # this isn't implemented in python yet, submitted a patch.
+ # /usr/include/linux/in.h says this is 19.
+ IP_TRANSPARENT = 19
+ warn("Your version of Python doesn't support socket.IP_TRANSPARENT. It could also be that you're running this on a non-Linux platform, which probably won't work.")
+
+class TProxyRequestHandler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ url = self.server.redirect % quote('http://' + self.headers['Host'] + self.path)
+ self.send_response(302, 'Captive Portal Login Required')
+ self.send_header('Content-Type', 'text/html')
+ self.send_header('Location', url)
+ self.send_header('Last-Modified', 'Thu, 01 Jan 1970 03:13:37 GMT')
+ self.send_header('Expires', 'Thu, 01 Jan 1970 03:13:37 GMT')
+ self.send_header('Pragma', 'no-cache')
+ self.send_header('Cache-Control', 'no-cache')
+ self.send_header('Connection', 'close')
+ self.end_headers()
+
+ page = """
+<html>
+ <head>
+ <title>Captive Portal Logon Required</title>
+ <meta http-equiv="refresh" content="0;URL=%(url)s">
+ </head>
+
+ <body>
+ <h1>Captive Portal Logon Required</h1>
+ <p>A captive portal logon is required to access the requested site. <a href="%(url)s">Click here to login.</a></p>
+ </body>
+</html>""" % dict(url=url)
+
+ # in py3, we need to do some encoding
+ if str != bytes:
+ page = bytes(page, 'UTF-8')
+
+ self.wfile.write(page)
+
+ do_HEAD = do_POST = do_GET
+
+class TProxyServer:
+ keep_running = True
+ server_address = ('', 50080)
+ mark = 1
+
+ def run(self):
+ self.httpd = HTTPServer(self.server_address, TProxyRequestHandler)
+ self.httpd.server_version = 'tollgate'
+ self.httpd.redirect = 'https://portal.onadelaide.blackhats.net.au/captive_landing/?u=%s'
+ self.httpd.socket.setsockopt(SOL_IP, IP_TRANSPARENT, self.mark)
+
+ while self.keep_running:
+ self.httpd.handle_request()
+
+if __name__ == '__main__':
+ # boot the httpd
+ print("Booting httpd.")
+ server = TProxyServer()
+ server.run()
+

0 comments on commit c2ec707

Please sign in to comment.