2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-09-06 17:05:14 +00:00
Files
kea/src/bin/ddns/ddns.py.in

398 lines
15 KiB
Python
Raw Normal View History

#!@PYTHON@
# Copyright (C) 2011 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import sys; sys.path.append ('@@PYTHONPATH@@')
import isc
import bind10_config
from isc.dns import *
import isc.ddns.session
from isc.config.ccsession import *
from isc.cc import SessionError, SessionTimeout
import isc.util.process
import isc.util.cio.socketsession
2012-05-29 14:51:12 -07:00
import isc.server_common.tsig_keyring
2011-12-29 19:43:42 +01:00
import select
2012-01-19 10:52:11 +01:00
import errno
from isc.log_messages.ddns_messages import *
from optparse import OptionParser, OptionValueError
import os
import os.path
import signal
import socket
isc.log.init("b10-ddns")
logger = isc.log.Logger("ddns")
TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
# If B10_FROM_SOURCE is set in the environment, we use data files
# from a directory relative to that, otherwise we use the ones
# installed on the system
if "B10_FROM_SOURCE" in os.environ:
SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
"src" + os.sep + "bin" + os.sep + "ddns" + os.sep + "ddns.spec"
else:
PREFIX = "@prefix@"
DATAROOTDIR = "@datarootdir@"
SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "ddns.spec"
SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\
.replace("${prefix}", PREFIX)
SOCKET_FILE = bind10_config.DATA_PATH + '/ddns_socket'
if "B10_FROM_BUILD" in os.environ:
if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
SOCKET_FILE = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"] + \
"/ddns_socket"
else:
SOCKET_FILE = os.environ["B10_FROM_BUILD"] + "/ddns_socket"
isc.util.process.rename()
class DDNSConfigError(Exception):
'''An exception indicating an error in updating ddns configuration.
This exception is raised when the ddns process encounters an error in
handling configuration updates. Not all syntax error can be caught
at the module-CC layer, so ddns needs to (explicitly or implicitly)
validate the given configuration data itself. When it finds an error
it raises this exception (either directly or by converting an exception
from other modules) as a unified error in configuration.
'''
pass
class DDNSSessionError(Exception):
'''An exception raised for some unexpected events during a ddns session.
'''
pass
class DDNSSession:
'''Class to handle one DDNS update'''
2011-12-14 16:43:14 -08:00
def __init__(self):
'''Initialize a DDNS Session'''
pass
def clear_socket():
'''
Removes the socket file, if it exists.
'''
if os.path.exists(SOCKET_FILE):
os.remove(SOCKET_FILE)
class DDNSServer:
def __init__(self, cc_session=None):
'''
Initialize the DDNS Server.
This sets up a ModuleCCSession for the BIND 10 system.
2011-12-15 16:16:50 +01:00
Parameters:
cc_session: If None (default), a new ModuleCCSession will be set up.
2011-12-15 16:16:50 +01:00
If specified, the given session will be used. This is
mainly used for testing.
'''
2011-12-15 16:16:50 +01:00
if cc_session is not None:
self._cc = cc_session
else:
self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
self.config_handler,
self.command_handler)
self._config_data = self._cc.get_full_config()
self._cc.start()
isc.server_common.tsig_keyring.init_keyring(self._cc)
self._shutdown = False
# List of the session receivers where we get the requests
self._socksession_receivers = {}
clear_socket()
self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._listen_socket.bind(SOCKET_FILE)
self._listen_socket.listen(16)
2012-05-29 14:51:12 -07:00
# The following attributes are essentially private and constant,
# but defined as "protected" so that test code can customize them.
# They should not be overridden for any other purposes.
#
# DDNS Protocol handling class.
self._UpdateSessionClass = isc.ddns.session.UpdateSession
class SessionError(Exception):
'''Exception for internal errors in an update session.
This exception is expected to be caught within the server class,
only used for controling the code flow.
'''
pass
def config_handler(self, new_config):
'''Update config data.'''
# TODO: Handle exceptions and turn them to an error response
# (once we have any configuration)
answer = create_answer(0)
return answer
def command_handler(self, cmd, args):
'''
Handle a CC session command, as sent from bindctl or other
BIND 10 modules.
'''
# TODO: Handle exceptions and turn them to an error response
if cmd == "shutdown":
logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND)
self.trigger_shutdown()
answer = create_answer(0)
else:
2011-12-15 16:16:50 +01:00
answer = create_answer(1, "Unknown command: " + str(cmd))
return answer
def trigger_shutdown(self):
'''Initiate a shutdown sequence.
This method is expected to be called in various ways including
in the middle of a signal handler, and is designed to be as simple
as possible to minimize side effects. Actual shutdown will take
place in a normal control flow.
'''
2011-12-16 18:21:45 +01:00
logger.info(DDNS_SHUTDOWN)
self._shutdown = True
def shutdown_cleanup(self):
'''
Perform any cleanup that is necessary when shutting down the server.
Do NOT call this to initialize shutdown, use trigger_shutdown().
Currently, it only causes the ModuleCCSession to send a message that
this module is stopping.
'''
2012-02-02 14:45:31 +01:00
self._cc.send_stopping()
2012-01-03 15:56:05 +01:00
def accept(self):
"""
Accept another connection and create the session receiver.
"""
2012-01-24 13:46:08 +01:00
try:
sock = self._listen_socket.accept()
fileno = sock.fileno()
logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
sock.getpeername())
receiver = isc.util.cio.socketsession.SocketSessionReceiver(sock)
2012-01-24 13:46:08 +01:00
self._socksession_receivers[fileno] = (sock, receiver)
except (socket.error, isc.util.cio.socketsession.SocketSessionError) \
2012-01-24 13:46:08 +01:00
as e:
# These exceptions mean the connection didn't work, but we can
# continue with the rest
logger.error(DDNS_ACCEPT_FAILURE, e)
2012-01-03 15:56:05 +01:00
2012-05-29 14:51:12 -07:00
def __check_request_tsig(self, msg, req_data):
'''TSIG checker for update requests.
This is a helper method for handle_request() below. It examines
the given update request message to see if it contains a TSIG RR,
and verifies the signature if it does. It returs the TSIG context
used for the verification, or None if the request doesn't contain
a TSIG. If the verification fails it simply raises an exception
as handle_request() assumes it should succeed.
'''
tsig_record = msg.get_tsig_record()
if tsig_record is None:
return None
tsig_ctx = TSIGContext(tsig_record.get_name(),
tsig_record.get_rdata().get_algorithm(),
isc.server_common.tsig_keyring.get_keyring())
2012-05-29 14:51:12 -07:00
tsig_error = tsig_ctx.verify(tsig_record, req_data)
if tsig_error != TSIGError.NOERROR:
raise SessionError("Failed to verify request's TSIG: " +
str(tsig_error))
return tsig_ctx
def handle_request(self, req_session):
"""
This is the place where the actual DDNS processing is done. Other
methods are either subroutines of this method or methods doing the
uninteresting "accounting" stuff, like accepting socket,
initialization, etc.
It is called with the request being session as received from
SocketSessionReceiver, i.e. tuple
(socket, local_address, remote_address, data).
"""
# give tuple elements intuitive names
(sock, local_addr, remote_addr, req_data) = req_session
# The session sender (b10-auth) should have made sure that this is
# a validly formed DNS message of OPCODE being UPDATE, and if it's
# TSIG signed, its key is known to the system and the signature is
# valid. Messages that don't meet these should have been resopnded
# or dropped by the sender, so if such error is detected we treat it
# as an internal error and don't bother to respond.
try:
msg = Message(Message.PARSE)
msg.from_wire(req_data)
if msg.get_opcode() != Opcode.UPDATE():
raise SessionError('Update request has unexpected opcode: ' +
str(msg.get_opcode()))
2012-05-29 14:51:12 -07:00
tsig_ctx = self.__check_request_tsig(msg, req_data)
except Exception as ex:
2012-05-29 14:51:12 -07:00
logger.error(DDNS_REQUEST_PARSE_FAIL, ex)
return False
# TODO: Don't propagate most of the exceptions (like datasrc errors),
# just drop the packet.
2012-05-29 14:51:12 -07:00
# Let an update session object handle the request.
update_session = self._UpdateSessionClass(msg, remote_addr, None)
result, zname, zclass = update_session.handle()
2012-05-29 14:51:12 -07:00
# If the request should be dropped, we're done; otherwise, send the
# response generated by the session object.
if result == isc.ddns.session.UPDATE_DROP:
return False
msg = update_session.get_message()
renderer = MessageRenderer()
2012-05-29 14:51:12 -07:00
if tsig_ctx is not None:
msg.to_wire(renderer, tsig_ctx)
else:
msg.to_wire(renderer)
sock.sendto(renderer.get_data(), remote_addr)
return True
def handle_session(self, fileno):
"""
Handle incoming session on the socket with given fileno.
"""
logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
(socket, receiver) = self._socksession_receivers[fileno]
try:
self.handle_request(receiver.pop())
except isc.util.cio.socketsession.SocketSessionError as se:
# No matter why this failed, the connection is in unknown, possibly
# broken state. So, we close the socket and remove the receiver.
del self._socksession_receivers[fileno]
socket.close()
logger.warn(DDNS_DROP_CONN, fileno, se)
def run(self):
'''
Get and process all commands sent from cfgmgr or other modules.
This loops waiting for events until self.shutdown() has been called.
'''
logger.info(DDNS_RUNNING)
2011-12-29 19:43:42 +01:00
cc_fileno = self._cc.get_socket().fileno()
listen_fileno = self._listen_socket.fileno()
while not self._shutdown:
# In this event loop, we propagate most of exceptions, which will
# subsequently kill the process. We expect the handling functions
# to catch their own exceptions which they can recover from
# (malformed packets, lost connections, etc). The rationale behind
# this is they know best which exceptions are recoverable there
# and an exception may be recoverable somewhere, but not elsewhere.
2011-12-29 19:43:42 +01:00
2012-01-19 10:52:11 +01:00
try:
(reads, writes, exceptions) = \
select.select([cc_fileno, listen_fileno] +
list(self._socksession_receivers.keys()), [],
[])
except select.error as se:
# In case it is just interrupted, we continue like nothing
# happened
if se.args[0] == errno.EINTR:
(reads, writes, exceptions) = ([], [], [])
else:
raise
2011-12-29 19:43:42 +01:00
for fileno in reads:
if fileno == cc_fileno:
2011-12-29 19:48:12 +01:00
self._cc.check_command(True)
2011-12-29 19:43:42 +01:00
elif fileno == listen_fileno:
self.accept()
else:
self.handle_session(fileno)
self.shutdown_cleanup()
2011-12-16 18:21:45 +01:00
logger.info(DDNS_STOPPED)
2011-12-15 16:16:50 +01:00
def create_signal_handler(ddns_server):
'''
2011-12-15 16:16:50 +01:00
This creates a signal_handler for use in set_signal_handler, which
shuts down the given DDNSServer (or any object that has a shutdown()
method)
'''
2011-12-15 16:16:50 +01:00
def signal_handler(signal, frame):
'''
Handler for process signals. Since only signals to shut down are sent
here, the actual signal is not checked and the server is simply shut
down.
'''
ddns_server.trigger_shutdown()
2011-12-15 16:16:50 +01:00
return signal_handler
2011-12-15 16:16:50 +01:00
def set_signal_handler(signal_handler):
'''
Sets the signal handler(s).
'''
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def set_cmd_options(parser):
'''
Helper function to set command-line options
'''
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
2011-12-16 18:21:45 +01:00
def main(ddns_server=None):
'''
The main function.
Parameters:
ddns_server: If None (default), a DDNSServer object is initialized.
If specified, the given DDNSServer will be used. This is
mainly used for testing.
cc_session: If None (default), a new ModuleCCSession will be set up.
If specified, the given session will be used. This is
mainly used for testing.
'''
try:
parser = OptionParser()
set_cmd_options(parser)
(options, args) = parser.parse_args()
2011-12-15 16:16:50 +01:00
if options.verbose:
print("[b10-ddns] Warning: -v verbose option is ignored at this point.")
2011-12-16 18:21:45 +01:00
if ddns_server is None:
ddns_server = DDNSServer()
2011-12-15 16:16:50 +01:00
set_signal_handler(create_signal_handler(ddns_server))
ddns_server.run()
except KeyboardInterrupt:
2011-12-15 16:16:50 +01:00
logger.info(DDNS_STOPPED_BY_KEYBOARD)
except SessionError as e:
logger.error(DDNS_CC_SESSION_ERROR, str(e))
except ModuleCCSessionError as e:
logger.error(DDNS_MODULECC_SESSION_ERROR, str(e))
except DDNSConfigError as e:
logger.error(DDNS_CONFIG_ERROR, str(e))
except SessionTimeout as e:
logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR)
2011-12-16 18:21:45 +01:00
except Exception as e:
logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
clear_socket()
2011-12-16 18:21:45 +01:00
if '__main__' == __name__:
main()