#!/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! }