xfrin.py.in 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. #!@PYTHON@
  2. # Copyright (C) 2009-2011 Internet Systems Consortium.
  3. #
  4. # Permission to use, copy, modify, and distribute this software for any
  5. # purpose with or without fee is hereby granted, provided that the above
  6. # copyright notice and this permission notice appear in all copies.
  7. #
  8. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  9. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  10. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  11. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  12. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  13. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  14. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  15. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. import sys; sys.path.append ('@@PYTHONPATH@@')
  17. import os
  18. import signal
  19. import isc
  20. import asyncore
  21. import struct
  22. import threading
  23. import socket
  24. import random
  25. from optparse import OptionParser, OptionValueError
  26. from isc.config.ccsession import *
  27. from isc.notify import notify_out
  28. import isc.util.process
  29. import isc.net.parse
  30. from xfrin_messages import *
  31. isc.log.init("b10-xfrin")
  32. logger = isc.log.Logger("xfrin")
  33. try:
  34. from pydnspp import *
  35. except ImportError as e:
  36. # C++ loadable module may not be installed; even so the xfrin process
  37. # must keep running, so we warn about it and move forward.
  38. logger.error(XFRIN_IMPORT_DNS, str(e))
  39. isc.util.process.rename()
  40. # If B10_FROM_BUILD is set in the environment, we use data files
  41. # from a directory relative to that, otherwise we use the ones
  42. # installed on the system
  43. if "B10_FROM_BUILD" in os.environ:
  44. SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/xfrin"
  45. AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
  46. else:
  47. PREFIX = "@prefix@"
  48. DATAROOTDIR = "@datarootdir@"
  49. SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
  50. AUTH_SPECFILE_PATH = SPECFILE_PATH
  51. SPECFILE_LOCATION = SPECFILE_PATH + "/xfrin.spec"
  52. AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"
  53. XFROUT_MODULE_NAME = 'Xfrout'
  54. ZONE_MANAGER_MODULE_NAME = 'Zonemgr'
  55. REFRESH_FROM_ZONEMGR = 'refresh_from_zonemgr'
  56. ZONE_XFRIN_FAILED = 'zone_xfrin_failed'
  57. # These two default are currently hard-coded. For config this isn't
  58. # necessary, but we need these defaults for optional command arguments
  59. # (TODO: have similar support to get default values for command
  60. # arguments as we do for config options)
  61. DEFAULT_MASTER_PORT = 53
  62. DEFAULT_ZONE_CLASS = RRClass.IN()
  63. __version__ = 'BIND10'
  64. # define xfrin rcode
  65. XFRIN_OK = 0
  66. XFRIN_FAIL = 1
  67. class XfrinException(Exception):
  68. pass
  69. class XfrinZoneInfoException(Exception):
  70. """This exception is raised if there is an error in the given
  71. configuration (part), or when a command does not have a required
  72. argument or has bad arguments, for instance when the zone's master
  73. address is not a valid IP address, when the zone does not
  74. have a name, or when multiple settings are given for the same
  75. zone."""
  76. pass
  77. def _check_zone_name(zone_name_str):
  78. """Checks if the given zone name is a valid domain name, and returns
  79. it as a Name object. Raises an XfrinException if it is not."""
  80. try:
  81. # In the _zones dict, part of the key is the zone name,
  82. # but due to a limitation in the Name class, we
  83. # cannot directly use it as a dict key, and we use to_text()
  84. #
  85. # Downcase the name here for that reason.
  86. return Name(zone_name_str, True)
  87. except (EmptyLabel, TooLongLabel, BadLabelType, BadEscape,
  88. TooLongName, IncompleteName) as ne:
  89. raise XfrinZoneInfoException("bad zone name: " + zone_name_str + " (" + str(ne) + ")")
  90. def _check_zone_class(zone_class_str):
  91. """If the given argument is a string: checks if the given class is
  92. a valid one, and returns an RRClass object if so.
  93. Raises XfrinZoneInfoException if not.
  94. If it is None, this function returns the default RRClass.IN()"""
  95. if zone_class_str is None:
  96. return DEFAULT_ZONE_CLASS
  97. try:
  98. return RRClass(zone_class_str)
  99. except InvalidRRClass as irce:
  100. raise XfrinZoneInfoException("bad zone class: " + zone_class_str + " (" + str(irce) + ")")
  101. class XfrinConnection(asyncore.dispatcher):
  102. '''Do xfrin in this class. '''
  103. def __init__(self,
  104. sock_map, zone_name, rrclass, db_file, shutdown_event,
  105. master_addrinfo, tsig_key = None, verbose = False,
  106. idle_timeout = 60):
  107. ''' idle_timeout: max idle time for read data from socket.
  108. db_file: specify the data source file.
  109. check_soa: when it's true, check soa first before sending xfr query
  110. '''
  111. asyncore.dispatcher.__init__(self, map=sock_map)
  112. self.create_socket(master_addrinfo[0], master_addrinfo[1])
  113. self._zone_name = zone_name
  114. self._sock_map = sock_map
  115. self._rrclass = rrclass
  116. self._db_file = db_file
  117. self._soa_rr_count = 0
  118. self._idle_timeout = idle_timeout
  119. self.setblocking(1)
  120. self._shutdown_event = shutdown_event
  121. self._verbose = verbose
  122. self._master_address = master_addrinfo[2]
  123. self._tsig_key = tsig_key
  124. self._tsig_ctx = None
  125. # tsig_ctx_creator is introduced to allow tests to use a mock class for
  126. # easier tests (in normal case we always use the default)
  127. self._tsig_ctx_creator = self.__create_tsig_ctx
  128. def __create_tsig_ctx(self, key):
  129. return TSIGContext(key)
  130. def connect_to_master(self):
  131. '''Connect to master in TCP.'''
  132. try:
  133. self.connect(self._master_address)
  134. return True
  135. except socket.error as e:
  136. logger.error(XFRIN_CONNECT_MASTER, self._master_address, str(e))
  137. return False
  138. def _create_query(self, query_type):
  139. '''Create dns query message. '''
  140. msg = Message(Message.RENDER)
  141. query_id = random.randint(0, 0xFFFF)
  142. self._query_id = query_id
  143. msg.set_qid(query_id)
  144. msg.set_opcode(Opcode.QUERY())
  145. msg.set_rcode(Rcode.NOERROR())
  146. query_question = Question(Name(self._zone_name), self._rrclass, query_type)
  147. msg.add_question(query_question)
  148. return msg
  149. def _send_data(self, data):
  150. size = len(data)
  151. total_count = 0
  152. while total_count < size:
  153. count = self.send(data[total_count:])
  154. total_count += count
  155. def _send_query(self, query_type):
  156. '''Send query message over TCP. '''
  157. msg = self._create_query(query_type)
  158. render = MessageRenderer()
  159. # XXX Currently, python wrapper doesn't accept 'None' parameter in this case,
  160. # we should remove the if statement and use a universal interface later.
  161. if self._tsig_key is not None:
  162. self._tsig_ctx = self._tsig_ctx_creator(self._tsig_key)
  163. msg.to_wire(render, self._tsig_ctx)
  164. else:
  165. msg.to_wire(render)
  166. header_len = struct.pack('H', socket.htons(render.get_length()))
  167. self._send_data(header_len)
  168. self._send_data(render.get_data())
  169. def _asyncore_loop(self):
  170. '''
  171. This method is a trivial wrapper for asyncore.loop(). It's extracted from
  172. _get_request_response so that we can test the rest of the code without
  173. involving actual communication with a remote server.'''
  174. asyncore.loop(self._idle_timeout, map=self._sock_map, count=1)
  175. def _get_request_response(self, size):
  176. recv_size = 0
  177. data = b''
  178. while recv_size < size:
  179. self._recv_time_out = True
  180. self._need_recv_size = size - recv_size
  181. self._asyncore_loop()
  182. if self._recv_time_out:
  183. raise XfrinException('receive data from socket time out.')
  184. recv_size += self._recvd_size
  185. data += self._recvd_data
  186. return data
  187. def _check_response_tsig(self, msg, response_data):
  188. tsig_record = msg.get_tsig_record()
  189. if self._tsig_ctx is not None:
  190. tsig_error = self._tsig_ctx.verify(tsig_record, response_data)
  191. if tsig_error != TSIGError.NOERROR:
  192. raise XfrinException('TSIG verify fail: %s' % str(tsig_error))
  193. elif tsig_record is not None:
  194. # If the response includes a TSIG while we didn't sign the query,
  195. # we treat it as an error. RFC doesn't say anything about this
  196. # case, but it clearly states the server must not sign a response
  197. # to an unsigned request. Although we could be flexible, no sane
  198. # implementation would return such a response, and since this is
  199. # part of security mechanism, it's probably better to be more
  200. # strict.
  201. raise XfrinException('Unexpected TSIG in response')
  202. def _check_soa_serial(self):
  203. ''' Compare the soa serial, if soa serial in master is less than
  204. the soa serial in local, Finish xfrin.
  205. False: soa serial in master is less or equal to the local one.
  206. True: soa serial in master is bigger
  207. '''
  208. self._send_query(RRType.SOA())
  209. data_len = self._get_request_response(2)
  210. msg_len = socket.htons(struct.unpack('H', data_len)[0])
  211. soa_response = self._get_request_response(msg_len)
  212. msg = Message(Message.PARSE)
  213. msg.from_wire(soa_response)
  214. # TSIG related checks, including an unexpected signed response
  215. self._check_response_tsig(msg, soa_response)
  216. # perform some minimal level validation. It's an open issue how
  217. # strict we should be (see the comment in _check_response_header())
  218. self._check_response_header(msg)
  219. # TODO, need select soa record from data source then compare the two
  220. # serial, current just return OK, since this function hasn't been used
  221. # now.
  222. return XFRIN_OK
  223. def do_xfrin(self, check_soa, ixfr_first = False):
  224. '''Do xfr by sending xfr request and parsing response. '''
  225. try:
  226. ret = XFRIN_OK
  227. if check_soa:
  228. logstr = 'SOA check for \'%s\' ' % self._zone_name
  229. ret = self._check_soa_serial()
  230. if ret == XFRIN_OK:
  231. logger.info(XFRIN_AXFR_TRANSFER_STARTED, self._zone_name)
  232. self._send_query(RRType.AXFR())
  233. isc.datasrc.sqlite3_ds.load(self._db_file, self._zone_name,
  234. self._handle_xfrin_response)
  235. logger.info(XFRIN_AXFR_TRANSFER_SUCCESS, self._zone_name)
  236. except XfrinException as e:
  237. logger.error(XFRIN_AXFR_TRANSFER_FAILURE, self._zone_name, str(e))
  238. ret = XFRIN_FAIL
  239. #TODO, recover data source.
  240. except isc.datasrc.sqlite3_ds.Sqlite3DSError as e:
  241. logger.error(XFRIN_AXFR_DATABASE_FAILURE, self._zone_name, str(e))
  242. ret = XFRIN_FAIL
  243. except UserWarning as e:
  244. # XXX: this is an exception from our C++ library via the
  245. # Boost.Python binding. It would be better to have more more
  246. # specific exceptions, but at this moment this is the finest
  247. # granularity.
  248. logger.error(XFRIN_AXFR_INTERNAL_FAILURE, self._zone_name, str(e))
  249. ret = XFRIN_FAIL
  250. finally:
  251. self.close()
  252. return ret
  253. def _check_response_header(self, msg):
  254. '''Perform minimal validation on responses'''
  255. # It's not clear how strict we should be about response validation.
  256. # BIND 9 ignores some cases where it would normally be considered a
  257. # bogus response. For example, it accepts a response even if its
  258. # opcode doesn't match that of the corresponding request.
  259. # According to an original developer of BIND 9 some of the missing
  260. # checks are deliberate to be kind to old implementations that would
  261. # cause interoperability trouble with stricter checks.
  262. msg_rcode = msg.get_rcode()
  263. if msg_rcode != Rcode.NOERROR():
  264. raise XfrinException('error response: %s' % msg_rcode.to_text())
  265. if not msg.get_header_flag(Message.HEADERFLAG_QR):
  266. raise XfrinException('response is not a response')
  267. if msg.get_qid() != self._query_id:
  268. raise XfrinException('bad query id')
  269. def _check_response_status(self, msg):
  270. '''Check validation of xfr response. '''
  271. self._check_response_header(msg)
  272. if msg.get_rr_count(Message.SECTION_ANSWER) == 0:
  273. raise XfrinException('answer section is empty')
  274. if msg.get_rr_count(Message.SECTION_QUESTION) > 1:
  275. raise XfrinException('query section count greater than 1')
  276. def _handle_answer_section(self, answer_section):
  277. '''Return a generator for the reponse in one tcp package to a zone transfer.'''
  278. for rrset in answer_section:
  279. rrset_name = rrset.get_name().to_text()
  280. rrset_ttl = int(rrset.get_ttl().to_text())
  281. rrset_class = rrset.get_class().to_text()
  282. rrset_type = rrset.get_type().to_text()
  283. for rdata in rrset.get_rdata():
  284. # Count the soa record count
  285. if rrset.get_type() == RRType.SOA():
  286. self._soa_rr_count += 1
  287. # XXX: the current DNS message parser can't preserve the
  288. # RR order or separete the beginning and ending SOA RRs.
  289. # As a short term workaround, we simply ignore the second
  290. # SOA, and ignore the erroneous case where the transfer
  291. # session doesn't end with an SOA.
  292. if (self._soa_rr_count == 2):
  293. # Avoid inserting soa record twice
  294. break
  295. rdata_text = rdata.to_text()
  296. yield (rrset_name, rrset_ttl, rrset_class, rrset_type,
  297. rdata_text)
  298. def _handle_xfrin_response(self):
  299. '''Return a generator for the response to a zone transfer. '''
  300. while True:
  301. data_len = self._get_request_response(2)
  302. msg_len = socket.htons(struct.unpack('H', data_len)[0])
  303. recvdata = self._get_request_response(msg_len)
  304. msg = Message(Message.PARSE)
  305. msg.from_wire(recvdata)
  306. # TSIG related checks, including an unexpected signed response
  307. self._check_response_tsig(msg, recvdata)
  308. # Perform response status validation
  309. self._check_response_status(msg)
  310. answer_section = msg.get_section(Message.SECTION_ANSWER)
  311. for rr in self._handle_answer_section(answer_section):
  312. yield rr
  313. if self._soa_rr_count == 2:
  314. break
  315. if self._shutdown_event.is_set():
  316. raise XfrinException('xfrin is forced to stop')
  317. def handle_read(self):
  318. '''Read query's response from socket. '''
  319. self._recvd_data = self.recv(self._need_recv_size)
  320. self._recvd_size = len(self._recvd_data)
  321. self._recv_time_out = False
  322. def writable(self):
  323. '''Ignore the writable socket. '''
  324. return False
  325. def log_info(self, msg, type='info'):
  326. # Overwrite the log function, log nothing
  327. pass
  328. def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
  329. shutdown_event, master_addrinfo, check_soa, verbose,
  330. tsig_key):
  331. xfrin_recorder.increment(zone_name)
  332. sock_map = {}
  333. conn = XfrinConnection(sock_map, zone_name, rrclass, db_file,
  334. shutdown_event, master_addrinfo,
  335. tsig_key, verbose)
  336. ret = XFRIN_FAIL
  337. if conn.connect_to_master():
  338. ret = conn.do_xfrin(check_soa)
  339. # Publish the zone transfer result news, so zonemgr can reset the
  340. # zone timer, and xfrout can notify the zone's slaves if the result
  341. # is success.
  342. server.publish_xfrin_news(zone_name, rrclass, ret)
  343. xfrin_recorder.decrement(zone_name)
  344. class XfrinRecorder:
  345. def __init__(self):
  346. self._lock = threading.Lock()
  347. self._zones = []
  348. def increment(self, zone_name):
  349. self._lock.acquire()
  350. self._zones.append(zone_name)
  351. self._lock.release()
  352. def decrement(self, zone_name):
  353. self._lock.acquire()
  354. if zone_name in self._zones:
  355. self._zones.remove(zone_name)
  356. self._lock.release()
  357. def xfrin_in_progress(self, zone_name):
  358. self._lock.acquire()
  359. ret = zone_name in self._zones
  360. self._lock.release()
  361. return ret
  362. def count(self):
  363. self._lock.acquire()
  364. ret = len(self._zones)
  365. self._lock.release()
  366. return ret
  367. class ZoneInfo:
  368. def __init__(self, config_data, module_cc):
  369. """Creates a zone_info with the config data element as
  370. specified by the 'zones' list in xfrin.spec. Module_cc is
  371. needed to get the defaults from the specification"""
  372. self._module_cc = module_cc
  373. self.set_name(config_data.get('name'))
  374. self.set_master_addr(config_data.get('master_addr'))
  375. self.set_master_port(config_data.get('master_port'))
  376. self.set_zone_class(config_data.get('class'))
  377. self.set_tsig_key(config_data.get('tsig_key'))
  378. def set_name(self, name_str):
  379. """Set the name for this zone given a name string.
  380. Raises XfrinZoneInfoException if name_str is None or if it
  381. cannot be parsed."""
  382. if name_str is None:
  383. raise XfrinZoneInfoException("Configuration zones list "
  384. "element does not contain "
  385. "'name' attribute")
  386. else:
  387. self.name = _check_zone_name(name_str)
  388. def set_master_addr(self, master_addr_str):
  389. """Set the master address for this zone given an IP address
  390. string. Raises XfrinZoneInfoException if master_addr_str is
  391. None or if it cannot be parsed."""
  392. if master_addr_str is None:
  393. raise XfrinZoneInfoException("master address missing from config data")
  394. else:
  395. try:
  396. self.master_addr = isc.net.parse.addr_parse(master_addr_str)
  397. except ValueError:
  398. logger.error(XFRIN_BAD_MASTER_ADDR_FORMAT, master_addr_str)
  399. errmsg = "bad format for zone's master: " + master_addr_str
  400. raise XfrinZoneInfoException(errmsg)
  401. def set_master_port(self, master_port_str):
  402. """Set the master port given a port number string. If
  403. master_port_str is None, the default from the specification
  404. for this module will be used. Raises XfrinZoneInfoException if
  405. the string contains an invalid port number"""
  406. if master_port_str is None:
  407. self.master_port = self._module_cc.get_default_value("zones/master_port")
  408. else:
  409. try:
  410. self.master_port = isc.net.parse.port_parse(master_port_str)
  411. except ValueError:
  412. logger.error(XFRIN_BAD_MASTER_PORT_FORMAT, master_port_str)
  413. errmsg = "bad format for zone's master port: " + master_port_str
  414. raise XfrinZoneInfoException(errmsg)
  415. def set_zone_class(self, zone_class_str):
  416. """Set the zone class given an RR class str (e.g. "IN"). If
  417. zone_class_str is None, it will default to what is specified
  418. in the specification file for this module. Raises
  419. XfrinZoneInfoException if the string cannot be parsed."""
  420. # TODO: remove _str
  421. self.class_str = zone_class_str or self._module_cc.get_default_value("zones/class")
  422. if zone_class_str == None:
  423. #TODO rrclass->zone_class
  424. self.rrclass = RRClass(self._module_cc.get_default_value("zones/class"))
  425. else:
  426. try:
  427. self.rrclass = RRClass(zone_class_str)
  428. except InvalidRRClass:
  429. logger.error(XFRIN_BAD_ZONE_CLASS, zone_class_str)
  430. errmsg = "invalid zone class: " + zone_class_str
  431. raise XfrinZoneInfoException(errmsg)
  432. def set_tsig_key(self, tsig_key_str):
  433. """Set the tsig_key for this zone, given a TSIG key string
  434. representation. If tsig_key_str is None, no TSIG key will
  435. be set. Raises XfrinZoneInfoException if tsig_key_str cannot
  436. be parsed."""
  437. if tsig_key_str is None:
  438. self.tsig_key = None
  439. else:
  440. try:
  441. self.tsig_key = TSIGKey(tsig_key_str)
  442. except InvalidParameter as ipe:
  443. logger.error(XFRIN_BAD_TSIG_KEY_STRING, tsig_key_str)
  444. errmsg = "bad TSIG key string: " + tsig_key_str
  445. raise XfrinZoneInfoException(errmsg)
  446. def get_master_addr_info(self):
  447. return (self.master_addr.family, socket.SOCK_STREAM,
  448. (str(self.master_addr), self.master_port))
  449. class Xfrin:
  450. def __init__(self, verbose = False):
  451. self._max_transfers_in = 10
  452. self._zones = {}
  453. self._cc_setup()
  454. self.recorder = XfrinRecorder()
  455. self._shutdown_event = threading.Event()
  456. self._verbose = verbose
  457. def _cc_setup(self):
  458. '''This method is used only as part of initialization, but is
  459. implemented separately for convenience of unit tests; by letting
  460. the test code override this method we can test most of this class
  461. without requiring a command channel.'''
  462. # Create one session for sending command to other modules, because the
  463. # listening session will block the send operation.
  464. self._send_cc_session = isc.cc.Session()
  465. self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
  466. self.config_handler,
  467. self.command_handler,
  468. None, True)
  469. self._module_cc.start()
  470. config_data = self._module_cc.get_full_config()
  471. self.config_handler(config_data)
  472. def _cc_check_command(self):
  473. '''This is a straightforward wrapper for cc.check_command,
  474. but provided as a separate method for the convenience
  475. of unit tests.'''
  476. self._module_cc.check_command(False)
  477. def _get_zone_info(self, name, rrclass):
  478. """Returns the ZoneInfo object containing the configured data
  479. for the given zone name. If the zone name did not have any
  480. data, returns None"""
  481. return self._zones.get((name.to_text(), rrclass.to_text()))
  482. def _add_zone_info(self, zone_info):
  483. """Add the zone info. Raises a XfrinZoneInfoException if a zone
  484. with the same name and class is already configured"""
  485. key = (zone_info.name.to_text(), zone_info.class_str)
  486. if key in self._zones:
  487. raise XfrinZoneInfoException("zone " + str(key) +
  488. " configured multiple times")
  489. self._zones[key] = zone_info
  490. def _clear_zone_info(self):
  491. self._zones = {}
  492. def config_handler(self, new_config):
  493. # backup all config data (should there be a problem in the new
  494. # data)
  495. old_max_transfers_in = self._max_transfers_in
  496. old_zones = self._zones
  497. self._max_transfers_in = new_config.get("transfers_in") or self._max_transfers_in
  498. if 'zones' in new_config:
  499. self._clear_zone_info()
  500. for zone_config in new_config.get('zones'):
  501. try:
  502. zone_info = ZoneInfo(zone_config, self._module_cc)
  503. self._add_zone_info(zone_info)
  504. except XfrinZoneInfoException as xce:
  505. self._zones = old_zones
  506. self._max_transfers_in = old_max_transfers_in
  507. return create_answer(1, str(xce))
  508. return create_answer(0)
  509. def shutdown(self):
  510. ''' shutdown the xfrin process. the thread which is doing xfrin should be
  511. terminated.
  512. '''
  513. self._shutdown_event.set()
  514. main_thread = threading.currentThread()
  515. for th in threading.enumerate():
  516. if th is main_thread:
  517. continue
  518. th.join()
  519. def command_handler(self, command, args):
  520. answer = create_answer(0)
  521. try:
  522. if command == 'shutdown':
  523. self._shutdown_event.set()
  524. elif command == 'notify' or command == REFRESH_FROM_ZONEMGR:
  525. # Xfrin receives the refresh/notify command from zone manager.
  526. # notify command maybe has the parameters which
  527. # specify the notifyfrom address and port, according the RFC1996, zone
  528. # transfer should starts first from the notifyfrom, but now, let 'TODO' it.
  529. # (using the value now, while we can only set one master address, would be
  530. # a security hole. Once we add the ability to have multiple master addresses,
  531. # we should check if it matches one of them, and then use it.)
  532. (zone_name, rrclass) = self._parse_zone_name_and_class(args)
  533. zone_info = self._get_zone_info(zone_name, rrclass)
  534. if zone_info is None:
  535. # TODO what to do? no info known about zone. defaults?
  536. errmsg = "Got notification to retransfer unknown zone " + zone_name.to_text()
  537. logger.error(XFRIN_RETRANSFER_UNKNOWN_ZONE, zone_name.to_text())
  538. answer = create_answer(1, errmsg)
  539. else:
  540. master_addr = zone_info.get_master_addr_info()
  541. ret = self.xfrin_start(zone_name,
  542. rrclass,
  543. self._get_db_file(),
  544. master_addr,
  545. zone_info.tsig_key,
  546. True)
  547. answer = create_answer(ret[0], ret[1])
  548. elif command == 'retransfer' or command == 'refresh':
  549. # Xfrin receives the retransfer/refresh from cmdctl(sent by bindctl).
  550. # If the command has specified master address, do transfer from the
  551. # master address, or else do transfer from the configured masters.
  552. (zone_name, rrclass) = self._parse_zone_name_and_class(args)
  553. master_addr = self._parse_master_and_port(args, zone_name,
  554. rrclass)
  555. zone_info = self._get_zone_info(zone_name, rrclass)
  556. tsig_key = None
  557. if zone_info:
  558. tsig_key = zone_info.tsig_key
  559. db_file = args.get('db_file') or self._get_db_file()
  560. ret = self.xfrin_start(zone_name,
  561. rrclass,
  562. db_file,
  563. master_addr,
  564. tsig_key,
  565. (False if command == 'retransfer' else True))
  566. answer = create_answer(ret[0], ret[1])
  567. else:
  568. answer = create_answer(1, 'unknown command: ' + command)
  569. except XfrinException as err:
  570. logger.error(XFRIN_COMMAND_ERROR, command, str(err))
  571. answer = create_answer(1, str(err))
  572. return answer
  573. def _parse_zone_name_and_class(self, args):
  574. zone_name_str = args.get('zone_name')
  575. if zone_name_str is None:
  576. raise XfrinException('zone name should be provided')
  577. return (_check_zone_name(zone_name_str), _check_zone_class(args.get('zone_class')))
  578. def _parse_master_and_port(self, args, zone_name, zone_class):
  579. """
  580. Return tuple (family, socktype, sockaddr) for address and port in given
  581. args dict.
  582. IPv4 and IPv6 are the only supported addresses now, so sockaddr will be
  583. (address, port). The socktype is socket.SOCK_STREAM for now.
  584. """
  585. # check if we have configured info about this zone, in case
  586. # port or master are not specified
  587. zone_info = self._get_zone_info(zone_name, zone_class)
  588. addr_str = args.get('master')
  589. if addr_str is None:
  590. if zone_info is not None:
  591. addr = zone_info.master_addr
  592. else:
  593. raise XfrinException("Master address not given or "
  594. "configured for " + zone_name.to_text())
  595. else:
  596. try:
  597. addr = isc.net.parse.addr_parse(addr_str)
  598. except ValueError as err:
  599. raise XfrinException("failed to resolve master address %s: %s" %
  600. (addr_str, str(err)))
  601. port_str = args.get('port')
  602. if port_str is None:
  603. if zone_info is not None:
  604. port = zone_info.master_port
  605. else:
  606. port = DEFAULT_MASTER_PORT
  607. else:
  608. try:
  609. port = isc.net.parse.port_parse(port_str)
  610. except ValueError as err:
  611. raise XfrinException("failed to parse port=%s: %s" %
  612. (port_str, str(err)))
  613. return (addr.family, socket.SOCK_STREAM, (str(addr), port))
  614. def _get_db_file(self):
  615. #TODO, the db file path should be got in auth server's configuration
  616. # if we need access to this configuration more often, we
  617. # should add it on start, and not remove it here
  618. # (or, if we have writable ds, we might not need this in
  619. # the first place)
  620. self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
  621. db_file, is_default = self._module_cc.get_remote_config_value("Auth", "database_file")
  622. if is_default and "B10_FROM_BUILD" in os.environ:
  623. # this too should be unnecessary, but currently the
  624. # 'from build' override isn't stored in the config
  625. # (and we don't have writable datasources yet)
  626. db_file = os.environ["B10_FROM_BUILD"] + os.sep + "bind10_zones.sqlite3"
  627. self._module_cc.remove_remote_config(AUTH_SPECFILE_LOCATION)
  628. return db_file
  629. def publish_xfrin_news(self, zone_name, zone_class, xfr_result):
  630. '''Send command to xfrout/zone manager module.
  631. If xfrin has finished successfully for one zone, tell the good
  632. news(command: zone_new_data_ready) to zone manager and xfrout.
  633. if xfrin failed, just tell the bad news to zone manager, so that
  634. it can reset the refresh timer for that zone. '''
  635. param = {'zone_name': zone_name, 'zone_class': zone_class.to_text()}
  636. if xfr_result == XFRIN_OK:
  637. msg = create_command(notify_out.ZONE_NEW_DATA_READY_CMD, param)
  638. # catch the exception, in case msgq has been killed.
  639. try:
  640. seq = self._send_cc_session.group_sendmsg(msg,
  641. XFROUT_MODULE_NAME)
  642. try:
  643. answer, env = self._send_cc_session.group_recvmsg(False,
  644. seq)
  645. except isc.cc.session.SessionTimeout:
  646. pass # for now we just ignore the failure
  647. seq = self._send_cc_session.group_sendmsg(msg, ZONE_MANAGER_MODULE_NAME)
  648. try:
  649. answer, env = self._send_cc_session.group_recvmsg(False,
  650. seq)
  651. except isc.cc.session.SessionTimeout:
  652. pass # for now we just ignore the failure
  653. except socket.error as err:
  654. logger.error(XFRIN_MSGQ_SEND_ERROR, XFROUT_MODULE_NAME, ZONE_MANAGER_MODULE_NAME)
  655. else:
  656. msg = create_command(ZONE_XFRIN_FAILED, param)
  657. # catch the exception, in case msgq has been killed.
  658. try:
  659. seq = self._send_cc_session.group_sendmsg(msg, ZONE_MANAGER_MODULE_NAME)
  660. try:
  661. answer, env = self._send_cc_session.group_recvmsg(False,
  662. seq)
  663. except isc.cc.session.SessionTimeout:
  664. pass # for now we just ignore the failure
  665. except socket.error as err:
  666. logger.error(XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER, ZONE_MANAGER_MODULE_NAME)
  667. def startup(self):
  668. while not self._shutdown_event.is_set():
  669. self._cc_check_command()
  670. def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo, tsig_key,
  671. check_soa = True):
  672. if "pydnspp" not in sys.modules:
  673. return (1, "xfrin failed, can't load dns message python library: 'pydnspp'")
  674. # check max_transfer_in, else return quota error
  675. if self.recorder.count() >= self._max_transfers_in:
  676. return (1, 'xfrin quota error')
  677. if self.recorder.xfrin_in_progress(zone_name):
  678. return (1, 'zone xfrin is in progress')
  679. xfrin_thread = threading.Thread(target = process_xfrin,
  680. args = (self,
  681. self.recorder,
  682. zone_name.to_text(),
  683. rrclass,
  684. db_file,
  685. self._shutdown_event,
  686. master_addrinfo, check_soa,
  687. self._verbose,
  688. tsig_key))
  689. xfrin_thread.start()
  690. return (0, 'zone xfrin is started')
  691. xfrind = None
  692. def signal_handler(signal, frame):
  693. if xfrind:
  694. xfrind.shutdown()
  695. sys.exit(0)
  696. def set_signal_handler():
  697. signal.signal(signal.SIGTERM, signal_handler)
  698. signal.signal(signal.SIGINT, signal_handler)
  699. def set_cmd_options(parser):
  700. parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
  701. help="display more about what is going on")
  702. def main(xfrin_class, use_signal = True):
  703. """The main loop of the Xfrin daemon.
  704. @param xfrin_class: A class of the Xfrin object. This is normally Xfrin,
  705. but can be a subclass of it for customization.
  706. @param use_signal: True if this process should catch signals. This is
  707. normally True, but may be disabled when this function is called in a
  708. testing context."""
  709. global xfrind
  710. try:
  711. parser = OptionParser(version = __version__)
  712. set_cmd_options(parser)
  713. (options, args) = parser.parse_args()
  714. if use_signal:
  715. set_signal_handler()
  716. xfrind = xfrin_class(verbose = options.verbose)
  717. xfrind.startup()
  718. except KeyboardInterrupt:
  719. logger.info(XFRIN_STOPPED_BY_KEYBOARD)
  720. except isc.cc.session.SessionError as e:
  721. logger.error(XFRIN_CC_SESSION_ERROR, str(e))
  722. except Exception as e:
  723. logger.error(XFRIN_UNKNOWN_ERROR, str(e))
  724. if xfrind:
  725. xfrind.shutdown()
  726. if __name__ == '__main__':
  727. main(Xfrin)