ddns.py.in 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. import bind10_config
  19. from isc.dns import *
  20. import isc.ddns.session
  21. from isc.ddns.zone_config import ZoneConfig
  22. from isc.config.ccsession import *
  23. from isc.cc import SessionError, SessionTimeout
  24. import isc.util.process
  25. import isc.util.cio.socketsession
  26. import isc.server_common.tsig_keyring
  27. from isc.datasrc import DataSourceClient
  28. import select
  29. import errno
  30. from isc.log_messages.ddns_messages import *
  31. from optparse import OptionParser, OptionValueError
  32. import os
  33. import os.path
  34. import signal
  35. import socket
  36. isc.log.init("b10-ddns")
  37. logger = isc.log.Logger("ddns")
  38. TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
  39. # Well known path settings. We need to define
  40. # SPECFILE_LOCATION: ddns configuration spec file
  41. # SOCKET_FILE: Unix domain socket file to communicate with b10-auth
  42. # AUTH_SPECFILE_LOCATION: b10-auth configuration spec file (tentatively
  43. # necessarily for sqlite3-only-and-older-datasrc-API stuff). This should be
  44. # gone once we migrate to the new API and start using generalized config.
  45. #
  46. # If B10_FROM_SOURCE is set in the environment, we use data files
  47. # from a directory relative to that, otherwise we use the ones
  48. # installed on the system
  49. if "B10_FROM_SOURCE" in os.environ:
  50. SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/ddns"
  51. else:
  52. PREFIX = "@prefix@"
  53. DATAROOTDIR = "@datarootdir@"
  54. SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR)
  55. SPECFILE_PATH = SPECFILE_PATH.replace("${prefix}", PREFIX)
  56. if "B10_FROM_BUILD" in os.environ:
  57. AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
  58. if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
  59. SOCKET_FILE_PATH = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"]
  60. else:
  61. SOCKET_FILE_PATH = os.environ["B10_FROM_BUILD"]
  62. else:
  63. SOCKET_FILE_PATH = bind10_config.DATA_PATH
  64. AUTH_SPECFILE_PATH = SPECFILE_PATH
  65. SPECFILE_LOCATION = SPECFILE_PATH + "/ddns.spec"
  66. SOCKET_FILE = SOCKET_FILE_PATH + '/ddns_socket'
  67. AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + '/auth.spec'
  68. isc.util.process.rename()
  69. class DDNSConfigError(Exception):
  70. '''An exception indicating an error in updating ddns configuration.
  71. This exception is raised when the ddns process encounters an error in
  72. handling configuration updates. Not all syntax error can be caught
  73. at the module-CC layer, so ddns needs to (explicitly or implicitly)
  74. validate the given configuration data itself. When it finds an error
  75. it raises this exception (either directly or by converting an exception
  76. from other modules) as a unified error in configuration.
  77. '''
  78. pass
  79. class DDNSSessionError(Exception):
  80. '''An exception raised for some unexpected events during a ddns session.
  81. '''
  82. pass
  83. class DDNSSession:
  84. '''Class to handle one DDNS update'''
  85. def __init__(self):
  86. '''Initialize a DDNS Session'''
  87. pass
  88. def clear_socket():
  89. '''
  90. Removes the socket file, if it exists.
  91. '''
  92. if os.path.exists(SOCKET_FILE):
  93. os.remove(SOCKET_FILE)
  94. def get_datasrc_client(cc_session):
  95. '''Return data source client for update requests.
  96. This is supposed to have a very short lifetime and should soon be replaced
  97. with generic data source configuration framework. Based on that
  98. observation we simply hardcode everything except the SQLite3 database file,
  99. which will be retrieved from the auth server configuration (this behavior
  100. will also be deprecated). When something goes wrong with it this function
  101. still returns a dummy client so that the caller doesn't have to bother
  102. to handle the error (which would also have to be replaced anyway).
  103. The caller will subsequently call its find_zone method via an update
  104. session object, which will result in an exception, and then result in
  105. a SERVFAIL response.
  106. Once we are ready for introducing the general framework, the whole
  107. function will simply be removed.
  108. '''
  109. try:
  110. HARDCODED_DATASRC_CLASS = RRClass.IN()
  111. file, is_default = cc_session.get_remote_config_value("Auth",
  112. "database_file")
  113. # See xfrout.py:get_db_file() for this trick:
  114. if is_default and "B10_FROM_BUILD" in os.environ:
  115. file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
  116. datasrc_config = '{ "database_file": "' + file + '"}'
  117. return HARDCODED_DATASRC_CLASS, DataSourceClient('sqlite3',
  118. datasrc_config)
  119. except isc.datasrc.Error as ex:
  120. class DummyDataSourceClient:
  121. def __init__(self, ex):
  122. self.__ex = ex
  123. def find_zone(self, zone_name):
  124. raise isc.datasrc.Error(self.__ex)
  125. return HARDCODED_DATASRC_CLASS, DummyDataSourceClient(ex)
  126. class DDNSServer:
  127. def __init__(self, cc_session=None):
  128. '''
  129. Initialize the DDNS Server.
  130. This sets up a ModuleCCSession for the BIND 10 system.
  131. Parameters:
  132. cc_session: If None (default), a new ModuleCCSession will be set up.
  133. If specified, the given session will be used. This is
  134. mainly used for testing.
  135. '''
  136. if cc_session is not None:
  137. self._cc = cc_session
  138. else:
  139. self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
  140. self.config_handler,
  141. self.command_handler)
  142. self._config_data = self._cc.get_full_config()
  143. self._cc.start()
  144. self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
  145. isc.server_common.tsig_keyring.init_keyring(self._cc)
  146. self._shutdown = False
  147. # List of the session receivers where we get the requests
  148. self._socksession_receivers = {}
  149. clear_socket()
  150. self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  151. self._listen_socket.bind(SOCKET_FILE)
  152. self._listen_socket.listen(16)
  153. # Create reusable resources
  154. self.__request_msg = Message(Message.PARSE)
  155. self.__response_renderer = MessageRenderer()
  156. # The following attribute(s) are essentially private and constant,
  157. # but defined as "protected" so that test code can customize them.
  158. # They should not be overridden for any other purposes.
  159. #
  160. # DDNS Protocol handling class.
  161. self._UpdateSessionClass = isc.ddns.session.UpdateSession
  162. class SessionError(Exception):
  163. '''Exception for internal errors in an update session.
  164. This exception is expected to be caught within the server class,
  165. only used for controling the code flow.
  166. '''
  167. pass
  168. def config_handler(self, new_config):
  169. '''Update config data.'''
  170. # TODO: Handle exceptions and turn them to an error response
  171. # (once we have any configuration)
  172. answer = create_answer(0)
  173. return answer
  174. def command_handler(self, cmd, args):
  175. '''
  176. Handle a CC session command, as sent from bindctl or other
  177. BIND 10 modules.
  178. '''
  179. # TODO: Handle exceptions and turn them to an error response
  180. if cmd == "shutdown":
  181. logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND)
  182. self.trigger_shutdown()
  183. answer = create_answer(0)
  184. else:
  185. answer = create_answer(1, "Unknown command: " + str(cmd))
  186. return answer
  187. def trigger_shutdown(self):
  188. '''Initiate a shutdown sequence.
  189. This method is expected to be called in various ways including
  190. in the middle of a signal handler, and is designed to be as simple
  191. as possible to minimize side effects. Actual shutdown will take
  192. place in a normal control flow.
  193. '''
  194. logger.info(DDNS_SHUTDOWN)
  195. self._shutdown = True
  196. def shutdown_cleanup(self):
  197. '''
  198. Perform any cleanup that is necessary when shutting down the server.
  199. Do NOT call this to initialize shutdown, use trigger_shutdown().
  200. Currently, it only causes the ModuleCCSession to send a message that
  201. this module is stopping.
  202. '''
  203. self._cc.send_stopping()
  204. def accept(self):
  205. """
  206. Accept another connection and create the session receiver.
  207. """
  208. try:
  209. sock = self._listen_socket.accept()
  210. fileno = sock.fileno()
  211. logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
  212. sock.getpeername())
  213. receiver = isc.util.cio.socketsession.SocketSessionReceiver(sock)
  214. self._socksession_receivers[fileno] = (sock, receiver)
  215. except (socket.error, isc.util.cio.socketsession.SocketSessionError) \
  216. as e:
  217. # These exceptions mean the connection didn't work, but we can
  218. # continue with the rest
  219. logger.error(DDNS_ACCEPT_FAILURE, e)
  220. def __check_request_tsig(self, msg, req_data):
  221. '''TSIG checker for update requests.
  222. This is a helper method for handle_request() below. It examines
  223. the given update request message to see if it contains a TSIG RR,
  224. and verifies the signature if it does. It returs the TSIG context
  225. used for the verification, or None if the request doesn't contain
  226. a TSIG. If the verification fails it simply raises an exception
  227. as handle_request() assumes it should succeed.
  228. '''
  229. tsig_record = msg.get_tsig_record()
  230. if tsig_record is None:
  231. return None
  232. tsig_ctx = TSIGContext(tsig_record.get_name(),
  233. tsig_record.get_rdata().get_algorithm(),
  234. isc.server_common.tsig_keyring.get_keyring())
  235. tsig_error = tsig_ctx.verify(tsig_record, req_data)
  236. if tsig_error != TSIGError.NOERROR:
  237. raise SessionError("Failed to verify request's TSIG: " +
  238. str(tsig_error))
  239. return tsig_ctx
  240. def handle_request(self, req_session):
  241. """
  242. This is the place where the actual DDNS processing is done. Other
  243. methods are either subroutines of this method or methods doing the
  244. uninteresting "accounting" stuff, like accepting socket,
  245. initialization, etc.
  246. It is called with the request being session as received from
  247. SocketSessionReceiver, i.e. tuple
  248. (socket, local_address, remote_address, data).
  249. """
  250. # give tuple elements intuitive names
  251. (sock, local_addr, remote_addr, req_data) = req_session
  252. # The session sender (b10-auth) should have made sure that this is
  253. # a validly formed DNS message of OPCODE being UPDATE, and if it's
  254. # TSIG signed, its key is known to the system and the signature is
  255. # valid. Messages that don't meet these should have been resopnded
  256. # or dropped by the sender, so if such error is detected we treat it
  257. # as an internal error and don't bother to respond.
  258. try:
  259. self.__request_msg.clear(Message.PARSE)
  260. self.__request_msg.from_wire(req_data)
  261. if self.__request_msg.get_opcode() != Opcode.UPDATE():
  262. raise SessionError('Update request has unexpected opcode: ' +
  263. str(self.__request_msg.get_opcode()))
  264. tsig_ctx = self.__check_request_tsig(self.__request_msg, req_data)
  265. except Exception as ex:
  266. logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
  267. return False
  268. # TODO: Don't propagate most of the exceptions (like datasrc errors),
  269. # just drop the packet.
  270. # Let an update session object handle the request. Note: things around
  271. # ZoneConfig will soon be substantially revised. For now we don't
  272. # bother to generalize it.
  273. datasrc_class, datasrc_client = get_datasrc_client(self._cc)
  274. zone_cfg = ZoneConfig([], datasrc_class, datasrc_client, {})
  275. update_session = self._UpdateSessionClass(self.__request_msg,
  276. remote_addr, zone_cfg)
  277. result, zname, zclass = update_session.handle()
  278. # If the request should be dropped, we're done; otherwise, send the
  279. # response generated by the session object.
  280. if result == isc.ddns.session.UPDATE_DROP:
  281. return False
  282. msg = update_session.get_message()
  283. self.__response_renderer.clear()
  284. if tsig_ctx is not None:
  285. msg.to_wire(self.__response_renderer, tsig_ctx)
  286. else:
  287. msg.to_wire(self.__response_renderer)
  288. sock.sendto(self.__response_renderer.get_data(), remote_addr)
  289. return True
  290. def handle_session(self, fileno):
  291. """
  292. Handle incoming session on the socket with given fileno.
  293. """
  294. logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
  295. (socket, receiver) = self._socksession_receivers[fileno]
  296. try:
  297. self.handle_request(receiver.pop())
  298. except isc.util.cio.socketsession.SocketSessionError as se:
  299. # No matter why this failed, the connection is in unknown, possibly
  300. # broken state. So, we close the socket and remove the receiver.
  301. del self._socksession_receivers[fileno]
  302. socket.close()
  303. logger.warn(DDNS_DROP_CONN, fileno, se)
  304. def run(self):
  305. '''
  306. Get and process all commands sent from cfgmgr or other modules.
  307. This loops waiting for events until self.shutdown() has been called.
  308. '''
  309. logger.info(DDNS_RUNNING)
  310. cc_fileno = self._cc.get_socket().fileno()
  311. listen_fileno = self._listen_socket.fileno()
  312. while not self._shutdown:
  313. # In this event loop, we propagate most of exceptions, which will
  314. # subsequently kill the process. We expect the handling functions
  315. # to catch their own exceptions which they can recover from
  316. # (malformed packets, lost connections, etc). The rationale behind
  317. # this is they know best which exceptions are recoverable there
  318. # and an exception may be recoverable somewhere, but not elsewhere.
  319. try:
  320. (reads, writes, exceptions) = \
  321. select.select([cc_fileno, listen_fileno] +
  322. list(self._socksession_receivers.keys()), [],
  323. [])
  324. except select.error as se:
  325. # In case it is just interrupted, we continue like nothing
  326. # happened
  327. if se.args[0] == errno.EINTR:
  328. (reads, writes, exceptions) = ([], [], [])
  329. else:
  330. raise
  331. for fileno in reads:
  332. if fileno == cc_fileno:
  333. self._cc.check_command(True)
  334. elif fileno == listen_fileno:
  335. self.accept()
  336. else:
  337. self.handle_session(fileno)
  338. self.shutdown_cleanup()
  339. logger.info(DDNS_STOPPED)
  340. def create_signal_handler(ddns_server):
  341. '''
  342. This creates a signal_handler for use in set_signal_handler, which
  343. shuts down the given DDNSServer (or any object that has a shutdown()
  344. method)
  345. '''
  346. def signal_handler(signal, frame):
  347. '''
  348. Handler for process signals. Since only signals to shut down are sent
  349. here, the actual signal is not checked and the server is simply shut
  350. down.
  351. '''
  352. ddns_server.trigger_shutdown()
  353. return signal_handler
  354. def set_signal_handler(signal_handler):
  355. '''
  356. Sets the signal handler(s).
  357. '''
  358. signal.signal(signal.SIGTERM, signal_handler)
  359. signal.signal(signal.SIGINT, signal_handler)
  360. def set_cmd_options(parser):
  361. '''
  362. Helper function to set command-line options
  363. '''
  364. parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
  365. help="display more about what is going on")
  366. def main(ddns_server=None):
  367. '''
  368. The main function.
  369. Parameters:
  370. ddns_server: If None (default), a DDNSServer object is initialized.
  371. If specified, the given DDNSServer will be used. This is
  372. mainly used for testing.
  373. cc_session: If None (default), a new ModuleCCSession will be set up.
  374. If specified, the given session will be used. This is
  375. mainly used for testing.
  376. '''
  377. try:
  378. parser = OptionParser()
  379. set_cmd_options(parser)
  380. (options, args) = parser.parse_args()
  381. if options.verbose:
  382. print("[b10-ddns] Warning: -v verbose option is ignored at this point.")
  383. if ddns_server is None:
  384. ddns_server = DDNSServer()
  385. set_signal_handler(create_signal_handler(ddns_server))
  386. ddns_server.run()
  387. except KeyboardInterrupt:
  388. logger.info(DDNS_STOPPED_BY_KEYBOARD)
  389. except SessionError as e:
  390. logger.error(DDNS_CC_SESSION_ERROR, str(e))
  391. except ModuleCCSessionError as e:
  392. logger.error(DDNS_MODULECC_SESSION_ERROR, str(e))
  393. except DDNSConfigError as e:
  394. logger.error(DDNS_CONFIG_ERROR, str(e))
  395. except SessionTimeout as e:
  396. logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR)
  397. except Exception as e:
  398. logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
  399. clear_socket()
  400. if '__main__' == __name__:
  401. main()