Pārlūkot izejas kodu

Merge pull request #9 from BarbarossaTM/feature/nftables-forward

Add support for generating forwarding/NAT nftables rules as well as uRPF
Philipp Fromme 3 gadi atpakaļ
vecāks
revīzija
ef8c13534b
3 mainītis faili ar 262 papildinājumiem un 7 dzēšanām
  1. 148 1
      _modules/ffho_netfilter.py
  2. 2 0
      nftables/init.sls
  3. 112 6
      nftables/nftables.conf.tmpl

+ 148 - 1
_modules/ffho_netfilter.py

@@ -3,6 +3,14 @@
 #
 
 import ipaddress
+import re
+
+import ffho_net
+
+
+# Prepare regex to match VLAN intefaces / extract IDs
+vlan_re = re.compile (r'^vlan(\d+)$')
+
 
 def generate_service_rules (services, acls, af):
 	rules = []
@@ -77,7 +85,7 @@ def generate_service_rules (services, acls, af):
 
 		# Multiple ports?
 		if len (srv['ports']) > 1:
-			ports = "{ %s }" % ", ".join (map (str, sorted (srv['ports'])))
+			ports = "{ %s }" % ", ".join (map (str, srv['ports']))
 		else:
 			ports = srv['ports'][0]
 
@@ -85,3 +93,142 @@ def generate_service_rules (services, acls, af):
 		rules.append (rule)
 
 	return rules
+
+
+def generate_forward_policy (policy, roles, config_context):
+	fp = {
+		# Get default policy for packets to be forwarded
+		'policy' : 'drop',
+		'policy_reason' : 'default',
+		'rules': {
+			4 : [],
+			6 : [],
+		},
+	}
+
+	if 'forward_default_policy' in policy:
+		fp['policy'] = policy['forward_default_policy']
+		fp['policy_reason'] = 'forward_default_policy'
+
+	# Does any local role warrants for forwarding packets?
+	accept_roles = [role for role in policy.get ('forward_accept_roles', []) if role in roles]
+	if accept_roles:
+		fp['policy'] = 'accept'
+		fp['policy_reason'] = "roles: " + ",".join (accept_roles)
+
+	try:
+		cust_rules = config_context['filter']['forward']
+		for af in [ 4, 6 ]:
+			if af not in cust_rules:
+				continue
+
+			if type (cust_rules[af]) != list:
+				raise ValueError ("nftables:filter:forward:%d in config context expected to be a list!" % af)
+
+				fp['rules'][af] = cust_rules[af]
+	except KeyError:
+		pass
+
+	return fp
+
+
+def generate_nat_policy (roles, config_context):
+	np = {
+		4 : {},
+		6 : {},
+	}
+
+	# Any custom rules?
+	cc_nat = config_context.get ('nat')
+	if cc_nat:
+		for chain in ['output', 'prerouting', 'postrouting']:
+			if chain not in cc_nat:
+				continue
+
+			for af in [ 4, 6 ]:
+				if str (af) in cc_nat[chain]:
+					np[af][chain] = cc_nat[chain][str (af)]
+
+	return np
+
+
+def _active_urpf (iface, iface_config):
+	# Ignore loopback
+	if iface == "lo":
+		return False
+
+	# Forcefully enable uRPF via tags on Netbox interface?
+	if 'urpf_enable' in iface_config.get ('tags', []):
+		return True
+
+	# No uRPF on infra VPNs
+	for vpn_prefix in ["gre_", "ovpn-", "wg-"]:
+		if iface.startswith (vpn_prefix):
+			return False
+
+	# No address, no uRPF
+	if not iface_config.get ('prefixes'):
+		return False
+
+	# Interface in vrf_external connect to the Internet
+	if iface_config.get ('vrf') in ['vrf_external']:
+		return False
+
+	# Ignore interfaces by VLAN
+	match = vlan_re.search (iface)
+	if match:
+		vid = int (match.group (1))
+
+		# Magic
+		if 900 <= vid <= 999:
+			return False
+
+		# Wired infrastructure stuff
+		if 1000 <= vid <= 1499:
+			return False
+
+		# Wireless infrastructure stuff
+		if 2000 <= vid <= 2299:
+			return False
+
+	return True
+
+
+def generate_urpf_policy (interfaces):
+	urpf = {}
+
+	for iface in sorted (interfaces.keys ()):
+		iface_config = interfaces[iface]
+
+		if not _active_urpf (iface, iface_config):
+			continue
+
+		# Ok this seems to be and edge interface
+		urpf[iface] = {
+			'iface' : iface,
+			'desc' : iface_config.get ('desc', ''),
+			4 : [],
+			6 : [],
+		}
+
+		# Gather configure prefixes
+		for address in iface_config.get ('prefixes'):
+			pfx = ipaddress.ip_network (address, strict = False)
+			urpf[iface][pfx.version].append ("%s/%s" % (pfx.network_address, pfx.prefixlen))
+
+	sorted_urpf = []
+
+	for iface in ffho_net.get_interface_list (urpf):
+		sorted_urpf.append (urpf[iface])
+
+	return sorted_urpf
+
+
+#
+# Check if at least one of the node roles are supposed to run DHCP
+def allow_dhcp (fw_policy, roles):
+	for dhcp_role in fw_policy.get ('dhcp_roles', []):
+		if dhcp_role in roles:
+			return True
+
+	return False

+ 2 - 0
nftables/init.sls

@@ -24,4 +24,6 @@ purge-iptables:
   pkg.purged:
     - pkgs:
       - iptables-persistent
+{%- if not 'docker' in salt['pillar.get']('nodes:' ~ grains['id'] ~ ':roles', []) %}
       - iptables
+{%- endif %}

+ 112 - 6
nftables/nftables.conf.tmpl

@@ -2,14 +2,24 @@
 #
 # /etc/nftables.conf - FFHO packet filter configuration
 #
-{%- set roles = salt['pillar.get']('nodes:' ~ grains['id'] ~ ':roles', []) %}
+{%- set node_config = salt['pillar.get']('nodes:' ~ grains['id']) %}
+{%- set nf_cc = node_config.get ('nftables', {}) %}
+{%- set roles = node_config.get ('roles', []) %}
+{%- set services = node_config.get ('services', []) %}
+
+{%- set fw_policy = salt['pillar.get']('firewall:policy', {}) %}
 {%- set acls = salt['pillar.get']('firewall:acls') %}
 {%- set admin_access = salt['pillar.get']('firewall:admin_access') %}
 {%- set ssh = salt['pillar.get']("firewall:ssh") %}
-{%- set services = salt['pillar.get']('nodes:' ~ grains['id'] ~ ':services', []) %}
+
 {%- set icinga2_queriers = salt['pillar.get']('monitoring:icinga2:queriers', []) %}
 {%- set nms_list = salt['pillar.get']('globals:snmp:nms_list', []) %}
 
+{%- set forward = salt['ffho_netfilter.generate_forward_policy'](fw_policy, roles, nf_cc) %}
+{%- set nat_policy = salt['ffho_netfilter.generate_nat_policy'](roles, nf_cc) %}
+{%- set urpf = salt['ffho_netfilter.generate_urpf_policy'](node_config['ifaces']) %}
+{%- set allow_dhcp = salt['ffho_netfilter.allow_dhcp'](fw_policy, roles) %}
+
 flush ruleset
 
 table ip filter {
@@ -25,8 +35,11 @@ table ip filter {
 	chain input {
 		type filter hook input priority 0; policy drop;
 		iifname "lo" counter accept
-		ip protocol icmp counter jump icmp_chain
+		udp dport 0 counter drop
 		tcp dport 7 counter drop comment "Ignore echo protocol queries"
+		udp dport 4789 jump vxlan
+		jump urpf
+		ip protocol icmp counter jump icmp_chain
 		ct state invalid counter drop
 		counter jump admin_access
 		counter jump monitoring
@@ -37,11 +50,26 @@ table ip filter {
 {%- endif %}
 		ct state related,established counter accept
 		counter jump services
-		limit rate 1/second burst 3 packets counter log prefix "netfilter: "
+		meta pkttype broadcast counter drop comment "Drop broadcasts before logging"
+		limit rate 1/second burst 3 packets counter log prefix "nf input: "
 		limit rate 1/second burst 3 packets counter reject with icmp type admin-prohibited
 		counter drop
 	}
 
+	chain forward {
+		type filter hook forward priority 0; policy {{ forward['policy'] }}; # {{ forward['policy_reason'] }}
+		jump urpf
+{#- custom rules #}
+{%- for rule in forward['rules'].get ('4', []) %}
+		{{ rule }}
+{%- endfor %}
+
+{%- if forward['policy'] == 'drop' %}
+		limit rate 1/second burst 3 packets counter log prefix "nf forward: "
+		limit rate 1/second burst 3 packets counter reject with icmp type admin-prohibited
+{%- endif %}
+	}
+
 	chain icmp_chain {
 		icmp type { echo-request, destination-unreachable, time-exceeded } counter accept
 	}
@@ -77,11 +105,31 @@ table ip filter {
 	}
 
 	chain services {
+{%- if allow_dhcp %}
+		udp dport 67 counter accept comment "DHCP"
+{%- endif %}
+
 {%- for rule in salt['ffho_netfilter.generate_service_rules'](services, acls, 4) %}
 		{{ rule }}
 {%- endfor %}
 	}
 
+	chain urpf {
+{%- for iface_cfg in urpf  %}
+  {%- for pfx in iface_cfg[4] %}
+		iif {{ iface_cfg['iface'] }} ip saddr {{ pfx }} return
+  {%- endfor %}
+		iif {{ iface_cfg['iface'] }} counter drop
+{%- endfor %}
+	}
+
+	chain vxlan {
+{%- for iface in node_config['ifaces'] if node_config['ifaces'][iface].get ('batman_connect_sites') %}
+		iif {{ iface }} accept
+{%- endfor %}
+		counter drop
+	}
+
 	chain log-drop {
 		limit rate 1/second burst 3 packets counter log prefix "netfilter: "
 		counter drop
@@ -107,9 +155,11 @@ table ip6 filter {
 	chain input {
 		type filter hook input priority 0; policy drop;
 		iifname "lo" counter accept
-		ip6 nexthdr icmpv6 counter jump icmp_chain
 		tcp dport 7 counter drop comment "Ignore echo protocol queries"
-		ct state invalid counter drop comment "Drop packets that do not make sense."
+		udp dport 4789 jump vxlan
+		jump urpf
+		meta l4proto icmpv6 counter jump icmp_chain
+		ct state invalid counter drop
 		counter jump admin_access
 		counter jump monitoring
 		tcp dport 22 counter jump ssh
@@ -124,9 +174,24 @@ table ip6 filter {
 		counter drop
 	}
 
+	chain forward {
+		type filter hook forward priority 0; policy {{ forward['policy'] }}; # {{ forward['policy_reason'] }}
+		jump urpf
+{#- custom rules #}
+{%- for rule in forward['rules'].get ('6', []) %}
+		{{ rule }}
+{%- endfor %}
+
+{%- if forward['policy'] == 'drop' %}
+		limit rate 1/second burst 3 packets counter log prefix "nf forward: "
+		limit rate 1/second burst 3 packets counter reject with icmp type admin-prohibited
+{%- endif %}
+	}
+
 	chain icmp_chain {
 		icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply } counter accept
 		icmpv6 type { nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert } ip6 hoplimit 255 counter accept
+		icmpv6 type { mld-listener-query, mld-listener-report } ip6 saddr fe80::/64 counter accept
 	}
 
 	chain admin_access {
@@ -163,6 +228,28 @@ table ip6 filter {
 {%- for rule in salt['ffho_netfilter.generate_service_rules'](services, acls, 6) %}
 		{{ rule }}
 {%- endfor %}
+
+{#- Allow respondd queries on gateways #}
+{%- if 'batman_gw' in roles %}
+		ip6 saddr fe80::/64 ip6 daddr ff05::2:1001 udp dport 1001 counter accept comment "responnd"
+{%- endif %}
+	}
+
+	chain urpf {
+		ip6 saddr fe80::/64 return
+{%- for iface_cfg in urpf  %}
+  {%- for pfx in iface_cfg[6] %}
+		iif {{ iface_cfg['iface'] }} ip6 saddr {{ pfx }} return
+  {%- endfor %}
+		iif {{ iface_cfg['iface'] }} counter drop
+{%- endfor %}
+	}
+
+	chain vxlan {
+{%- for iface in node_config['ifaces'] if node_config['ifaces'][iface].get ('batman_connect_sites') %}
+		iif {{ iface }} accept
+{%- endfor %}
+		counter drop
 	}
 
 	chain log-drop {
@@ -176,3 +263,22 @@ table ip6 filter {
 		counter drop
 	}
 }
+
+{#-
+ # NAT
+ #}
+{%- for af in [ 4, 6 ] %}
+  {%- if nat_policy[af] %}
+    {%- set af_name = "ip" if af == 4 else "ip6" %}
+table {{ af_name }} nat {
+    {%- for chain in ['output', 'prerouting', 'postrouting'] if chain in nat_policy[af] %}
+	chain {{ chain }} {
+		type nat hook {{ chain }} priority 0; policy accept;
+      {%- for rule in nat_policy[af][chain] %}
+		{{ rule }}
+      {%- endfor %}
+	}
+    {%- endfor %}
+}
+  {%- endif %}
+{%- endfor %}