/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2009-01-28 11:23:12 UTC
  • mto: (237.2.90 mandos)
  • mto: This revision was merged to the branch mainline in revision 264.
  • Revision ID: teddy@fukt.bsnet.se-20090128112312-fccq15ef11z330u3
Start of new pipe-based IPC mechanism.

* mandos (TCP_handler.handle): Open the IPC pipe, write messages to it
                               and close it when done.
  (ForkingMixInWithPipe): New.
  (IPv6_TCPServer): Inherit from "ForkingMixInWithPipe" instead of
                   "SocketServer.ForkingMixIn".
  (IPv6_TCPServer.handle_ipc): New.

Show diffs side-by-side

added added

removed removed

Lines of Context:
66
66
import ctypes
67
67
import ctypes.util
68
68
 
69
 
version = "1.0.2"
 
69
version = "1.0.5"
70
70
 
71
71
logger = logging.Logger('mandos')
72
72
syslogger = (logging.handlers.SysLogHandler
178
178
class Client(dbus.service.Object):
179
179
    """A representation of a client host served by this server.
180
180
    Attributes:
181
 
    name:       string; from the config file, used in log messages
 
181
    name:       string; from the config file, used in log messages and
 
182
                        D-Bus identifiers
182
183
    fingerprint: string (40 or 32 hexadecimal digits); used to
183
184
                 uniquely identify the client
184
185
    secret:     bytestring; sent verbatim (over TLS) to client
225
226
        if config is None:
226
227
            config = {}
227
228
        logger.debug(u"Creating client %r", self.name)
228
 
        self.use_dbus = use_dbus
229
 
        if self.use_dbus:
230
 
            self.dbus_object_path = (dbus.ObjectPath
231
 
                                     ("/Mandos/clients/"
232
 
                                      + self.name.replace(".", "_")))
233
 
            dbus.service.Object.__init__(self, bus,
234
 
                                         self.dbus_object_path)
 
229
        self.use_dbus = False   # During __init__
235
230
        # Uppercase and remove spaces from fingerprint for later
236
231
        # comparison purposes with return value from the fingerprint()
237
232
        # function
261
256
        self.disable_initiator_tag = None
262
257
        self.checker_callback_tag = None
263
258
        self.checker_command = config["checker"]
 
259
        self.last_connect = None
 
260
        # Only now, when this client is initialized, can it show up on
 
261
        # the D-Bus
 
262
        self.use_dbus = use_dbus
 
263
        if self.use_dbus:
 
264
            self.dbus_object_path = (dbus.ObjectPath
 
265
                                     ("/clients/"
 
266
                                      + self.name.replace(".", "_")))
 
267
            dbus.service.Object.__init__(self, bus,
 
268
                                         self.dbus_object_path)
264
269
    
265
270
    def enable(self):
266
271
        """Start this client's checker and timeout hooks"""
319
324
            # Emit D-Bus signal
320
325
            self.PropertyChanged(dbus.String(u"checker_running"),
321
326
                                 dbus.Boolean(False, variant_level=1))
322
 
        if (os.WIFEXITED(condition)
323
 
            and (os.WEXITSTATUS(condition) == 0)):
324
 
            logger.info(u"Checker for %(name)s succeeded",
325
 
                        vars(self))
 
327
        if os.WIFEXITED(condition):
 
328
            exitstatus = os.WEXITSTATUS(condition)
 
329
            if exitstatus == 0:
 
330
                logger.info(u"Checker for %(name)s succeeded",
 
331
                            vars(self))
 
332
                self.checked_ok()
 
333
            else:
 
334
                logger.info(u"Checker for %(name)s failed",
 
335
                            vars(self))
326
336
            if self.use_dbus:
327
337
                # Emit D-Bus signal
328
 
                self.CheckerCompleted(dbus.Boolean(True),
329
 
                                      dbus.UInt16(condition),
 
338
                self.CheckerCompleted(dbus.Int16(exitstatus),
 
339
                                      dbus.Int64(condition),
330
340
                                      dbus.String(command))
331
 
            self.bump_timeout()
332
 
        elif not os.WIFEXITED(condition):
 
341
        else:
333
342
            logger.warning(u"Checker for %(name)s crashed?",
334
343
                           vars(self))
335
344
            if self.use_dbus:
336
345
                # Emit D-Bus signal
337
 
                self.CheckerCompleted(dbus.Boolean(False),
338
 
                                      dbus.UInt16(condition),
339
 
                                      dbus.String(command))
340
 
        else:
341
 
            logger.info(u"Checker for %(name)s failed",
342
 
                        vars(self))
343
 
            if self.use_dbus:
344
 
                # Emit D-Bus signal
345
 
                self.CheckerCompleted(dbus.Boolean(False),
346
 
                                      dbus.UInt16(condition),
 
346
                self.CheckerCompleted(dbus.Int16(-1),
 
347
                                      dbus.Int64(condition),
347
348
                                      dbus.String(command))
348
349
    
349
 
    def bump_timeout(self):
 
350
    def checked_ok(self):
350
351
        """Bump up the timeout for this client.
351
352
        This should only be called when the client has been seen,
352
353
        alive and well.
448
449
            return now < (self.last_checked_ok + self.timeout)
449
450
    
450
451
    ## D-Bus methods & signals
451
 
    _interface = u"org.mandos_system.Mandos.Client"
 
452
    _interface = u"se.bsnet.fukt.Mandos.Client"
452
453
    
453
 
    # BumpTimeout - method
454
 
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
455
 
    BumpTimeout.__name__ = "BumpTimeout"
 
454
    # CheckedOK - method
 
455
    CheckedOK = dbus.service.method(_interface)(checked_ok)
 
456
    CheckedOK.__name__ = "CheckedOK"
456
457
    
457
458
    # CheckerCompleted - signal
458
 
    @dbus.service.signal(_interface, signature="bqs")
459
 
    def CheckerCompleted(self, success, condition, command):
 
459
    @dbus.service.signal(_interface, signature="nxs")
 
460
    def CheckerCompleted(self, exitcode, waitstatus, command):
460
461
        "D-Bus signal"
461
462
        pass
462
463
    
503
504
                dbus.String("checker_running"):
504
505
                    dbus.Boolean(self.checker is not None,
505
506
                                 variant_level=1),
 
507
                dbus.String("object_path"):
 
508
                    dbus.ObjectPath(self.dbus_object_path,
 
509
                                    variant_level=1)
506
510
                }, signature="sv")
507
511
    
508
512
    # IsStillValid - method
591
595
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
592
596
        # ...do the normal thing
593
597
        return session.peer_certificate
594
 
    list_size = ctypes.c_uint()
 
598
    list_size = ctypes.c_uint(1)
595
599
    cert_list = (gnutls.library.functions
596
600
                 .gnutls_certificate_get_peers
597
601
                 (session._c_object, ctypes.byref(list_size)))
 
602
    if not bool(cert_list) and list_size.value != 0:
 
603
        raise gnutls.errors.GNUTLSError("error getting peer"
 
604
                                        " certificate")
598
605
    if list_size.value == 0:
599
606
        return None
600
607
    cert = cert_list[0]
649
656
    def handle(self):
650
657
        logger.info(u"TCP connection from: %s",
651
658
                    unicode(self.client_address))
652
 
        session = (gnutls.connection
653
 
                   .ClientSession(self.request,
654
 
                                  gnutls.connection
655
 
                                  .X509Credentials()))
656
 
        
657
 
        line = self.request.makefile().readline()
658
 
        logger.debug(u"Protocol version: %r", line)
659
 
        try:
660
 
            if int(line.strip().split()[0]) > 1:
661
 
                raise RuntimeError
662
 
        except (ValueError, IndexError, RuntimeError), error:
663
 
            logger.error(u"Unknown protocol version: %s", error)
664
 
            return
665
 
        
666
 
        # Note: gnutls.connection.X509Credentials is really a generic
667
 
        # GnuTLS certificate credentials object so long as no X.509
668
 
        # keys are added to it.  Therefore, we can use it here despite
669
 
        # using OpenPGP certificates.
670
 
        
671
 
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
672
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
673
 
        #                "+DHE-DSS"))
674
 
        # Use a fallback default, since this MUST be set.
675
 
        priority = self.server.settings.get("priority", "NORMAL")
676
 
        (gnutls.library.functions
677
 
         .gnutls_priority_set_direct(session._c_object,
678
 
                                     priority, None))
679
 
        
680
 
        try:
681
 
            session.handshake()
682
 
        except gnutls.errors.GNUTLSError, error:
683
 
            logger.warning(u"Handshake failed: %s", error)
684
 
            # Do not run session.bye() here: the session is not
685
 
            # established.  Just abandon the request.
686
 
            return
687
 
        try:
688
 
            fpr = fingerprint(peer_certificate(session))
689
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
690
 
            logger.warning(u"Bad certificate: %s", error)
691
 
            session.bye()
692
 
            return
693
 
        logger.debug(u"Fingerprint: %s", fpr)
694
 
        for c in self.server.clients:
695
 
            if c.fingerprint == fpr:
696
 
                client = c
697
 
                break
698
 
        else:
699
 
            logger.warning(u"Client not found for fingerprint: %s",
700
 
                           fpr)
701
 
            session.bye()
702
 
            return
703
 
        # Have to check if client.still_valid(), since it is possible
704
 
        # that the client timed out while establishing the GnuTLS
705
 
        # session.
706
 
        if not client.still_valid():
707
 
            logger.warning(u"Client %(name)s is invalid",
708
 
                           vars(client))
709
 
            session.bye()
710
 
            return
711
 
        ## This won't work here, since we're in a fork.
712
 
        # client.bump_timeout()
713
 
        sent_size = 0
714
 
        while sent_size < len(client.secret):
715
 
            sent = session.send(client.secret[sent_size:])
716
 
            logger.debug(u"Sent: %d, remaining: %d",
717
 
                         sent, len(client.secret)
718
 
                         - (sent_size + sent))
719
 
            sent_size += sent
720
 
        session.bye()
721
 
 
722
 
 
723
 
class IPv6_TCPServer(SocketServer.ForkingMixIn,
 
659
        logger.debug(u"Pipe: %d", self.server.pipe[1])
 
660
        # Open IPC pipe to parent process
 
661
        with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
 
662
            session = (gnutls.connection
 
663
                       .ClientSession(self.request,
 
664
                                      gnutls.connection
 
665
                                      .X509Credentials()))
 
666
            
 
667
            line = self.request.makefile().readline()
 
668
            logger.debug(u"Protocol version: %r", line)
 
669
            try:
 
670
                if int(line.strip().split()[0]) > 1:
 
671
                    raise RuntimeError
 
672
            except (ValueError, IndexError, RuntimeError), error:
 
673
                logger.error(u"Unknown protocol version: %s", error)
 
674
                return
 
675
            
 
676
            # Note: gnutls.connection.X509Credentials is really a
 
677
            # generic GnuTLS certificate credentials object so long as
 
678
            # no X.509 keys are added to it.  Therefore, we can use it
 
679
            # here despite using OpenPGP certificates.
 
680
            
 
681
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
682
            #                     "+AES-256-CBC", "+SHA1",
 
683
            #                     "+COMP-NULL", "+CTYPE-OPENPGP",
 
684
            #                     "+DHE-DSS"))
 
685
            # Use a fallback default, since this MUST be set.
 
686
            priority = self.server.settings.get("priority", "NORMAL")
 
687
            (gnutls.library.functions
 
688
             .gnutls_priority_set_direct(session._c_object,
 
689
                                         priority, None))
 
690
 
 
691
            try:
 
692
                session.handshake()
 
693
            except gnutls.errors.GNUTLSError, error:
 
694
                logger.warning(u"Handshake failed: %s", error)
 
695
                # Do not run session.bye() here: the session is not
 
696
                # established.  Just abandon the request.
 
697
                return
 
698
            logger.debug(u"Handshake succeeded")
 
699
            try:
 
700
                fpr = fingerprint(peer_certificate(session))
 
701
            except (TypeError, gnutls.errors.GNUTLSError), error:
 
702
                logger.warning(u"Bad certificate: %s", error)
 
703
                session.bye()
 
704
                return
 
705
            logger.debug(u"Fingerprint: %s", fpr)
 
706
            for c in self.server.clients:
 
707
                if c.fingerprint == fpr:
 
708
                    client = c
 
709
                    break
 
710
            else:
 
711
                logger.warning(u"Client not found for fingerprint: %s",
 
712
                               fpr)
 
713
                ipc.write("NOTFOUND %s\n" % fpr)
 
714
                session.bye()
 
715
                return
 
716
            # Have to check if client.still_valid(), since it is
 
717
            # possible that the client timed out while establishing
 
718
            # the GnuTLS session.
 
719
            if not client.still_valid():
 
720
                logger.warning(u"Client %(name)s is invalid",
 
721
                               vars(client))
 
722
                ipc.write("INVALID %s\n" % client.name)
 
723
                session.bye()
 
724
                return
 
725
            ipc.write("SENDING %s\n" % client.name)
 
726
            ## This won't work here, since we're in a fork.
 
727
            # client.checked_ok()
 
728
            sent_size = 0
 
729
            while sent_size < len(client.secret):
 
730
                sent = session.send(client.secret[sent_size:])
 
731
                logger.debug(u"Sent: %d, remaining: %d",
 
732
                             sent, len(client.secret)
 
733
                             - (sent_size + sent))
 
734
                sent_size += sent
 
735
            session.bye()
 
736
 
 
737
 
 
738
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
 
739
    """Like SocketServer.ForkingMixIn, but also pass a pipe.
 
740
    Assumes a gobject.MainLoop event loop.
 
741
    """
 
742
    def process_request(self, request, client_address):
 
743
        """This overrides and wraps the original process_request().
 
744
        This function creates a new pipe in self.pipe 
 
745
        """
 
746
        self.pipe = os.pipe()
 
747
        super(ForkingMixInWithPipe,
 
748
              self).process_request(request, client_address)
 
749
        os.close(self.pipe[1])  # close write end
 
750
        # Call "handle_ipc" for both data and EOF events
 
751
        gobject.io_add_watch(self.pipe[0],
 
752
                             gobject.IO_IN | gobject.IO_HUP,
 
753
                             self.handle_ipc)
 
754
    def handle_ipc(source, condition):
 
755
        """Dummy function; override as necessary"""
 
756
        os.close(source)
 
757
        return False
 
758
 
 
759
 
 
760
class IPv6_TCPServer(ForkingMixInWithPipe,
724
761
                     SocketServer.TCPServer, object):
725
762
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
726
763
    Attributes:
778
815
            return super(IPv6_TCPServer, self).server_activate()
779
816
    def enable(self):
780
817
        self.enabled = True
 
818
    def handle_ipc(self, source, condition, file_objects={}):
 
819
        logger.debug("Handling IPC: %r : %r", source, condition)
 
820
        
 
821
        # Turn a file descriptor into a Python file object
 
822
        if source not in file_objects:
 
823
            file_objects[source] = os.fdopen(source, "r", 1)
 
824
        
 
825
        # Read a line from the file object
 
826
        cmdline = file_objects[source].readline()
 
827
        if not cmdline:             # Empty line means end of file
 
828
            # close the IPC pipe
 
829
            logger.debug("Closing: %r", source)
 
830
            file_objects[source].close()
 
831
            del file_objects[source]
 
832
 
 
833
            # Stop calling this function
 
834
            return False
 
835
        
 
836
        logger.debug("IPC command: %r\n" % cmdline)
 
837
        
 
838
        # Parse and act on command
 
839
        cmd, args = cmdline.split(None, 1)
 
840
        if cmd == "NOTFOUND":
 
841
            pass                # xxx
 
842
        elif cmd == "INVALID":
 
843
            pass                # xxx
 
844
        elif cmd == "SENDING":
 
845
            pass                # xxx
 
846
        else:
 
847
            logger.error("Unknown IPC command: %r", cmdline)
 
848
        
 
849
        # Keep calling this function
 
850
        return True
781
851
 
782
852
 
783
853
def string_to_delta(interval):
911
981
                      " files")
912
982
    parser.add_option("--no-dbus", action="store_false",
913
983
                      dest="use_dbus",
914
 
                      help=optparse.SUPPRESS_HELP)
 
984
                      help="Do not provide D-Bus system bus"
 
985
                      " interface")
915
986
    options = parser.parse_args()[0]
916
987
    
917
988
    if options.check:
936
1007
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
937
1008
    # Convert the SafeConfigParser object to a dict
938
1009
    server_settings = server_config.defaults()
939
 
    # Use getboolean on the boolean config options
940
 
    server_settings["debug"] = (server_config.getboolean
941
 
                                ("DEFAULT", "debug"))
942
 
    server_settings["use_dbus"] = (server_config.getboolean
943
 
                                   ("DEFAULT", "use_dbus"))
 
1010
    # Use the appropriate methods on the non-string config options
 
1011
    server_settings["debug"] = server_config.getboolean("DEFAULT",
 
1012
                                                        "debug")
 
1013
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
 
1014
                                                           "use_dbus")
 
1015
    if server_settings["port"]:
 
1016
        server_settings["port"] = server_config.getint("DEFAULT",
 
1017
                                                       "port")
944
1018
    del server_config
945
1019
    
946
1020
    # Override the settings from the config file with command line
957
1031
    # For convenience
958
1032
    debug = server_settings["debug"]
959
1033
    use_dbus = server_settings["use_dbus"]
960
 
    use_dbus = False
961
1034
    
962
1035
    if not debug:
963
1036
        syslogger.setLevel(logging.WARNING)
972
1045
    # Parse config file with clients
973
1046
    client_defaults = { "timeout": "1h",
974
1047
                        "interval": "5m",
975
 
                        "checker": "fping -q -- %(host)s",
 
1048
                        "checker": "fping -q -- %%(host)s",
976
1049
                        "host": "",
977
1050
                        }
978
1051
    client_config = ConfigParser.SafeConfigParser(client_defaults)
993
1066
    
994
1067
    try:
995
1068
        uid = pwd.getpwnam("_mandos").pw_uid
 
1069
        gid = pwd.getpwnam("_mandos").pw_gid
996
1070
    except KeyError:
997
1071
        try:
998
1072
            uid = pwd.getpwnam("mandos").pw_uid
 
1073
            gid = pwd.getpwnam("mandos").pw_gid
999
1074
        except KeyError:
1000
1075
            try:
1001
1076
                uid = pwd.getpwnam("nobody").pw_uid
 
1077
                gid = pwd.getpwnam("nogroup").pw_gid
1002
1078
            except KeyError:
1003
1079
                uid = 65534
1004
 
    try:
1005
 
        gid = pwd.getpwnam("_mandos").pw_gid
1006
 
    except KeyError:
1007
 
        try:
1008
 
            gid = pwd.getpwnam("mandos").pw_gid
1009
 
        except KeyError:
1010
 
            try:
1011
 
                gid = pwd.getpwnam("nogroup").pw_gid
1012
 
            except KeyError:
1013
1080
                gid = 65534
1014
1081
    try:
1015
1082
        os.setuid(uid)
1037
1104
                            avahi.DBUS_INTERFACE_SERVER)
1038
1105
    # End of Avahi example code
1039
1106
    if use_dbus:
1040
 
        bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
1041
 
                                        bus)
 
1107
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1042
1108
    
1043
1109
    clients.update(Set(Client(name = section,
1044
1110
                              config
1098
1164
        class MandosServer(dbus.service.Object):
1099
1165
            """A D-Bus proxy object"""
1100
1166
            def __init__(self):
1101
 
                dbus.service.Object.__init__(self, bus,
1102
 
                                             "/Mandos")
1103
 
            _interface = u"org.mandos_system.Mandos"
1104
 
 
 
1167
                dbus.service.Object.__init__(self, bus, "/")
 
1168
            _interface = u"se.bsnet.fukt.Mandos"
 
1169
            
1105
1170
            @dbus.service.signal(_interface, signature="oa{sv}")
1106
1171
            def ClientAdded(self, objpath, properties):
1107
1172
                "D-Bus signal"
1108
1173
                pass
1109
 
 
1110
 
            @dbus.service.signal(_interface, signature="o")
1111
 
            def ClientRemoved(self, objpath):
 
1174
            
 
1175
            @dbus.service.signal(_interface, signature="os")
 
1176
            def ClientRemoved(self, objpath, name):
1112
1177
                "D-Bus signal"
1113
1178
                pass
1114
 
 
 
1179
            
1115
1180
            @dbus.service.method(_interface, out_signature="ao")
1116
1181
            def GetAllClients(self):
 
1182
                "D-Bus method"
1117
1183
                return dbus.Array(c.dbus_object_path for c in clients)
1118
 
 
 
1184
            
1119
1185
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1120
1186
            def GetAllClientsWithProperties(self):
 
1187
                "D-Bus method"
1121
1188
                return dbus.Dictionary(
1122
1189
                    ((c.dbus_object_path, c.GetAllProperties())
1123
1190
                     for c in clients),
1124
1191
                    signature="oa{sv}")
1125
 
 
 
1192
            
1126
1193
            @dbus.service.method(_interface, in_signature="o")
1127
1194
            def RemoveClient(self, object_path):
 
1195
                "D-Bus method"
1128
1196
                for c in clients:
1129
1197
                    if c.dbus_object_path == object_path:
1130
1198
                        clients.remove(c)
1132
1200
                        c.use_dbus = False
1133
1201
                        c.disable()
1134
1202
                        # Emit D-Bus signal
1135
 
                        self.ClientRemoved(object_path)
 
1203
                        self.ClientRemoved(object_path, c.name)
1136
1204
                        return
1137
1205
                raise KeyError
1138
 
            @dbus.service.method(_interface)
1139
 
            def Quit(self):
1140
 
                main_loop.quit()
1141
 
 
 
1206
            
1142
1207
            del _interface
1143
 
    
 
1208
        
1144
1209
        mandos_server = MandosServer()
1145
1210
    
1146
1211
    for client in clients: