/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-15 05:27:55 UTC
  • mfrom: (237.2.9 mandos-release)
  • Revision ID: teddy@fukt.bsnet.se-20090115052755-luw0gjx8727p24o3
Merge from release branch.

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.6"
 
69
version = "1.0.4"
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,
181
178
class Client(dbus.service.Object):
182
179
    """A representation of a client host served by this server.
183
180
    Attributes:
184
 
    name:       string; from the config file, used in log messages and
185
 
                        D-Bus identifiers
 
181
    name:       string; from the config file, used in log messages
186
182
    fingerprint: string (40 or 32 hexadecimal digits); used to
187
183
                 uniquely identify the client
188
184
    secret:     bytestring; sent verbatim (over TLS) to client
205
201
                     client lives.  %() expansions are done at
206
202
                     runtime with vars(self) as dict, so that for
207
203
                     instance %(name)s can be used in the command.
208
 
    current_checker_command: string; current running checker_command
209
204
    use_dbus: bool(); Whether to provide D-Bus interface and signals
210
205
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
211
206
    """
230
225
        if config is None:
231
226
            config = {}
232
227
        logger.debug(u"Creating client %r", self.name)
233
 
        self.use_dbus = False   # During __init__
 
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)
234
235
        # Uppercase and remove spaces from fingerprint for later
235
236
        # comparison purposes with return value from the fingerprint()
236
237
        # function
260
261
        self.disable_initiator_tag = None
261
262
        self.checker_callback_tag = None
262
263
        self.checker_command = config["checker"]
263
 
        self.current_checker_command = None
264
 
        self.last_connect = None
265
 
        # Only now, when this client is initialized, can it show up on
266
 
        # the D-Bus
267
 
        self.use_dbus = use_dbus
268
 
        if self.use_dbus:
269
 
            self.dbus_object_path = (dbus.ObjectPath
270
 
                                     ("/clients/"
271
 
                                      + self.name.replace(".", "_")))
272
 
            dbus.service.Object.__init__(self, bus,
273
 
                                         self.dbus_object_path)
274
264
    
275
265
    def enable(self):
276
266
        """Start this client's checker and timeout hooks"""
329
319
            # Emit D-Bus signal
330
320
            self.PropertyChanged(dbus.String(u"checker_running"),
331
321
                                 dbus.Boolean(False, variant_level=1))
332
 
        if os.WIFEXITED(condition):
333
 
            exitstatus = os.WEXITSTATUS(condition)
334
 
            if exitstatus == 0:
335
 
                logger.info(u"Checker for %(name)s succeeded",
336
 
                            vars(self))
337
 
                self.checked_ok()
338
 
            else:
339
 
                logger.info(u"Checker for %(name)s failed",
340
 
                            vars(self))
 
322
        if (os.WIFEXITED(condition)
 
323
            and (os.WEXITSTATUS(condition) == 0)):
 
324
            logger.info(u"Checker for %(name)s succeeded",
 
325
                        vars(self))
341
326
            if self.use_dbus:
342
327
                # Emit D-Bus signal
343
 
                self.CheckerCompleted(dbus.Int16(exitstatus),
344
 
                                      dbus.Int64(condition),
 
328
                self.CheckerCompleted(dbus.Boolean(True),
 
329
                                      dbus.UInt16(condition),
345
330
                                      dbus.String(command))
346
 
        else:
 
331
            self.bump_timeout()
 
332
        elif not os.WIFEXITED(condition):
347
333
            logger.warning(u"Checker for %(name)s crashed?",
348
334
                           vars(self))
349
335
            if self.use_dbus:
350
336
                # Emit D-Bus signal
351
 
                self.CheckerCompleted(dbus.Int16(-1),
352
 
                                      dbus.Int64(condition),
 
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),
353
347
                                      dbus.String(command))
354
348
    
355
 
    def checked_ok(self):
 
349
    def bump_timeout(self):
356
350
        """Bump up the timeout for this client.
357
351
        This should only be called when the client has been seen,
358
352
        alive and well.
381
375
        # checkers alone, the checker would have to take more time
382
376
        # than 'timeout' for the client to be declared invalid, which
383
377
        # 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
378
        if self.checker is None:
395
379
            try:
396
380
                # In case checker_command has exactly one % operator
406
390
                    logger.error(u'Could not format string "%s":'
407
391
                                 u' %s', self.checker_command, error)
408
392
                    return True # Try again later
409
 
                self.current_checker_command = command
410
393
            try:
411
394
                logger.info(u"Starting checker %r for %s",
412
395
                            command, self.name)
427
410
                                             (self.checker.pid,
428
411
                                              self.checker_callback,
429
412
                                              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
413
            except OSError, error:
437
414
                logger.error(u"Failed to start subprocess: %s",
438
415
                             error)
471
448
            return now < (self.last_checked_ok + self.timeout)
472
449
    
473
450
    ## D-Bus methods & signals
474
 
    _interface = u"se.bsnet.fukt.Mandos.Client"
 
451
    _interface = u"org.mandos_system.Mandos.Client"
475
452
    
476
 
    # CheckedOK - method
477
 
    CheckedOK = dbus.service.method(_interface)(checked_ok)
478
 
    CheckedOK.__name__ = "CheckedOK"
 
453
    # BumpTimeout - method
 
454
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
 
455
    BumpTimeout.__name__ = "BumpTimeout"
479
456
    
480
457
    # CheckerCompleted - signal
481
 
    @dbus.service.signal(_interface, signature="nxs")
482
 
    def CheckerCompleted(self, exitcode, waitstatus, command):
 
458
    @dbus.service.signal(_interface, signature="bqs")
 
459
    def CheckerCompleted(self, success, condition, command):
483
460
        "D-Bus signal"
484
461
        pass
485
462
    
526
503
                dbus.String("checker_running"):
527
504
                    dbus.Boolean(self.checker is not None,
528
505
                                 variant_level=1),
529
 
                dbus.String("object_path"):
530
 
                    dbus.ObjectPath(self.dbus_object_path,
531
 
                                    variant_level=1)
532
506
                }, signature="sv")
533
507
    
534
508
    # IsStillValid - method
617
591
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
618
592
        # ...do the normal thing
619
593
        return session.peer_certificate
620
 
    list_size = ctypes.c_uint(1)
 
594
    list_size = ctypes.c_uint()
621
595
    cert_list = (gnutls.library.functions
622
596
                 .gnutls_certificate_get_peers
623
597
                 (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
598
    if list_size.value == 0:
628
599
        return None
629
600
    cert = cert_list[0]
698
669
        # using OpenPGP certificates.
699
670
        
700
671
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
701
 
        #                     "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
702
 
        #                     "+DHE-DSS"))
 
672
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
673
        #                "+DHE-DSS"))
703
674
        # Use a fallback default, since this MUST be set.
704
675
        priority = self.server.settings.get("priority", "NORMAL")
705
676
        (gnutls.library.functions
713
684
            # Do not run session.bye() here: the session is not
714
685
            # established.  Just abandon the request.
715
686
            return
716
 
        logger.debug(u"Handshake succeeded")
717
687
        try:
718
688
            fpr = fingerprint(peer_certificate(session))
719
689
        except (TypeError, gnutls.errors.GNUTLSError), error:
721
691
            session.bye()
722
692
            return
723
693
        logger.debug(u"Fingerprint: %s", fpr)
724
 
        
725
694
        for c in self.server.clients:
726
695
            if c.fingerprint == fpr:
727
696
                client = c
740
709
            session.bye()
741
710
            return
742
711
        ## This won't work here, since we're in a fork.
743
 
        # client.checked_ok()
 
712
        # client.bump_timeout()
744
713
        sent_size = 0
745
714
        while sent_size < len(client.secret):
746
715
            sent = session.send(client.secret[sent_size:])
753
722
 
754
723
class IPv6_TCPServer(SocketServer.ForkingMixIn,
755
724
                     SocketServer.TCPServer, object):
756
 
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
 
725
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
757
726
    Attributes:
758
727
        settings:       Server settings
759
728
        clients:        Set() of Client objects
767
736
        if "clients" in kwargs:
768
737
            self.clients = kwargs["clients"]
769
738
            del kwargs["clients"]
770
 
        if "use_ipv6" in kwargs:
771
 
            if not kwargs["use_ipv6"]:
772
 
                self.address_family = socket.AF_INET
773
 
            del kwargs["use_ipv6"]
774
739
        self.enabled = False
775
740
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
776
741
    def server_bind(self):
790
755
                                 u" bind to interface %s",
791
756
                                 self.settings["interface"])
792
757
                else:
793
 
                    raise
 
758
                    raise error
794
759
        # Only bind(2) the socket if we really need to.
795
760
        if self.server_address[0] or self.server_address[1]:
796
761
            if not self.server_address[0]:
797
 
                if self.address_family == socket.AF_INET6:
798
 
                    any_address = "::" # in6addr_any
799
 
                else:
800
 
                    any_address = socket.INADDR_ANY
801
 
                self.server_address = (any_address,
 
762
                in6addr_any = "::"
 
763
                self.server_address = (in6addr_any,
802
764
                                       self.server_address[1])
803
765
            elif not self.server_address[1]:
804
766
                self.server_address = (self.server_address[0],
820
782
 
821
783
def string_to_delta(interval):
822
784
    """Parse a string and return a datetime.timedelta
823
 
    
 
785
 
824
786
    >>> string_to_delta('7d')
825
787
    datetime.timedelta(7)
826
788
    >>> string_to_delta('60s')
951
913
                      dest="use_dbus",
952
914
                      help="Do not provide D-Bus system bus"
953
915
                      " interface")
954
 
    parser.add_option("--no-ipv6", action="store_false",
955
 
                      dest="use_ipv6", help="Do not use IPv6")
956
916
    options = parser.parse_args()[0]
957
917
    
958
918
    if options.check:
969
929
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
970
930
                        "servicename": "Mandos",
971
931
                        "use_dbus": "True",
972
 
                        "use_ipv6": "True",
973
932
                        }
974
933
    
975
934
    # Parse config file for server-global settings
978
937
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
979
938
    # Convert the SafeConfigParser object to a dict
980
939
    server_settings = server_config.defaults()
981
 
    # Use the appropriate methods on the non-string config options
982
 
    server_settings["debug"] = server_config.getboolean("DEFAULT",
983
 
                                                        "debug")
984
 
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
985
 
                                                           "use_dbus")
986
 
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
987
 
                                                           "use_ipv6")
988
 
    if server_settings["port"]:
989
 
        server_settings["port"] = server_config.getint("DEFAULT",
990
 
                                                       "port")
 
940
    # Use getboolean on the boolean config options
 
941
    server_settings["debug"] = (server_config.getboolean
 
942
                                ("DEFAULT", "debug"))
 
943
    server_settings["use_dbus"] = (server_config.getboolean
 
944
                                   ("DEFAULT", "use_dbus"))
991
945
    del server_config
992
946
    
993
947
    # Override the settings from the config file with command line
994
948
    # options, if set.
995
949
    for option in ("interface", "address", "port", "debug",
996
950
                   "priority", "servicename", "configdir",
997
 
                   "use_dbus", "use_ipv6"):
 
951
                   "use_dbus"):
998
952
        value = getattr(options, option)
999
953
        if value is not None:
1000
954
            server_settings[option] = value
1004
958
    # For convenience
1005
959
    debug = server_settings["debug"]
1006
960
    use_dbus = server_settings["use_dbus"]
1007
 
    use_ipv6 = server_settings["use_ipv6"]
1008
961
    
1009
962
    if not debug:
1010
963
        syslogger.setLevel(logging.WARNING)
1031
984
                                 server_settings["port"]),
1032
985
                                TCP_handler,
1033
986
                                settings=server_settings,
1034
 
                                clients=clients, use_ipv6=use_ipv6)
 
987
                                clients=clients)
1035
988
    pidfilename = "/var/run/mandos.pid"
1036
989
    try:
1037
990
        pidfile = open(pidfilename, "w")
1038
 
    except IOError:
 
991
    except IOError, error:
1039
992
        logger.error("Could not open file %r", pidfilename)
1040
993
    
1041
994
    try:
1053
1006
                uid = 65534
1054
1007
                gid = 65534
1055
1008
    try:
 
1009
        os.setuid(uid)
1056
1010
        os.setgid(gid)
1057
 
        os.setuid(uid)
1058
1011
    except OSError, error:
1059
1012
        if error[0] != errno.EPERM:
1060
1013
            raise error
1061
1014
    
1062
 
    # Enable all possible GnuTLS debugging
1063
 
    if debug:
1064
 
        # "Use a log level over 10 to enable all debugging options."
1065
 
        # - GnuTLS manual
1066
 
        gnutls.library.functions.gnutls_global_set_log_level(11)
1067
 
        
1068
 
        @gnutls.library.types.gnutls_log_func
1069
 
        def debug_gnutls(level, string):
1070
 
            logger.debug("GnuTLS: %s", string[:-1])
1071
 
        
1072
 
        (gnutls.library.functions
1073
 
         .gnutls_global_set_log_function(debug_gnutls))
1074
 
    
1075
1015
    global service
1076
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1077
1016
    service = AvahiService(name = server_settings["servicename"],
1078
 
                           servicetype = "_mandos._tcp",
1079
 
                           protocol = protocol)
 
1017
                           servicetype = "_mandos._tcp", )
1080
1018
    if server_settings["interface"]:
1081
1019
        service.interface = (if_nametoindex
1082
1020
                             (server_settings["interface"]))
1093
1031
                            avahi.DBUS_INTERFACE_SERVER)
1094
1032
    # End of Avahi example code
1095
1033
    if use_dbus:
1096
 
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
 
1034
        bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
 
1035
                                        bus)
1097
1036
    
1098
1037
    clients.update(Set(Client(name = section,
1099
1038
                              config
1153
1092
        class MandosServer(dbus.service.Object):
1154
1093
            """A D-Bus proxy object"""
1155
1094
            def __init__(self):
1156
 
                dbus.service.Object.__init__(self, bus, "/")
1157
 
            _interface = u"se.bsnet.fukt.Mandos"
1158
 
            
 
1095
                dbus.service.Object.__init__(self, bus,
 
1096
                                             "/Mandos")
 
1097
            _interface = u"org.mandos_system.Mandos"
 
1098
 
1159
1099
            @dbus.service.signal(_interface, signature="oa{sv}")
1160
1100
            def ClientAdded(self, objpath, properties):
1161
1101
                "D-Bus signal"
1162
1102
                pass
1163
 
            
1164
 
            @dbus.service.signal(_interface, signature="os")
1165
 
            def ClientRemoved(self, objpath, name):
 
1103
 
 
1104
            @dbus.service.signal(_interface, signature="o")
 
1105
            def ClientRemoved(self, objpath):
1166
1106
                "D-Bus signal"
1167
1107
                pass
1168
 
            
 
1108
 
1169
1109
            @dbus.service.method(_interface, out_signature="ao")
1170
1110
            def GetAllClients(self):
1171
 
                "D-Bus method"
1172
1111
                return dbus.Array(c.dbus_object_path for c in clients)
1173
 
            
 
1112
 
1174
1113
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1175
1114
            def GetAllClientsWithProperties(self):
1176
 
                "D-Bus method"
1177
1115
                return dbus.Dictionary(
1178
1116
                    ((c.dbus_object_path, c.GetAllProperties())
1179
1117
                     for c in clients),
1180
1118
                    signature="oa{sv}")
1181
 
            
 
1119
 
1182
1120
            @dbus.service.method(_interface, in_signature="o")
1183
1121
            def RemoveClient(self, object_path):
1184
 
                "D-Bus method"
1185
1122
                for c in clients:
1186
1123
                    if c.dbus_object_path == object_path:
1187
1124
                        clients.remove(c)
1189
1126
                        c.use_dbus = False
1190
1127
                        c.disable()
1191
1128
                        # Emit D-Bus signal
1192
 
                        self.ClientRemoved(object_path, c.name)
 
1129
                        self.ClientRemoved(object_path)
1193
1130
                        return
1194
1131
                raise KeyError
1195
 
            
 
1132
            @dbus.service.method(_interface)
 
1133
            def Quit(self):
 
1134
                main_loop.quit()
 
1135
 
1196
1136
            del _interface
1197
 
        
 
1137
    
1198
1138
        mandos_server = MandosServer()
1199
1139
    
1200
1140
    for client in clients:
1209
1149
    
1210
1150
    # Find out what port we got
1211
1151
    service.port = tcp_server.socket.getsockname()[1]
1212
 
    if use_ipv6:
1213
 
        logger.info(u"Now listening on address %r, port %d,"
1214
 
                    " flowinfo %d, scope_id %d"
1215
 
                    % tcp_server.socket.getsockname())
1216
 
    else:                       # IPv4
1217
 
        logger.info(u"Now listening on address %r, port %d"
1218
 
                    % tcp_server.socket.getsockname())
 
1152
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
1153
                u" scope_id %d" % tcp_server.socket.getsockname())
1219
1154
    
1220
1155
    #service.interface = tcp_server.socket.getsockname()[3]
1221
1156
    
1241
1176
        sys.exit(1)
1242
1177
    except KeyboardInterrupt:
1243
1178
        if debug:
1244
 
            print >> sys.stderr
1245
 
        logger.debug("Server received KeyboardInterrupt")
1246
 
    logger.debug("Server exiting")
 
1179
            print
1247
1180
 
1248
1181
if __name__ == '__main__':
1249
1182
    main()