ddns.py.in 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. #!@PYTHON@
  2. # Copyright (C) 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 isc
  18. from isc.acl.dns import REQUEST_LOADER
  19. import bind10_config
  20. from isc.dns import *
  21. import isc.ddns.session
  22. from isc.ddns.zone_config import ZoneConfig
  23. from isc.ddns.logger import ClientFormatter, ZoneFormatter
  24. from isc.config.ccsession import *
  25. from isc.config.module_spec import ModuleSpecError
  26. from isc.cc import SessionError, SessionTimeout, ProtocolError
  27. import isc.util.process
  28. import isc.util.cio.socketsession
  29. import isc.server_common.tsig_keyring
  30. from isc.server_common.dns_tcp import DNSTCPContext
  31. from isc.datasrc import DataSourceClient
  32. from isc.server_common.auth_command import auth_loadzone_command
  33. import select
  34. import time
  35. import errno
  36. from isc.log_messages.ddns_messages import *
  37. from optparse import OptionParser, OptionValueError
  38. import os
  39. import os.path
  40. import signal
  41. import socket
  42. isc.log.init("b10-ddns", buffer=True)
  43. logger = isc.log.Logger("ddns")
  44. TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
  45. # Well known path settings. We need to define
  46. # SPECFILE_LOCATION: ddns configuration spec file
  47. # SOCKET_FILE: Unix domain socket file to communicate with b10-auth
  48. # AUTH_SPECFILE_LOCATION: b10-auth configuration spec file (tentatively
  49. # necessarily for sqlite3-only-and-older-datasrc-API stuff). This should be
  50. # gone once we migrate to the new API and start using generalized config.
  51. #
  52. # If B10_FROM_SOURCE is set in the environment, we use data files
  53. # from a directory relative to that, otherwise we use the ones
  54. # installed on the system
  55. if "B10_FROM_SOURCE" in os.environ:
  56. SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/ddns"
  57. else:
  58. PREFIX = "@prefix@"
  59. DATAROOTDIR = "@datarootdir@"
  60. SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR)
  61. SPECFILE_PATH = SPECFILE_PATH.replace("${prefix}", PREFIX)
  62. if "B10_FROM_BUILD" in os.environ:
  63. if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
  64. SOCKET_FILE_PATH = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"]
  65. else:
  66. SOCKET_FILE_PATH = os.environ["B10_FROM_BUILD"]
  67. else:
  68. SOCKET_FILE_PATH = bind10_config.DATA_PATH
  69. SPECFILE_LOCATION = SPECFILE_PATH + "/ddns.spec"
  70. SOCKET_FILE = SOCKET_FILE_PATH + '/ddns_socket'
  71. # Cooperating or dependency modules
  72. AUTH_MODULE_NAME = 'Auth'
  73. XFROUT_MODULE_NAME = 'Xfrout'
  74. ZONEMGR_MODULE_NAME = 'Zonemgr'
  75. isc.util.process.rename()
  76. class DDNSConfigError(Exception):
  77. '''An exception indicating an error in updating ddns configuration.
  78. This exception is raised when the ddns process encounters an error in
  79. handling configuration updates. Not all syntax error can be caught
  80. at the module-CC layer, so ddns needs to (explicitly or implicitly)
  81. validate the given configuration data itself. When it finds an error
  82. it raises this exception (either directly or by converting an exception
  83. from other modules) as a unified error in configuration.
  84. '''
  85. pass
  86. class DDNSSessionError(Exception):
  87. '''An exception raised for some unexpected events during a ddns session.
  88. '''
  89. pass
  90. class DDNSSession:
  91. '''Class to handle one DDNS update'''
  92. def __init__(self):
  93. '''Initialize a DDNS Session'''
  94. pass
  95. def clear_socket():
  96. '''
  97. Removes the socket file, if it exists.
  98. '''
  99. if os.path.exists(SOCKET_FILE):
  100. os.remove(SOCKET_FILE)
  101. def get_datasrc_client(cc_session):
  102. '''Return data source client for update requests.
  103. This is supposed to have a very short lifetime and should soon be replaced
  104. with generic data source configuration framework. Based on that
  105. observation we simply hardcode everything except the SQLite3 database file,
  106. which will be retrieved from the auth server configuration (this behavior
  107. will also be deprecated). When something goes wrong with it this function
  108. still returns a dummy client so that the caller doesn't have to bother
  109. to handle the error (which would also have to be replaced anyway).
  110. The caller will subsequently call its find_zone method via an update
  111. session object, which will result in an exception, and then result in
  112. a SERVFAIL response.
  113. Once we are ready for introducing the general framework, the whole
  114. function will simply be removed.
  115. '''
  116. HARDCODED_DATASRC_CLASS = RRClass.IN()
  117. file, is_default = cc_session.get_remote_config_value("Auth",
  118. "database_file")
  119. # See xfrout.py:get_db_file() for this trick:
  120. if is_default and "B10_FROM_BUILD" in os.environ:
  121. file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
  122. datasrc_config = '{ "database_file": "' + file + '"}'
  123. try:
  124. return (HARDCODED_DATASRC_CLASS,
  125. DataSourceClient('sqlite3', datasrc_config), file)
  126. except isc.datasrc.Error as ex:
  127. class DummyDataSourceClient:
  128. def __init__(self, ex):
  129. self.__ex = ex
  130. def find_zone(self, zone_name):
  131. raise isc.datasrc.Error(self.__ex)
  132. return (HARDCODED_DATASRC_CLASS, DummyDataSourceClient(ex), file)
  133. def add_pause(sec):
  134. '''Pause a specified period for inter module synchronization.
  135. This is a trivial wrapper of time.sleep, but defined as a separate function
  136. so tests can customize it.
  137. '''
  138. time.sleep(sec)
  139. class DDNSServer:
  140. # The number of TCP clients that can be handled by the server at the same
  141. # time (this should be configurable parameter).
  142. TCP_CLIENTS = 10
  143. def __init__(self, cc_session=None):
  144. '''
  145. Initialize the DDNS Server.
  146. This sets up a ModuleCCSession for the BIND 10 system.
  147. Parameters:
  148. cc_session: If None (default), a new ModuleCCSession will be set up.
  149. If specified, the given session will be used. This is
  150. mainly used for testing.
  151. '''
  152. if cc_session is not None:
  153. self._cc = cc_session
  154. else:
  155. self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
  156. self.config_handler,
  157. self.command_handler)
  158. # Initialize configuration with defaults. Right now 'zones' is the
  159. # only configuration, so we simply directly set it here.
  160. self._config_data = self._cc.get_full_config()
  161. self._zone_config = self.__update_zone_config(
  162. self._cc.get_default_value('zones'))
  163. self._cc.start()
  164. # Internal attributes derived from other modules. They will be
  165. # initialized via dd_remote_xxx below and will be kept updated
  166. # through their callbacks. They are defined as 'protected' so tests
  167. # can examine them; but they are essentially private to the class.
  168. #
  169. # Datasource client used for handling update requests: when set,
  170. # should a tuple of RRClass and DataSourceClient. Constructed and
  171. # maintained based on auth configuration.
  172. self._datasrc_info = None
  173. # A set of secondary zones, retrieved from zonemgr configuration.
  174. self._secondary_zones = None
  175. # Get necessary configurations from remote modules.
  176. for mod in [(AUTH_MODULE_NAME, self.__auth_config_handler),
  177. (ZONEMGR_MODULE_NAME, self.__zonemgr_config_handler)]:
  178. self.__add_remote_module(mod[0], mod[1])
  179. # This should succeed as long as cfgmgr is up.
  180. isc.server_common.tsig_keyring.init_keyring(self._cc)
  181. self._shutdown = False
  182. # List of the session receivers where we get the requests
  183. self._socksession_receivers = {}
  184. clear_socket()
  185. self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  186. self._listen_socket.bind(SOCKET_FILE)
  187. self._listen_socket.listen(16)
  188. # Create reusable resources
  189. self.__request_msg = Message(Message.PARSE)
  190. self.__response_renderer = MessageRenderer()
  191. # The following attribute(s) are essentially private, but defined as
  192. # "protected" so that test code can customize/inspect them.
  193. # They should not be overridden/referenced for any other purposes.
  194. #
  195. # DDNS Protocol handling class.
  196. self._UpdateSessionClass = isc.ddns.session.UpdateSession
  197. # Outstanding TCP context: fileno=>(context_obj, dst)
  198. self._tcp_ctxs = {}
  199. # Notify Auth server that DDNS update messages can now be forwarded
  200. self.__notify_start_forwarder()
  201. class InternalError(Exception):
  202. '''Exception for internal errors in an update session.
  203. This exception is expected to be caught within the server class,
  204. only used for controling the code flow.
  205. '''
  206. pass
  207. def config_handler(self, new_config):
  208. '''Update config data.'''
  209. try:
  210. if 'zones' in new_config:
  211. self._zone_config = \
  212. self.__update_zone_config(new_config['zones'])
  213. return create_answer(0)
  214. except Exception as ex:
  215. # We catch any exception here. That includes any syntax error
  216. # against the configuration spec. The config interface is too
  217. # complicated and it's not clear how much validation is performed
  218. # there, so, while assuming it's unlikely to happen, we act
  219. # proactively.
  220. logger.error(DDNS_CONFIG_HANDLER_ERROR, ex)
  221. return create_answer(1, "Failed to handle new configuration: " +
  222. str(ex))
  223. def __update_zone_config(self, new_zones_config):
  224. '''Handle zones configuration update.'''
  225. new_zones = {}
  226. for zone_config in new_zones_config:
  227. origin = Name(zone_config['origin'])
  228. rrclass = RRClass(zone_config['class'])
  229. update_acl = zone_config['update_acl']
  230. new_zones[(origin, rrclass)] = REQUEST_LOADER.load(update_acl)
  231. return new_zones
  232. def command_handler(self, cmd, args):
  233. '''
  234. Handle a CC session command, as sent from bindctl or other
  235. BIND 10 modules.
  236. '''
  237. # TODO: Handle exceptions and turn them to an error response
  238. if cmd == "shutdown":
  239. logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND)
  240. self.trigger_shutdown()
  241. answer = create_answer(0)
  242. elif cmd == "auth_started":
  243. self.__notify_start_forwarder()
  244. answer = None
  245. else:
  246. answer = create_answer(1, "Unknown command: " + str(cmd))
  247. return answer
  248. def __add_remote_module(self, mod_name, callback):
  249. '''Register interest in other module's config with a callback.'''
  250. # Due to startup timing, add_remote_config can fail. We could make it
  251. # more sophisticated, but for now we simply retry a few times, each
  252. # separated by a short period (3 times and 1 sec, arbitrary chosen,
  253. # and hardcoded for now). In practice this should be more than
  254. # sufficient, but if it turns out to be a bigger problem we can
  255. # consider more elegant solutions.
  256. for n_try in range(0, 3):
  257. try:
  258. # by_name() version can fail with ModuleSpecError in getting
  259. # the module spec because cfgmgr returns a "successful" answer
  260. # with empty data if it cannot find the specified module.
  261. # This seems to be a deviant behavior (see Trac #2039), but
  262. # we need to deal with it.
  263. self._cc.add_remote_config_by_name(mod_name, callback)
  264. return
  265. except (ModuleSpecError, ModuleCCSessionError) as ex:
  266. logger.warn(DDNS_GET_REMOTE_CONFIG_FAIL, mod_name, n_try + 1,
  267. ex)
  268. last_ex = ex
  269. add_pause(1)
  270. raise last_ex
  271. def __auth_config_handler(self, new_config, module_config):
  272. logger.info(DDNS_RECEIVED_AUTH_UPDATE)
  273. # If we've got the config before and the new config doesn't update
  274. # the DB file, there's nothing we should do with it.
  275. # Note: there seems to be a bug either in bindctl or cfgmgr, and
  276. # new_config can contain 'database_file' even if it's not really
  277. # updated. We still perform the check so we can avoid redundant
  278. # resetting when the bug is fixed. The redundant reset itself is not
  279. # good, but such configuration update should not happen so often and
  280. # it should be acceptable in practice.
  281. if self._datasrc_info is not None and \
  282. not 'database_file' in new_config:
  283. return
  284. rrclass, client, db_file = get_datasrc_client(self._cc)
  285. self._datasrc_info = (rrclass, client)
  286. logger.info(DDNS_AUTH_DBFILE_UPDATE, db_file)
  287. def __zonemgr_config_handler(self, new_config, module_config):
  288. logger.info(DDNS_RECEIVED_ZONEMGR_UPDATE)
  289. # If we've got the config before and the new config doesn't update
  290. # the secondary zone list, there's nothing we should do with it.
  291. # (Same note as that for auth's config applies)
  292. if self._secondary_zones is not None and \
  293. not 'secondary_zones' in new_config:
  294. return
  295. # Get the latest secondary zones. Use get_remote_config_value() so
  296. # it can work for both the initial default case and updates.
  297. sec_zones, _ = self._cc.get_remote_config_value(ZONEMGR_MODULE_NAME,
  298. 'secondary_zones')
  299. new_secondary_zones = set()
  300. try:
  301. # Parse the new config and build a new list of secondary zones.
  302. # Unfortunately, in the current implementation, even an observer
  303. # module needs to perform full validation. This should be changed
  304. # so that only post-validation (done by the main module) config is
  305. # delivered to observer modules, but until it's supported we need
  306. # to protect ourselves.
  307. for zone_spec in sec_zones:
  308. zname = Name(zone_spec['name'])
  309. # class has the default value in case it's unspecified.
  310. # ideally this should be merged within the config module, but
  311. # the current implementation doesn't esnure that, so we need to
  312. # subsitute it ourselves.
  313. if 'class' in zone_spec:
  314. zclass = RRClass(zone_spec['class'])
  315. else:
  316. zclass = RRClass(module_config.get_default_value(
  317. 'secondary_zones/class'))
  318. new_secondary_zones.add((zname, zclass))
  319. self._secondary_zones = new_secondary_zones
  320. logger.info(DDNS_SECONDARY_ZONES_UPDATE, len(self._secondary_zones))
  321. except Exception as ex:
  322. logger.error(DDNS_SECONDARY_ZONES_UPDATE_FAIL, ex)
  323. def trigger_shutdown(self):
  324. '''Initiate a shutdown sequence.
  325. This method is expected to be called in various ways including
  326. in the middle of a signal handler, and is designed to be as simple
  327. as possible to minimize side effects. Actual shutdown will take
  328. place in a normal control flow.
  329. '''
  330. logger.info(DDNS_SHUTDOWN)
  331. self._shutdown = True
  332. def shutdown_cleanup(self):
  333. '''
  334. Perform any cleanup that is necessary when shutting down the server.
  335. Do NOT call this to initialize shutdown, use trigger_shutdown().
  336. '''
  337. # tell Auth not to forward UPDATE messages anymore
  338. self.__notify_stop_forwarder()
  339. # tell the ModuleCCSession to send a message that this module is
  340. # stopping.
  341. self._cc.send_stopping()
  342. # make sure any open socket is explicitly closed, per Python
  343. # convention.
  344. self._listen_socket.close()
  345. def accept(self):
  346. """
  347. Accept another connection and create the session receiver.
  348. """
  349. try:
  350. (sock, remote_addr) = self._listen_socket.accept()
  351. fileno = sock.fileno()
  352. logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
  353. remote_addr if remote_addr else '<anonymous address>')
  354. receiver = isc.util.cio.socketsession.SocketSessionReceiver(sock)
  355. self._socksession_receivers[fileno] = (sock, receiver)
  356. except (socket.error, isc.util.cio.socketsession.SocketSessionError) \
  357. as e:
  358. # These exceptions mean the connection didn't work, but we can
  359. # continue with the rest
  360. logger.error(DDNS_ACCEPT_FAILURE, e)
  361. def __check_request_tsig(self, msg, req_data):
  362. '''TSIG checker for update requests.
  363. This is a helper method for handle_request() below. It examines
  364. the given update request message to see if it contains a TSIG RR,
  365. and verifies the signature if it does. It returs the TSIG context
  366. used for the verification, or None if the request doesn't contain
  367. a TSIG. If the verification fails it simply raises an exception
  368. as handle_request() assumes it should succeed.
  369. '''
  370. tsig_record = msg.get_tsig_record()
  371. if tsig_record is None:
  372. return None
  373. tsig_ctx = TSIGContext(tsig_record.get_name(),
  374. tsig_record.get_rdata().get_algorithm(),
  375. isc.server_common.tsig_keyring.get_keyring())
  376. tsig_error = tsig_ctx.verify(tsig_record, req_data)
  377. if tsig_error != TSIGError.NOERROR:
  378. raise self.InternalError("Failed to verify request's TSIG: " +
  379. str(tsig_error))
  380. return tsig_ctx
  381. def handle_request(self, req_session):
  382. """
  383. This is the place where the actual DDNS processing is done. Other
  384. methods are either subroutines of this method or methods doing the
  385. uninteresting "accounting" stuff, like accepting socket,
  386. initialization, etc.
  387. It is called with the request being session as received from
  388. SocketSessionReceiver, i.e. tuple
  389. (socket, local_address, remote_address, data).
  390. In general, this method doesn't propagate exceptions outside the
  391. method. Most of protocol or system errors will result in an error
  392. response to the update client or dropping the update request.
  393. The update session class should also ensure this. Critical exceptions
  394. such as memory allocation failure will be propagated, however, and
  395. will subsequently terminate the server process.
  396. Return: True if a response to the request is successfully sent;
  397. False otherwise. The return value wouldn't be useful for the server
  398. itself; it's provided mainly for testing purposes.
  399. """
  400. # give tuple elements intuitive names
  401. (sock, local_addr, remote_addr, req_data) = req_session
  402. # The session sender (b10-auth) should have made sure that this is
  403. # a validly formed DNS message of OPCODE being UPDATE, and if it's
  404. # TSIG signed, its key is known to the system and the signature is
  405. # valid. Messages that don't meet these should have been resopnded
  406. # or dropped by the sender, so if such error is detected we treat it
  407. # as an internal error and don't bother to respond.
  408. try:
  409. self.__request_msg.clear(Message.PARSE)
  410. # specify PRESERVE_ORDER as we need to handle each RR separately.
  411. self.__request_msg.from_wire(req_data, Message.PRESERVE_ORDER)
  412. if self.__request_msg.get_opcode() != Opcode.UPDATE():
  413. raise self.InternalError('Update request has unexpected '
  414. 'opcode: ' +
  415. str(self.__request_msg.get_opcode()))
  416. tsig_ctx = self.__check_request_tsig(self.__request_msg, req_data)
  417. except Exception as ex:
  418. logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
  419. return False
  420. # Let an update session object handle the request. Note: things around
  421. # ZoneConfig will soon be substantially revised. For now we don't
  422. # bother to generalize it.
  423. zone_cfg = ZoneConfig(self._secondary_zones, self._datasrc_info[0],
  424. self._datasrc_info[1], self._zone_config)
  425. update_session = self._UpdateSessionClass(self.__request_msg,
  426. remote_addr, zone_cfg)
  427. result, zname, zclass = update_session.handle()
  428. # If the request should be dropped, we're done; otherwise, send the
  429. # response generated by the session object.
  430. if result == isc.ddns.session.UPDATE_DROP:
  431. return False
  432. msg = update_session.get_message()
  433. self.__response_renderer.clear()
  434. if tsig_ctx is not None:
  435. msg.to_wire(self.__response_renderer, tsig_ctx)
  436. else:
  437. msg.to_wire(self.__response_renderer)
  438. ret = self.__send_response(sock, self.__response_renderer.get_data(),
  439. remote_addr)
  440. if result == isc.ddns.session.UPDATE_SUCCESS:
  441. self.__notify_auth(zname, zclass)
  442. self.__notify_xfrout(zname, zclass)
  443. return ret
  444. def __send_response(self, sock, data, dest):
  445. '''Send DDNS response to the client.
  446. Right now, this is a straightforward subroutine of handle_request(),
  447. but is intended to be extended evetually so that it can handle more
  448. comlicated operations for TCP (which requires asynchronous write).
  449. Further, when we support multiple requests over a single TCP
  450. connection, this method may even be shared by multiple methods.
  451. Parameters:
  452. sock: (python socket) the socket to which the response should be sent.
  453. data: (binary) the response data
  454. dest: (python socket address) the destion address to which the response
  455. should be sent.
  456. Return: True if the send operation succeds; otherwise False.
  457. '''
  458. try:
  459. if sock.proto == socket.IPPROTO_UDP:
  460. sock.sendto(data, dest)
  461. else:
  462. tcp_ctx = DNSTCPContext(sock)
  463. send_result = tcp_ctx.send(data)
  464. if send_result == DNSTCPContext.SENDING:
  465. self._tcp_ctxs[sock.fileno()] = (tcp_ctx, dest)
  466. elif send_result == DNSTCPContext.CLOSED:
  467. raise socket.error("socket error in TCP send")
  468. else:
  469. tcp_ctx.close()
  470. except socket.error as ex:
  471. logger.warn(DDNS_RESPONSE_SOCKET_ERROR, ClientFormatter(dest), ex)
  472. return False
  473. return True
  474. def __notify_start_forwarder(self):
  475. '''Notify auth that DDNS Update messages can now be forwarded'''
  476. try:
  477. seq = self._cc._session.group_sendmsg(create_command(
  478. "start_ddns_forwarder"), AUTH_MODULE_NAME)
  479. answer, _ = self._cc._session.group_recvmsg(False, seq)
  480. rcode, error_msg = parse_answer(answer)
  481. if rcode != 0:
  482. logger.error(DDNS_START_FORWARDER_ERROR, error_msg)
  483. except (SessionTimeout, SessionError, ProtocolError) as ex:
  484. logger.error(DDNS_START_FORWARDER_FAIL, ex)
  485. def __notify_stop_forwarder(self):
  486. '''Notify auth that DDNS Update messages should no longer be forwarded.
  487. '''
  488. try:
  489. seq = self._cc._session.group_sendmsg(create_command(
  490. "stop_ddns_forwarder"), AUTH_MODULE_NAME)
  491. answer, _ = self._cc._session.group_recvmsg(False, seq)
  492. rcode, error_msg = parse_answer(answer)
  493. if rcode != 0:
  494. logger.error(DDNS_STOP_FORWARDER_ERROR, error_msg)
  495. except (SessionTimeout, SessionError, ProtocolError) as ex:
  496. logger.error(DDNS_STOP_FORWARDER_FAIL, ex)
  497. def __notify_auth(self, zname, zclass):
  498. '''Notify auth of the update, if necessary.'''
  499. msg = auth_loadzone_command(self._cc, zname, zclass)
  500. if msg is not None:
  501. self.__notify_update(AUTH_MODULE_NAME, msg, zname, zclass)
  502. def __notify_xfrout(self, zname, zclass):
  503. '''Notify xfrout of the update.'''
  504. param = {'zone_name': zname.to_text(), 'zone_class': zclass.to_text()}
  505. msg = create_command('notify', param)
  506. self.__notify_update(XFROUT_MODULE_NAME, msg, zname, zclass)
  507. def __notify_update(self, modname, msg, zname, zclass):
  508. '''Notify other module of the update.
  509. Note that we use blocking communication here. While the internal
  510. communication bus is generally expected to be pretty responsive and
  511. error free, notable delay can still occur, and in worse cases timeouts
  512. or connection reset can happen. In these cases, even if the trouble
  513. is temporary, the update service will be suspended for a while.
  514. For a longer term we'll need to switch to asynchronous communication,
  515. but for now we rely on the blocking operation.
  516. Note also that we directly refer to the "protected" member of
  517. ccsession (_cc._session) rather than creating a separate channel.
  518. It's probably not the best practice, but hopefully we can introduce
  519. a cleaner way when we support asynchronous communication.
  520. At the moment we prefer the brevity with the use of internal channel
  521. of the cc session.
  522. '''
  523. try:
  524. seq = self._cc._session.group_sendmsg(msg, modname)
  525. answer, _ = self._cc._session.group_recvmsg(False, seq)
  526. rcode, error_msg = parse_answer(answer)
  527. except (SessionTimeout, SessionError, ProtocolError) as ex:
  528. rcode = 1
  529. error_msg = str(ex)
  530. if rcode == 0:
  531. logger.debug(TRACE_BASIC, DDNS_UPDATE_NOTIFY, modname,
  532. ZoneFormatter(zname, zclass))
  533. else:
  534. logger.error(DDNS_UPDATE_NOTIFY_FAIL, modname,
  535. ZoneFormatter(zname, zclass), error_msg)
  536. def handle_session(self, fileno):
  537. """Handle incoming session on the socket with given fileno.
  538. Return True if a response (whether positive or negative) has been
  539. sent; otherwise False. The return value isn't expected to be used
  540. for other purposes than testing.
  541. """
  542. logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
  543. (session_socket, receiver) = self._socksession_receivers[fileno]
  544. try:
  545. req_session = receiver.pop()
  546. (sock, remote_addr) = (req_session[0], req_session[2])
  547. # If this is a TCP client, check the quota, and immediately reject
  548. # it if we cannot accept more.
  549. if sock.proto == socket.IPPROTO_TCP and \
  550. len(self._tcp_ctxs) >= self.TCP_CLIENTS:
  551. logger.warn(DDNS_REQUEST_TCP_QUOTA,
  552. ClientFormatter(remote_addr), len(self._tcp_ctxs))
  553. sock.close()
  554. return False
  555. return self.handle_request(req_session)
  556. except isc.util.cio.socketsession.SocketSessionError as se:
  557. # No matter why this failed, the connection is in unknown, possibly
  558. # broken state. So, we close the socket and remove the receiver.
  559. del self._socksession_receivers[fileno]
  560. session_socket.close()
  561. logger.warn(DDNS_DROP_CONN, fileno, se)
  562. return False
  563. def run(self):
  564. '''
  565. Get and process all commands sent from cfgmgr or other modules.
  566. This loops waiting for events until self.shutdown() has been called.
  567. '''
  568. logger.info(DDNS_STARTED)
  569. cc_fileno = self._cc.get_socket().fileno()
  570. listen_fileno = self._listen_socket.fileno()
  571. while not self._shutdown:
  572. # In this event loop, we propagate most of exceptions, which will
  573. # subsequently kill the process. We expect the handling functions
  574. # to catch their own exceptions which they can recover from
  575. # (malformed packets, lost connections, etc). The rationale behind
  576. # this is they know best which exceptions are recoverable there
  577. # and an exception may be recoverable somewhere, but not elsewhere.
  578. try:
  579. (reads, writes, exceptions) = \
  580. select.select([cc_fileno, listen_fileno] +
  581. list(self._socksession_receivers.keys()),
  582. list(self._tcp_ctxs.keys()), [])
  583. except select.error as se:
  584. # In case it is just interrupted, we continue like nothing
  585. # happened
  586. if se.args[0] == errno.EINTR:
  587. (reads, writes, exceptions) = ([], [], [])
  588. else:
  589. raise
  590. for fileno in reads:
  591. if fileno == cc_fileno:
  592. self._cc.check_command(True)
  593. elif fileno == listen_fileno:
  594. self.accept()
  595. else:
  596. self.handle_session(fileno)
  597. for fileno in writes:
  598. ctx = self._tcp_ctxs[fileno]
  599. result = ctx[0].send_ready()
  600. if result != DNSTCPContext.SENDING:
  601. if result == DNSTCPContext.CLOSED:
  602. logger.warn(DDNS_RESPONSE_TCP_SOCKET_ERROR,
  603. ClientFormatter(ctx[1]))
  604. ctx[0].close()
  605. del self._tcp_ctxs[fileno]
  606. self.shutdown_cleanup()
  607. logger.info(DDNS_STOPPED)
  608. def create_signal_handler(ddns_server):
  609. '''
  610. This creates a signal_handler for use in set_signal_handler, which
  611. shuts down the given DDNSServer (or any object that has a shutdown()
  612. method)
  613. '''
  614. def signal_handler(signal, frame):
  615. '''
  616. Handler for process signals. Since only signals to shut down are sent
  617. here, the actual signal is not checked and the server is simply shut
  618. down.
  619. '''
  620. ddns_server.trigger_shutdown()
  621. return signal_handler
  622. def set_signal_handler(signal_handler):
  623. '''
  624. Sets the signal handler(s).
  625. '''
  626. signal.signal(signal.SIGTERM, signal_handler)
  627. signal.signal(signal.SIGINT, signal_handler)
  628. def set_cmd_options(parser):
  629. '''
  630. Helper function to set command-line options
  631. '''
  632. parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
  633. help="display more about what is going on")
  634. def main(ddns_server=None):
  635. '''
  636. The main function.
  637. Parameters:
  638. ddns_server: If None (default), a DDNSServer object is initialized.
  639. If specified, the given DDNSServer will be used. This is
  640. mainly used for testing.
  641. cc_session: If None (default), a new ModuleCCSession will be set up.
  642. If specified, the given session will be used. This is
  643. mainly used for testing.
  644. '''
  645. try:
  646. parser = OptionParser()
  647. set_cmd_options(parser)
  648. (options, args) = parser.parse_args()
  649. if options.verbose:
  650. print("[b10-ddns] Warning: -v verbose option is ignored at this point.")
  651. if ddns_server is None:
  652. ddns_server = DDNSServer()
  653. set_signal_handler(create_signal_handler(ddns_server))
  654. ddns_server.run()
  655. except KeyboardInterrupt:
  656. logger.info(DDNS_STOPPED_BY_KEYBOARD)
  657. except SessionError as e:
  658. logger.error(DDNS_CC_SESSION_ERROR, str(e))
  659. except (ModuleSpecError, ModuleCCSessionError) as e:
  660. logger.error(DDNS_MODULECC_SESSION_ERROR, str(e))
  661. except DDNSConfigError as e:
  662. logger.error(DDNS_CONFIG_ERROR, str(e))
  663. except SessionTimeout as e:
  664. logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR)
  665. except Exception as e:
  666. logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
  667. clear_socket()
  668. if '__main__' == __name__:
  669. main()