ffho_netfilter.py 13 KB

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