btreezone.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
  2. # A derivative of a dnspython VersionedZone and related classes, using a BTreeDict and
  3. # a separate per-version delegation index. These additions let us
  4. #
  5. # 1) Do efficient CoW versioning (useful for future online updates).
  6. # 2) Maintain sort order
  7. # 3) Allow delegations to be found easily
  8. # 4) Handle glue
  9. # 5) Add Node flags ORIGIN, DELEGATION, and GLUE whenever relevant. The ORIGIN
  10. # flag is set at the origin node, the DELEGATION FLAG is set at delegation
  11. # points, and the GLUE flag is set on nodes beneath delegation points.
  12. import enum
  13. from dataclasses import dataclass
  14. from typing import Callable, MutableMapping, Tuple, cast
  15. import dns.btree
  16. import dns.immutable
  17. import dns.name
  18. import dns.node
  19. import dns.rdataclass
  20. import dns.rdataset
  21. import dns.rdatatype
  22. import dns.versioned
  23. import dns.zone
  24. class NodeFlags(enum.IntFlag):
  25. ORIGIN = 0x01
  26. DELEGATION = 0x02
  27. GLUE = 0x04
  28. class Node(dns.node.Node):
  29. __slots__ = ["flags", "id"]
  30. def __init__(self, flags: NodeFlags | None = None):
  31. super().__init__()
  32. if flags is None:
  33. # We allow optional flags rather than a default
  34. # as pyright doesn't like assigning a literal 0
  35. # to flags.
  36. flags = NodeFlags(0)
  37. self.flags = flags
  38. self.id = 0
  39. def is_delegation(self):
  40. return (self.flags & NodeFlags.DELEGATION) != 0
  41. def is_glue(self):
  42. return (self.flags & NodeFlags.GLUE) != 0
  43. def is_origin(self):
  44. return (self.flags & NodeFlags.ORIGIN) != 0
  45. def is_origin_or_glue(self):
  46. return (self.flags & (NodeFlags.ORIGIN | NodeFlags.GLUE)) != 0
  47. @dns.immutable.immutable
  48. class ImmutableNode(Node):
  49. def __init__(self, node: Node):
  50. super().__init__()
  51. self.id = node.id
  52. self.rdatasets = tuple( # type: ignore
  53. [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
  54. )
  55. self.flags = node.flags
  56. def find_rdataset(
  57. self,
  58. rdclass: dns.rdataclass.RdataClass,
  59. rdtype: dns.rdatatype.RdataType,
  60. covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
  61. create: bool = False,
  62. ) -> dns.rdataset.Rdataset:
  63. if create:
  64. raise TypeError("immutable")
  65. return super().find_rdataset(rdclass, rdtype, covers, False)
  66. def get_rdataset(
  67. self,
  68. rdclass: dns.rdataclass.RdataClass,
  69. rdtype: dns.rdatatype.RdataType,
  70. covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
  71. create: bool = False,
  72. ) -> dns.rdataset.Rdataset | None:
  73. if create:
  74. raise TypeError("immutable")
  75. return super().get_rdataset(rdclass, rdtype, covers, False)
  76. def delete_rdataset(
  77. self,
  78. rdclass: dns.rdataclass.RdataClass,
  79. rdtype: dns.rdatatype.RdataType,
  80. covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
  81. ) -> None:
  82. raise TypeError("immutable")
  83. def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
  84. raise TypeError("immutable")
  85. def is_immutable(self) -> bool:
  86. return True
  87. class Delegations(dns.btree.BTreeSet[dns.name.Name]):
  88. def get_delegation(self, name: dns.name.Name) -> Tuple[dns.name.Name | None, bool]:
  89. """Get the delegation applicable to *name*, if it exists.
  90. If there delegation, then return a tuple consisting of the name of
  91. the delegation point, and a boolean which is `True` if the name is a proper
  92. subdomain of the delegation point, and `False` if it is equal to the delegation
  93. point.
  94. """
  95. cursor = self.cursor()
  96. cursor.seek(name, before=False)
  97. prev = cursor.prev()
  98. if prev is None:
  99. return None, False
  100. cut = prev.key()
  101. reln, _, _ = name.fullcompare(cut)
  102. is_subdomain = reln == dns.name.NameRelation.SUBDOMAIN
  103. if is_subdomain or reln == dns.name.NameRelation.EQUAL:
  104. return cut, is_subdomain
  105. else:
  106. return None, False
  107. def is_glue(self, name: dns.name.Name) -> bool:
  108. """Is *name* glue, i.e. is it beneath a delegation?"""
  109. cursor = self.cursor()
  110. cursor.seek(name, before=False)
  111. cut, is_subdomain = self.get_delegation(name)
  112. if cut is None:
  113. return False
  114. return is_subdomain
  115. class WritableVersion(dns.zone.WritableVersion):
  116. def __init__(self, zone: dns.zone.Zone, replacement: bool = False):
  117. super().__init__(zone, True)
  118. if not replacement:
  119. assert isinstance(zone, dns.versioned.Zone)
  120. version = zone._versions[-1]
  121. self.nodes: dns.btree.BTreeDict[dns.name.Name, Node] = dns.btree.BTreeDict(
  122. original=version.nodes # type: ignore
  123. )
  124. self.delegations = Delegations(original=version.delegations) # type: ignore
  125. else:
  126. self.delegations = Delegations()
  127. def _is_origin(self, name: dns.name.Name) -> bool:
  128. # Assumes name has already been validated (and thus adjusted to the right
  129. # relativity too)
  130. if self.zone.relativize:
  131. return name == dns.name.empty
  132. else:
  133. return name == self.zone.origin
  134. def _maybe_cow_with_name(
  135. self, name: dns.name.Name
  136. ) -> Tuple[dns.node.Node, dns.name.Name]:
  137. (node, name) = super()._maybe_cow_with_name(name)
  138. node = cast(Node, node)
  139. if self._is_origin(name):
  140. node.flags |= NodeFlags.ORIGIN
  141. elif self.delegations.is_glue(name):
  142. node.flags |= NodeFlags.GLUE
  143. return (node, name)
  144. def update_glue_flag(self, name: dns.name.Name, is_glue: bool) -> None:
  145. cursor = self.nodes.cursor() # type: ignore
  146. cursor.seek(name, False)
  147. updates = []
  148. while True:
  149. elt = cursor.next()
  150. if elt is None:
  151. break
  152. ename = elt.key()
  153. if not ename.is_subdomain(name):
  154. break
  155. node = cast(dns.node.Node, elt.value())
  156. if ename not in self.changed:
  157. new_node = self.zone.node_factory()
  158. new_node.id = self.id # type: ignore
  159. new_node.rdatasets.extend(node.rdatasets)
  160. self.changed.add(ename)
  161. node = new_node
  162. assert isinstance(node, Node)
  163. if is_glue:
  164. node.flags |= NodeFlags.GLUE
  165. else:
  166. node.flags &= ~NodeFlags.GLUE
  167. # We don't update node here as any insertion could disturb the
  168. # btree and invalidate our cursor. We could use the cursor in a
  169. # with block and avoid this, but it would do a lot of parking and
  170. # unparking so the deferred update mode may still be better.
  171. updates.append((ename, node))
  172. for ename, node in updates:
  173. self.nodes[ename] = node
  174. def delete_node(self, name: dns.name.Name) -> None:
  175. name = self._validate_name(name)
  176. node = self.nodes.get(name)
  177. if node is not None:
  178. if node.is_delegation(): # type: ignore
  179. self.delegations.discard(name)
  180. self.update_glue_flag(name, False)
  181. del self.nodes[name]
  182. self.changed.add(name)
  183. def put_rdataset(
  184. self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset
  185. ) -> None:
  186. (node, name) = self._maybe_cow_with_name(name)
  187. if (
  188. rdataset.rdtype == dns.rdatatype.NS and not node.is_origin_or_glue() # type: ignore
  189. ):
  190. node.flags |= NodeFlags.DELEGATION # type: ignore
  191. if name not in self.delegations:
  192. self.delegations.add(name)
  193. self.update_glue_flag(name, True)
  194. node.replace_rdataset(rdataset)
  195. def delete_rdataset(
  196. self,
  197. name: dns.name.Name,
  198. rdtype: dns.rdatatype.RdataType,
  199. covers: dns.rdatatype.RdataType,
  200. ) -> None:
  201. (node, name) = self._maybe_cow_with_name(name)
  202. if rdtype == dns.rdatatype.NS and name in self.delegations: # type: ignore
  203. node.flags &= ~NodeFlags.DELEGATION # type: ignore
  204. self.delegations.discard(name) # type: ignore
  205. self.update_glue_flag(name, False)
  206. node.delete_rdataset(self.zone.rdclass, rdtype, covers)
  207. if len(node) == 0:
  208. del self.nodes[name]
  209. @dataclass(frozen=True)
  210. class Bounds:
  211. name: dns.name.Name
  212. left: dns.name.Name
  213. right: dns.name.Name | None
  214. closest_encloser: dns.name.Name
  215. is_equal: bool
  216. is_delegation: bool
  217. def __str__(self):
  218. if self.is_equal:
  219. op = "="
  220. else:
  221. op = "<"
  222. if self.is_delegation:
  223. zonecut = " zonecut"
  224. else:
  225. zonecut = ""
  226. return (
  227. f"{self.left} {op} {self.name} < {self.right}{zonecut}; "
  228. f"{self.closest_encloser}"
  229. )
  230. @dns.immutable.immutable
  231. class ImmutableVersion(dns.zone.Version):
  232. def __init__(self, version: dns.zone.Version):
  233. if not isinstance(version, WritableVersion):
  234. raise ValueError(
  235. "a dns.btreezone.ImmutableVersion requires a "
  236. "dns.btreezone.WritableVersion"
  237. )
  238. super().__init__(version.zone, True)
  239. self.id = version.id
  240. self.origin = version.origin
  241. for name in version.changed:
  242. node = version.nodes.get(name)
  243. if node:
  244. version.nodes[name] = ImmutableNode(node)
  245. # the cast below is for mypy
  246. self.nodes = cast(MutableMapping[dns.name.Name, dns.node.Node], version.nodes)
  247. self.nodes.make_immutable() # type: ignore
  248. self.delegations = version.delegations
  249. self.delegations.make_immutable()
  250. def bounds(self, name: dns.name.Name | str) -> Bounds:
  251. """Return the 'bounds' of *name* in its zone.
  252. The bounds information is useful when making an authoritative response, as
  253. it can be used to determine whether the query name is at or beneath a delegation
  254. point. The other data in the ``Bounds`` object is useful for making on-the-fly
  255. DNSSEC signatures.
  256. The left bound of *name* is *name* itself if it is in the zone, or the greatest
  257. predecessor which is in the zone.
  258. The right bound of *name* is the least successor of *name*, or ``None`` if
  259. no name in the zone is greater than *name*.
  260. The closest encloser of *name* is *name* itself, if *name* is in the zone;
  261. otherwise it is the name with the largest number of labels in common with
  262. *name* that is in the zone, either explicitly or by the implied existence
  263. of empty non-terminals.
  264. The bounds *is_equal* field is ``True`` if and only if *name* is equal to
  265. its left bound.
  266. The bounds *is_delegation* field is ``True`` if and only if the left bound is a
  267. delegation point.
  268. """
  269. assert self.origin is not None
  270. # validate the origin because we may need to relativize
  271. origin = self.zone._validate_name(self.origin)
  272. name = self.zone._validate_name(name)
  273. cut, _ = self.delegations.get_delegation(name)
  274. if cut is not None:
  275. target = cut
  276. is_delegation = True
  277. else:
  278. target = name
  279. is_delegation = False
  280. c = cast(dns.btree.BTreeDict, self.nodes).cursor()
  281. c.seek(target, False)
  282. left = c.prev()
  283. assert left is not None
  284. c.next() # skip over left
  285. while True:
  286. right = c.next()
  287. if right is None or not right.value().is_glue():
  288. break
  289. left_comparison = left.key().fullcompare(name)
  290. if right is not None:
  291. right_key = right.key()
  292. right_comparison = right_key.fullcompare(name)
  293. else:
  294. right_comparison = (
  295. dns.name.NAMERELN_COMMONANCESTOR,
  296. -1,
  297. len(origin),
  298. )
  299. right_key = None
  300. closest_encloser = dns.name.Name(
  301. name[-max(left_comparison[2], right_comparison[2]) :]
  302. )
  303. return Bounds(
  304. name,
  305. left.key(),
  306. right_key,
  307. closest_encloser,
  308. left_comparison[0] == dns.name.NameRelation.EQUAL,
  309. is_delegation,
  310. )
  311. class Zone(dns.versioned.Zone):
  312. node_factory: Callable[[], dns.node.Node] = Node
  313. map_factory: Callable[[], MutableMapping[dns.name.Name, dns.node.Node]] = cast(
  314. Callable[[], MutableMapping[dns.name.Name, dns.node.Node]],
  315. dns.btree.BTreeDict[dns.name.Name, Node],
  316. )
  317. writable_version_factory: (
  318. Callable[[dns.zone.Zone, bool], dns.zone.Version] | None
  319. ) = WritableVersion
  320. immutable_version_factory: Callable[[dns.zone.Version], dns.zone.Version] | None = (
  321. ImmutableVersion
  322. )