/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos

Merge in branch to interpret an empty device name to mean
"autodetect".

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
 
from contextlib import closing
 
58
import contextlib
59
59
import struct
60
60
import fcntl
61
61
import functools
 
62
import cPickle as pickle
 
63
import select
62
64
 
63
65
import dbus
64
66
import dbus.service
191
193
        self.group.Commit()
192
194
    def entry_group_state_changed(self, state, error):
193
195
        """Derived from the Avahi example code"""
194
 
        logger.debug(u"Avahi state change: %i", state)
 
196
        logger.debug(u"Avahi entry group state change: %i", state)
195
197
        
196
198
        if state == avahi.ENTRY_GROUP_ESTABLISHED:
197
199
            logger.debug(u"Zeroconf service established.")
210
212
            self.group = None
211
213
    def server_state_changed(self, state):
212
214
        """Derived from the Avahi example code"""
 
215
        logger.debug(u"Avahi server state change: %i", state)
213
216
        if state == avahi.SERVER_COLLISION:
214
217
            logger.error(u"Zeroconf server name collision")
215
218
            self.remove()
242
245
    enabled:    bool()
243
246
    last_checked_ok: datetime.datetime(); (UTC) or None
244
247
    timeout:    datetime.timedelta(); How long from last_checked_ok
245
 
                                      until this client is invalid
 
248
                                      until this client is disabled
246
249
    interval:   datetime.timedelta(); How often to start a new checker
247
250
    disable_hook:  If set, called by disable() as disable_hook(self)
248
251
    checker:    subprocess.Popen(); a running checker process used
290
293
        if u"secret" in config:
291
294
            self.secret = config[u"secret"].decode(u"base64")
292
295
        elif u"secfile" in config:
293
 
            with closing(open(os.path.expanduser
294
 
                              (os.path.expandvars
295
 
                               (config[u"secfile"])),
296
 
                              "rb")) as secfile:
 
296
            with open(os.path.expanduser(os.path.expandvars
 
297
                                         (config[u"secfile"])),
 
298
                      "rb") as secfile:
297
299
                self.secret = secfile.read()
298
300
        else:
299
301
            raise TypeError(u"No secret or secfile for client %s"
396
398
        # client would inevitably timeout, since no checker would get
397
399
        # a chance to run to completion.  If we instead leave running
398
400
        # checkers alone, the checker would have to take more time
399
 
        # than 'timeout' for the client to be declared invalid, which
400
 
        # is as it should be.
 
401
        # than 'timeout' for the client to be disabled, which is as it
 
402
        # should be.
401
403
        
402
404
        # If a checker exists, make sure it is not a zombie
403
405
        try:
475
477
            if error.errno != errno.ESRCH: # No such process
476
478
                raise
477
479
        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)
488
480
 
489
481
 
490
482
def dbus_service_property(dbus_interface, signature=u"v",
499
491
    dbus.service.method, except there is only "signature", since the
500
492
    type from Get() and the type sent to Set() is the same.
501
493
    """
 
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)
502
499
    def decorator(func):
503
500
        func._dbus_is_property = True
504
501
        func._dbus_interface = dbus_interface
590
587
        if prop._dbus_access == u"read":
591
588
            raise DBusPropertyAccessException(property_name)
592
589
        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
593
594
            value = dbus.ByteArray(''.join(unichr(byte)
594
595
                                           for byte in value))
595
596
        prop(value)
781
782
                                 dbus.Boolean(False, variant_level=1))
782
783
        return r
783
784
    
784
 
    ## D-Bus methods & signals
 
785
    ## D-Bus methods, signals & properties
785
786
    _interface = u"se.bsnet.fukt.Mandos.Client"
786
787
    
787
 
    # CheckedOK - method
788
 
    @dbus.service.method(_interface)
789
 
    def CheckedOK(self):
790
 
        return self.checked_ok()
 
788
    ## Signals
791
789
    
792
790
    # CheckerCompleted - signal
793
791
    @dbus.service.signal(_interface, signature=u"nxs")
819
817
        "D-Bus signal"
820
818
        pass
821
819
    
 
820
    ## Methods
 
821
    
 
822
    # CheckedOK - method
 
823
    @dbus.service.method(_interface)
 
824
    def CheckedOK(self):
 
825
        return self.checked_ok()
 
826
    
822
827
    # Enable - method
823
828
    @dbus.service.method(_interface)
824
829
    def Enable(self):
842
847
    def StopChecker(self):
843
848
        self.stop_checker()
844
849
    
 
850
    ## Properties
 
851
    
845
852
    # name - property
846
853
    @dbus_service_property(_interface, signature=u"s", access=u"read")
847
854
    def name_dbus_property(self):
990
997
    def handle(self):
991
998
        logger.info(u"TCP connection from: %s",
992
999
                    unicode(self.client_address))
993
 
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
 
1000
        logger.debug(u"IPC Pipe FD: %d",
 
1001
                     self.server.child_pipe[1].fileno())
994
1002
        # Open IPC pipe to parent process
995
 
        with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
 
1003
        with contextlib.nested(self.server.child_pipe[1],
 
1004
                               self.server.parent_pipe[0]
 
1005
                               ) as (ipc, ipc_return):
996
1006
            session = (gnutls.connection
997
1007
                       .ClientSession(self.request,
998
1008
                                      gnutls.connection
999
1009
                                      .X509Credentials()))
1000
1010
            
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
 
            
1010
1011
            # Note: gnutls.connection.X509Credentials is really a
1011
1012
            # generic GnuTLS certificate credentials object so long as
1012
1013
            # no X.509 keys are added to it.  Therefore, we can use it
1024
1025
             .gnutls_priority_set_direct(session._c_object,
1025
1026
                                         priority, None))
1026
1027
            
 
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
1027
1040
            try:
1028
1041
                session.handshake()
1029
1042
            except gnutls.errors.GNUTLSError, error:
1033
1046
                return
1034
1047
            logger.debug(u"Handshake succeeded")
1035
1048
            try:
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()
 
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()
1068
1099
    
1069
1100
    @staticmethod
1070
1101
    def peer_certificate(session):
1130
1161
        return hex_fpr
1131
1162
 
1132
1163
 
1133
 
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
1134
 
    """Like socketserver.ForkingMixIn, but also pass a pipe."""
 
1164
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
 
1165
    """Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1135
1166
    def process_request(self, request, client_address):
1136
1167
        """Overrides and wraps the original process_request().
1137
1168
        
1138
1169
        This function creates a new pipe in self.pipe
1139
1170
        """
1140
 
        self.pipe = os.pipe()
1141
 
        super(ForkingMixInWithPipe,
 
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,
1142
1176
              self).process_request(request, client_address)
1143
 
        os.close(self.pipe[1])  # close write end
1144
 
        self.add_pipe(self.pipe[0])
1145
 
    def add_pipe(self, pipe):
 
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):
1146
1182
        """Dummy function; override as necessary"""
1147
 
        os.close(pipe)
1148
 
 
1149
 
 
1150
 
class IPv6_TCPServer(ForkingMixInWithPipe,
 
1183
        child_pipe_fd.close()
 
1184
        parent_pipe_fd.close()
 
1185
 
 
1186
 
 
1187
class IPv6_TCPServer(ForkingMixInWithPipes,
1151
1188
                     socketserver.TCPServer, object):
1152
1189
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
1153
1190
    
1238
1275
            return socketserver.TCPServer.server_activate(self)
1239
1276
    def enable(self):
1240
1277
        self.enabled = True
1241
 
    def add_pipe(self, pipe):
 
1278
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1242
1279
        # Call "handle_ipc" for both data and EOF events
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={}):
 
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):
1246
1286
        condition_names = {
1247
1287
            gobject.IO_IN: u"IN",   # There is data to read.
1248
1288
            gobject.IO_OUT: u"OUT", # Data can be written (without
1260
1300
        logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1261
1301
                     conditions_string)
1262
1302
        
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
 
        
1267
1303
        # Read a line from the file object
1268
 
        cmdline = file_objects[source].readline()
 
1304
        cmdline = sender.readline()
1269
1305
        if not cmdline:             # Empty line means end of file
1270
 
            # close the IPC pipe
1271
 
            file_objects[source].close()
1272
 
            del file_objects[source]
 
1306
            # close the IPC pipes
 
1307
            sender.close()
 
1308
            reply.close()
1273
1309
            
1274
1310
            # Stop calling this function
1275
1311
            return False
1286
1322
            if self.use_dbus:
1287
1323
                # Emit D-Bus signal
1288
1324
                mandos_dbus_service.ClientNotFound(fpr, address)
1289
 
        elif cmd == u"INVALID":
 
1325
        elif cmd == u"DISABLED":
1290
1326
            for client in self.clients:
1291
1327
                if client.name == args:
1292
 
                    logger.warning(u"Client %s is invalid", args)
 
1328
                    logger.warning(u"Client %s is disabled", args)
1293
1329
                    if self.use_dbus:
1294
1330
                        # Emit D-Bus signal
1295
1331
                        client.Rejected()
1296
1332
                    break
1297
1333
            else:
1298
 
                logger.error(u"Unknown client %s is invalid", args)
 
1334
                logger.error(u"Unknown client %s is disabled", args)
1299
1335
        elif cmd == u"SENDING":
1300
1336
            for client in self.clients:
1301
1337
                if client.name == args:
1308
1344
            else:
1309
1345
                logger.error(u"Sending secret to unknown client %s",
1310
1346
                             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)
1311
1360
        else:
1312
1361
            logger.error(u"Unknown IPC command: %r", cmdline)
1313
1362
        
1368
1417
        def if_nametoindex(interface):
1369
1418
            "Get an interface index the hard way, i.e. using fcntl()"
1370
1419
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
1371
 
            with closing(socket.socket()) as s:
 
1420
            with contextlib.closing(socket.socket()) as s:
1372
1421
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1373
1422
                                    struct.pack(str(u"16s16x"),
1374
1423
                                                interface))
1516
1565
    tcp_server = MandosServer((server_settings[u"address"],
1517
1566
                               server_settings[u"port"]),
1518
1567
                              ClientHandler,
1519
 
                              interface=server_settings[u"interface"],
 
1568
                              interface=(server_settings[u"interface"]
 
1569
                                         or None),
1520
1570
                              use_ipv6=use_ipv6,
1521
1571
                              gnutls_priority=
1522
1572
                              server_settings[u"priority"],
1607
1657
        daemon()
1608
1658
    
1609
1659
    try:
1610
 
        with closing(pidfile):
 
1660
        with pidfile:
1611
1661
            pid = os.getpid()
1612
1662
            pidfile.write(str(pid) + "\n")
1613
1663
        del pidfile
1631
1681
                dbus.service.Object.__init__(self, bus, u"/")
1632
1682
            _interface = u"se.bsnet.fukt.Mandos"
1633
1683
            
1634
 
            @dbus.service.signal(_interface, signature=u"oa{sv}")
1635
 
            def ClientAdded(self, objpath, properties):
 
1684
            @dbus.service.signal(_interface, signature=u"o")
 
1685
            def ClientAdded(self, objpath):
1636
1686
                "D-Bus signal"
1637
1687
                pass
1638
1688
            
1700
1750
    for client in tcp_server.clients:
1701
1751
        if use_dbus:
1702
1752
            # Emit D-Bus signal
1703
 
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
1704
 
                                            client.GetAll(u""))
 
1753
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
1705
1754
        client.enable()
1706
1755
    
1707
1756
    tcp_server.enable()