=== modified file 'TODO' --- TODO 2010-03-27 18:39:02 +0000 +++ TODO 2010-08-22 13:34:42 +0000 @@ -1,5 +1,7 @@ -*- org -*- +* _attribute_((nonnull)) + * mandos-client ** TODO [#B] use scandir(3) instead of readdir(3) ** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name @@ -38,6 +40,8 @@ ** TODO [#C] use same file name rules as run-parts(8) ** kernel command line option for debug info ** TODO [#B] use error() instead of perror() +** TODO [$B] Use openat() and readdir64() + http://udrepper.livejournal.com/19395.html * mandos (server) ** TODO [#B] Log level :BUGS: @@ -68,6 +72,19 @@ A client needs manual approval on the server before it gets the secret ** TODO [#B] Support RFC 3339 time duration syntax +** More D-Bus methods +*** NeedsApproval(50, True) -> timeout, default approve + Default approval is configurable, but True by default + + Approval(True) -> approve sending saved + + Approval(False) -> Close client connection immediately +*** NeedsPassword(50) - Timeout, default disapprove + + SetPass(u"gazonk", True) -> Approval, persistent + + Approval(False) -> Close client connection immediately +** TODO [#C] python-parsedatetime +** TODO [#C] systemd/launchd + http://0pointer.de/blog/projects/systemd.html +** TODO Separate logging logic to own object +** TODO make clients to a dict! * mandos.xml ** [[file:mandos.xml::XXX][Document D-Bus interface]] @@ -98,6 +115,8 @@ For testing decryption before rebooting. * Makefile +** TODO Add "--Xlinker --as-needed" + http://udrepper.livejournal.com/19395.html ** TODO [#C] Implement DEB_BUILD_OPTIONS http://www.debian.org/doc/debian-policy/ch-source.html#s-debianrules-options @@ -109,5 +128,9 @@ ** TODO [#C] /etc/bash_completion.d/mandos From XML sources directly? +* Side Stuff +** TODO Locate which packet move the other bin/sh when busy box is deactivated +** TODO contact owner of packet, and ask them to have that shell static in position regardless of busybox + #+STARTUP: showall === modified file 'mandos' --- mandos 2010-05-18 16:56:54 +0000 +++ mandos 2010-08-22 13:34:42 +0000 @@ -60,6 +60,7 @@ import fcntl import functools import cPickle as pickle +import multiprocessing import dbus import dbus.service @@ -97,6 +98,8 @@ u' %(message)s')) logger.addHandler(console) +multiprocessing_manager = multiprocessing.Manager() + class AvahiError(Exception): def __init__(self, value, *args, **kwargs): self.value = value @@ -228,6 +231,10 @@ self.server_state_changed(self.server.GetState()) +# XXX Need to add: +# approved_by_default (Config option for each client) +# approved_delay (config option for each client) +# approved_duration (config option for each client) class Client(object): """A representation of a client host served by this server. @@ -257,10 +264,9 @@ runtime with vars(self) as dict, so that for instance %(name)s can be used in the command. current_checker_command: string; current running checker_command - changesignal: File descriptor; written to on object change - _reset: File descriptor; for flushing changesignal - delay: datetime.timedelta(); how long to wait for approval/secret - approved: bool(); None if not yet approved/disapproved + approved_delay: datetime.timedelta(); Time to wait for approval + _approved: bool(); 'None' if not yet approved/disapproved + approved_duration: datetime.timedelta(); Duration of one approval """ @staticmethod @@ -277,6 +283,9 @@ def interval_milliseconds(self): "Return the 'interval' attribute in milliseconds" return self._timedelta_to_milliseconds(self.interval) + + def approved_delay_milliseconds(self): + return self._timedelta_to_milliseconds(self.approved_delay) def __init__(self, name = None, disable_hook=None, config=None): """Note: the 'checker' key in 'config' sets the @@ -293,12 +302,12 @@ .replace(u" ", u"")) logger.debug(u" Fingerprint: %s", self.fingerprint) if u"secret" in config: - self._secret = config[u"secret"].decode(u"base64") + self.secret = config[u"secret"].decode(u"base64") elif u"secfile" in config: with open(os.path.expanduser(os.path.expandvars (config[u"secfile"])), "rb") as secfile: - self._secret = secfile.read() + self.secret = secfile.read() else: #XXX Need to allow secret on demand! raise TypeError(u"No secret or secfile for client %s" @@ -318,31 +327,26 @@ self.checker_command = config[u"checker"] self.current_checker_command = None self.last_connect = None - self.changesignal, self._reset = os.pipe() - self._approved = None #XXX should be based on configfile - self.delay = 10; #XXX Should be based on configfile - - def setsecret(self, value): - self._secret = value - os.write(self.changesignal, "\0") - os.read(self._reset, 1) - - secret = property(lambda self: self._secret, setsecret) - del setsecret - - def setapproved(self, value): - self._approved = value - os.write(self.changesignal, "\0") - os.read(self._reset, 1) - - approved = property(lambda self: self._approved, setapproved) - del setapproved - + self._approved = None + self.approved_by_default = config.get(u"approved_by_default", + False) + self.approved_delay = string_to_delta( + config[u"approved_delay"]) + self.approved_duration = string_to_delta( + config[u"approved_duration"]) + self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock()) + + def send_changedstate(self): + self.changedstate.acquire() + self.changedstate.notify_all() + self.changedstate.release() + def enable(self): """Start this client's checker and timeout hooks""" if getattr(self, u"enabled", False): # Already enabled return + self.send_changedstate() self.last_enabled = datetime.datetime.utcnow() # Schedule a new checker to be started an 'interval' from now, # and every interval from then on. @@ -362,6 +366,8 @@ if not getattr(self, "enabled", False): return False if not quiet: + self.send_changedstate() + if not quiet: logger.info(u"Disabling client %s", self.name) if getattr(self, u"disable_initiator_tag", False): gobject.source_remove(self.disable_initiator_tag) @@ -500,7 +506,6 @@ raise self.checker = None - def dbus_service_property(dbus_interface, signature=u"v", access=u"readwrite", byte_arrays=False): """Decorators for marking methods of a DBusObjectWithProperties to @@ -803,6 +808,14 @@ self.PropertyChanged(dbus.String(u"checker_running"), dbus.Boolean(False, variant_level=1)) return r + + def _reset_approved(self): + self._approved = None + return False + + def approve(self, value=True): + self._approved = value + gobject.timeout_add(self._timedelta_to_milliseconds(self.approved_duration, self._reset_approved)) ## D-Bus methods, signals & properties _interface = u"se.bsnet.fukt.Mandos.Client" @@ -834,13 +847,24 @@ pass # Rejected - signal - @dbus.service.signal(_interface) - def Rejected(self): + @dbus.service.signal(_interface, signature=u"s") + def Rejected(self, reason): + "D-Bus signal" + pass + + # NeedApproval - signal + @dbus.service.signal(_interface, signature=u"db") + def NeedApproval(self, timeout, default): "D-Bus signal" pass ## Methods - + + # Approve - method + @dbus.service.method(_interface, in_signature=u"b") + def Approve(self, value): + self.approve(value) + # CheckedOK - method @dbus.service.method(_interface) def CheckedOK(self): @@ -871,6 +895,8 @@ ## Properties + # xxx 3 new properties + # name - property @dbus_service_property(_interface, signature=u"s", access=u"read") def name_dbus_property(self): @@ -1010,6 +1036,32 @@ del _interface +class ProxyClient(object): + def __init__(self, child_pipe, fpr, address): + self._pipe = child_pipe + self._pipe.send(('init', fpr, address)) + if not self._pipe.recv(): + raise KeyError() + + def __getattribute__(self, name): + if(name == '_pipe'): + return super(ProxyClient, self).__getattribute__(name) + self._pipe.send(('getattr', name)) + data = self._pipe.recv() + if data[0] == 'data': + return data[1] + if data[0] == 'function': + def func(*args, **kwargs): + self._pipe.send(('funcall', name, args, kwargs)) + return self._pipe.recv()[1] + return func + + def __setattr__(self, name, value): + if(name == '_pipe'): + return super(ProxyClient, self).__setattr__(name, value) + self._pipe.send(('setattr', name, value)) + + class ClientHandler(socketserver.BaseRequestHandler, object): """A class to handle client connections. @@ -1017,33 +1069,22 @@ Note: This will run in its own forked process.""" def handle(self): - logger.info(u"TCP connection from: %s", - unicode(self.client_address)) - logger.debug(u"IPC Pipe FD: %d", - self.server.child_pipe[1].fileno()) - # Open IPC pipe to parent process - with contextlib.nested(self.server.child_pipe[1], - self.server.parent_pipe[0] - ) as (ipc, ipc_return): + with contextlib.closing(self.server.child_pipe) as child_pipe: + logger.info(u"TCP connection from: %s", + unicode(self.client_address)) + logger.debug(u"Pipe FD: %d", + self.server.child_pipe.fileno()) + session = (gnutls.connection .ClientSession(self.request, gnutls.connection .X509Credentials())) - - line = self.request.makefile().readline() - logger.debug(u"Protocol version: %r", line) - try: - if int(line.strip().split()[0]) > 1: - raise RuntimeError - except (ValueError, IndexError, RuntimeError), error: - logger.error(u"Unknown protocol version: %s", error) - return - + # Note: gnutls.connection.X509Credentials is really a # generic GnuTLS certificate credentials object so long as # no X.509 keys are added to it. Therefore, we can use it # here despite using OpenPGP certificates. - + #priority = u':'.join((u"NONE", u"+VERS-TLS1.1", # u"+AES-256-CBC", u"+SHA1", # u"+COMP-NULL", u"+CTYPE-OPENPGP", @@ -1055,7 +1096,19 @@ (gnutls.library.functions .gnutls_priority_set_direct(session._c_object, priority, None)) - + + # Start communication using the Mandos protocol + # Get protocol number + line = self.request.makefile().readline() + logger.debug(u"Protocol version: %r", line) + try: + if int(line.strip().split()[0]) > 1: + raise RuntimeError + except (ValueError, IndexError, RuntimeError), error: + logger.error(u"Unknown protocol version: %s", error) + return + + # Start GnuTLS connection try: session.handshake() except gnutls.errors.GNUTLSError, error: @@ -1073,49 +1126,77 @@ return logger.debug(u"Fingerprint: %s", fpr) - for c in self.server.clients: - if c.fingerprint == fpr: - client = c - break - else: - ipc.write(u"NOTFOUND %s %s\n" - % (fpr, unicode(self.client_address))) + try: + client = ProxyClient(child_pipe, fpr, + self.client_address) + except KeyError: return - - class proxyclient(object): - def __getattribute__(self, name): - ipc.write(u"GETATTR %s %s\n" % name, client.fpr) - return pickle.load(ipc_reply) - p = proxyclient(client) - + + delay = client.approved_delay while True: - if not p.client.enabled: - icp.write("DISABLED %s\n" % client.fpr) - return - if p.client.approved == False: - icp.write("Disaproved") - return - - if not p.client.secret: - icp.write("No password") - elif not p.client.approved: - icp.write("Need approval"): - else: + if not client.enabled: + logger.warning(u"Client %s is disabled", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Disabled") + return + if client._approved is None: + logger.info(u"Client %s need approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.NeedApproval( + client.approved_delay_milliseconds(), + client.approved_by_default) + elif client._approved: #We have a password and are approved break - i, o, e = select(p.changesignal, (), (), client.delay) - if not i: - icp.write("Timeout passed") + else: + logger.warning(u"Client %s was not approved", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Disapproved") return - ipc.write(u"SENDING %s\n" % client.name) + #wait until timeout or approved + #x = float(client._timedelta_to_milliseconds(delay)) + time = datetime.datetime.now() + client.changedstate.acquire() + client.changedstate.wait(float(client._timedelta_to_milliseconds(delay) / 1000)) + client.changedstate.release() + time2 = datetime.datetime.now() + if (time2 - time) >= delay: + if not client.approved_by_default: + logger.warning("Client %s timed out while" + " waiting for approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Time out") + return + else: + break + else: + delay -= time2 - time + sent_size = 0 while sent_size < len(client.secret): + # XXX handle session exception sent = session.send(client.secret[sent_size:]) logger.debug(u"Sent: %d, remaining: %d", sent, len(client.secret) - (sent_size + sent)) sent_size += sent + + logger.info(u"Sending secret to %s", client.name) + # bump the timeout as if seen + client.checked_ok() + if self.server.use_dbus: + # Emit D-Bus signal + client.GotSecret() + finally: session.bye() @@ -1183,30 +1264,37 @@ return hex_fpr -class ForkingMixInWithPipes(socketserver.ForkingMixIn, object): - """Like socketserver.ForkingMixIn, but also pass a pipe pair.""" +class MultiprocessingMixIn(object): + """Like socketserver.ThreadingMixIn, but with multiprocessing""" + def sub_process_main(self, request, address): + try: + self.finish_request(request, address) + except: + self.handle_error(request, address) + self.close_request(request) + + def process_request(self, request, address): + """Start a new process to process the request.""" + multiprocessing.Process(target = self.sub_process_main, + args = (request, address)).start() + +class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object): + """ adds a pipe to the MixIn """ def process_request(self, request, client_address): """Overrides and wraps the original process_request(). This function creates a new pipe in self.pipe """ - # Child writes to child_pipe - self.child_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0)) - # Parent writes to parent_pipe - self.parent_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0)) - super(ForkingMixInWithPipes, + parent_pipe, self.child_pipe = multiprocessing.Pipe() + + super(MultiprocessingMixInWithPipe, self).process_request(request, client_address) - # Close unused ends for parent - self.parent_pipe[0].close() # close read end - self.child_pipe[1].close() # close write end - self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1]) - def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd): + self.add_pipe(parent_pipe) + def add_pipe(self, parent_pipe): """Dummy function; override as necessary""" - child_pipe_fd.close() - parent_pipe_fd.close() - - -class IPv6_TCPServer(ForkingMixInWithPipes, + pass + +class IPv6_TCPServer(MultiprocessingMixInWithPipe, socketserver.TCPServer, object): """IPv6-capable TCP server. Accepts 'None' as address and/or port @@ -1297,14 +1385,15 @@ return socketserver.TCPServer.server_activate(self) def enable(self): self.enabled = True - def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd): + def add_pipe(self, parent_pipe): # Call "handle_ipc" for both data and EOF events - gobject.io_add_watch(child_pipe_fd.fileno(), + gobject.io_add_watch(parent_pipe.fileno(), gobject.IO_IN | gobject.IO_HUP, functools.partial(self.handle_ipc, - reply = parent_pipe_fd, - sender= child_pipe_fd)) - def handle_ipc(self, source, condition, reply=None, sender=None): + parent_pipe = parent_pipe)) + + def handle_ipc(self, source, condition, parent_pipe=None, + client_object=None): condition_names = { gobject.IO_IN: u"IN", # There is data to read. gobject.IO_OUT: u"OUT", # Data can be written (without @@ -1322,67 +1411,54 @@ logger.debug(u"Handling IPC: FD = %d, condition = %s", source, conditions_string) - # Read a line from the file object - cmdline = sender.readline() - if not cmdline: # Empty line means end of file - # close the IPC pipes - sender.close() - reply.close() - - # Stop calling this function + # Read a request from the child + request = parent_pipe.recv() + command = request[0] + + if command == 'init': + fpr = request[1] + address = request[2] + + for c in self.clients: + if c.fingerprint == fpr: + client = c + break + else: + logger.warning(u"Client not found for fingerprint: %s, ad" + u"dress: %s", fpr, address) + if self.use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientNotFound(fpr, address) + parent_pipe.send(False) + return False + + gobject.io_add_watch(parent_pipe.fileno(), + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe = parent_pipe, + client_object = client)) + parent_pipe.send(True) + # remove the old hook in favor of the new above hook on same fileno return False - - logger.debug(u"IPC command: %r", cmdline) - - # Parse and act on command - cmd, args = cmdline.rstrip(u"\r\n").split(None, 1) - - if cmd == u"NOTFOUND": - fpr, address = args.split(None, 1) - logger.warning(u"Client not found for fingerprint: %s, ad" - u"dress: %s", fpr, address) - if self.use_dbus: - # Emit D-Bus signal - mandos_dbus_service.ClientNotFound(fpr, address) - elif cmd == u"DISABLED": - for client in self.clients: - if client.name == args: - logger.warning(u"Client %s is disabled", args) - if self.use_dbus: - # Emit D-Bus signal - client.Rejected() - break - else: - logger.error(u"Unknown client %s is disabled", args) - elif cmd == u"SENDING": - for client in self.clients: - if client.name == args: - logger.info(u"Sending secret to %s", client.name) - client.checked_ok() - if self.use_dbus: - # Emit D-Bus signal - client.GotSecret() - break - else: - logger.error(u"Sending secret to unknown client %s", - args) - elif cmd == u"GETATTR": - attr_name, fpr = args.split(None, 1) - for client in self.clients: - if client.fingerprint == fpr: - attr_value = getattr(client, attr_name, None) - logger.debug("IPC reply: %r", attr_value) - pickle.dump(attr_value, reply) - break - else: - logger.error(u"Client %s on address %s requesting " - u"attribute %s not found", fpr, address, - attr_name) - pickle.dump(None, reply) - else: - logger.error(u"Unknown IPC command: %r", cmdline) - - # Keep calling this function + if command == 'funcall': + funcname = request[1] + args = request[2] + kwargs = request[3] + + parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs))) + + if command == 'getattr': + attrname = request[1] + if callable(client_object.__getattribute__(attrname)): + parent_pipe.send(('function',)) + else: + parent_pipe.send(('data', client_object.__getattribute__(attrname))) + + if command == 'setattr': + attrname = request[1] + value = request[2] + setattr(client_object, attrname, value) + return True @@ -1576,6 +1652,8 @@ u"interval": u"5m", u"checker": u"fping -q -- %%(host)s", u"host": u"", + u"approved_delay": u"5m", + u"approved_duration": u"1s", } client_config = configparser.SafeConfigParser(client_defaults) client_config.read(os.path.join(server_settings[u"configdir"], @@ -1658,9 +1736,22 @@ client_class = Client if use_dbus: client_class = functools.partial(ClientDBus, bus = bus) + def client_config_items(config, section): + special_settings = { + "approve_by_default": + lambda: config.getboolean(section, + "approve_by_default"), + } + for name, value in config.items(section): + try: + yield special_settings[name]() + except KeyError: + yield (name, value) + tcp_server.clients.update(set( client_class(name = section, - config= dict(client_config.items(section))) + config= dict(client_config_items( + client_config, section))) for section in client_config.sections())) if not tcp_server.clients: logger.warning(u"No clients defined")