xfr.py 13 KB

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