/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-11-09 07:35:16 UTC
  • Revision ID: teddy@fukt.bsnet.se-20091109073516-v1vem352uz0vuwrd
* dbus-mandos.conf: New; to be copied to
                    "/etc/dbus-1/system.d/mandos.conf".

Show diffs side-by-side

added added

removed removed

Lines of Context:
55
55
import logging
56
56
import logging.handlers
57
57
import pwd
58
 
import contextlib
 
58
from contextlib import closing
59
59
import struct
60
60
import fcntl
61
61
import functools
62
 
import cPickle as pickle
63
 
import select
64
62
 
65
63
import dbus
66
64
import dbus.service
193
191
        self.group.Commit()
194
192
    def entry_group_state_changed(self, state, error):
195
193
        """Derived from the Avahi example code"""
196
 
        logger.debug(u"Avahi entry group state change: %i", state)
 
194
        logger.debug(u"Avahi state change: %i", state)
197
195
        
198
196
        if state == avahi.ENTRY_GROUP_ESTABLISHED:
199
197
            logger.debug(u"Zeroconf service established.")
212
210
            self.group = None
213
211
    def server_state_changed(self, state):
214
212
        """Derived from the Avahi example code"""
215
 
        logger.debug(u"Avahi server state change: %i", state)
216
213
        if state == avahi.SERVER_COLLISION:
217
214
            logger.error(u"Zeroconf server name collision")
218
215
            self.remove()
245
242
    enabled:    bool()
246
243
    last_checked_ok: datetime.datetime(); (UTC) or None
247
244
    timeout:    datetime.timedelta(); How long from last_checked_ok
248
 
                                      until this client is disabled
 
245
                                      until this client is invalid
249
246
    interval:   datetime.timedelta(); How often to start a new checker
250
247
    disable_hook:  If set, called by disable() as disable_hook(self)
251
248
    checker:    subprocess.Popen(); a running checker process used
293
290
        if u"secret" in config:
294
291
            self.secret = config[u"secret"].decode(u"base64")
295
292
        elif u"secfile" in config:
296
 
            with open(os.path.expanduser(os.path.expandvars
297
 
                                         (config[u"secfile"])),
298
 
                      "rb") as secfile:
 
293
            with closing(open(os.path.expanduser
 
294
                              (os.path.expandvars
 
295
                               (config[u"secfile"])),
 
296
                              "rb")) as secfile:
299
297
                self.secret = secfile.read()
300
298
        else:
301
299
            raise TypeError(u"No secret or secfile for client %s"
398
396
        # client would inevitably timeout, since no checker would get
399
397
        # a chance to run to completion.  If we instead leave running
400
398
        # checkers alone, the checker would have to take more time
401
 
        # than 'timeout' for the client to be disabled, which is as it
402
 
        # should be.
 
399
        # than 'timeout' for the client to be declared invalid, which
 
400
        # is as it should be.
403
401
        
404
402
        # If a checker exists, make sure it is not a zombie
405
403
        try:
477
475
            if error.errno != errno.ESRCH: # No such process
478
476
                raise
479
477
        self.checker = None
 
478
    
 
479
    def still_valid(self):
 
480
        """Has the timeout not yet passed for this client?"""
 
481
        if not getattr(self, u"enabled", False):
 
482
            return False
 
483
        now = datetime.datetime.utcnow()
 
484
        if self.last_checked_ok is None:
 
485
            return now < (self.created + self.timeout)
 
486
        else:
 
487
            return now < (self.last_checked_ok + self.timeout)
480
488
 
481
489
 
482
490
def dbus_service_property(dbus_interface, signature=u"v",
491
499
    dbus.service.method, except there is only "signature", since the
492
500
    type from Get() and the type sent to Set() is the same.
493
501
    """
494
 
    # Encoding deeply encoded byte arrays is not supported yet by the
495
 
    # "Set" method, so we fail early here:
496
 
    if byte_arrays and signature != u"ay":
497
 
        raise ValueError(u"Byte arrays not supported for non-'ay'"
498
 
                         u" signature %r" % signature)
499
502
    def decorator(func):
500
503
        func._dbus_is_property = True
501
504
        func._dbus_interface = dbus_interface
587
590
        if prop._dbus_access == u"read":
588
591
            raise DBusPropertyAccessException(property_name)
589
592
        if prop._dbus_get_args_options[u"byte_arrays"]:
590
 
            # The byte_arrays option is not supported yet on
591
 
            # signatures other than "ay".
592
 
            if prop._dbus_signature != u"ay":
593
 
                raise ValueError
594
593
            value = dbus.ByteArray(''.join(unichr(byte)
595
594
                                           for byte in value))
596
595
        prop(value)
782
781
                                 dbus.Boolean(False, variant_level=1))
783
782
        return r
784
783
    
785
 
    ## D-Bus methods, signals & properties
 
784
    ## D-Bus methods & signals
786
785
    _interface = u"se.bsnet.fukt.Mandos.Client"
787
786
    
788
 
    ## Signals
 
787
    # CheckedOK - method
 
788
    @dbus.service.method(_interface)
 
789
    def CheckedOK(self):
 
790
        return self.checked_ok()
789
791
    
790
792
    # CheckerCompleted - signal
791
793
    @dbus.service.signal(_interface, signature=u"nxs")
817
819
        "D-Bus signal"
818
820
        pass
819
821
    
820
 
    ## Methods
821
 
    
822
 
    # CheckedOK - method
823
 
    @dbus.service.method(_interface)
824
 
    def CheckedOK(self):
825
 
        return self.checked_ok()
826
 
    
827
822
    # Enable - method
828
823
    @dbus.service.method(_interface)
829
824
    def Enable(self):
847
842
    def StopChecker(self):
848
843
        self.stop_checker()
849
844
    
850
 
    ## Properties
851
 
    
852
845
    # name - property
853
846
    @dbus_service_property(_interface, signature=u"s", access=u"read")
854
847
    def name_dbus_property(self):
997
990
    def handle(self):
998
991
        logger.info(u"TCP connection from: %s",
999
992
                    unicode(self.client_address))
1000
 
        logger.debug(u"IPC Pipe FD: %d",
1001
 
                     self.server.child_pipe[1].fileno())
 
993
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
1002
994
        # Open IPC pipe to parent process
1003
 
        with contextlib.nested(self.server.child_pipe[1],
1004
 
                               self.server.parent_pipe[0]
1005
 
                               ) as (ipc, ipc_return):
 
995
        with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
1006
996
            session = (gnutls.connection
1007
997
                       .ClientSession(self.request,
1008
998
                                      gnutls.connection
1009
999
                                      .X509Credentials()))
1010
1000
            
 
1001
            line = self.request.makefile().readline()
 
1002
            logger.debug(u"Protocol version: %r", line)
 
1003
            try:
 
1004
                if int(line.strip().split()[0]) > 1:
 
1005
                    raise RuntimeError
 
1006
            except (ValueError, IndexError, RuntimeError), error:
 
1007
                logger.error(u"Unknown protocol version: %s", error)
 
1008
                return
 
1009
            
1011
1010
            # Note: gnutls.connection.X509Credentials is really a
1012
1011
            # generic GnuTLS certificate credentials object so long as
1013
1012
            # no X.509 keys are added to it.  Therefore, we can use it
1025
1024
             .gnutls_priority_set_direct(session._c_object,
1026
1025
                                         priority, None))
1027
1026
            
1028
 
            # Start communication using the Mandos protocol
1029
 
            # Get protocol number
1030
 
            line = self.request.makefile().readline()
1031
 
            logger.debug(u"Protocol version: %r", line)
1032
 
            try:
1033
 
                if int(line.strip().split()[0]) > 1:
1034
 
                    raise RuntimeError
1035
 
            except (ValueError, IndexError, RuntimeError), error:
1036
 
                logger.error(u"Unknown protocol version: %s", error)
1037
 
                return
1038
 
            
1039
 
            # Start GnuTLS connection
1040
1027
            try:
1041
1028
                session.handshake()
1042
1029
            except gnutls.errors.GNUTLSError, error:
1046
1033
                return
1047
1034
            logger.debug(u"Handshake succeeded")
1048
1035
            try:
1049
 
                try:
1050
 
                    fpr = self.fingerprint(self.peer_certificate
1051
 
                                           (session))
1052
 
                except (TypeError, gnutls.errors.GNUTLSError), error:
1053
 
                    logger.warning(u"Bad certificate: %s", error)
1054
 
                    return
1055
 
                logger.debug(u"Fingerprint: %s", fpr)
1056
 
 
1057
 
                for c in self.server.clients:
1058
 
                    if c.fingerprint == fpr:
1059
 
                        client = c
1060
 
                        break
1061
 
                else:
1062
 
                    ipc.write(u"NOTFOUND %s %s\n"
1063
 
                              % (fpr, unicode(self.client_address)))
1064
 
                    return
1065
 
                
1066
 
                class ClientProxy(object):
1067
 
                    """Client proxy object.  Not for calling methods."""
1068
 
                    def __init__(self, client):
1069
 
                        self.client = client
1070
 
                    def __getattr__(self, name):
1071
 
                        if name.startswith("ipc_"):
1072
 
                            def tempfunc():
1073
 
                                ipc.write("%s %s\n" % (name[4:].upper(),
1074
 
                                                       self.client.name))
1075
 
                            return tempfunc
1076
 
                        if not hasattr(self.client, name):
1077
 
                            raise AttributeError
1078
 
                        ipc.write(u"GETATTR %s %s\n"
1079
 
                                  % (name, self.client.fingerprint))
1080
 
                        return pickle.load(ipc_return)
1081
 
                clientproxy = ClientProxy(client)
1082
 
                # Have to check if client.enabled, since it is
1083
 
                # possible that the client was disabled since the
1084
 
                # GnuTLS session was established.
1085
 
                if not clientproxy.enabled:
1086
 
                    clientproxy.ipc_disabled()
1087
 
                    return
1088
 
                
1089
 
                clientproxy.ipc_sending()
1090
 
                sent_size = 0
1091
 
                while sent_size < len(client.secret):
1092
 
                    sent = session.send(client.secret[sent_size:])
1093
 
                    logger.debug(u"Sent: %d, remaining: %d",
1094
 
                                 sent, len(client.secret)
1095
 
                                 - (sent_size + sent))
1096
 
                    sent_size += sent
1097
 
            finally:
1098
 
                session.bye()
 
1036
                fpr = self.fingerprint(self.peer_certificate(session))
 
1037
            except (TypeError, gnutls.errors.GNUTLSError), error:
 
1038
                logger.warning(u"Bad certificate: %s", error)
 
1039
                session.bye()
 
1040
                return
 
1041
            logger.debug(u"Fingerprint: %s", fpr)
 
1042
            
 
1043
            for c in self.server.clients:
 
1044
                if c.fingerprint == fpr:
 
1045
                    client = c
 
1046
                    break
 
1047
            else:
 
1048
                ipc.write(u"NOTFOUND %s %s\n"
 
1049
                          % (fpr, unicode(self.client_address)))
 
1050
                session.bye()
 
1051
                return
 
1052
            # Have to check if client.still_valid(), since it is
 
1053
            # possible that the client timed out while establishing
 
1054
            # the GnuTLS session.
 
1055
            if not client.still_valid():
 
1056
                ipc.write(u"INVALID %s\n" % client.name)
 
1057
                session.bye()
 
1058
                return
 
1059
            ipc.write(u"SENDING %s\n" % client.name)
 
1060
            sent_size = 0
 
1061
            while sent_size < len(client.secret):
 
1062
                sent = session.send(client.secret[sent_size:])
 
1063
                logger.debug(u"Sent: %d, remaining: %d",
 
1064
                             sent, len(client.secret)
 
1065
                             - (sent_size + sent))
 
1066
                sent_size += sent
 
1067
            session.bye()
1099
1068
    
1100
1069
    @staticmethod
1101
1070
    def peer_certificate(session):
1161
1130
        return hex_fpr
1162
1131
 
1163
1132
 
1164
 
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
1165
 
    """Like socketserver.ForkingMixIn, but also pass a pipe pair."""
 
1133
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
 
1134
    """Like socketserver.ForkingMixIn, but also pass a pipe."""
1166
1135
    def process_request(self, request, client_address):
1167
1136
        """Overrides and wraps the original process_request().
1168
1137
        
1169
1138
        This function creates a new pipe in self.pipe
1170
1139
        """
1171
 
        # Child writes to child_pipe
1172
 
        self.child_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1173
 
        # Parent writes to parent_pipe
1174
 
        self.parent_pipe = map(os.fdopen, os.pipe(), u"rw", (1, 0))
1175
 
        super(ForkingMixInWithPipes,
 
1140
        self.pipe = os.pipe()
 
1141
        super(ForkingMixInWithPipe,
1176
1142
              self).process_request(request, client_address)
1177
 
        # Close unused ends for parent
1178
 
        self.parent_pipe[0].close() # close read end
1179
 
        self.child_pipe[1].close()  # close write end
1180
 
        self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
1181
 
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
 
1143
        os.close(self.pipe[1])  # close write end
 
1144
        self.add_pipe(self.pipe[0])
 
1145
    def add_pipe(self, pipe):
1182
1146
        """Dummy function; override as necessary"""
1183
 
        child_pipe_fd.close()
1184
 
        parent_pipe_fd.close()
1185
 
 
1186
 
 
1187
 
class IPv6_TCPServer(ForkingMixInWithPipes,
 
1147
        os.close(pipe)
 
1148
 
 
1149
 
 
1150
class IPv6_TCPServer(ForkingMixInWithPipe,
1188
1151
                     socketserver.TCPServer, object):
1189
1152
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
1190
1153
    
1275
1238
            return socketserver.TCPServer.server_activate(self)
1276
1239
    def enable(self):
1277
1240
        self.enabled = True
1278
 
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
 
1241
    def add_pipe(self, pipe):
1279
1242
        # Call "handle_ipc" for both data and EOF events
1280
 
        gobject.io_add_watch(child_pipe_fd.fileno(),
1281
 
                             gobject.IO_IN | gobject.IO_HUP,
1282
 
                             functools.partial(self.handle_ipc,
1283
 
                                               reply = parent_pipe_fd,
1284
 
                                               sender= child_pipe_fd))
1285
 
    def handle_ipc(self, source, condition, reply=None, sender=None):
 
1243
        gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
 
1244
                             self.handle_ipc)
 
1245
    def handle_ipc(self, source, condition, file_objects={}):
1286
1246
        condition_names = {
1287
1247
            gobject.IO_IN: u"IN",   # There is data to read.
1288
1248
            gobject.IO_OUT: u"OUT", # Data can be written (without
1300
1260
        logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1301
1261
                     conditions_string)
1302
1262
        
 
1263
        # Turn the pipe file descriptor into a Python file object
 
1264
        if source not in file_objects:
 
1265
            file_objects[source] = os.fdopen(source, u"r", 1)
 
1266
        
1303
1267
        # Read a line from the file object
1304
 
        cmdline = sender.readline()
 
1268
        cmdline = file_objects[source].readline()
1305
1269
        if not cmdline:             # Empty line means end of file
1306
 
            # close the IPC pipes
1307
 
            sender.close()
1308
 
            reply.close()
 
1270
            # close the IPC pipe
 
1271
            file_objects[source].close()
 
1272
            del file_objects[source]
1309
1273
            
1310
1274
            # Stop calling this function
1311
1275
            return False
1316
1280
        cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1317
1281
        
1318
1282
        if cmd == u"NOTFOUND":
1319
 
            fpr, address = args.split(None, 1)
1320
 
            logger.warning(u"Client not found for fingerprint: %s, ad"
1321
 
                           u"dress: %s", fpr, address)
 
1283
            logger.warning(u"Client not found for fingerprint: %s",
 
1284
                           args)
1322
1285
            if self.use_dbus:
1323
1286
                # Emit D-Bus signal
1324
 
                mandos_dbus_service.ClientNotFound(fpr, address)
1325
 
        elif cmd == u"DISABLED":
 
1287
                mandos_dbus_service.ClientNotFound(args)
 
1288
        elif cmd == u"INVALID":
1326
1289
            for client in self.clients:
1327
1290
                if client.name == args:
1328
 
                    logger.warning(u"Client %s is disabled", args)
 
1291
                    logger.warning(u"Client %s is invalid", args)
1329
1292
                    if self.use_dbus:
1330
1293
                        # Emit D-Bus signal
1331
1294
                        client.Rejected()
1332
1295
                    break
1333
1296
            else:
1334
 
                logger.error(u"Unknown client %s is disabled", args)
 
1297
                logger.error(u"Unknown client %s is invalid", args)
1335
1298
        elif cmd == u"SENDING":
1336
1299
            for client in self.clients:
1337
1300
                if client.name == args:
1344
1307
            else:
1345
1308
                logger.error(u"Sending secret to unknown client %s",
1346
1309
                             args)
1347
 
        elif cmd == u"GETATTR":
1348
 
            attr_name, fpr = args.split(None, 1)
1349
 
            for client in self.clients:
1350
 
                if client.fingerprint == fpr:
1351
 
                    attr_value = getattr(client, attr_name, None)
1352
 
                    logger.debug("IPC reply: %r", attr_value)
1353
 
                    pickle.dump(attr_value, reply)
1354
 
                    break
1355
 
            else:
1356
 
                logger.error(u"Client %s on address %s requesting "
1357
 
                             u"attribute %s not found", fpr, address,
1358
 
                             attr_name)
1359
 
                pickle.dump(None, reply)
1360
1310
        else:
1361
1311
            logger.error(u"Unknown IPC command: %r", cmdline)
1362
1312
        
1417
1367
        def if_nametoindex(interface):
1418
1368
            "Get an interface index the hard way, i.e. using fcntl()"
1419
1369
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
1420
 
            with contextlib.closing(socket.socket()) as s:
 
1370
            with closing(socket.socket()) as s:
1421
1371
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1422
1372
                                    struct.pack(str(u"16s16x"),
1423
1373
                                                interface))
1656
1606
        daemon()
1657
1607
    
1658
1608
    try:
1659
 
        with pidfile:
 
1609
        with closing(pidfile):
1660
1610
            pid = os.getpid()
1661
1611
            pidfile.write(str(pid) + "\n")
1662
1612
        del pidfile
1680
1630
                dbus.service.Object.__init__(self, bus, u"/")
1681
1631
            _interface = u"se.bsnet.fukt.Mandos"
1682
1632
            
1683
 
            @dbus.service.signal(_interface, signature=u"o")
1684
 
            def ClientAdded(self, objpath):
 
1633
            @dbus.service.signal(_interface, signature=u"oa{sv}")
 
1634
            def ClientAdded(self, objpath, properties):
1685
1635
                "D-Bus signal"
1686
1636
                pass
1687
1637
            
1688
 
            @dbus.service.signal(_interface, signature=u"ss")
1689
 
            def ClientNotFound(self, fingerprint, address):
 
1638
            @dbus.service.signal(_interface, signature=u"s")
 
1639
            def ClientNotFound(self, fingerprint):
1690
1640
                "D-Bus signal"
1691
1641
                pass
1692
1642
            
1749
1699
    for client in tcp_server.clients:
1750
1700
        if use_dbus:
1751
1701
            # Emit D-Bus signal
1752
 
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
 
1702
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
 
1703
                                            client.GetAll(u""))
1753
1704
        client.enable()
1754
1705
    
1755
1706
    tcp_server.enable()