ffho_netfilter.py 12 KB

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