xfr.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
  2. # Copyright (C) 2003-2017 Nominum, Inc.
  3. #
  4. # Permission to use, copy, modify, and distribute this software and its
  5. # documentation for any purpose with or without fee is hereby granted,
  6. # provided that the above copyright notice and this permission notice
  7. # appear in all copies.
  8. #
  9. # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
  10. # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  11. # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
  12. # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  13. # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  14. # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  15. # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. from typing import Any, List, Tuple, cast
  17. import dns.edns
  18. import dns.exception
  19. import dns.message
  20. import dns.name
  21. import dns.rcode
  22. import dns.rdata
  23. import dns.rdataset
  24. import dns.rdatatype
  25. import dns.rdtypes
  26. import dns.rdtypes.ANY
  27. import dns.rdtypes.ANY.SMIMEA
  28. import dns.rdtypes.ANY.SOA
  29. import dns.rdtypes.svcbbase
  30. import dns.serial
  31. import dns.transaction
  32. import dns.tsig
  33. import dns.zone
  34. class TransferError(dns.exception.DNSException):
  35. """A zone transfer response got a non-zero rcode."""
  36. def __init__(self, rcode):
  37. message = f"Zone transfer error: {dns.rcode.to_text(rcode)}"
  38. super().__init__(message)
  39. self.rcode = rcode
  40. class SerialWentBackwards(dns.exception.FormError):
  41. """The current serial number is less than the serial we know."""
  42. class UseTCP(dns.exception.DNSException):
  43. """This IXFR cannot be completed with UDP."""
  44. class Inbound:
  45. """
  46. State machine for zone transfers.
  47. """
  48. def __init__(
  49. self,
  50. txn_manager: dns.transaction.TransactionManager,
  51. rdtype: dns.rdatatype.RdataType = dns.rdatatype.AXFR,
  52. serial: int | None = None,
  53. is_udp: bool = False,
  54. ):
  55. """Initialize an inbound zone transfer.
  56. *txn_manager* is a :py:class:`dns.transaction.TransactionManager`.
  57. *rdtype* can be `dns.rdatatype.AXFR` or `dns.rdatatype.IXFR`
  58. *serial* is the base serial number for IXFRs, and is required in
  59. that case.
  60. *is_udp*, a ``bool`` indidicates if UDP is being used for this
  61. XFR.
  62. """
  63. self.txn_manager = txn_manager
  64. self.txn: dns.transaction.Transaction | None = None
  65. self.rdtype = rdtype
  66. if rdtype == dns.rdatatype.IXFR:
  67. if serial is None:
  68. raise ValueError("a starting serial must be supplied for IXFRs")
  69. self.incremental = True
  70. elif rdtype == dns.rdatatype.AXFR:
  71. if is_udp:
  72. raise ValueError("is_udp specified for AXFR")
  73. self.incremental = False
  74. else:
  75. raise ValueError("rdtype is not IXFR or AXFR")
  76. self.serial = serial
  77. self.is_udp = is_udp
  78. (_, _, self.origin) = txn_manager.origin_information()
  79. self.soa_rdataset: dns.rdataset.Rdataset | None = None
  80. self.done = False
  81. self.expecting_SOA = False
  82. self.delete_mode = False
  83. def process_message(self, message: dns.message.Message) -> bool:
  84. """Process one message in the transfer.
  85. The message should have the same relativization as was specified when
  86. the `dns.xfr.Inbound` was created. The message should also have been
  87. created with `one_rr_per_rrset=True` because order matters.
  88. Returns `True` if the transfer is complete, and `False` otherwise.
  89. """
  90. if self.txn is None:
  91. self.txn = self.txn_manager.writer(not self.incremental)
  92. rcode = message.rcode()
  93. if rcode != dns.rcode.NOERROR:
  94. raise TransferError(rcode)
  95. #
  96. # We don't require a question section, but if it is present is
  97. # should be correct.
  98. #
  99. if len(message.question) > 0:
  100. if message.question[0].name != self.origin:
  101. raise dns.exception.FormError("wrong question name")
  102. if message.question[0].rdtype != self.rdtype:
  103. raise dns.exception.FormError("wrong question rdatatype")
  104. answer_index = 0
  105. if self.soa_rdataset is None:
  106. #
  107. # This is the first message. We're expecting an SOA at
  108. # the origin.
  109. #
  110. if not message.answer or message.answer[0].name != self.origin:
  111. raise dns.exception.FormError("No answer or RRset not for zone origin")
  112. rrset = message.answer[0]
  113. rdataset = rrset
  114. if rdataset.rdtype != dns.rdatatype.SOA:
  115. raise dns.exception.FormError("first RRset is not an SOA")
  116. answer_index = 1
  117. self.soa_rdataset = rdataset.copy() # pyright: ignore
  118. if self.incremental:
  119. assert self.soa_rdataset is not None
  120. soa = cast(dns.rdtypes.ANY.SOA.SOA, self.soa_rdataset[0])
  121. if soa.serial == self.serial:
  122. #
  123. # We're already up-to-date.
  124. #
  125. self.done = True
  126. elif dns.serial.Serial(soa.serial) < self.serial:
  127. # It went backwards!
  128. raise SerialWentBackwards
  129. else:
  130. if self.is_udp and len(message.answer[answer_index:]) == 0:
  131. #
  132. # There are no more records, so this is the
  133. # "truncated" response. Say to use TCP
  134. #
  135. raise UseTCP
  136. #
  137. # Note we're expecting another SOA so we can detect
  138. # if this IXFR response is an AXFR-style response.
  139. #
  140. self.expecting_SOA = True
  141. #
  142. # Process the answer section (other than the initial SOA in
  143. # the first message).
  144. #
  145. for rrset in message.answer[answer_index:]:
  146. name = rrset.name
  147. rdataset = rrset
  148. if self.done:
  149. raise dns.exception.FormError("answers after final SOA")
  150. assert self.txn is not None # for mypy
  151. if rdataset.rdtype == dns.rdatatype.SOA and name == self.origin:
  152. #
  153. # Every time we see an origin SOA delete_mode inverts
  154. #
  155. if self.incremental:
  156. self.delete_mode = not self.delete_mode
  157. #
  158. # If this SOA Rdataset is equal to the first we saw
  159. # then we're finished. If this is an IXFR we also
  160. # check that we're seeing the record in the expected
  161. # part of the response.
  162. #
  163. if rdataset == self.soa_rdataset and (
  164. (not self.incremental) or self.delete_mode
  165. ):
  166. #
  167. # This is the final SOA
  168. #
  169. soa = cast(dns.rdtypes.ANY.SOA.SOA, rdataset[0])
  170. if self.expecting_SOA:
  171. # We got an empty IXFR sequence!
  172. raise dns.exception.FormError("empty IXFR sequence")
  173. if self.incremental and self.serial != soa.serial:
  174. raise dns.exception.FormError("unexpected end of IXFR sequence")
  175. self.txn.replace(name, rdataset)
  176. self.txn.commit()
  177. self.txn = None
  178. self.done = True
  179. else:
  180. #
  181. # This is not the final SOA
  182. #
  183. self.expecting_SOA = False
  184. soa = cast(dns.rdtypes.ANY.SOA.SOA, rdataset[0])
  185. if self.incremental:
  186. if self.delete_mode:
  187. # This is the start of an IXFR deletion set
  188. if soa.serial != self.serial:
  189. raise dns.exception.FormError(
  190. "IXFR base serial mismatch"
  191. )
  192. else:
  193. # This is the start of an IXFR addition set
  194. self.serial = soa.serial
  195. self.txn.replace(name, rdataset)
  196. else:
  197. # We saw a non-final SOA for the origin in an AXFR.
  198. raise dns.exception.FormError("unexpected origin SOA in AXFR")
  199. continue
  200. if self.expecting_SOA:
  201. #
  202. # We made an IXFR request and are expecting another
  203. # SOA RR, but saw something else, so this must be an
  204. # AXFR response.
  205. #
  206. self.incremental = False
  207. self.expecting_SOA = False
  208. self.delete_mode = False
  209. self.txn.rollback()
  210. self.txn = self.txn_manager.writer(True)
  211. #
  212. # Note we are falling through into the code below
  213. # so whatever rdataset this was gets written.
  214. #
  215. # Add or remove the data
  216. if self.delete_mode:
  217. self.txn.delete_exact(name, rdataset)
  218. else:
  219. self.txn.add(name, rdataset)
  220. if self.is_udp and not self.done:
  221. #
  222. # This is a UDP IXFR and we didn't get to done, and we didn't
  223. # get the proper "truncated" response
  224. #
  225. raise dns.exception.FormError("unexpected end of UDP IXFR")
  226. return self.done
  227. #
  228. # Inbounds are context managers.
  229. #
  230. def __enter__(self):
  231. return self
  232. def __exit__(self, exc_type, exc_val, exc_tb):
  233. if self.txn:
  234. self.txn.rollback()
  235. return False
  236. def make_query(
  237. txn_manager: dns.transaction.TransactionManager,
  238. serial: int | None = 0,
  239. use_edns: int | bool | None = None,
  240. ednsflags: int | None = None,
  241. payload: int | None = None,
  242. request_payload: int | None = None,
  243. options: List[dns.edns.Option] | None = None,
  244. keyring: Any = None,
  245. keyname: dns.name.Name | None = None,
  246. keyalgorithm: dns.name.Name | str = dns.tsig.default_algorithm,
  247. ) -> Tuple[dns.message.QueryMessage, int | None]:
  248. """Make an AXFR or IXFR query.
  249. *txn_manager* is a ``dns.transaction.TransactionManager``, typically a
  250. ``dns.zone.Zone``.
  251. *serial* is an ``int`` or ``None``. If 0, then IXFR will be
  252. attempted using the most recent serial number from the
  253. *txn_manager*; it is the caller's responsibility to ensure there
  254. are no write transactions active that could invalidate the
  255. retrieved serial. If a serial cannot be determined, AXFR will be
  256. forced. Other integer values are the starting serial to use.
  257. ``None`` forces an AXFR.
  258. Please see the documentation for :py:func:`dns.message.make_query` and
  259. :py:func:`dns.message.Message.use_tsig` for details on the other parameters
  260. to this function.
  261. Returns a `(query, serial)` tuple.
  262. """
  263. (zone_origin, _, origin) = txn_manager.origin_information()
  264. if zone_origin is None:
  265. raise ValueError("no zone origin")
  266. if serial is None:
  267. rdtype = dns.rdatatype.AXFR
  268. elif not isinstance(serial, int):
  269. raise ValueError("serial is not an integer")
  270. elif serial == 0:
  271. with txn_manager.reader() as txn:
  272. rdataset = txn.get(origin, "SOA")
  273. if rdataset:
  274. soa = cast(dns.rdtypes.ANY.SOA.SOA, rdataset[0])
  275. serial = soa.serial
  276. rdtype = dns.rdatatype.IXFR
  277. else:
  278. serial = None
  279. rdtype = dns.rdatatype.AXFR
  280. elif serial > 0 and serial < 4294967296:
  281. rdtype = dns.rdatatype.IXFR
  282. else:
  283. raise ValueError("serial out-of-range")
  284. rdclass = txn_manager.get_class()
  285. q = dns.message.make_query(
  286. zone_origin,
  287. rdtype,
  288. rdclass,
  289. use_edns,
  290. False,
  291. ednsflags,
  292. payload,
  293. request_payload,
  294. options,
  295. )
  296. if serial is not None:
  297. rdata = dns.rdata.from_text(rdclass, "SOA", f". . {serial} 0 0 0 0")
  298. rrset = q.find_rrset(
  299. q.authority, zone_origin, rdclass, dns.rdatatype.SOA, create=True
  300. )
  301. rrset.add(rdata, 0)
  302. if keyring is not None:
  303. q.use_tsig(keyring, keyname, algorithm=keyalgorithm)
  304. return (q, serial)
  305. def extract_serial_from_query(query: dns.message.Message) -> int | None:
  306. """Extract the SOA serial number from query if it is an IXFR and return
  307. it, otherwise return None.
  308. *query* is a dns.message.QueryMessage that is an IXFR or AXFR request.
  309. Raises if the query is not an IXFR or AXFR, or if an IXFR doesn't have
  310. an appropriate SOA RRset in the authority section.
  311. """
  312. if not isinstance(query, dns.message.QueryMessage):
  313. raise ValueError("query not a QueryMessage")
  314. question = query.question[0]
  315. if question.rdtype == dns.rdatatype.AXFR:
  316. return None
  317. elif question.rdtype != dns.rdatatype.IXFR:
  318. raise ValueError("query is not an AXFR or IXFR")
  319. soa_rrset = query.find_rrset(
  320. query.authority, question.name, question.rdclass, dns.rdatatype.SOA
  321. )
  322. soa = cast(dns.rdtypes.ANY.SOA.SOA, soa_rrset[0])
  323. return soa.serial