Browse Source

DNS: Update and activate base-line DNS generator based on NetBox data

Signed-off-by: Maximilian Wilhelm <max@sdn.clinic>
Maximilian Wilhelm 9 months ago
parent
commit
29b2c1fba9
4 changed files with 191 additions and 103 deletions
  1. 158 0
      _modules/ffho_dns.py
  2. 0 100
      _modules/ffho_net.py
  3. 27 3
      dns-server/init.sls
  4. 6 0
      dns-server/zone.gen.tmpl

+ 158 - 0
_modules/ffho_dns.py

@@ -0,0 +1,158 @@
+#!/usr/bin/python3
+#
+# Maximilian Wilhelm <max@sdn.clinic>
+#  --  Sun 23 Jul 2023 04:46:19 PM CEST
+#
+
+from functools import cmp_to_key
+import ipaddress
+import re
+
+import ffho
+
+# The DNS zone base names used for generating zone files from IP address
+# configured on nodes interfaces.
+DNS_zone_names = {
+	'forward' : 'ffho.net',
+	'rev_v4'  : [
+		'132.10.in-addr.arpa',
+		'30.172.in-addr.arpa',
+		],
+	'rev_v6'  : [
+		'2.4.3.2.0.6.2.2.3.0.a.2.ip6.arpa',
+	]
+}
+
+
+def _PTR_sort (PTR_entry_a, PTR_entry_b):
+	PTR_a_octets = PTR_entry_a.split('.')
+	PTR_b_octets = PTR_entry_b.split('.')
+
+	# If both PTRs smell like IPv4, calculate 16 bit value and compare
+	if len(PTR_a_octets) == 2 and len(PTR_b_octets) == 2:
+		# Try to parse the octets as int and compare the values
+		try:
+			a_val = int(PTR_a_octets[1]) * 256 + int(PTR_a_octets[0])
+			b_val = int(PTR_b_octets[1]) * 256 + int(PTR_b_octets[0])
+			return ffho.cmp(a_val, b_val)
+		except ValueError:
+			# If that fails, falls back to comparing regularly
+			pass
+
+	# If both PTRs smell like an IPv6 PTR, reverse them and sort
+	if len(PTR_entry_a) > 7 and len(PTR_entry_b) > 7:
+		return ffho.cmp(PTR_entry_a[::-1], PTR_entry_b[::-1])
+
+	return ffho.cmp(PTR_entry_a, PTR_entry_b)
+
+
+def generate_DNS_entries (nodes_config, sites_config):
+	forward_zone_name = ""
+	forward_zone = []
+	zones = {
+		# <forward_zone_name>: [],
+		# <rev_zone1_name>: [],
+		# <rev_zone2_name>: [],
+		# ...
+	}
+
+	zone_entries = {
+		# <zone> : {
+		#	<RR> : <value>
+		# },
+	}
+
+	# Fill zones dict with zones configured in DNS_zone_names at the top of this file.
+	# Make sure the zone base names provided start with a leading . so the string
+	# operations later can be done easily and safely. Proceed with fingers crossed.
+	for entry, value in DNS_zone_names.items ():
+		if entry == "forward":
+			zone = value
+			if not zone.startswith ('.'):
+				zone = ".%s" % zone
+
+			zones[zone] = forward_zone
+			forward_zone_name = zone
+
+		if entry in [ 'rev_v4', 'rev_v6' ]:
+			for zone in value:
+				if not zone.startswith ('.'):
+					zone = ".%s" % zone
+
+				zones[zone] = []
+				zone_entries[zone] = {}
+
+
+	# Process all interfaace of all nodes defined in pillar and generate forward
+	# and reverse entries for all zones defined in DNS_zone_names. Automagically
+	# put reverse entries into correct zone.
+	for fqdn in sorted (nodes_config):
+		node_config = nodes_config.get (fqdn)
+		ifaces = node_config.get("ifaces", {})
+
+		for iface in sorted (ifaces):
+			iface_config = ifaces.get (iface)
+
+			# We only care for interfaces with IPs configured
+			prefixes = iface_config.get ("prefixes", None)
+			if prefixes is None:
+				continue
+
+			# Ignore any interface in $VRF
+			if iface_config.get ('vrf') is not None:
+				continue
+
+			if iface in ["anycast_srv", "srv"] or "_" in iface:
+				continue
+
+			for prefix in sorted (prefixes):
+				ip = ipaddress.ip_address (u'%s' % prefix.split ('/')[0])
+				proto = 'v%s' % ip.version
+
+				# The entry name is
+				#             <fqdn>         if it's the primary IP
+				# <interface>.<fqdn>         else
+
+				entry_name = "%s.%s" % (iface, fqdn)
+				if prefix == node_config['primary_ips'].get(str(ip.version)):
+					entry_name = fqdn
+
+				# Ignore any anycast or service IP, or anything else configured on lo
+				elif iface in ["lo"]:
+					continue
+
+				# Strip forward zone name from entry_name and store forward entry
+				# with correct entry type for found IP address.
+				forward_entry_name = re.sub (forward_zone_name, "", entry_name)
+				forward_entry_typ = "A      " if ip.version == 4 else "AAAA   "
+				# Longtest value currently present is 25 chars, so aling for 32 chars
+				indent = "    " + " " * (32 - len(forward_entry_name))
+				forward_zone.append (f"{forward_entry_name}{indent}IN {forward_entry_typ} {ip}")
+
+				# Find correct reverse zone, if configured and strip reverse zone name
+				# from calculated reverse pointer name. Store reverse entry if we found
+				# a zone for it. If no configured reverse zone did match, this reverse
+				# entry will be ignored.
+				for zone in zones:
+					if ip.reverse_pointer.find (zone) > 0:
+						PTR_entry = re.sub (zone, "", ip.reverse_pointer)
+
+						# IPv6 PTRs are always the same length (for /64 prefixes)...
+						indent = "    "
+						# ... IPv4 are (for /16 prefixes), so align them nicely
+						if ip.version == 4:
+							indent += " " * (7 - len(PTR_entry))
+
+						zone_entries[zone][PTR_entry] = f"{indent}IN PTR {entry_name}."
+
+						break
+
+	for zone, entries in zone_entries.items():
+		if not entries:
+			continue
+
+		for PTR in sorted(entries.keys(), key = cmp_to_key(_PTR_sort)):
+			zones[zone].append(f"{PTR}{entries[PTR]}")
+
+	return zones
+

+ 0 - 100
_modules/ffho_net.py

@@ -84,19 +84,6 @@ loopback_prefix = {
 }
 
 
-# The DNS zone base names used for generating zone files from IP address
-# configured on nodes interfaces.
-DNS_zone_names = {
-	'forward' : 'ffho.net',
-	'rev_v4'  : [
-		'132.10.in-addr.arpa',
-		'30.172.in-addr.arpa',
-		],
-	'rev_v6'  : [
-		'2.4.3.2.0.6.2.2.3.0.a.2.ip6.arpa',
-	]
-}
-
 # MTU configuration
 MTU = {
 	# The default MTU for any interface which does not have a MTU configured
@@ -1409,93 +1396,6 @@ def get_te_prefixes (te_node_config, grains_id, proto):
 	return te_config
 
 
-
-def generate_DNS_entries (nodes_config, sites_config):
-	forward_zone_name = ""
-	forward_zone = []
-	zones = {
-		# <forward_zone_name>: [],
-		# <rev_zone1_name>: [],
-		# <rev_zone2_name>: [],
-		# ...
-	}
-
-	# Fill zones dict with zones configured in DNS_zone_names at the top of this file.
-	# Make sure the zone base names provided start with a leading . so the string
-	# operations later can be done easily and safely. Proceed with fingers crossed.
-	for entry, value in DNS_zone_names.items ():
-		if entry == "forward":
-			zone = value
-			if not zone.startswith ('.'):
-				zone = ".%s" % zone
-
-			zones[zone] = forward_zone
-			forward_zone_name = zone
-
-		if entry in [ 'rev_v4', 'rev_v6' ]:
-			for zone in value:
-				if not zone.startswith ('.'):
-					zone = ".%s" % zone
-
-				zones[zone] = []
-
-
-	# Process all interfaace of all nodes defined in pillar and generate forward
-	# and reverse entries for all zones defined in DNS_zone_names. Automagically
-	# put reverse entries into correct zone.
-	for node_id in sorted (nodes_config):
-		node_config = nodes_config.get (node_id)
-		ifaces = get_interface_config (node_config, sites_config, node_id)
-
-		for iface in sorted (ifaces):
-			iface_config = ifaces.get (iface)
-
-			# We only care for interfaces with IPs configured
-			prefixes = iface_config.get ("prefixes", None)
-			if prefixes == None:
-				continue
-
-			# Ignore any interface in $VRF
-			if iface_config.get ('vrf', "") in [ 'vrf_external' ]:
-				continue
-
-			for prefix in sorted (prefixes):
-				ip = ipaddress.ip_address (u'%s' % prefix.split ('/')[0])
-				proto = 'v%s' % ip.version
-
-				# The entry name is
-				#             <node_id>         when interface 'lo'
-				# <node_name>.srv.<residual>    when interface 'srv' (or magically detected internal srv record)
-				# <interface>.<node_id>         else
-				entry_name = node_id
-				if iface != "lo":
-					entry_name = "%s.%s" % (iface, node_id)
-
-				elif iface == 'srv' or re.search (r'^(10.132.251|2a03:2260:2342:f251:)', prefix):
-					entry_name = re.sub (r'^([^.]+)\.(.+)$', r'\g<1>.srv.\g<2>', entry_name)
-
-
-				# Strip forward zone name from entry_name and store forward entry
-				# with correct entry type for found IP address.
-				forward_entry_name = re.sub (forward_zone_name, "", entry_name)
-				forward_entry_name = re.sub (forward_zone_name, "", entry_name)
-				forward_entry_typ = "A" if ip.version == 4 else "AAAA"
-				forward_zone.append		("%s		IN %s	%s" % (forward_entry_name, forward_entry_typ, ip))
-
-				# Find correct reverse zone, if configured and strip reverse zone name
-				# from calculated reverse pointer name. Store reverse entry if we found
-				# a zone for it. If no configured reverse zone did match, this reverse
-				# entry will be ignored.
-				for zone in zones:
-					if ip.reverse_pointer.find (zone) > 0:
-						PTR_entry = re.sub (zone, "", ip.reverse_pointer)
-						zones[zone].append ("%s		IN PTR	%s." % (PTR_entry, entry_name))
-
-						break
-
-	return zones
-
-
 # Convert the CIDR network from the given prefix into a dotted netmask
 def cidr_to_dotted_mask (prefix):
 	return str (ipaddress.ip_network (prefix, strict = False).netmask)

+ 27 - 3
dns-server/init.sls

@@ -70,10 +70,10 @@ rndc-reload:
       - cmd: rndc-reload
 
 
-# Copy generated zone files
-/etc/bind/zones/generated:
+# Install hybrid zone templates
+/etc/bind/zones/hybrid:
   file.recurse:
-    - source: salt://dns-server/zones/generated/
+    - source: salt://dns-server/zones/hybrid/
     - file_mode: 644
     - dir_mode: 755
     - user: root
@@ -83,3 +83,27 @@ rndc-reload:
       - file: /etc/bind/zones/
     - watch_in:
       - cmd: rndc-reload
+
+# Generate node/interface/PTR entries from NetBox
+{% set nodes_config = salt['pillar.get'] ('nodes', {}) %}
+{% set sites_config = salt['pillar.get'] ('sites', {}) %}
+{% set zones = salt['ffho_dns.generate_DNS_entries'] (nodes_config, sites_config) %}
+
+{% for zone, entries in zones.items () %}
+/etc/bind/zones/generated/gen{{ zone }}.zone:
+  file.managed:
+    - source: salt://dns-server/zone.gen.tmpl
+    - template: jinja
+    - context:
+      zone: {{ zone }}
+      entries: {{ entries }}
+    - require_in:
+      - file: Clean /etc/bind/zones/generated
+    - watch_in:
+      - cmd: rndc-reload
+{% endfor %}
+
+Clean /etc/bind/zones/generated:
+  file.directory:
+    - name: /etc/bind/zones/generated
+    - clean: True

+ 6 - 0
dns-server/zone.gen.tmpl

@@ -0,0 +1,6 @@
+;;
+;; {{ zone }} (Salt managed)
+;;
+{%- for entry in entries %}
+{{ entry }}
+{%- endfor %}