ffho_dns.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. #!/usr/bin/python3
  2. #
  3. # Maximilian Wilhelm <max@sdn.clinic>
  4. # -- Sun 23 Jul 2023 04:46:19 PM CEST
  5. #
  6. from functools import cmp_to_key
  7. import ipaddress
  8. import re
  9. import ffho
  10. # The DNS zone base names used for generating zone files from IP address
  11. # configured on nodes interfaces.
  12. DNS_zone_names = {
  13. 'forward' : 'ffho.net',
  14. 'rev_v4' : [
  15. '132.10.in-addr.arpa',
  16. '30.172.in-addr.arpa',
  17. ],
  18. 'rev_v6' : [
  19. '2.4.3.2.0.6.2.2.3.0.a.2.ip6.arpa',
  20. ]
  21. }
  22. def _PTR_sort (PTR_entry_a, PTR_entry_b):
  23. PTR_a_octets = PTR_entry_a.split('.')
  24. PTR_b_octets = PTR_entry_b.split('.')
  25. # If both PTRs smell like IPv4, calculate 16 bit value and compare
  26. if len(PTR_a_octets) == 2 and len(PTR_b_octets) == 2:
  27. # Try to parse the octets as int and compare the values
  28. try:
  29. a_val = int(PTR_a_octets[1]) * 256 + int(PTR_a_octets[0])
  30. b_val = int(PTR_b_octets[1]) * 256 + int(PTR_b_octets[0])
  31. return ffho.cmp(a_val, b_val)
  32. except ValueError:
  33. # If that fails, falls back to comparing regularly
  34. pass
  35. # If both PTRs smell like an IPv6 PTR, reverse them and sort
  36. if len(PTR_entry_a) > 7 and len(PTR_entry_b) > 7:
  37. return ffho.cmp(PTR_entry_a[::-1], PTR_entry_b[::-1])
  38. return ffho.cmp(PTR_entry_a, PTR_entry_b)
  39. def generate_DNS_entries (nodes_config, sites_config):
  40. forward_zone_name = ""
  41. forward_zone = []
  42. zones = {
  43. # <forward_zone_name>: [],
  44. # <rev_zone1_name>: [],
  45. # <rev_zone2_name>: [],
  46. # ...
  47. }
  48. zone_entries = {
  49. # <zone> : {
  50. # <RR> : <value>
  51. # },
  52. }
  53. # Fill zones dict with zones configured in DNS_zone_names at the top of this file.
  54. # Make sure the zone base names provided start with a leading . so the string
  55. # operations later can be done easily and safely. Proceed with fingers crossed.
  56. for entry, value in DNS_zone_names.items ():
  57. if entry == "forward":
  58. zone = value
  59. if not zone.startswith ('.'):
  60. zone = ".%s" % zone
  61. zones[zone] = forward_zone
  62. forward_zone_name = zone
  63. if entry in [ 'rev_v4', 'rev_v6' ]:
  64. for zone in value:
  65. if not zone.startswith ('.'):
  66. zone = ".%s" % zone
  67. zones[zone] = []
  68. zone_entries[zone] = {}
  69. # Process all interfaace of all nodes defined in pillar and generate forward
  70. # and reverse entries for all zones defined in DNS_zone_names. Automagically
  71. # put reverse entries into correct zone.
  72. for fqdn in sorted (nodes_config):
  73. node_config = nodes_config.get (fqdn)
  74. ifaces = node_config.get("ifaces", {})
  75. for iface in sorted (ifaces):
  76. iface_config = ifaces.get (iface)
  77. # We only care for interfaces with IPs configured
  78. prefixes = iface_config.get ("prefixes", None)
  79. if prefixes is None:
  80. continue
  81. # Ignore any interface in $VRF
  82. if iface_config.get ('vrf') is not None:
  83. continue
  84. if iface in ["anycast_srv", "srv"] or "_" in iface:
  85. continue
  86. for prefix in sorted (prefixes):
  87. ip = ipaddress.ip_address (u'%s' % prefix.split ('/')[0])
  88. proto = 'v%s' % ip.version
  89. # The entry name is
  90. # <fqdn> if it's the primary IP
  91. # <interface>.<fqdn> else
  92. entry_name = "%s.%s" % (iface, fqdn)
  93. if prefix == node_config['primary_ips'].get(str(ip.version)):
  94. entry_name = fqdn
  95. # Ignore any anycast or service IP, or anything else configured on lo
  96. elif iface in ["lo"]:
  97. continue
  98. # Strip forward zone name from entry_name and store forward entry
  99. # with correct entry type for found IP address.
  100. forward_entry_name = re.sub (forward_zone_name, "", entry_name)
  101. forward_entry_typ = "A " if ip.version == 4 else "AAAA "
  102. # Longtest value currently present is 25 chars, so aling for 32 chars
  103. indent = " " + " " * (32 - len(forward_entry_name))
  104. forward_zone.append (f"{forward_entry_name}{indent}IN {forward_entry_typ} {ip}")
  105. # Find correct reverse zone, if configured and strip reverse zone name
  106. # from calculated reverse pointer name. Store reverse entry if we found
  107. # a zone for it. If no configured reverse zone did match, this reverse
  108. # entry will be ignored.
  109. for zone in zones:
  110. if ip.reverse_pointer.find (zone) > 0:
  111. PTR_entry = re.sub (zone, "", ip.reverse_pointer)
  112. # IPv6 PTRs are always the same length (for /64 prefixes)...
  113. indent = " "
  114. # ... IPv4 are (for /16 prefixes), so align them nicely
  115. if ip.version == 4:
  116. indent += " " * (7 - len(PTR_entry))
  117. zone_entries[zone][PTR_entry] = f"{indent}IN PTR {entry_name}."
  118. break
  119. for zone, entries in zone_entries.items():
  120. if not entries:
  121. continue
  122. for PTR in sorted(entries.keys(), key = cmp_to_key(_PTR_sort)):
  123. zones[zone].append(f"{PTR}{entries[PTR]}")
  124. return zones