DNS zonefiles for the `cosi.clarkson.edu`, `cslabs.clarkson.edu`, and `csprojects.clarkson.edu` subdomains
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

203 lines
6.3 KiB

  1. #!/usr/bin/env python3
  2. import re
  3. ORIGINS = {
  4. 'db.cslabs': 'cslabs.clarkson.edu',
  5. 'db.cosi': 'cosi.clarkson.edu',
  6. #'db.csprojects': 'csprojects.clarkson.edu',
  7. }
  8. REVERSE = {
  9. #'db.cslabs.rvs': ?,
  10. 'db.cslabs.rvs.144': '128.153.144.',
  11. 'db.cslabs.rvs.145': '128.153.145.',
  12. 'db.cslabs.rvs.146': '128.153.146.',
  13. 'db.cslabs.rvs.c051': '2605:6480:c051:',
  14. }
  15. RECPAT = re.compile(r'''
  16. ^\s*
  17. (?P<name>[a-zA-Z0-9_\.-]+)
  18. (?:\s+
  19. (?P<class>[a-zA-Z]+))?
  20. \s+
  21. (?P<type>[a-zA-Z]+)
  22. \s+
  23. (?P<value>.*$)
  24. ''', re.X)
  25. class Record(object):
  26. __slots__ = ['file', 'line', 'name', 'cls', 'tp', 'value']
  27. def __init__(self, file, line, name, cls, tp, value):
  28. self.file = file
  29. self.line = line
  30. self.name = name
  31. self.cls = cls
  32. self.tp = tp
  33. self.value = value
  34. @classmethod
  35. def from_match(cls, file, line, match):
  36. return cls(
  37. file, line,
  38. *match.group('name', 'class', 'type', 'value')
  39. )
  40. def __repr__(self):
  41. return f'Record(file={self.file!r}, line={self.line!r}, name={self.name!r}, cls={self.cls!r}, tp={self.tp!r}, value={self.value!r})'
  42. def __hash__(self):
  43. return hash((self.name, self.cls, self.tp, self.value))
  44. def __eq__(self, other):
  45. if not isinstance(other, Record):
  46. return False
  47. return (self.name, self.cls, self.tp, self.value) == (other.name, other.cls, other.tp, other.value)
  48. def loc(self):
  49. return f'{self.file}:{self.line}'
  50. FWD = {} # zonefile -> [record]
  51. RVS = {} # id
  52. def get_recs_from_file(f):
  53. rv = []
  54. for lno, line in enumerate(open(fn)):
  55. line = line.partition(';')[0].strip()
  56. if not line:
  57. continue
  58. match = RECPAT.match(line)
  59. if not match:
  60. #print(f'rejected line: {line!r}')
  61. continue
  62. rec = Record.from_match(fn, lno + 1, match)
  63. rv.append(rec)
  64. return rv
  65. def expand_ip6(s):
  66. groups = s.split(':')
  67. zs = 8 - len(list(filter(None, groups)))
  68. if zs > 0:
  69. fi = groups.index('')
  70. groups = groups[:fi] + ['0000'] * zs + groups[fi+1:]
  71. return ':'.join(i.rjust(4, '0') for i in groups)
  72. def into_digits(addr):
  73. return [c for c in addr if c != ':']
  74. def canon_ip6(digits):
  75. groups = [''.join(digits[i*4:(i+1)*4]) for i in range(8)]
  76. fz, lz = None, None
  77. try:
  78. fz = groups.index('0000')
  79. except ValueError:
  80. pass
  81. else:
  82. lz = len(groups) - list(reversed(groups)).index('0000')
  83. groups = [i.lstrip('0') for i in groups]
  84. if fz is not None:
  85. return ':'.join(groups[:fz]) + '::' + ':'.join(groups[lz:])
  86. return ':'.join(groups)
  87. def cons_addr(rec, org):
  88. if rec.tp != 'PTR':
  89. raise ValueError(rec)
  90. if '.' in org:
  91. parts = '.'.join(reversed(rec.name.split('.')))
  92. return org + parts
  93. elif ':' in org:
  94. digits = list(reversed(rec.name.split('.')))
  95. digits = [i for i in org if i != ':'] + digits
  96. return canon_ip6(digits)
  97. for fn, zone in ORIGINS.items():
  98. FWD[fn] = get_recs_from_file(open(fn))
  99. for fn, rv in REVERSE.items():
  100. RVS[fn] = get_recs_from_file(open(fn))
  101. print('\n# Comparisons')
  102. for za in ORIGINS.keys():
  103. for zb in ORIGINS.keys():
  104. if za <= zb:
  105. continue
  106. print(f'\n## Comparison between `{za}` and `{zb}`')
  107. sa = set(FWD[za])
  108. sb = set(FWD[zb])
  109. ma = sb - sa
  110. mb = sa - sb
  111. print(f'\nMissing from `{za}`:\n')
  112. for i in ma:
  113. print(f'- `{i}`')
  114. print(f'\nMissing from `{zb}`:\n')
  115. for i in mb:
  116. print(f'- `{i}`')
  117. mrvs = {} # canonaddr -> rec
  118. seen_addrs_cls = {'A': {}, 'AAAA': {}} # class -> addr -> oldname
  119. seen_names_cls = {'A': {}, 'AAAA': {}} # name -> firstaddr
  120. for z in ORIGINS.keys():
  121. for rec in FWD[z]:
  122. if rec.tp == 'A':
  123. mrvs[rec.value] = rec
  124. elif rec.tp == 'AAAA':
  125. mrvs[canon_ip6(into_digits(expand_ip6(rec.value)))] = rec
  126. print('\n# Reverse records')
  127. for r, addr in REVERSE.items():
  128. cls = 'AAAA' if ':' in addr else 'A'
  129. seen_addrs = seen_addrs_cls[cls]
  130. seen_names = seen_names_cls[cls]
  131. for rec in RVS[r]:
  132. if rec.tp != 'PTR':
  133. continue
  134. a = cons_addr(rec, addr)
  135. if a in seen_addrs:
  136. print(f'- `{rec.loc()}`: Rvs `{a}` (for `{rec.value}`) has already been seen as `{seen_addrs[a]}`')
  137. else:
  138. try:
  139. del mrvs[a]
  140. except KeyError:
  141. print(f'- `{rec.loc()}`: Rvs `{a}` (supposedly `{rec.value}`) doesn\'t correspond to any forward record')
  142. seen_addrs[a] = rec.value
  143. if rec.value in seen_names:
  144. if a == seen_names[rec.value]:
  145. print(f'- `{rec.loc()}`: Redundant record for `{rec.value}` to `{a}`')
  146. else:
  147. print(f'- `{rec.loc()}`: Name `{rec.value}` (for `{a}`) has already been seen as `{seen_names[rec.value]}`')
  148. else:
  149. seen_names[rec.value] = a
  150. for addr, rec in mrvs.items():
  151. print(f'- `{rec.loc()}`: No rvs for `{addr}` (`{rec.name}.{ORIGINS[rec.file]}`)')
  152. mfwd = {'A': {}, 'AAAA': {}} # class -> nm -> (addr, rec)
  153. for z in ORIGINS.keys():
  154. for rec in FWD[z]:
  155. if rec.tp == 'A':
  156. mfwd[rec.tp][rec.name + '.' + ORIGINS[z]] = (rec.value, rec)
  157. elif rec.tp == 'AAAA':
  158. mfwd[rec.tp][rec.name + '.' + ORIGINS[z]] = (canon_ip6(into_digits(expand_ip6(rec.value))), rec)
  159. print('\n# V4/V6')
  160. names_v4 = set(mfwd['A'].keys())
  161. names_v6 = set(mfwd['AAAA'].keys())
  162. print('\n## Names not yet added to IPv6')
  163. for nm in names_v4 - names_v6:
  164. print(f'- `{nm}` (at `{mfwd["A"][nm][0]}`)')
  165. print('\n## Names exclusive to IPv6')
  166. for nm in names_v6 - names_v4:
  167. print(f'- `{nm}` (at `{mfwd["AAAA"][nm][0]}`)')
  168. print('\n## Names in both')
  169. for nm in names_v4 & names_v6:
  170. print(f'- `{nm}` (at `{mfwd["A"][nm][0]}` and `{mfwd["AAAA"][nm][0]}`)')
  171. #for fn, recs in FWD.items():
  172. # for rec in recs:
  173. # print(fn, ':', rec)
  174. # if rec.tp == 'AAAA':
  175. # print(canon_ip6(into_digits(expand_ip6(rec.value))))
  176. #for fn, recs in RVS.items():
  177. # for rec in recs:
  178. # print(fn, ':', rec)
  179. # if rec.tp == 'PTR':
  180. # print(cons_addr(rec, REVERSE[fn]))