mirror of
https://codeberg.org/prof_x_pvt_ltd/captive.whump.shanti-portal
synced 2024-08-14 22:46:42 +00:00
Merge branch 'master' into rs
This commit is contained in:
commit
962ebe85bb
10 changed files with 209 additions and 85 deletions
|
@ -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.
|
||||
|
|
9
docs/examples/iptables/README.md
Normal file
9
docs/examples/iptables/README.md
Normal 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.
|
27
docs/examples/iptables/cp_iptables.sh.j2
Normal file
27
docs/examples/iptables/cp_iptables.sh.j2
Normal 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
|
98
docs/examples/iptables/iptables.j2
Normal file
98
docs/examples/iptables/iptables.j2
Normal 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
|
||||
|
15
docs/examples/iptables/rmtrack.sh.j2
Normal file
15
docs/examples/iptables/rmtrack.sh.j2
Normal 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}}
|
|
@ -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}"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue