/mandos/trunk

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2009-01-20 02:23:59 UTC
  • Revision ID: teddy@fukt.bsnet.se-20090120022359-0go4voqdz5pxc1pu
* mandos (Client.CheckerCompleted): Changed signature to "nxs"; return
                                    exitstatus and waitstatus, not
                                    just a bool and waitstatus.  All
                                    emitters changed.

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.8"
 
69
version = "1.0.5"
70
70
 
71
71
logger = logging.Logger('mandos')
72
72
syslogger = (logging.handlers.SysLogHandler
73
73
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
74
              address = "/dev/log"))
75
75
syslogger.setFormatter(logging.Formatter
76
 
                       ('Mandos [%(process)d]: %(levelname)s:'
77
 
                        ' %(message)s'))
 
76
                       ('Mandos: %(levelname)s: %(message)s'))
78
77
logger.addHandler(syslogger)
79
78
 
80
79
console = logging.StreamHandler()
81
 
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
82
 
                                       ' %(levelname)s: %(message)s'))
 
80
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
 
81
                                       ' %(message)s'))
83
82
logger.addHandler(console)
84
83
 
85
84
class AvahiError(Exception):
114
113
    """
115
114
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
116
115
                 servicetype = None, port = None, TXT = None,
117
 
                 domain = "", host = "", max_renames = 32768,
118
 
                 protocol = avahi.PROTO_UNSPEC):
 
116
                 domain = "", host = "", max_renames = 32768):
119
117
        self.interface = interface
120
118
        self.name = name
121
119
        self.type = servicetype
125
123
        self.host = host
126
124
        self.rename_count = 0
127
125
        self.max_renames = max_renames
128
 
        self.protocol = protocol
129
126
    def rename(self):
130
127
        """Derived from the Avahi example code"""
131
128
        if self.rename_count >= self.max_renames:
160
157
                     service.name, service.type)
161
158
        group.AddService(
162
159
                self.interface,         # interface
163
 
                self.protocol,          # protocol
 
160
                avahi.PROTO_INET6,      # protocol
164
161
                dbus.UInt32(0),         # flags
165
162
                self.name, self.type,
166
163
                self.domain, self.host,
205
202
                     client lives.  %() expansions are done at
206
203
                     runtime with vars(self) as dict, so that for
207
204
                     instance %(name)s can be used in the command.
208
 
    current_checker_command: string; current running checker_command
209
205
    use_dbus: bool(); Whether to provide D-Bus interface and signals
210
206
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
211
207
    """
260
256
        self.disable_initiator_tag = None
261
257
        self.checker_callback_tag = None
262
258
        self.checker_command = config["checker"]
263
 
        self.current_checker_command = None
264
259
        self.last_connect = None
265
260
        # Only now, when this client is initialized, can it show up on
266
261
        # the D-Bus
334
329
            if exitstatus == 0:
335
330
                logger.info(u"Checker for %(name)s succeeded",
336
331
                            vars(self))
337
 
                self.checked_ok()
 
332
                self.bump_timeout()
338
333
            else:
339
334
                logger.info(u"Checker for %(name)s failed",
340
335
                            vars(self))
352
347
                                      dbus.Int64(condition),
353
348
                                      dbus.String(command))
354
349
    
355
 
    def checked_ok(self):
 
350
    def bump_timeout(self):
356
351
        """Bump up the timeout for this client.
357
352
        This should only be called when the client has been seen,
358
353
        alive and well.
381
376
        # checkers alone, the checker would have to take more time
382
377
        # than 'timeout' for the client to be declared invalid, which
383
378
        # is as it should be.
384
 
        
385
 
        # If a checker exists, make sure it is not a zombie
386
 
        if self.checker is not None:
387
 
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
388
 
            if pid:
389
 
                logger.warning("Checker was a zombie")
390
 
                gobject.source_remove(self.checker_callback_tag)
391
 
                self.checker_callback(pid, status,
392
 
                                      self.current_checker_command)
393
 
        # Start a new checker if needed
394
379
        if self.checker is None:
395
380
            try:
396
381
                # In case checker_command has exactly one % operator
406
391
                    logger.error(u'Could not format string "%s":'
407
392
                                 u' %s', self.checker_command, error)
408
393
                    return True # Try again later
409
 
                self.current_checker_command = command
410
394
            try:
411
395
                logger.info(u"Starting checker %r for %s",
412
396
                            command, self.name)
427
411
                                             (self.checker.pid,
428
412
                                              self.checker_callback,
429
413
                                              data=command))
430
 
                # The checker may have completed before the gobject
431
 
                # watch was added.  Check for this.
432
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
433
 
                if pid:
434
 
                    gobject.source_remove(self.checker_callback_tag)
435
 
                    self.checker_callback(pid, status, command)
436
414
            except OSError, error:
437
415
                logger.error(u"Failed to start subprocess: %s",
438
416
                             error)
473
451
    ## D-Bus methods & signals
474
452
    _interface = u"se.bsnet.fukt.Mandos.Client"
475
453
    
476
 
    # CheckedOK - method
477
 
    CheckedOK = dbus.service.method(_interface)(checked_ok)
478
 
    CheckedOK.__name__ = "CheckedOK"
 
454
    # BumpTimeout - method
 
455
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
 
456
    BumpTimeout.__name__ = "BumpTimeout"
479
457
    
480
458
    # CheckerCompleted - signal
481
459
    @dbus.service.signal(_interface, signature="nxs")
617
595
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
618
596
        # ...do the normal thing
619
597
        return session.peer_certificate
620
 
    list_size = ctypes.c_uint(1)
 
598
    list_size = ctypes.c_uint()
621
599
    cert_list = (gnutls.library.functions
622
600
                 .gnutls_certificate_get_peers
623
601
                 (session._c_object, ctypes.byref(list_size)))
624
 
    if not bool(cert_list) and list_size.value != 0:
625
 
        raise gnutls.errors.GNUTLSError("error getting peer"
626
 
                                        " certificate")
627
602
    if list_size.value == 0:
628
603
        return None
629
604
    cert = cert_list[0]
678
653
    def handle(self):
679
654
        logger.info(u"TCP connection from: %s",
680
655
                    unicode(self.client_address))
681
 
        logger.debug(u"Pipe: %d", self.server.pipe[1])
682
 
        # Open IPC pipe to parent process
683
 
        with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
684
 
            session = (gnutls.connection
685
 
                       .ClientSession(self.request,
686
 
                                      gnutls.connection
687
 
                                      .X509Credentials()))
688
 
            
689
 
            line = self.request.makefile().readline()
690
 
            logger.debug(u"Protocol version: %r", line)
691
 
            try:
692
 
                if int(line.strip().split()[0]) > 1:
693
 
                    raise RuntimeError
694
 
            except (ValueError, IndexError, RuntimeError), error:
695
 
                logger.error(u"Unknown protocol version: %s", error)
696
 
                return
697
 
            
698
 
            # Note: gnutls.connection.X509Credentials is really a
699
 
            # generic GnuTLS certificate credentials object so long as
700
 
            # no X.509 keys are added to it.  Therefore, we can use it
701
 
            # here despite using OpenPGP certificates.
702
 
            
703
 
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
704
 
            #                     "+AES-256-CBC", "+SHA1",
705
 
            #                     "+COMP-NULL", "+CTYPE-OPENPGP",
706
 
            #                     "+DHE-DSS"))
707
 
            # Use a fallback default, since this MUST be set.
708
 
            priority = self.server.settings.get("priority", "NORMAL")
709
 
            (gnutls.library.functions
710
 
             .gnutls_priority_set_direct(session._c_object,
711
 
                                         priority, None))
712
 
            
713
 
            try:
714
 
                session.handshake()
715
 
            except gnutls.errors.GNUTLSError, error:
716
 
                logger.warning(u"Handshake failed: %s", error)
717
 
                # Do not run session.bye() here: the session is not
718
 
                # established.  Just abandon the request.
719
 
                return
720
 
            logger.debug(u"Handshake succeeded")
721
 
            try:
722
 
                fpr = fingerprint(peer_certificate(session))
723
 
            except (TypeError, gnutls.errors.GNUTLSError), error:
724
 
                logger.warning(u"Bad certificate: %s", error)
725
 
                session.bye()
726
 
                return
727
 
            logger.debug(u"Fingerprint: %s", fpr)
728
 
            
729
 
            for c in self.server.clients:
730
 
                if c.fingerprint == fpr:
731
 
                    client = c
732
 
                    break
733
 
            else:
734
 
                logger.warning(u"Client not found for fingerprint: %s",
735
 
                               fpr)
736
 
                ipc.write("NOTFOUND %s\n" % fpr)
737
 
                session.bye()
738
 
                return
739
 
            # Have to check if client.still_valid(), since it is
740
 
            # possible that the client timed out while establishing
741
 
            # the GnuTLS session.
742
 
            if not client.still_valid():
743
 
                logger.warning(u"Client %(name)s is invalid",
744
 
                               vars(client))
745
 
                ipc.write("INVALID %s\n" % client.name)
746
 
                session.bye()
747
 
                return
748
 
            ipc.write("SENDING %s\n" % client.name)
749
 
            sent_size = 0
750
 
            while sent_size < len(client.secret):
751
 
                sent = session.send(client.secret[sent_size:])
752
 
                logger.debug(u"Sent: %d, remaining: %d",
753
 
                             sent, len(client.secret)
754
 
                             - (sent_size + sent))
755
 
                sent_size += sent
756
 
            session.bye()
757
 
 
758
 
 
759
 
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
760
 
    """Like SocketServer.ForkingMixIn, but also pass a pipe.
761
 
    Assumes a gobject.MainLoop event loop.
762
 
    """
763
 
    def process_request(self, request, client_address):
764
 
        """This overrides and wraps the original process_request().
765
 
        This function creates a new pipe in self.pipe 
766
 
        """
767
 
        self.pipe = os.pipe()
768
 
        super(ForkingMixInWithPipe,
769
 
              self).process_request(request, client_address)
770
 
        os.close(self.pipe[1])  # close write end
771
 
        # Call "handle_ipc" for both data and EOF events
772
 
        gobject.io_add_watch(self.pipe[0],
773
 
                             gobject.IO_IN | gobject.IO_HUP,
774
 
                             self.handle_ipc)
775
 
    def handle_ipc(source, condition):
776
 
        """Dummy function; override as necessary"""
777
 
        os.close(source)
778
 
        return False
779
 
 
780
 
 
781
 
class IPv6_TCPServer(ForkingMixInWithPipe,
 
656
        session = (gnutls.connection
 
657
                   .ClientSession(self.request,
 
658
                                  gnutls.connection
 
659
                                  .X509Credentials()))
 
660
        
 
661
        line = self.request.makefile().readline()
 
662
        logger.debug(u"Protocol version: %r", line)
 
663
        try:
 
664
            if int(line.strip().split()[0]) > 1:
 
665
                raise RuntimeError
 
666
        except (ValueError, IndexError, RuntimeError), error:
 
667
            logger.error(u"Unknown protocol version: %s", error)
 
668
            return
 
669
        
 
670
        # Note: gnutls.connection.X509Credentials is really a generic
 
671
        # GnuTLS certificate credentials object so long as no X.509
 
672
        # keys are added to it.  Therefore, we can use it here despite
 
673
        # using OpenPGP certificates.
 
674
        
 
675
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
676
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
677
        #                "+DHE-DSS"))
 
678
        # Use a fallback default, since this MUST be set.
 
679
        priority = self.server.settings.get("priority", "NORMAL")
 
680
        (gnutls.library.functions
 
681
         .gnutls_priority_set_direct(session._c_object,
 
682
                                     priority, None))
 
683
        
 
684
        try:
 
685
            session.handshake()
 
686
        except gnutls.errors.GNUTLSError, error:
 
687
            logger.warning(u"Handshake failed: %s", error)
 
688
            # Do not run session.bye() here: the session is not
 
689
            # established.  Just abandon the request.
 
690
            return
 
691
        try:
 
692
            fpr = fingerprint(peer_certificate(session))
 
693
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
694
            logger.warning(u"Bad certificate: %s", error)
 
695
            session.bye()
 
696
            return
 
697
        logger.debug(u"Fingerprint: %s", fpr)
 
698
        for c in self.server.clients:
 
699
            if c.fingerprint == fpr:
 
700
                client = c
 
701
                break
 
702
        else:
 
703
            logger.warning(u"Client not found for fingerprint: %s",
 
704
                           fpr)
 
705
            session.bye()
 
706
            return
 
707
        # Have to check if client.still_valid(), since it is possible
 
708
        # that the client timed out while establishing the GnuTLS
 
709
        # session.
 
710
        if not client.still_valid():
 
711
            logger.warning(u"Client %(name)s is invalid",
 
712
                           vars(client))
 
713
            session.bye()
 
714
            return
 
715
        ## This won't work here, since we're in a fork.
 
716
        # client.bump_timeout()
 
717
        sent_size = 0
 
718
        while sent_size < len(client.secret):
 
719
            sent = session.send(client.secret[sent_size:])
 
720
            logger.debug(u"Sent: %d, remaining: %d",
 
721
                         sent, len(client.secret)
 
722
                         - (sent_size + sent))
 
723
            sent_size += sent
 
724
        session.bye()
 
725
 
 
726
 
 
727
class IPv6_TCPServer(SocketServer.ForkingMixIn,
782
728
                     SocketServer.TCPServer, object):
783
 
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
 
729
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
784
730
    Attributes:
785
731
        settings:       Server settings
786
732
        clients:        Set() of Client objects
794
740
        if "clients" in kwargs:
795
741
            self.clients = kwargs["clients"]
796
742
            del kwargs["clients"]
797
 
        if "use_ipv6" in kwargs:
798
 
            if not kwargs["use_ipv6"]:
799
 
                self.address_family = socket.AF_INET
800
 
            del kwargs["use_ipv6"]
801
743
        self.enabled = False
802
744
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
803
745
    def server_bind(self):
817
759
                                 u" bind to interface %s",
818
760
                                 self.settings["interface"])
819
761
                else:
820
 
                    raise
 
762
                    raise error
821
763
        # Only bind(2) the socket if we really need to.
822
764
        if self.server_address[0] or self.server_address[1]:
823
765
            if not self.server_address[0]:
824
 
                if self.address_family == socket.AF_INET6:
825
 
                    any_address = "::" # in6addr_any
826
 
                else:
827
 
                    any_address = socket.INADDR_ANY
828
 
                self.server_address = (any_address,
 
766
                in6addr_any = "::"
 
767
                self.server_address = (in6addr_any,
829
768
                                       self.server_address[1])
830
769
            elif not self.server_address[1]:
831
770
                self.server_address = (self.server_address[0],
843
782
            return super(IPv6_TCPServer, self).server_activate()
844
783
    def enable(self):
845
784
        self.enabled = True
846
 
    def handle_ipc(self, source, condition, file_objects={}):
847
 
        logger.debug("Handling IPC: %r : %r", source, condition)
848
 
        
849
 
        # Turn a file descriptor into a Python file object
850
 
        if source not in file_objects:
851
 
            file_objects[source] = os.fdopen(source, "r", 1)
852
 
        
853
 
        # Read a line from the file object
854
 
        cmdline = file_objects[source].readline()
855
 
        if not cmdline:             # Empty line means end of file
856
 
            # close the IPC pipe
857
 
            logger.debug("Closing: %r", source)
858
 
            file_objects[source].close()
859
 
            del file_objects[source]
860
 
 
861
 
            # Stop calling this function
862
 
            return False
863
 
        
864
 
        logger.debug("IPC command: %r\n" % cmdline)
865
 
        
866
 
        # Parse and act on command
867
 
        cmd, args = cmdline.split(None, 1)
868
 
        if cmd == "NOTFOUND":
869
 
            pass                # xxx
870
 
        elif cmd == "INVALID":
871
 
            pass                # xxx
872
 
        elif cmd == "SENDING":
873
 
            pass                # xxx
874
 
            # client.checked_ok()
875
 
        else:
876
 
            logger.error("Unknown IPC command: %r", cmdline)
877
 
        
878
 
        # Keep calling this function
879
 
        return True
880
785
 
881
786
 
882
787
def string_to_delta(interval):
883
788
    """Parse a string and return a datetime.timedelta
884
 
    
 
789
 
885
790
    >>> string_to_delta('7d')
886
791
    datetime.timedelta(7)
887
792
    >>> string_to_delta('60s')
1012
917
                      dest="use_dbus",
1013
918
                      help="Do not provide D-Bus system bus"
1014
919
                      " interface")
1015
 
    parser.add_option("--no-ipv6", action="store_false",
1016
 
                      dest="use_ipv6", help="Do not use IPv6")
1017
920
    options = parser.parse_args()[0]
1018
921
    
1019
922
    if options.check:
1030
933
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1031
934
                        "servicename": "Mandos",
1032
935
                        "use_dbus": "True",
1033
 
                        "use_ipv6": "True",
1034
936
                        }
1035
937
    
1036
938
    # Parse config file for server-global settings
1039
941
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
1040
942
    # Convert the SafeConfigParser object to a dict
1041
943
    server_settings = server_config.defaults()
1042
 
    # Use the appropriate methods on the non-string config options
1043
 
    server_settings["debug"] = server_config.getboolean("DEFAULT",
1044
 
                                                        "debug")
1045
 
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
1046
 
                                                           "use_dbus")
1047
 
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
1048
 
                                                           "use_ipv6")
1049
 
    if server_settings["port"]:
1050
 
        server_settings["port"] = server_config.getint("DEFAULT",
1051
 
                                                       "port")
 
944
    # Use getboolean on the boolean config options
 
945
    server_settings["debug"] = (server_config.getboolean
 
946
                                ("DEFAULT", "debug"))
 
947
    server_settings["use_dbus"] = (server_config.getboolean
 
948
                                   ("DEFAULT", "use_dbus"))
1052
949
    del server_config
1053
950
    
1054
951
    # Override the settings from the config file with command line
1055
952
    # options, if set.
1056
953
    for option in ("interface", "address", "port", "debug",
1057
954
                   "priority", "servicename", "configdir",
1058
 
                   "use_dbus", "use_ipv6"):
 
955
                   "use_dbus"):
1059
956
        value = getattr(options, option)
1060
957
        if value is not None:
1061
958
            server_settings[option] = value
1065
962
    # For convenience
1066
963
    debug = server_settings["debug"]
1067
964
    use_dbus = server_settings["use_dbus"]
1068
 
    use_ipv6 = server_settings["use_ipv6"]
1069
965
    
1070
966
    if not debug:
1071
967
        syslogger.setLevel(logging.WARNING)
1092
988
                                 server_settings["port"]),
1093
989
                                TCP_handler,
1094
990
                                settings=server_settings,
1095
 
                                clients=clients, use_ipv6=use_ipv6)
 
991
                                clients=clients)
1096
992
    pidfilename = "/var/run/mandos.pid"
1097
993
    try:
1098
994
        pidfile = open(pidfilename, "w")
1099
 
    except IOError:
 
995
    except IOError, error:
1100
996
        logger.error("Could not open file %r", pidfilename)
1101
997
    
1102
998
    try:
1114
1010
                uid = 65534
1115
1011
                gid = 65534
1116
1012
    try:
 
1013
        os.setuid(uid)
1117
1014
        os.setgid(gid)
1118
 
        os.setuid(uid)
1119
1015
    except OSError, error:
1120
1016
        if error[0] != errno.EPERM:
1121
1017
            raise error
1122
1018
    
1123
 
    # Enable all possible GnuTLS debugging
1124
 
    if debug:
1125
 
        # "Use a log level over 10 to enable all debugging options."
1126
 
        # - GnuTLS manual
1127
 
        gnutls.library.functions.gnutls_global_set_log_level(11)
1128
 
        
1129
 
        @gnutls.library.types.gnutls_log_func
1130
 
        def debug_gnutls(level, string):
1131
 
            logger.debug("GnuTLS: %s", string[:-1])
1132
 
        
1133
 
        (gnutls.library.functions
1134
 
         .gnutls_global_set_log_function(debug_gnutls))
1135
 
    
1136
1019
    global service
1137
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1138
1020
    service = AvahiService(name = server_settings["servicename"],
1139
 
                           servicetype = "_mandos._tcp",
1140
 
                           protocol = protocol)
 
1021
                           servicetype = "_mandos._tcp", )
1141
1022
    if server_settings["interface"]:
1142
1023
        service.interface = (if_nametoindex
1143
1024
                             (server_settings["interface"]))
1229
1110
            
1230
1111
            @dbus.service.method(_interface, out_signature="ao")
1231
1112
            def GetAllClients(self):
1232
 
                "D-Bus method"
1233
1113
                return dbus.Array(c.dbus_object_path for c in clients)
1234
1114
            
1235
1115
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1236
1116
            def GetAllClientsWithProperties(self):
1237
 
                "D-Bus method"
1238
1117
                return dbus.Dictionary(
1239
1118
                    ((c.dbus_object_path, c.GetAllProperties())
1240
1119
                     for c in clients),
1242
1121
            
1243
1122
            @dbus.service.method(_interface, in_signature="o")
1244
1123
            def RemoveClient(self, object_path):
1245
 
                "D-Bus method"
1246
1124
                for c in clients:
1247
1125
                    if c.dbus_object_path == object_path:
1248
1126
                        clients.remove(c)
1270
1148
    
1271
1149
    # Find out what port we got
1272
1150
    service.port = tcp_server.socket.getsockname()[1]
1273
 
    if use_ipv6:
1274
 
        logger.info(u"Now listening on address %r, port %d,"
1275
 
                    " flowinfo %d, scope_id %d"
1276
 
                    % tcp_server.socket.getsockname())
1277
 
    else:                       # IPv4
1278
 
        logger.info(u"Now listening on address %r, port %d"
1279
 
                    % tcp_server.socket.getsockname())
 
1151
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
1152
                u" scope_id %d" % tcp_server.socket.getsockname())
1280
1153
    
1281
1154
    #service.interface = tcp_server.socket.getsockname()[3]
1282
1155
    
1302
1175
        sys.exit(1)
1303
1176
    except KeyboardInterrupt:
1304
1177
        if debug:
1305
 
            print >> sys.stderr
1306
 
        logger.debug("Server received KeyboardInterrupt")
1307
 
    logger.debug("Server exiting")
 
1178
            print
1308
1179
 
1309
1180
if __name__ == '__main__':
1310
1181
    main()