Merge branch 'master' into rs

This commit is contained in:
Stefan Midjich 2016-12-13 15:39:53 +01:00
commit 962ebe85bb
10 changed files with 209 additions and 85 deletions

View file

@ -11,6 +11,10 @@ This is a commonly seen setup in public Wifi networks or hotspots.
This app was specifically written for such a hotspot and as such requires a lot of other configuration around it. This is an ongoing [documentation project here](https://wiki.sydit.se/teknik:guider:networking:captive_portal_med_iptables).
## More documentation
I've moved all examples from the [aforementioned wiki-page](https://wiki.sydit.se/teknik:guider:networking:captive_portal_med_iptables) to the docs/examples directory.
# Plugins
Plugins are executed when the user clicks through the captive portal form, whether they submit data or just approve an EULA these plugins are executed.

View file

@ -0,0 +1,9 @@
# IPtables example
The example is written for Ansible so it contains Jinja2 brackets for things like input NIC, output NIC and other items important for a captive portal firewall configuration.
Server configurations vary so it's not applicable to any situation but it might help as guidance and it's well commented.
# Script examples
They're also written as Ansible templates because I copy them straight from my Ansible playbooks for deploying the captive portal. But it matters less for them, no important values to keep track of, everything is argument input.

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Captiveportal iptables wrapper script
#iptables_mac = iptables -t mangle -I internet 1 -m mac --mac-source {mac_address} -j RETURN
# First argument must be IP-address of client
test -n "$1" || exit 1
client_ip="$1"
ipt=/sbin/iptables
# Enable client traffic in internet chain by jumping over the mark
$ipt -t mangle -I internet 1 -p tcp --source "$client_ip" -j RETURN &>/dev/null && \
$ipt -t mangle -I internet 1 -p udp --source "$client_ip" -j RETURN &>/dev/null
iptables_rc=$?
# Delete conntrack info for client IP
/usr/local/sbin/rmtrack.sh "$client_ip" &>/dev/null
rmtrack_rc=$?
if [[ $iptables_rc == 0 && $rmtrack_rc == 0 ]]; then
# Success
exit 0
else
echo "Error: iptables[$iptables_rc], rmtrack[$rmtrack_rc]" 1&>2
exit 1
fi

View file

@ -0,0 +1,98 @@
# {{ ansible_managed }}
#
# These rules are for the Captive Portal project.
# by Stefan Midjich - 2016/03
# Routing of traffic requires: sysctl net.ipv4.ip_forward = 1
#
# {{captiveportal_conf.input_nic}} is LAN and used as default route on LAN clients.
# {{captiveportal_conf.output_nic}} is WAN.
# {{captiveportal_conf.webportal_ip}} is the same as IP on {{captiveportal_conf.input_nic}}
# Mangle table allows the marking of traffic. If you use -j RETURN before
# -j MARK you jump out of the internet chain and your traffic is not marked.
*mangle
:PREROUTING ACCEPT
:INPUT ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
# Create custom chain in mangle table called "internet"
:internet - [0:0]
# Run all traffic from {{captiveportal_conf.input_nic}} through the internet chain
-A PREROUTING -i {{captiveportal_conf.input_nic}} -j internet
# Example to allow authorized clients in by MAC address
#-A internet -m mac --mac-source "xx:xx:xx:xx:56:eb" -j RETURN
# Live example: -I internet 1 -m mac --mac-source "xx:xx:xx:xx:56:eb" -j RETURN
# inserts at the top of the rules before the mark rule.
#
# iptables -t mangle -I internet -m tcp -p tcp --source 1.2.3.4 -j RETURN
# iptables -t mangle -I internet -m udp -p udp --source 1.2.3.4 -j RETURN
# For MGMT SSH traffic return out of internet chain so it's not marked
-A internet -p tcp -d {{captiveportal_conf.webportal_ip}} --dport ssh -j RETURN
# Bypass NTP also
#-A internet -p udp --dport ntp -j RETURN
# Mark all other traffic in the internet chain with 99. Any traffic after
# this rule is marked and blocked.
-A internet -j MARK --set-mark 99
COMMIT
# NAT rules that redirect traffic and allow the portal server to act as gateway.
*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
# Redirect all marked HTTP traffic to the webportal IP
-A PREROUTING -m mark --mark 99 -p tcp --dport http -j DNAT --to-destination {{captiveportal_conf.webportal_ip}}
-A PREROUTING -m mark --mark 99 -p tcp --dport https -j DNAT --to-destination {{captiveportal_conf.webportal_ip}}
# Redirect all marked DNS traffic to the webportal IP
-A PREROUTING -m mark --mark 99 -p udp --dport domain -j DNAT --to-destination {{captiveportal_conf.webportal_ip}}
-A PREROUTING -m mark --mark 99 -p tcp --dport domain -j DNAT --to-destination {{captiveportal_conf.webportal_ip}}
# Redirect all ICMP to the webportal IP
-A PREROUTING -m mark --mark 99 -p icmp -j DNAT --to-destination {{captiveportal_conf.webportal_ip}}
# Redirect all unmarked DNS traffic to upstream DNS servers
{% for server in captiveportal_conf.upstream_dns %}
-A PREROUTING -p udp --dport domain -j DNAT --to-destination {{server}}
-A PREROUTING -p tcp --dport domain -j DNAT --to-destination {{server}}
{% endfor %}
# Route any traffic out through the output NIC to act as gateway
-A POSTROUTING -o {{captiveportal_conf.output_nic}} -j MASQUERADE
COMMIT
# Filter rules that determine access to the portal server.
*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
# Enable stateful connections
-I OUTPUT -o {{captiveportal_conf.output_nic}} -d 0.0.0.0/0 -j ACCEPT
-I INPUT -i {{captiveportal_conf.input_nic}} -m state --state ESTABLISHED,RELATED -j ACCEPT
# Accept HTTP traffic both to the server and forwarded
-A INPUT -p tcp --dport http -j ACCEPT
-A FORWARD -p tcp --dport http -j ACCEPT
# Accept DNS traffic to self
-A INPUT -p udp --dport domain -j ACCEPT
-A INPUT -p tcp --dport domain -j ACCEPT
# Drop all other traffic marked 99
-A FORWARD -m mark --mark 99 -j DROP
-A INPUT -m mark --mark 99 -j DROP
COMMIT

View file

@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Conntracking keeps track of active connections so even if a user
# authenticates with a captive portal and new firewall rules are
# created it will take a while before the client takes these new
# routes. So conntrack -D can expedite that process.
test -n "$1" || exit 1
client_ip=$1
conntrack_cmd=/sbin/conntrack
# Deletes all conntracking entries for connections originating from
# to webportal server IP so that hopefully new connections can be
# initiated directly to destination.
$conntrack_cmd -D --orig-src $client_ip --orig-dst {{captiveportal_conf.webportal_ip}}

View file

@ -13,13 +13,11 @@ mandatory = True
enabled = False
debug = True
# If you know you won't be able to get the clients HW address then use this.
only_ip = True
# Command templates for arping and iptables.
# Arping might block so make sure you use a timeout and limit the number of
# packets it sends.
arping = -f -c 1 -w 30 -I eth0 {ip_address}
iptables_mac = -t mangle -I internet 1 -m mac --mac-source {mac_address} -j RETURN
iptables_ip = -t mangle -I internet 1 -m tcp -p tcp --source {ip_address} -j RETURN
# This is a command to run to create iptables rules. Two arguments are
# passed and replace these two placeholders.
iptables_cmd = /usr/local/sbin/cp_iptables.sh "{ip_address}" "{mac_address}"

View file

@ -1,4 +1,7 @@
# Add an iptables rule
# This actually runs a command, so you can either define an iptables
# command or a script. See the plugins.cfg for the options that are
# replaced into the command line.
import re
import socket
@ -13,9 +16,11 @@ except ImportError:
from portal import logHandler, logFormatter
# Try to import arping for mac_from_ip()
use_arping = True
try:
from sh import arping
except ImportError:
use_arping = False
pass
# By default run iptables through sudo, so the worker process must run with
@ -38,6 +43,7 @@ def run(arg):
l.setLevel(DEBUG)
l.debug('debug logging enabled')
# Get client IP from webapp
client_ip = environ.get(
'HTTP_X_FORWARDED_FOR',
environ.get('REMOTE_ADDR')
@ -46,7 +52,7 @@ def run(arg):
error_msg = None
iptables_failed = False
# Verify IP
# Verify client IP
try:
socket.inet_aton(client_ip)
except socket.error:
@ -56,7 +62,8 @@ def run(arg):
'failed': True
}
# Attempt to get client HW address first.
# Attempt to get client HW address with arping
if use_arping:
try:
client_mac = mac_from_ip(
l,
@ -70,86 +77,37 @@ def run(arg):
error_msg = str(e)
pass
# If HW address was found, use it now.
if client_mac:
l.debug('Found client HW address: {hw}'.format(
hw=client_mac
))
# Create tuple out of iptables command
iptables_mac = config.get('iptables', 'iptables_mac').format(
if client_ip:
iptables_cmd = config.get('iptables', 'iptables_cmd').format(
ip_address=client_ip,
mac_address=client_mac
)
iptables_mac = tuple(iptables_mac.split(' '))
output = BytesIO()
error = BytesIO()
try:
rc = sudo.iptables(iptables_mac, _out=output, _err=error)
if rc.exit_code == 0:
l.debug('Created iptables MAC rule successfully')
return {
'error': error_msg,
'failed': False
}
# The two arguments must not contain spaces of course.
rc = sudo(tuple(iptables_cmd.split(' ')), _out=output, _err=error)
except ErrorReturnCode:
error.seek(0)
error_msg = error.read()
l.warn('{cmd}: exited badly: {error}'.format(
cmd=('iptables', iptables_mac),
cmd=('iptables', iptables_cmd),
error=error_msg
))
iptables_failed = True
pass
except Exception as e:
l.warn('{cmd}: failed: {error}'.format(
cmd=('iptables', iptables_mac),
cmd=('iptables', iptables_cmd),
error=str(e)
))
error_msg = str(e)
iptables_failed = True
pass
# Fallback on IP if HW address fails
if client_ip:
l.debug('Using client IP: {ip}'.format(
ip=client_ip
))
iptables_ip = config.get('iptables', 'iptables_ip').format(
ip_address=client_ip
)
iptables_ip = tuple(iptables_ip.split(' '))
output = BytesIO()
error = BytesIO()
try:
rc = sudo.iptables(iptables_ip, _out=output, _err=error)
if rc.exit_code == 0:
l.debug('Created iptables IP rule successfully')
return {
'error': error_msg,
'failed': False
}
except ErrorReturnCode:
error.seek(0)
error_msg = error.read()
l.warn('{cmd}: exited badly: {error}'.format(
cmd=('iptables', iptables_ip),
error=error_msg
))
iptables_failed = True
pass
except Exception as e:
l.warn('{cmd}: failed: {error}'.format(
cmd=('iptables', iptables_ip),
error=str(e)
))
error_msg = str(e)
iptables_failed = True
pass
# If all else fails, error! This will be shown to end users.
return {
@ -183,3 +141,4 @@ def mac_from_ip(l, arping_args, ip):
if line.startswith(line_start):
m = re.search('(([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2}))', line)
if m: return m.group(0)

View file

@ -209,7 +209,15 @@ def job_status(job_id):
@app.route('/approve', method='POST')
def approve_client():
response.content_type = 'application/json'
try:
jobs = dispatch_plugins()
except Exception as e:
response.status = 500
jobs = {
'result': {
'error': str(e)
}
}
return json.dumps(jobs)

View file

@ -23,18 +23,25 @@ var getUrlParameter = function getUrlParameter(sParam) {
// This function ensures the user gets redirect to the correct destination once
// all jobs have succeeded in the portal software.
function do_success() {
console.log('success: '+window.location);
var url = getUrlParameter('url');
// If url does not start with http the window.location redirect
// won't work. So prefix http to url.
if (!url.startsWith('http')) {
url = 'http://'+url;
}
console.log('success: '+url);
var url = getUrlParameter('url');
// Do something like refresh the window or go to another URL.
window.location = url;
location.reload(true);
}
// Show an error to the user
function do_error(message) {
console.log('failure: '+message);
$('#approveButton').prop('disabled', false);
$('#error-box').show();
$('#form-row').hide();
@ -127,7 +134,8 @@ function poll_jobs(data) {
}
if (success) {
do_success();
// Will hopefully try a redirect until it succeeds.
var timer = setInterval(do_success, 2000);
}
}, function(reason) {
do_error(reason);
@ -149,10 +157,10 @@ $('#approveForm').submit(function (event) {
// just replacing it.
if ($('#approveCheckbox').is(':checked')) {
$('#approveButton').prop('disabled', true);
$('#approveButton').val('');
//$('#approveButton').val('');
$('#approveButton').addClass('button-loading');
$('#approveButtonDiv').replaceWith('<img src="/static/images/radio.svg" alt="Loading, please wait..." />');
//$('#approveButtonDiv').replaceWith('<img src="/static/images/radio.svg" alt="Loading, please wait..." />');
var ajaxReq = $.post(api_url);
ajaxReq.done(poll_jobs);

View file

@ -9,8 +9,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/captiveportal.css">