add contrib
This commit is contained in:
		@@ -10,6 +10,8 @@ Either clone repository (and install dependencies either through distribution or
 | 
			
		||||
 | 
			
		||||
In production put a reverse proxy in front of the local web ui (on 127.0.0.1:8000), and handle `/static` path either to `src/capport/api/static/` or your customized version of static files.
 | 
			
		||||
 | 
			
		||||
See the `contrib` directory for config of other software needed to setup a captive portal.
 | 
			
		||||
 | 
			
		||||
## Customization
 | 
			
		||||
 | 
			
		||||
Create `custom/templates` and put customized templates (from `src/capport/api/templates`) there.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								contrib/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								contrib/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
# Various other parts of a captive portal setup
 | 
			
		||||
 | 
			
		||||
Network (HA) setup with IPv4 NAT:
 | 
			
		||||
- two nodes
 | 
			
		||||
- shared uplink L2, some transfer network (can be private or public)
 | 
			
		||||
  * virtual addresses on active node for IPv4 and IPv6 to route traffic to
 | 
			
		||||
- shared downlink L2
 | 
			
		||||
  * virtual addresses on active node for IPv4 and IPv6 as gateway for clients
 | 
			
		||||
    * using `fe80::1` as gateway, but also add a public IPv6 virtual address
 | 
			
		||||
  * connected: private IPv4 prefix (e.g. CGNAT), not routed
 | 
			
		||||
  * connected: public IPv6 prefix (routed to virtual uplink address of nodes)
 | 
			
		||||
- public IPv4 prefix routed virtual uplink address of nodes to use for NAT
 | 
			
		||||
  * IPv4-traffic from clients will be (S)NATted from this prefix; size depends
 | 
			
		||||
    on number of parallel connections you want to support.
 | 
			
		||||
- webserver on nodes:
 | 
			
		||||
  * port 8080: receives transparent http redirects from the firewall; should return a temporary redirect to your portal page.
 | 
			
		||||
  * port 80: redirect to https
 | 
			
		||||
  * port 443: reverse-proxy to 127.0.0.1:8000 (the webui backend), but serve `/static` directly from directory (see main README)
 | 
			
		||||
 | 
			
		||||
To access the portal page on the clients you'll need a DNS-name; it should point to the virtual addresses.  In some ways downlink address is preferred, but you also might want to avoid private addresses - i.e. use the uplink IPv4 address and the downlink IPv6 address.
 | 
			
		||||
 | 
			
		||||
Also the management traffic for the virtual address should use the uplink interface if possible (`keepalived` supports this).
 | 
			
		||||
 | 
			
		||||
## ISC dhcpd
 | 
			
		||||
 | 
			
		||||
See `dhcpd.conf.erb` and `dhcpd6.conf.erb`.
 | 
			
		||||
 | 
			
		||||
Note: don't use too large IPv4 pools or dhcpd will take a long time to sync and build up the leases files.
 | 
			
		||||
 | 
			
		||||
## Firewall / NAT
 | 
			
		||||
 | 
			
		||||
See `nftables.conf.erb` for forwarding rules; if you want traffic shaping as well see `shape_non_whitelisted.sh`.
 | 
			
		||||
Local policies (ssh access and normal "host protection") are not included in the example.
 | 
			
		||||
 | 
			
		||||
You also might want to set a high `net.netfilter.nf_conntrack_max` with sysctl (e.g. `16777216`).
 | 
			
		||||
 | 
			
		||||
## Conntrackd
 | 
			
		||||
 | 
			
		||||
Active/failover configuration TBD.
 | 
			
		||||
 | 
			
		||||
I strongly recommend not to enable any tracking helpers; they often make significant holes into your stateful firewall (i.e. make clients reachable from the outside in ways they didn't actually want).
 | 
			
		||||
 | 
			
		||||
## Keepalived (for virtual addresses)
 | 
			
		||||
 | 
			
		||||
See `keepalived.conf.erb`.
 | 
			
		||||
 | 
			
		||||
## Apache2
 | 
			
		||||
 | 
			
		||||
See `apache2.conf` (only contains "interesting" parts, probably won't start that way).
 | 
			
		||||
Any other webserver configured in a similar way should do just as well.
 | 
			
		||||
 | 
			
		||||
## systemd units
 | 
			
		||||
 | 
			
		||||
See the `systemd` directory for examples of systemd units.
 | 
			
		||||
							
								
								
									
										43
									
								
								contrib/apache2.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								contrib/apache2.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
Listen 80
 | 
			
		||||
Listen 443
 | 
			
		||||
Listen 8080
 | 
			
		||||
 | 
			
		||||
<VirtualHost *:8080>
 | 
			
		||||
  ServerName redirect
 | 
			
		||||
 | 
			
		||||
  Header always set Cache-Control "no-store"
 | 
			
		||||
  # trailing '?' drops request query string:
 | 
			
		||||
  RedirectMatch seeother  ^.*$ https://portal.example.com?
 | 
			
		||||
  KeepAlive off
 | 
			
		||||
</VirtualHost>
 | 
			
		||||
 | 
			
		||||
<VirtualHost *:80>
 | 
			
		||||
  ServerName portal.example.com
 | 
			
		||||
  ServerAlias portal-node1.example.com
 | 
			
		||||
 | 
			
		||||
  Redirect permanent / https://portal.example.com/
 | 
			
		||||
</VirtualHost>
 | 
			
		||||
 | 
			
		||||
<VirtualHost *:443>
 | 
			
		||||
  ServerName portal.example.com
 | 
			
		||||
  ServerAlias portal-node1.example.com
 | 
			
		||||
 | 
			
		||||
  SSLEngine on
 | 
			
		||||
  SSLCertificateFile      "/etc/ssl/certs/portal.example.com-with-chain.crt"
 | 
			
		||||
  SSLCertificateKeyFile   "/etc/ssl/private/portal.example.com.key"
 | 
			
		||||
 | 
			
		||||
  # The static directory of your theme (or the builtin one)
 | 
			
		||||
  Alias /static "/var/lib/python-capport/custom/static"
 | 
			
		||||
 | 
			
		||||
  Header always set X-Frame-Options DENY
 | 
			
		||||
  Header always set Referrer-Policy same-origin
 | 
			
		||||
  Header always set X-Content-Type-Options nosniff
 | 
			
		||||
  Header always set Strict-Transport-Security "max-age=31556926;"
 | 
			
		||||
 | 
			
		||||
  RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
 | 
			
		||||
 | 
			
		||||
  ProxyRequests Off
 | 
			
		||||
  ProxyPreserveHost On
 | 
			
		||||
  ProxyPass /static !
 | 
			
		||||
  ProxyPass / http://127.0.0.1:8000/
 | 
			
		||||
</VirtualHost>
 | 
			
		||||
							
								
								
									
										35
									
								
								contrib/dhcpd.conf.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								contrib/dhcpd.conf.erb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
option domain-name-servers <%= ', '.join(@dns_resolvers_ipv4) %>;
 | 
			
		||||
option ntp-servers <%= ', '.join(@ntp_servers_ipv4) %>;
 | 
			
		||||
 | 
			
		||||
# specify API server URL (RFC8910)
 | 
			
		||||
option default-url "https://<%= @service_name %>/api/captive-portal";
 | 
			
		||||
 | 
			
		||||
default-lease-time 600;
 | 
			
		||||
max-lease-time 3600;
 | 
			
		||||
 | 
			
		||||
authoritative;
 | 
			
		||||
 | 
			
		||||
<% if @instances.length == 2 -%>
 | 
			
		||||
failover peer "dhcp-peer" {
 | 
			
		||||
    <% if @instance_index == 0 %>primary<% else %>secondary<% end %>;
 | 
			
		||||
    address <%= @instances[@instance_index]['external_ipv4'] %>;
 | 
			
		||||
    peer address <%= @instances[1-@instance_index]['external_ipv4'] %>;
 | 
			
		||||
    max-response-delay 60;
 | 
			
		||||
    max-unacked-updates 10;
 | 
			
		||||
    load balance max seconds 3;
 | 
			
		||||
<% if @instance_index == 0 -%>
 | 
			
		||||
    split 128;
 | 
			
		||||
    mclt 180;
 | 
			
		||||
<%- end %>
 | 
			
		||||
}
 | 
			
		||||
<%- end %>
 | 
			
		||||
 | 
			
		||||
subnet <%= @client_ipv4_net %> netmask <%= @client_netmask %> {
 | 
			
		||||
    option routers <%= @client_ipv4_gateway %>;
 | 
			
		||||
    pool {
 | 
			
		||||
        range <%= @client_ipv4_dhcp_from %> <%= @client_ipv4_dhcp_to %>;
 | 
			
		||||
<% if @instances.length == 2 -%>
 | 
			
		||||
        failover peer "dhcp-peer";
 | 
			
		||||
<%- end %>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								contrib/dhcpd6.conf.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								contrib/dhcpd6.conf.erb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
option dhcp6.name-servers <%= ', '.join(@dns_resolvers_ipv6) %>;
 | 
			
		||||
option dhcp6.sntp-servers <%= ', '.join(@ntp_servers_ipv6) %>;
 | 
			
		||||
 | 
			
		||||
# specify API server URL (RFC8910)
 | 
			
		||||
option dhcp6.v6-captive-portal "https://<%= @service_name %>/api/captive-portal";
 | 
			
		||||
 | 
			
		||||
# The delay before information-request refresh
 | 
			
		||||
#  (minimum is 10 minutes, maximum one day, default is to not refresh)
 | 
			
		||||
#  (set to 6 hours)
 | 
			
		||||
option dhcp6.info-refresh-time 3600;
 | 
			
		||||
 | 
			
		||||
subnet6 <%= @client_ipv6 %> {
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								contrib/keepalived.conf.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								contrib/keepalived.conf.erb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
global_defs {
 | 
			
		||||
  vrrp_no_swap
 | 
			
		||||
  checker_no_swap
 | 
			
		||||
  script_user nobody
 | 
			
		||||
  enable_script_security
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
vrrp_instance capport_ipv4_default {
 | 
			
		||||
  state BACKUP
 | 
			
		||||
 | 
			
		||||
  interface <%= @uplink_interface %>
 | 
			
		||||
 | 
			
		||||
  virtual_router_id 1
 | 
			
		||||
 | 
			
		||||
  priority 100
 | 
			
		||||
 | 
			
		||||
  virtual_ipaddress {
 | 
			
		||||
    <% @uplink_virtual_ipv4 %>
 | 
			
		||||
    <% @client_virtual_ipv4 %> dev <%= @client_interface %>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  promote_secondaries
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
vrrp_instance capport_ipv6_default {
 | 
			
		||||
  state BACKUP
 | 
			
		||||
 | 
			
		||||
  interface <%= @uplink_interface %>
 | 
			
		||||
 | 
			
		||||
  virtual_router_id 2
 | 
			
		||||
 | 
			
		||||
  priority 100
 | 
			
		||||
 | 
			
		||||
  virtual_ipaddress {
 | 
			
		||||
    fe80::1:1
 | 
			
		||||
    <%= @uplink_virtual_ipv6 %>
 | 
			
		||||
    fe80::1 dev <%= @client_interface %>
 | 
			
		||||
    <%= @client_virtual_ipv6 %> dev <%= @client_interface %>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  promote_secondaries
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
vrrp_sync_group capport_default {
 | 
			
		||||
  group {
 | 
			
		||||
    capport_ipv4_default
 | 
			
		||||
    capport_ipv6_default
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										360
									
								
								contrib/nftables.conf.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								contrib/nftables.conf.erb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,360 @@
 | 
			
		||||
#!/usr/sbin/nft -f
 | 
			
		||||
 | 
			
		||||
# Template notes: most variables should have an obvious meaning, but:
 | 
			
		||||
# - client_ipv4: private IPv4 network for clients (not routed outside), connected
 | 
			
		||||
# - client_ipv4_public: public IPv4 network for clients, must be routed to this host
 | 
			
		||||
#   and should be blackholed here.
 | 
			
		||||
# - client_ipv6: public IPv6 network, must be routed to this host, connected
 | 
			
		||||
 | 
			
		||||
# NOTE: mustn't flush full ruleset; need to keep the table `captive_mark` and its set around
 | 
			
		||||
# DON'T ENABLE THIS:
 | 
			
		||||
#   flush ruleset
 | 
			
		||||
 | 
			
		||||
# fully whitelist certain sites, e.g. VPN gateways your users are
 | 
			
		||||
# allowed to connect to even without accepting the terms.
 | 
			
		||||
define full_ipv4 = {
 | 
			
		||||
<%- @whitelist_full_ipv4.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
define full_ipv6 = {
 | 
			
		||||
<%- @whitelist_full_ipv6.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# whitelist http[s] traffic to certain sites, e.g. your
 | 
			
		||||
# homepage hosting the terms, OCSP responders, websites
 | 
			
		||||
# to setup other Wifi configurations (cat.eduroam.org)
 | 
			
		||||
define http_server_ipv4 = {
 | 
			
		||||
<%- @whitelist_http_ipv4.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
define http_server_ipv6 = {
 | 
			
		||||
<%- @whitelist_http_ipv6.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
option ntp-servers <%= ', '.join(@ntp_servers_ipv4) %>;
 | 
			
		||||
option dhcp6.sntp-servers <%= ', '.join(@ntp_servers_ipv6) %>;
 | 
			
		||||
 | 
			
		||||
# whitelist your DNS resolvers
 | 
			
		||||
define dns_server_ipv4 = {
 | 
			
		||||
<%- @dns_resolvers_ipv4.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
define dns_server_ipv6 = {
 | 
			
		||||
<%- @dns_resolvers_ipv6.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# whitelist your (and possible other friendly) NTP servers
 | 
			
		||||
define ntp_server_ipv4 = {
 | 
			
		||||
<%- @ntp_servers_ipv4.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
define ntp_server_ipv6 = {
 | 
			
		||||
<%- @ntp_servers_ipv6.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# ports to block traffic for completely
 | 
			
		||||
## block traffic from clients to certain server ports
 | 
			
		||||
define backlist_tcp = {
 | 
			
		||||
    25, # SMTP
 | 
			
		||||
    161, # SNMP
 | 
			
		||||
    135, # epmap (netbios/portmapping)
 | 
			
		||||
    137, # TCP netbios-ns
 | 
			
		||||
    138, # TCP netbios-dgm
 | 
			
		||||
    139, # netbios-ssn
 | 
			
		||||
    445, # microsoft-ds (cifs, samba)
 | 
			
		||||
}
 | 
			
		||||
define backlist_udp = {
 | 
			
		||||
    25, # SMTP
 | 
			
		||||
    161, # SNMP
 | 
			
		||||
    135, # UDP epmap (netbios/portmapping)
 | 
			
		||||
    137, # netbios-ns
 | 
			
		||||
    138, # netbios-dgm
 | 
			
		||||
    139, # UDP netbios-ssn
 | 
			
		||||
    445, # UDP microsoft-ds (cifs, samba)
 | 
			
		||||
    5353, # mDNS
 | 
			
		||||
}
 | 
			
		||||
## block traffic from certain client ports
 | 
			
		||||
define backlist_udp_source = {
 | 
			
		||||
    162, # SNMP trap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# once a client accepted the terms:
 | 
			
		||||
# * define "good" (whitelisted) traffic with the following lists
 | 
			
		||||
# * decide in "chain forward" below whether to block other traffic completely or
 | 
			
		||||
#   e.g. shape it to low bandwidth (also see shape_non_whitelisted.sh)
 | 
			
		||||
define whitelist_tcp = {
 | 
			
		||||
    22, # ssh
 | 
			
		||||
    53, # dns
 | 
			
		||||
    80, # http
 | 
			
		||||
    443, # https
 | 
			
		||||
    3128, # http proxy (squid)
 | 
			
		||||
    8080, # http alt
 | 
			
		||||
    110, # pop3
 | 
			
		||||
    995, # pop3s
 | 
			
		||||
    143, # imap
 | 
			
		||||
    993, # imaps
 | 
			
		||||
    587, # submission
 | 
			
		||||
    465, # submissions
 | 
			
		||||
    1194, # openvpn default
 | 
			
		||||
}
 | 
			
		||||
# https://help.webex.com/en-us/article/WBX264/How-Do-I-Allow-Webex-Meetings-Traffic-on-My-Network
 | 
			
		||||
define whitelist_udp = {
 | 
			
		||||
    53, # dns
 | 
			
		||||
    123, # ntp
 | 
			
		||||
    443, # http/3
 | 
			
		||||
    1194, # openvpn default
 | 
			
		||||
    51820, # wireguard default
 | 
			
		||||
    500, # IPsec isakmp
 | 
			
		||||
    4500, # IPsec ipsec-nat-t
 | 
			
		||||
    10000, # IPSec Cisco NAT-T
 | 
			
		||||
    9000, # Primary Webex Client Media
 | 
			
		||||
    5004, # Webex Client Media
 | 
			
		||||
}
 | 
			
		||||
# whitelist traffic to local sites
 | 
			
		||||
define whitelist_dest_ipv4 = {
 | 
			
		||||
<%- @local_site_prefixes_ipv4.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
define whitelist_dest_ipv6 = {
 | 
			
		||||
<%- @local_site_prefixes_ipv6.each do |n| -%>
 | 
			
		||||
    <%= n %>,
 | 
			
		||||
<%- end -%>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# IPv4 HTTP redirect + SNAT
 | 
			
		||||
table ip nat4 {}
 | 
			
		||||
flush table ip nat4
 | 
			
		||||
table ip nat4 {
 | 
			
		||||
    chain prerouting {
 | 
			
		||||
        type nat hook prerouting priority -100;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        # needs to be marked from client interface (bit 0), captive (bit 1), and http dnat (bit 2) - otherwise accept
 | 
			
		||||
        meta mark & 0x00000007 != 0x00000007 accept
 | 
			
		||||
        tcp dport 80 redirect to 8080
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain postrouting {
 | 
			
		||||
        type nat hook postrouting priority 100;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        # needs to be marked from client interface (bit 0) - otherwise no NAT
 | 
			
		||||
        meta mark & 0x00000001 != 0x00000001 accept
 | 
			
		||||
        oifname <%= @uplink_interface %> snat to <%= @client_ipv4_public %>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# IPv6 HTTP redirect
 | 
			
		||||
table ip6 nat6 {}
 | 
			
		||||
flush table ip6 nat6
 | 
			
		||||
table ip6 nat6 {
 | 
			
		||||
    chain prerouting {
 | 
			
		||||
        type nat hook prerouting priority -100;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        # needs to be marked from client interface (bit 0), captive (bit 1), and http dnat (bit 2) - otherwise accept
 | 
			
		||||
        meta mark & 0x00000007 != 0x00000007 accept
 | 
			
		||||
        tcp dport 80 redirect to 8080
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# ipv4 + ipv6
 | 
			
		||||
table inet captive_filter {}
 | 
			
		||||
flush table inet captive_filter
 | 
			
		||||
table inet captive_filter {
 | 
			
		||||
    chain antispoof_input {
 | 
			
		||||
        # need to accept packet for input and forward!
 | 
			
		||||
        meta mark & 0x00000001 == 0 accept comment "accept from non-client interface"
 | 
			
		||||
        ip saddr { 0.0.0.0, <%= @client_ipv4 %> } return
 | 
			
		||||
        ip6 saddr { fe80::/64, <%= @client_ipv6 %> } return
 | 
			
		||||
        drop
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # we need the "redirect" decision before DNAT in prerouting:-100
 | 
			
		||||
    chain mark_clients {
 | 
			
		||||
        type filter hook prerouting priority -110;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        meta mark & 0x00000001 == 0 accept comment "accept from non-client interface"
 | 
			
		||||
        jump antispoof_input
 | 
			
		||||
        meta mark & 0x00000002 == 0 accept comment "accept packets from non-captive clients"
 | 
			
		||||
        # now accept all traffic to destinations allowed in captive state, and mark "redirect" packets:
 | 
			
		||||
        jump captive_allowed
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain input {
 | 
			
		||||
        type filter hook input priority 0;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        # TODO: limit services available to clients? iptconf might already be enough
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain forward_down {
 | 
			
		||||
        # only filter uplink -> client here:
 | 
			
		||||
        iifname != <%= @uplink_interface %> accept
 | 
			
		||||
        oifname != <%= @client_interface %> accept
 | 
			
		||||
        # established connections
 | 
			
		||||
        ct state established,related accept
 | 
			
		||||
        # allow incoming ipv6 ping (ipv4 ping can't work due to NAT)
 | 
			
		||||
        icmpv6 type echo-request accept
 | 
			
		||||
        drop
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain antispoof_forward {
 | 
			
		||||
        meta mark & 0x00000001 == 0 goto forward_down comment "handle forwardings not from client interface"
 | 
			
		||||
        ip saddr { <%= @client_ipv4 %> } return
 | 
			
		||||
        ip6 saddr { <%= @client_ipv6 %> } return
 | 
			
		||||
        drop
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain captive_allowed_icmp {
 | 
			
		||||
        # allow all pings to servers we allow other kind of traffic
 | 
			
		||||
        icmp type echo-request accept
 | 
			
		||||
        icmpv6 type echo-request accept
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain captive_allowed_http {
 | 
			
		||||
        # http + https (but not QUIC)
 | 
			
		||||
        tcp dport { 80, 443 } accept
 | 
			
		||||
        goto captive_allowed_icmp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain captive_allowed_dns {
 | 
			
		||||
        # DNS, DoT, DoH
 | 
			
		||||
        udp dport { 53, 853 } accept
 | 
			
		||||
        tcp dport { 53, 443, 853 } accept
 | 
			
		||||
        goto captive_allowed_icmp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain captive_allowed_ntp {
 | 
			
		||||
        # only NTP
 | 
			
		||||
        udp dport 123 accept
 | 
			
		||||
        goto captive_allowed_icmp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain captive_allowed {
 | 
			
		||||
        # all protocols for fully whitelisted
 | 
			
		||||
        ip daddr $full_ipv4 accept
 | 
			
		||||
        ip6 daddr $full_ipv6 accept
 | 
			
		||||
 | 
			
		||||
        ip daddr $http_server_ipv4 jump captive_allowed_http
 | 
			
		||||
        ip6 daddr $http_server_ipv6 jump captive_allowed_http
 | 
			
		||||
 | 
			
		||||
        ip daddr $dns_server_ipv4 jump captive_allowed_dns
 | 
			
		||||
        ip6 daddr $dns_server_ipv6 jump captive_allowed_dns
 | 
			
		||||
 | 
			
		||||
        ip daddr $ntp_server_ipv4 jump captive_allowed_ntp
 | 
			
		||||
        ip6 daddr $ntp_server_ipv6 jump captive_allowed_ntp
 | 
			
		||||
 | 
			
		||||
        # mark (new) http clients to redirect to local http server with bit 2
 | 
			
		||||
        tcp dport 80 ct state new meta mark set meta mark | 0x00000004 accept comment "DNAT HTTP"
 | 
			
		||||
        # mark packets to reject in forward with bit 3
 | 
			
		||||
        meta mark set meta mark | 0x00000008 comment "reject in forwarding"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # for DNS+NTP
 | 
			
		||||
    ct timeout udp-oneshot {
 | 
			
		||||
        protocol udp;
 | 
			
		||||
        policy = { unreplied: 10, replied: 0 }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain forward_reject {
 | 
			
		||||
        # could reject TCP connections with tcp reset, but ICMP unreachable should be good enough
 | 
			
		||||
        # (and it's also semantically correct):
 | 
			
		||||
        # ip protocol tcp reject with tcp reset
 | 
			
		||||
        # ip6 nexthdr tcp reject with tcp reset
 | 
			
		||||
 | 
			
		||||
        # but we need to close existing tcp sessions: (when client moves to captive state)
 | 
			
		||||
        ct state != new ip protocol tcp reject with tcp reset
 | 
			
		||||
        ct state != new ip6 nexthdr tcp reject with tcp reset
 | 
			
		||||
 | 
			
		||||
        # default icmp reject
 | 
			
		||||
        reject with icmpx type admin-prohibited
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # block some ports always
 | 
			
		||||
    chain blacklist {
 | 
			
		||||
        tcp dport $backlist_tcp goto forward_reject
 | 
			
		||||
        udp dport $backlist_udp goto forward_reject
 | 
			
		||||
        udp sport $backlist_udp_source goto forward_reject
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # ports we assume are "proper" - still only allowed in non-captive state
 | 
			
		||||
    chain whitelist {
 | 
			
		||||
        ip daddr $whitelist_dest_ipv4 accept
 | 
			
		||||
        ip6 daddr $whitelist_dest_ipv6 accept
 | 
			
		||||
        tcp dport $whitelist_tcp accept
 | 
			
		||||
        udp dport $whitelist_udp accept
 | 
			
		||||
        ip protocol esp accept
 | 
			
		||||
        ip6 nexthdr esp accept
 | 
			
		||||
        icmp type echo-request accept
 | 
			
		||||
        icmpv6 type echo-request accept
 | 
			
		||||
        # icmp related to existing connections, ...
 | 
			
		||||
        ct state established,related accept
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain forward {
 | 
			
		||||
        type filter hook forward priority 0;
 | 
			
		||||
        policy drop;
 | 
			
		||||
        jump antispoof_forward
 | 
			
		||||
        # optimize conntrack timeouts for DNS and NTP
 | 
			
		||||
        udp dport { 53, 123 } ct timeout set "udp-oneshot"
 | 
			
		||||
        jump blacklist
 | 
			
		||||
        # reject packets marked for rejection in mark_clients/captive_allowed (bit 3)
 | 
			
		||||
        meta mark & 0x00000008 != 0 goto forward_reject
 | 
			
		||||
        jump whitelist
 | 
			
		||||
        # optional (policy):
 | 
			
		||||
        goto forward_reject comment "drop not-whitelisted traffic completely"
 | 
			
		||||
        # (conntrack) mark connection with bit 4 for shaping
 | 
			
		||||
        ct mark set ct mark | 0x00000010 counter accept comment "accept shaped traffic"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain forward_con_shaped_mark {
 | 
			
		||||
        type filter hook forward priority 10;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        # copy conntrack mark bit 4 to meta mark bit 4
 | 
			
		||||
        ct mark & 0x00000010 == 0 accept comment "non shaped connection"
 | 
			
		||||
        meta mark set meta mark | 0x00000010 counter accept comment "shaped connection"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE: mustn't flush this table to keep the set around
 | 
			
		||||
# DON'T ENABLE THIS:
 | 
			
		||||
#   flush table inet captive_mark
 | 
			
		||||
 | 
			
		||||
# as table wasn't flushed, at least delete the single chain we're expecting
 | 
			
		||||
table inet captive_mark {
 | 
			
		||||
    chain prerouting { }
 | 
			
		||||
}
 | 
			
		||||
delete chain inet captive_mark prerouting;
 | 
			
		||||
 | 
			
		||||
table inet captive_mark {
 | 
			
		||||
    # set isn't recreated, i.e. keeps dynamically added members
 | 
			
		||||
    set allowed {
 | 
			
		||||
        type ether_addr
 | 
			
		||||
        flags timeout
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chain prerouting {
 | 
			
		||||
        type filter hook prerouting priority -150;
 | 
			
		||||
        policy accept;
 | 
			
		||||
        iifname != <%= @client_interface %> accept
 | 
			
		||||
        # mark packets from client interface with bit 0
 | 
			
		||||
        meta mark set meta mark | 0x00000001
 | 
			
		||||
        ether saddr @allowed accept
 | 
			
		||||
        # mark "captive" clients with bit 1
 | 
			
		||||
        meta mark set meta mark | 0x00000002
 | 
			
		||||
        accept
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # further existing elements in this table won't be cleared by loading this file!
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								contrib/radvd.conf.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								contrib/radvd.conf.erb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
interface <%= client_interface %>
 | 
			
		||||
{
 | 
			
		||||
  AdvSendAdvert on;
 | 
			
		||||
  AdvDefaultPreference high;
 | 
			
		||||
  AdvSourceLLAddress off;
 | 
			
		||||
  AdvRASrcAddress {
 | 
			
		||||
      fe80::1;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  RDNSS <%= ' '.join(@dns_resolvers_ipv6) %> {
 | 
			
		||||
    FlushRDNSS off;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  AdvOtherConfigFlag on;
 | 
			
		||||
 | 
			
		||||
  # will require radvd > 2.19 (not released yet)
 | 
			
		||||
  # AdvCaptivePortalAPI "https://<%= @service_name %>/api/captive-portal";
 | 
			
		||||
 | 
			
		||||
  prefix <%= client_ipv6 %>
 | 
			
		||||
  {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										31
									
								
								contrib/shape_non_whitelisted.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								contrib/shape_non_whitelisted.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
limit_iface() {
 | 
			
		||||
  local dev=$1
 | 
			
		||||
  local shape_rate=$2  # "guaranteed"
 | 
			
		||||
  local shape_ceil=$3  # "upper limit"
 | 
			
		||||
 | 
			
		||||
  tc qdisc delete dev "${dev}" root 2>/dev/null
 | 
			
		||||
  tc qdisc add dev "${dev}" root handle 1: htb default 0x11
 | 
			
		||||
  # basically no limit for default traffic
 | 
			
		||||
  tc class add dev "${dev}" parent 1: classid 1:11 htb rate 10Gbit ceil 100Gbit quantum 100000
 | 
			
		||||
  # limit "bad" (not whitelisted) traffic
 | 
			
		||||
  tc class add dev "${dev}" parent 1: classid 1:12 htb prio 1 rate "${shape_rate}" ceil "${shape_ceil}"
 | 
			
		||||
  # use "codel" qdisc for both classes, but with larger queue for default traffic
 | 
			
		||||
  tc qdisc add dev "${dev}" parent 1:11 handle 11: codel limit 20000
 | 
			
		||||
  tc qdisc add dev "${dev}" parent 1:12 handle 12: codel
 | 
			
		||||
 | 
			
		||||
  # sort into bad class based on netfilter mark (if bit 0x10 is set)
 | 
			
		||||
  tc filter add dev "${dev}" parent 1: prio 1 basic match 'meta(nf_mark mask 0x10 eq 0x10)' classid 1:12
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uplink=$1
 | 
			
		||||
downlink=$2
 | 
			
		||||
 | 
			
		||||
if [ -z "${uplink}" -o -z "${downlink}" ]; then
 | 
			
		||||
  echo >&2 "Missing uplink and downlink interface names"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
limit_iface "${uplink}" "1Mbit" "1Mbit"
 | 
			
		||||
limit_iface "${downlink}" "1Mbit" "1Mbit"
 | 
			
		||||
							
								
								
									
										18
									
								
								contrib/systemd/capport-enforcement.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								contrib/systemd/capport-enforcement.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Captive Portal enforcement service
 | 
			
		||||
Wants=basic.target
 | 
			
		||||
After=basic.target network.target
 | 
			
		||||
ConditionFileIsExecutable=/var/lib/python-capport/start-control.sh
 | 
			
		||||
ConditionPathIsDirectory=/var/lib/python-capport/venv
 | 
			
		||||
 | 
			
		||||
# TODO: start as unprivileged user but with CAP_NET_ADMIN ?
 | 
			
		||||
[Service]
 | 
			
		||||
Type=notify
 | 
			
		||||
WatchdogSec=10
 | 
			
		||||
ExecStart=/var/lib/python-capport/start-control.sh
 | 
			
		||||
Restart=always
 | 
			
		||||
ProtectSystem=full
 | 
			
		||||
ProtectHome=true
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
							
								
								
									
										18
									
								
								contrib/systemd/capport-nftables.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								contrib/systemd/capport-nftables.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=NFT Firewall Shim for Captive Portal
 | 
			
		||||
Wants=network-pre.target
 | 
			
		||||
Before=network-pre.target shutdown.target
 | 
			
		||||
Conflicts=shutdown.target
 | 
			
		||||
DefaultDependencies=no
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=oneshot
 | 
			
		||||
RemainAfterExit=yes
 | 
			
		||||
StandardInput=null
 | 
			
		||||
ProtectSystem=full
 | 
			
		||||
ProtectHome=true
 | 
			
		||||
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
 | 
			
		||||
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=sysinit.target
 | 
			
		||||
							
								
								
									
										15
									
								
								contrib/systemd/capport-shaping.service.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								contrib/systemd/capport-shaping.service.erb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Captive Portal traffic shaping
 | 
			
		||||
Wants=basic.target
 | 
			
		||||
After=basic.target network.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=oneshot
 | 
			
		||||
RemainAfterExit=yes
 | 
			
		||||
StandardInput=null
 | 
			
		||||
ProtectSystem=full
 | 
			
		||||
ProtectHome=true
 | 
			
		||||
ExecStart=/etc/capport-tc.sh <%= @uplink_interface %> <%= @client_interface %>
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
							
								
								
									
										18
									
								
								contrib/systemd/capport-webui.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								contrib/systemd/capport-webui.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Captive Portal web ui service
 | 
			
		||||
Wants=basic.target
 | 
			
		||||
After=basic.target network.target
 | 
			
		||||
ConditionFileIsExecutable=/var/lib/python-capport/start-control.sh
 | 
			
		||||
ConditionPathIsDirectory=/var/lib/python-capport/venv
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
User=capport
 | 
			
		||||
Type=notify
 | 
			
		||||
WatchdogSec=10
 | 
			
		||||
ExecStart=/var/lib/python-capport/start-api.sh
 | 
			
		||||
Restart=always
 | 
			
		||||
ProtectSystem=full
 | 
			
		||||
ProtectHome=true
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
		Reference in New Issue
	
	Block a user