/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-13 05:38:21 UTC
  • Revision ID: teddy@fukt.bsnet.se-20090213053821-03e696gckk4nbjps
Support not using IPv6 in server:

* mandos (AvahiService.__init__): Take new "protocol" parameter.  All
                                  callers changed.
  (IPv6_TCPServer.__init__): Take new "use_ipv6" parameter.  All
                             callers changed.
  (IPv6_TCPServer.server_bind): Create IPv4 socket if not using IPv6.
  (main): New "--no-ipv6" command line option.  New "use_ipv6" config
          option.
* mandos-options.xml ([@id="address"]): Document conditional IPv4
                                        address support.
  ([@id="ipv6"]): New paragraph.
* mandos.conf (use_ipv6): New config option.
* mandos.conf.xml (OPTIONS): Document new "use_dbus" option.
  (EXAMPLE): Changed to use IPv6 link-local address.  Added "use_ipv6"
             option.
* mandos.xml (SYNOPSIS): New "--no-ipv6" option.
  (OPTIONS): Document new "--no-ipv6" option.

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.2"
 
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: %(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
225
229
        if config is None:
226
230
            config = {}
227
231
        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)
 
232
        self.use_dbus = False   # During __init__
235
233
        # Uppercase and remove spaces from fingerprint for later
236
234
        # comparison purposes with return value from the fingerprint()
237
235
        # function
261
259
        self.disable_initiator_tag = None
262
260
        self.checker_callback_tag = None
263
261
        self.checker_command = config["checker"]
 
262
        self.last_connect = None
 
263
        # Only now, when this client is initialized, can it show up on
 
264
        # the D-Bus
 
265
        self.use_dbus = use_dbus
 
266
        if self.use_dbus:
 
267
            self.dbus_object_path = (dbus.ObjectPath
 
268
                                     ("/clients/"
 
269
                                      + self.name.replace(".", "_")))
 
270
            dbus.service.Object.__init__(self, bus,
 
271
                                         self.dbus_object_path)
264
272
    
265
273
    def enable(self):
266
274
        """Start this client's checker and timeout hooks"""
319
327
            # Emit D-Bus signal
320
328
            self.PropertyChanged(dbus.String(u"checker_running"),
321
329
                                 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))
 
330
        if os.WIFEXITED(condition):
 
331
            exitstatus = os.WEXITSTATUS(condition)
 
332
            if exitstatus == 0:
 
333
                logger.info(u"Checker for %(name)s succeeded",
 
334
                            vars(self))
 
335
                self.checked_ok()
 
336
            else:
 
337
                logger.info(u"Checker for %(name)s failed",
 
338
                            vars(self))
326
339
            if self.use_dbus:
327
340
                # Emit D-Bus signal
328
 
                self.CheckerCompleted(dbus.Boolean(True),
329
 
                                      dbus.UInt16(condition),
 
341
                self.CheckerCompleted(dbus.Int16(exitstatus),
 
342
                                      dbus.Int64(condition),
330
343
                                      dbus.String(command))
331
 
            self.bump_timeout()
332
 
        elif not os.WIFEXITED(condition):
 
344
        else:
333
345
            logger.warning(u"Checker for %(name)s crashed?",
334
346
                           vars(self))
335
347
            if self.use_dbus:
336
348
                # 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),
 
349
                self.CheckerCompleted(dbus.Int16(-1),
 
350
                                      dbus.Int64(condition),
347
351
                                      dbus.String(command))
348
352
    
349
 
    def bump_timeout(self):
 
353
    def checked_ok(self):
350
354
        """Bump up the timeout for this client.
351
355
        This should only be called when the client has been seen,
352
356
        alive and well.
410
414
                                             (self.checker.pid,
411
415
                                              self.checker_callback,
412
416
                                              data=command))
 
417
                # The checker may have completed before the gobject
 
418
                # watch was added.  Check for this.
 
419
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
420
                if pid:
 
421
                    gobject.source_remove(self.checker_callback_tag)
 
422
                    self.checker_callback(pid, status, command)
413
423
            except OSError, error:
414
424
                logger.error(u"Failed to start subprocess: %s",
415
425
                             error)
448
458
            return now < (self.last_checked_ok + self.timeout)
449
459
    
450
460
    ## D-Bus methods & signals
451
 
    _interface = u"org.mandos_system.Mandos.Client"
 
461
    _interface = u"se.bsnet.fukt.Mandos.Client"
452
462
    
453
 
    # BumpTimeout - method
454
 
    BumpTimeout = dbus.service.method(_interface)(bump_timeout)
455
 
    BumpTimeout.__name__ = "BumpTimeout"
 
463
    # CheckedOK - method
 
464
    CheckedOK = dbus.service.method(_interface)(checked_ok)
 
465
    CheckedOK.__name__ = "CheckedOK"
456
466
    
457
467
    # CheckerCompleted - signal
458
 
    @dbus.service.signal(_interface, signature="bqs")
459
 
    def CheckerCompleted(self, success, condition, command):
 
468
    @dbus.service.signal(_interface, signature="nxs")
 
469
    def CheckerCompleted(self, exitcode, waitstatus, command):
460
470
        "D-Bus signal"
461
471
        pass
462
472
    
503
513
                dbus.String("checker_running"):
504
514
                    dbus.Boolean(self.checker is not None,
505
515
                                 variant_level=1),
 
516
                dbus.String("object_path"):
 
517
                    dbus.ObjectPath(self.dbus_object_path,
 
518
                                    variant_level=1)
506
519
                }, signature="sv")
507
520
    
508
521
    # IsStillValid - method
591
604
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
592
605
        # ...do the normal thing
593
606
        return session.peer_certificate
594
 
    list_size = ctypes.c_uint()
 
607
    list_size = ctypes.c_uint(1)
595
608
    cert_list = (gnutls.library.functions
596
609
                 .gnutls_certificate_get_peers
597
610
                 (session._c_object, ctypes.byref(list_size)))
 
611
    if not bool(cert_list) and list_size.value != 0:
 
612
        raise gnutls.errors.GNUTLSError("error getting peer"
 
613
                                        " certificate")
598
614
    if list_size.value == 0:
599
615
        return None
600
616
    cert = cert_list[0]
669
685
        # using OpenPGP certificates.
670
686
        
671
687
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
672
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
673
 
        #                "+DHE-DSS"))
 
688
        #                     "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
689
        #                     "+DHE-DSS"))
674
690
        # Use a fallback default, since this MUST be set.
675
691
        priority = self.server.settings.get("priority", "NORMAL")
676
692
        (gnutls.library.functions
684
700
            # Do not run session.bye() here: the session is not
685
701
            # established.  Just abandon the request.
686
702
            return
 
703
        logger.debug(u"Handshake succeeded")
687
704
        try:
688
705
            fpr = fingerprint(peer_certificate(session))
689
706
        except (TypeError, gnutls.errors.GNUTLSError), error:
691
708
            session.bye()
692
709
            return
693
710
        logger.debug(u"Fingerprint: %s", fpr)
 
711
        
694
712
        for c in self.server.clients:
695
713
            if c.fingerprint == fpr:
696
714
                client = c
709
727
            session.bye()
710
728
            return
711
729
        ## This won't work here, since we're in a fork.
712
 
        # client.bump_timeout()
 
730
        # client.checked_ok()
713
731
        sent_size = 0
714
732
        while sent_size < len(client.secret):
715
733
            sent = session.send(client.secret[sent_size:])
722
740
 
723
741
class IPv6_TCPServer(SocketServer.ForkingMixIn,
724
742
                     SocketServer.TCPServer, object):
725
 
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
743
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port.
726
744
    Attributes:
727
745
        settings:       Server settings
728
746
        clients:        Set() of Client objects
736
754
        if "clients" in kwargs:
737
755
            self.clients = kwargs["clients"]
738
756
            del kwargs["clients"]
 
757
        if "use_ipv6" in kwargs:
 
758
            if not kwargs["use_ipv6"]:
 
759
                self.address_family = socket.AF_INET
 
760
            del kwargs["use_ipv6"]
739
761
        self.enabled = False
740
762
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
741
763
    def server_bind(self):
755
777
                                 u" bind to interface %s",
756
778
                                 self.settings["interface"])
757
779
                else:
758
 
                    raise error
 
780
                    raise
759
781
        # Only bind(2) the socket if we really need to.
760
782
        if self.server_address[0] or self.server_address[1]:
761
783
            if not self.server_address[0]:
762
 
                in6addr_any = "::"
763
 
                self.server_address = (in6addr_any,
 
784
                if self.address_family == socket.AF_INET6:
 
785
                    any_address = "::" # in6addr_any
 
786
                else:
 
787
                    any_address = socket.INADDR_ANY
 
788
                self.server_address = (any_address,
764
789
                                       self.server_address[1])
765
790
            elif not self.server_address[1]:
766
791
                self.server_address = (self.server_address[0],
782
807
 
783
808
def string_to_delta(interval):
784
809
    """Parse a string and return a datetime.timedelta
785
 
 
 
810
    
786
811
    >>> string_to_delta('7d')
787
812
    datetime.timedelta(7)
788
813
    >>> string_to_delta('60s')
889
914
 
890
915
 
891
916
def main():
892
 
    parser = OptionParser(version = "%%prog %s" % version)
 
917
    parser = optparse.OptionParser(version = "%%prog %s" % version)
893
918
    parser.add_option("-i", "--interface", type="string",
894
919
                      metavar="IF", help="Bind to interface IF")
895
920
    parser.add_option("-a", "--address", type="string",
913
938
                      dest="use_dbus",
914
939
                      help="Do not provide D-Bus system bus"
915
940
                      " interface")
 
941
    parser.add_option("--no-ipv6", action="store_false",
 
942
                      dest="use_ipv6", help="Do not use IPv6")
916
943
    options = parser.parse_args()[0]
917
944
    
918
945
    if options.check:
929
956
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
930
957
                        "servicename": "Mandos",
931
958
                        "use_dbus": "True",
 
959
                        "use_ipv6": "True",
932
960
                        }
933
961
    
934
962
    # Parse config file for server-global settings
937
965
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
938
966
    # Convert the SafeConfigParser object to a dict
939
967
    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"))
 
968
    # Use the appropriate methods on the non-string config options
 
969
    server_settings["debug"] = server_config.getboolean("DEFAULT",
 
970
                                                        "debug")
 
971
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
 
972
                                                           "use_dbus")
 
973
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
 
974
                                                           "use_ipv6")
 
975
    if server_settings["port"]:
 
976
        server_settings["port"] = server_config.getint("DEFAULT",
 
977
                                                       "port")
945
978
    del server_config
946
979
    
947
980
    # Override the settings from the config file with command line
948
981
    # options, if set.
949
982
    for option in ("interface", "address", "port", "debug",
950
983
                   "priority", "servicename", "configdir",
951
 
                   "use_dbus"):
 
984
                   "use_dbus", "use_ipv6"):
952
985
        value = getattr(options, option)
953
986
        if value is not None:
954
987
            server_settings[option] = value
958
991
    # For convenience
959
992
    debug = server_settings["debug"]
960
993
    use_dbus = server_settings["use_dbus"]
 
994
    use_ipv6 = server_settings["use_ipv6"]
961
995
    
962
996
    if not debug:
963
997
        syslogger.setLevel(logging.WARNING)
972
1006
    # Parse config file with clients
973
1007
    client_defaults = { "timeout": "1h",
974
1008
                        "interval": "5m",
975
 
                        "checker": "fping -q -- %(host)s",
 
1009
                        "checker": "fping -q -- %%(host)s",
976
1010
                        "host": "",
977
1011
                        }
978
1012
    client_config = ConfigParser.SafeConfigParser(client_defaults)
984
1018
                                 server_settings["port"]),
985
1019
                                TCP_handler,
986
1020
                                settings=server_settings,
987
 
                                clients=clients)
 
1021
                                clients=clients, use_ipv6=use_ipv6)
988
1022
    pidfilename = "/var/run/mandos.pid"
989
1023
    try:
990
1024
        pidfile = open(pidfilename, "w")
991
 
    except IOError, error:
 
1025
    except IOError:
992
1026
        logger.error("Could not open file %r", pidfilename)
993
1027
    
994
1028
    try:
995
1029
        uid = pwd.getpwnam("_mandos").pw_uid
 
1030
        gid = pwd.getpwnam("_mandos").pw_gid
996
1031
    except KeyError:
997
1032
        try:
998
1033
            uid = pwd.getpwnam("mandos").pw_uid
 
1034
            gid = pwd.getpwnam("mandos").pw_gid
999
1035
        except KeyError:
1000
1036
            try:
1001
1037
                uid = pwd.getpwnam("nobody").pw_uid
 
1038
                gid = pwd.getpwnam("nogroup").pw_gid
1002
1039
            except KeyError:
1003
1040
                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
1041
                gid = 65534
1014
1042
    try:
 
1043
        os.setgid(gid)
1015
1044
        os.setuid(uid)
1016
 
        os.setgid(gid)
1017
1045
    except OSError, error:
1018
1046
        if error[0] != errno.EPERM:
1019
1047
            raise error
1020
1048
    
 
1049
    # Enable all possible GnuTLS debugging
 
1050
    if debug:
 
1051
        # "Use a log level over 10 to enable all debugging options."
 
1052
        # - GnuTLS manual
 
1053
        gnutls.library.functions.gnutls_global_set_log_level(11)
 
1054
        
 
1055
        @gnutls.library.types.gnutls_log_func
 
1056
        def debug_gnutls(level, string):
 
1057
            logger.debug("GnuTLS: %s", string[:-1])
 
1058
        
 
1059
        (gnutls.library.functions
 
1060
         .gnutls_global_set_log_function(debug_gnutls))
 
1061
    
1021
1062
    global service
 
1063
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1022
1064
    service = AvahiService(name = server_settings["servicename"],
1023
 
                           servicetype = "_mandos._tcp", )
 
1065
                           servicetype = "_mandos._tcp",
 
1066
                           protocol = protocol)
1024
1067
    if server_settings["interface"]:
1025
1068
        service.interface = (if_nametoindex
1026
1069
                             (server_settings["interface"]))
1037
1080
                            avahi.DBUS_INTERFACE_SERVER)
1038
1081
    # End of Avahi example code
1039
1082
    if use_dbus:
1040
 
        bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
1041
 
                                        bus)
 
1083
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1042
1084
    
1043
1085
    clients.update(Set(Client(name = section,
1044
1086
                              config
1098
1140
        class MandosServer(dbus.service.Object):
1099
1141
            """A D-Bus proxy object"""
1100
1142
            def __init__(self):
1101
 
                dbus.service.Object.__init__(self, bus,
1102
 
                                             "/Mandos")
1103
 
            _interface = u"org.mandos_system.Mandos"
1104
 
 
 
1143
                dbus.service.Object.__init__(self, bus, "/")
 
1144
            _interface = u"se.bsnet.fukt.Mandos"
 
1145
            
1105
1146
            @dbus.service.signal(_interface, signature="oa{sv}")
1106
1147
            def ClientAdded(self, objpath, properties):
1107
1148
                "D-Bus signal"
1108
1149
                pass
1109
 
 
1110
 
            @dbus.service.signal(_interface, signature="o")
1111
 
            def ClientRemoved(self, objpath):
 
1150
            
 
1151
            @dbus.service.signal(_interface, signature="os")
 
1152
            def ClientRemoved(self, objpath, name):
1112
1153
                "D-Bus signal"
1113
1154
                pass
1114
 
 
 
1155
            
1115
1156
            @dbus.service.method(_interface, out_signature="ao")
1116
1157
            def GetAllClients(self):
 
1158
                "D-Bus method"
1117
1159
                return dbus.Array(c.dbus_object_path for c in clients)
1118
 
 
 
1160
            
1119
1161
            @dbus.service.method(_interface, out_signature="a{oa{sv}}")
1120
1162
            def GetAllClientsWithProperties(self):
 
1163
                "D-Bus method"
1121
1164
                return dbus.Dictionary(
1122
1165
                    ((c.dbus_object_path, c.GetAllProperties())
1123
1166
                     for c in clients),
1124
1167
                    signature="oa{sv}")
1125
 
 
 
1168
            
1126
1169
            @dbus.service.method(_interface, in_signature="o")
1127
1170
            def RemoveClient(self, object_path):
 
1171
                "D-Bus method"
1128
1172
                for c in clients:
1129
1173
                    if c.dbus_object_path == object_path:
1130
1174
                        clients.remove(c)
1132
1176
                        c.use_dbus = False
1133
1177
                        c.disable()
1134
1178
                        # Emit D-Bus signal
1135
 
                        self.ClientRemoved(object_path)
 
1179
                        self.ClientRemoved(object_path, c.name)
1136
1180
                        return
1137
1181
                raise KeyError
1138
 
            @dbus.service.method(_interface)
1139
 
            def Quit(self):
1140
 
                main_loop.quit()
1141
 
 
 
1182
            
1142
1183
            del _interface
1143
 
    
 
1184
        
1144
1185
        mandos_server = MandosServer()
1145
1186
    
1146
1187
    for client in clients:
1155
1196
    
1156
1197
    # Find out what port we got
1157
1198
    service.port = tcp_server.socket.getsockname()[1]
1158
 
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1159
 
                u" scope_id %d" % tcp_server.socket.getsockname())
 
1199
    if use_ipv6:
 
1200
        logger.info(u"Now listening on address %r, port %d,"
 
1201
                    " flowinfo %d, scope_id %d"
 
1202
                    % tcp_server.socket.getsockname())
 
1203
    else:                       # IPv4
 
1204
        logger.info(u"Now listening on address %r, port %d"
 
1205
                    % tcp_server.socket.getsockname())
1160
1206
    
1161
1207
    #service.interface = tcp_server.socket.getsockname()[3]
1162
1208
    
1182
1228
        sys.exit(1)
1183
1229
    except KeyboardInterrupt:
1184
1230
        if debug:
1185
 
            print
 
1231
            print >> sys.stderr
 
1232
        logger.debug("Server received KeyboardInterrupt")
 
1233
    logger.debug("Server exiting")
1186
1234
 
1187
1235
if __name__ == '__main__':
1188
1236
    main()