ffho_netfilter.py 12 KB

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