ffho_netfilter.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. #
  2. # FFHO netfilter helper functions
  3. #
  4. import ipaddress
  5. import re
  6. import ffho_net
  7. # Prepare regex to match VLAN intefaces / extract IDs
  8. vlan_re = re.compile (r'^vlan(\d+)$')
  9. ################################################################################
  10. # Internal helper functions #
  11. ################################################################################
  12. #
  13. # Check if at least one of the node roles are supposed to run DHCP
  14. def _allow_dhcp (fw_policy, roles):
  15. for dhcp_role in fw_policy.get ('dhcp_roles', []):
  16. if dhcp_role in roles:
  17. return True
  18. return False
  19. # Generate services rules for the given AF
  20. def _generate_service_rules (services, acls, af):
  21. rules = []
  22. for srv in services:
  23. rule = ""
  24. comment = srv['descr']
  25. acl_comment = ""
  26. src_prefixes = []
  27. # If there are no DST IPs set at all or DST IPs for this AF set, we have a rule to build,
  28. # if this is NOT the case, there is no rule for this AF to generate, carry on.
  29. if not ((not srv['ips']['4'] and not srv['ips']['6']) or srv['ips'][str(af)]):
  30. continue
  31. # Is/are IP(s) set for this service?
  32. if srv['ips'][str(af)]:
  33. rule += "ip" if af == 4 else "ip6"
  34. dst_ips = srv['ips'][str(af)]
  35. if len (dst_ips) == 1:
  36. rule += " daddr %s " % dst_ips[0]
  37. else:
  38. rule += " daddr { %s } " % ", ".join (dst_ips)
  39. # ACLs defined for this service?
  40. if srv['acl']:
  41. srv_acl = sorted (srv['acl'])
  42. for ace in srv_acl:
  43. ace_pfx = (acls[ace][af])
  44. # Many entries
  45. if type (ace_pfx) == list:
  46. src_prefixes.extend (ace_pfx)
  47. else:
  48. src_prefixes.append (ace_pfx)
  49. acl_comment = "acl: %s" % ", ".join (srv_acl)
  50. # Additional prefixes defined for this service?
  51. if srv['additional_prefixes']:
  52. add_pfx = []
  53. # Additional prefixes are given as a space separated list
  54. for entry in srv['additional_prefixes'].split ():
  55. # Strip commas and spaces, just in case
  56. pfx_str = entry.strip (" ,")
  57. pfx_obj = ipaddress.ip_network (pfx_str)
  58. # We only care for additional pfx for this AF
  59. if pfx_obj.version != af:
  60. continue
  61. add_pfx.append (pfx_str)
  62. if add_pfx:
  63. src_prefixes.extend (add_pfx)
  64. if acl_comment:
  65. acl_comment += ", "
  66. acl_comment += "additional pfx"
  67. # Combine ACL + additional prefixes (if any)
  68. if src_prefixes:
  69. rule += "ip" if af == 4 else "ip6"
  70. if len (src_prefixes) > 1:
  71. rule += " saddr { %s } " % ", ".join (src_prefixes)
  72. else:
  73. rule += " saddr %s " % src_prefixes[0]
  74. if acl_comment:
  75. comment += " (%s)" % acl_comment
  76. # Multiple ports?
  77. if len (srv['ports']) > 1:
  78. ports = "{ %s }" % ", ".join (map (str, srv['ports']))
  79. else:
  80. ports = srv['ports'][0]
  81. rule += "%s dport %s counter accept comment \"%s\"" % (srv['proto'], ports, comment)
  82. rules.append (rule)
  83. return rules
  84. def _generate_wireguard_rule (node_config):
  85. ports = []
  86. wg = node_config.get ('wireguard')
  87. if not wg or not 'tunnels' in wg:
  88. return None
  89. for iface, wg_cfg in node_config['wireguard']['tunnels'].items ():
  90. if wg_cfg['mode'] == 'server':
  91. ports.append (wg_cfg['port'])
  92. if not ports:
  93. return None
  94. if len (ports) > 1:
  95. ports = "{ %s }" % ", ".join (map (str, ports))
  96. else:
  97. ports = ports[0]
  98. return "udp dport %s counter accept comment Wireguard" % ports
  99. def _active_urpf (iface, iface_config):
  100. # Ignore loopbacks
  101. if iface == 'lo' or iface_config.get ('link-type', '') == 'dummy':
  102. return False
  103. # Forcefully enable/disable uRPF via tags on Netbox interface?
  104. if 'urpf' in iface_config:
  105. return iface_config['urpf']
  106. # No uRPF on infra VPNs
  107. for vpn_prefix in ["gre_", "ovpn-", "wg-"]:
  108. if iface.startswith (vpn_prefix):
  109. return False
  110. # No address, no uRPF
  111. if not iface_config.get ('prefixes'):
  112. return False
  113. # Interface in vrf_external connect to the Internet
  114. if iface_config.get ('vrf') in ['vrf_external']:
  115. return False
  116. # Default gateway pointing towards this interface?
  117. if iface_config.get ('gateway'):
  118. return False
  119. # Ignore interfaces by VLAN
  120. match = vlan_re.search (iface)
  121. if match:
  122. vid = int (match.group (1))
  123. # Magic
  124. if 900 <= vid <= 999:
  125. return False
  126. # Wired infrastructure stuff
  127. if 1000 <= vid <= 1499:
  128. return False
  129. # Wireless infrastructure stuff
  130. if 2000 <= vid <= 2299:
  131. return False
  132. return True
  133. ################################################################################
  134. # Public functions #
  135. ################################################################################
  136. #
  137. # Generate rules to allow access to services running on this node.
  138. # Services can either be allow programmatically here or explicitly
  139. # as Services applied to the device/VM in Netbox
  140. def generate_service_rules (fw_config, node_config):
  141. acls = fw_config.get ('acls', {})
  142. fw_policy = fw_config.get ('policy', {})
  143. services = node_config.get ('services', [])
  144. roles = node_config.get ('roles', [])
  145. rules = {
  146. 4 : [],
  147. 6 : [],
  148. }
  149. #
  150. # Add rules based on roles and tunnels
  151. #
  152. # Does this node run a DHCP server?
  153. if _allow_dhcp (fw_policy, roles):
  154. rules[4].append ('udp dport 67 counter accept comment "DHCP"')
  155. # Allow respondd queries on B.A.T.M.A.N. adv. nodes
  156. if 'batman' in roles:
  157. rules[6].append ('ip6 saddr fe80::/64 ip6 daddr ff05::2:1001 udp dport 1001 counter accept comment "responnd"')
  158. # Allow respondd replies to yanic
  159. if 'yanic' in roles:
  160. rules[6].append ('ip6 saddr fe80::/64 udp sport 1001 counter accept comment "respondd replies to yanic"')
  161. # Allow Wireguard tunnels
  162. wg_rule = _generate_wireguard_rule (node_config)
  163. if wg_rule:
  164. rules[4].append (wg_rule)
  165. for af in [ 4, 6 ]:
  166. comment = "Generated rules" if rules[af] else "No generated rules"
  167. rules[af].insert (0, "# %s" % comment)
  168. #
  169. # Generate and add rules for services from Netbox, if any
  170. #
  171. for af in [ 4, 6 ]:
  172. srv_rules = _generate_service_rules (services, acls, af)
  173. if not srv_rules:
  174. rules[af].append ("# No services defined in Netbox")
  175. continue
  176. rules[af].append ("# Services defined in Netbox")
  177. rules[af].extend (srv_rules)
  178. return rules
  179. def generate_forward_policy (fw_config, node_config):
  180. policy = fw_config.get ('policy', {})
  181. roles = node_config.get ('roles', [])
  182. nf_cc = node_config.get ('nftables', {})
  183. fp = {
  184. # Get default policy for packets to be forwarded
  185. 'policy' : 'drop',
  186. 'policy_reason' : 'default',
  187. 'rules': {
  188. 4 : [],
  189. 6 : [],
  190. },
  191. }
  192. if 'forward_default_policy' in policy:
  193. fp['policy'] = policy['forward_default_policy']
  194. fp['policy_reason'] = 'forward_default_policy'
  195. # Does any local role warrants for forwarding packets?
  196. accept_roles = [role for role in policy.get ('forward_accept_roles', []) if role in roles]
  197. if accept_roles:
  198. fp['policy'] = 'accept'
  199. fp['policy_reason'] = "roles: " + ",".join (accept_roles)
  200. try:
  201. cust_rules = nf_cc['filter']['forward']
  202. for af in [ 4, 6 ]:
  203. if af not in cust_rules:
  204. continue
  205. if type (cust_rules[af]) != list:
  206. raise ValueError ("nftables:filter:forward:%d in config context expected to be a list!" % af)
  207. fp['rules'][af] = cust_rules[af]
  208. except KeyError:
  209. pass
  210. return fp
  211. def generate_mgmt_config (fw_config, node_config):
  212. # If this box is not a router, it will not be responsible for providing
  213. # access to any management network, so there's nothing to do here.
  214. roles = node_config.get ('roles', [])
  215. if 'router' not in roles:
  216. return None
  217. # Get management prefixes from firewall configuration.
  218. # If there are no prefixes defined, there's nothing we can do here.
  219. mgmt_prefixes = fw_config.get ('acls', {}).get ('Management networks', {})
  220. if not mgmt_prefixes:
  221. return None
  222. # We only care for IPv4 prefixes for now.
  223. if 4 not in mgmt_prefixes:
  224. return None
  225. config = {
  226. 'ifaces': [],
  227. 'prefixes': mgmt_prefixes,
  228. }
  229. mgmt_interfaces = []
  230. interfaces = node_config['ifaces']
  231. for iface in interfaces.keys ():
  232. match = vlan_re.match (iface)
  233. if match:
  234. vlan_id = int (match.group (1))
  235. if vlan_id >= 3000 and vlan_id < 3099:
  236. config['ifaces'].append (iface)
  237. if len (config['ifaces']) == 0:
  238. return None
  239. return config
  240. def generate_nat_policy (node_config):
  241. roles = node_config.get ('roles', [])
  242. nf_cc = node_config.get ('nftables', {})
  243. np = {
  244. 4 : {},
  245. 6 : {},
  246. }
  247. # Any custom rules?
  248. cc_nat = nf_cc.get ('nat')
  249. if cc_nat:
  250. for chain in ['output', 'prerouting', 'postrouting']:
  251. if chain not in cc_nat:
  252. continue
  253. for af in [ 4, 6 ]:
  254. if str (af) in cc_nat[chain]:
  255. np[af][chain] = cc_nat[chain][str (af)]
  256. return np
  257. def generate_urpf_policy (node_config):
  258. roles = node_config.get ('roles', [])
  259. # If this box is not a router, all traffic will come in via the internal/
  260. # external interface an uRPF doesn't make any sense here, so we don't even
  261. # have to look at the interfaces.
  262. if 'router' not in roles:
  263. return []
  264. urpf = {}
  265. interfaces = node_config['ifaces']
  266. for iface in sorted (interfaces.keys ()):
  267. iface_config = interfaces[iface]
  268. if not _active_urpf (iface, iface_config):
  269. continue
  270. # Ok this seems to be and edge interface
  271. urpf[iface] = {
  272. 'iface' : iface,
  273. 'desc' : iface_config.get ('desc', ''),
  274. 4 : [],
  275. 6 : [],
  276. }
  277. # Gather configure prefixes
  278. for address in iface_config.get ('prefixes'):
  279. pfx = ipaddress.ip_network (address, strict = False)
  280. urpf[iface][pfx.version].append ("%s/%s" % (pfx.network_address, pfx.prefixlen))
  281. sorted_urpf = []
  282. for iface in ffho_net.get_interface_list (urpf):
  283. sorted_urpf.append (urpf[iface])
  284. return sorted_urpf
  285. #
  286. # Get a list of interfaces which will form OSPF adjacencies
  287. def get_ospf_active_interface (node_config):
  288. ifaces = []
  289. ospf_config = ffho_net.get_ospf_config (node_config, "doesnt_matter_here")
  290. for area in sorted (ospf_config.keys ()):
  291. area_ifaces = ospf_config[area]
  292. for iface in ffho_net.get_interface_list (area_ifaces):
  293. if not area_ifaces[iface].get ('stub', False):
  294. ifaces.append (iface)
  295. return ifaces
  296. #
  297. # Get a list of interfaces to allow VXLAN encapsulated traffic on
  298. def get_vxlan_interfaces (interfaces):
  299. vxlan_ifaces = []
  300. for iface in interfaces:
  301. if interfaces[iface].get ('batman_connect_sites'):
  302. vxlan_ifaces.append (iface)
  303. return vxlan_ifaces