/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-02-23 11:52:42 UTC
  • Revision ID: teddy@fukt.bsnet.se-20090223115242-5930ewin7m4o6zpr
* README (The Plugin System): Improve wording.

Show diffs side-by-side

added added

removed removed

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