/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

  • Committer: Teddy Hogeborn
  • Date: 2008-09-12 19:12:40 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080912191240-edjlcll43eoijkx0
* Makefile (install): Use "install-client-nokey".
  (install-server): Create "/etc/default" and "/usr/sbin", too.
  (install-client): Do not depend on "$(INITRAMFSTOOLS)/hooks/.".
                    Renamed to "install-client-nokey".  Split out
                    post-installation-stuff to new "install-client"
                    target.

* mandos-clients.conf.xml: White space adjustments.
* mandos-keygen.xml: - '' -
* mandos.conf.xml: - '' -
* mandos.xml: - '' -
* plugin-runner.xml: - '' -
* plugins.d/mandos-client.xml: - '' -

* overview.xml: Improved grammar.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
# and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008 Teddy Hogeborn & Björn Påhlsson
 
14
# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson
15
15
16
16
# This program is free software: you can redistribute it and/or modify
17
17
# it under the terms of the GNU General Public License as published by
30
30
# Contact the authors at <mandos@fukt.bsnet.se>.
31
31
32
32
 
33
 
from __future__ import division, with_statement, absolute_import
 
33
from __future__ import division
34
34
 
35
35
import SocketServer
36
36
import socket
 
37
import select
37
38
from optparse import OptionParser
38
39
import datetime
39
40
import errno
55
56
import logging
56
57
import logging.handlers
57
58
import pwd
58
 
from contextlib import closing
59
59
 
60
60
import dbus
61
 
import dbus.service
62
61
import gobject
63
62
import avahi
64
63
from dbus.mainloop.glib import DBusGMainLoop
65
64
import ctypes
66
 
import ctypes.util
67
65
 
68
 
version = "1.0.2"
 
66
version = "1.0"
69
67
 
70
68
logger = logging.Logger('mandos')
71
69
syslogger = logging.handlers.SysLogHandler\
83
81
class AvahiError(Exception):
84
82
    def __init__(self, value):
85
83
        self.value = value
86
 
        super(AvahiError, self).__init__()
87
84
    def __str__(self):
88
85
        return repr(self.value)
89
86
 
111
108
                  a sensible number of times
112
109
    """
113
110
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
114
 
                 servicetype = None, port = None, TXT = None, domain = "",
 
111
                 type = None, port = None, TXT = None, domain = "",
115
112
                 host = "", max_renames = 32768):
116
113
        self.interface = interface
117
114
        self.name = name
118
 
        self.type = servicetype
 
115
        self.type = type
119
116
        self.port = port
120
117
        if TXT is None:
121
118
            self.TXT = []
130
127
        if self.rename_count >= self.max_renames:
131
128
            logger.critical(u"No suitable Zeroconf service name found"
132
129
                            u" after %i retries, exiting.",
133
 
                            self.rename_count)
 
130
                            rename_count)
134
131
            raise AvahiServiceError("Too many renames")
135
132
        self.name = server.GetAlternativeServiceName(self.name)
136
133
        logger.info(u"Changing Zeroconf service name to %r ...",
172
169
# End of Avahi example code
173
170
 
174
171
 
175
 
class Client(dbus.service.Object):
 
172
class Client(object):
176
173
    """A representation of a client host served by this server.
177
174
    Attributes:
178
175
    name:      string; from the config file, used in log messages
181
178
    secret:    bytestring; sent verbatim (over TLS) to client
182
179
    host:      string; available for use by the checker command
183
180
    created:   datetime.datetime(); object creation, not client host
184
 
    started:   bool()
185
181
    last_checked_ok: datetime.datetime() or None if not yet checked OK
186
182
    timeout:   datetime.timedelta(); How long from last_checked_ok
187
183
                                     until this client is invalid
203
199
    _timeout_milliseconds: Used when calling gobject.timeout_add()
204
200
    _interval_milliseconds: - '' -
205
201
    """
206
 
    interface = u"org.mandos_system.Mandos.Clients"
207
 
    
208
 
    @dbus.service.method(interface, out_signature="s")
209
 
    def getName(self):
210
 
        "D-Bus getter method"
211
 
        return self.name
212
 
    
213
 
    @dbus.service.method(interface, out_signature="s")
214
 
    def getFingerprint(self):
215
 
        "D-Bus getter method"
216
 
        return self.fingerprint
217
 
    
218
 
    @dbus.service.method(interface, in_signature="ay",
219
 
                         byte_arrays=True)
220
 
    def setSecret(self, secret):
221
 
        "D-Bus setter method"
222
 
        self.secret = secret
223
 
    
224
202
    def _set_timeout(self, timeout):
225
 
        "Setter function for the 'timeout' attribute"
 
203
        "Setter function for 'timeout' attribute"
226
204
        self._timeout = timeout
227
205
        self._timeout_milliseconds = ((self.timeout.days
228
206
                                       * 24 * 60 * 60 * 1000)
229
207
                                      + (self.timeout.seconds * 1000)
230
208
                                      + (self.timeout.microseconds
231
209
                                         // 1000))
232
 
        # Emit D-Bus signal
233
 
        self.TimeoutChanged(self._timeout_milliseconds)
234
 
    timeout = property(lambda self: self._timeout, _set_timeout)
 
210
    timeout = property(lambda self: self._timeout,
 
211
                       _set_timeout)
235
212
    del _set_timeout
236
 
    
237
 
    @dbus.service.method(interface, out_signature="t")
238
 
    def getTimeout(self):
239
 
        "D-Bus getter method"
240
 
        return self._timeout_milliseconds
241
 
    
242
 
    @dbus.service.signal(interface, signature="t")
243
 
    def TimeoutChanged(self, t):
244
 
        "D-Bus signal"
245
 
        pass
246
 
    
247
213
    def _set_interval(self, interval):
248
 
        "Setter function for the 'interval' attribute"
 
214
        "Setter function for 'interval' attribute"
249
215
        self._interval = interval
250
216
        self._interval_milliseconds = ((self.interval.days
251
217
                                        * 24 * 60 * 60 * 1000)
253
219
                                          * 1000)
254
220
                                       + (self.interval.microseconds
255
221
                                          // 1000))
256
 
        # Emit D-Bus signal
257
 
        self.IntervalChanged(self._interval_milliseconds)
258
 
    interval = property(lambda self: self._interval, _set_interval)
 
222
    interval = property(lambda self: self._interval,
 
223
                        _set_interval)
259
224
    del _set_interval
260
 
    
261
 
    @dbus.service.method(interface, out_signature="t")
262
 
    def getInterval(self):
263
 
        "D-Bus getter method"
264
 
        return self._interval_milliseconds
265
 
    
266
 
    @dbus.service.signal(interface, signature="t")
267
 
    def IntervalChanged(self, t):
268
 
        "D-Bus signal"
269
 
        pass
270
 
    
271
 
    def __init__(self, name = None, stop_hook=None, config=None):
 
225
    def __init__(self, name = None, stop_hook=None, config={}):
272
226
        """Note: the 'checker' key in 'config' sets the
273
227
        'checker_command' attribute and *not* the 'checker'
274
228
        attribute."""
275
 
        dbus.service.Object.__init__(self, bus,
276
 
                                     "/Mandos/Clients/%s"
277
 
                                     % name.replace(".", "_"))
278
 
        if config is None:
279
 
            config = {}
280
229
        self.name = name
281
230
        logger.debug(u"Creating client %r", self.name)
282
231
        # Uppercase and remove spaces from fingerprint for later
288
237
        if "secret" in config:
289
238
            self.secret = config["secret"].decode(u"base64")
290
239
        elif "secfile" in config:
291
 
            with closing(open(os.path.expanduser
292
 
                              (os.path.expandvars
293
 
                               (config["secfile"])))) \
294
 
                               as secfile:
295
 
                self.secret = secfile.read()
 
240
            sf = open(config["secfile"])
 
241
            self.secret = sf.read()
 
242
            sf.close()
296
243
        else:
297
244
            raise TypeError(u"No secret or secfile for client %s"
298
245
                            % self.name)
299
246
        self.host = config.get("host", "")
300
247
        self.created = datetime.datetime.now()
301
 
        self.started = False
302
248
        self.last_checked_ok = None
303
249
        self.timeout = string_to_delta(config["timeout"])
304
250
        self.interval = string_to_delta(config["interval"])
308
254
        self.stop_initiator_tag = None
309
255
        self.checker_callback_tag = None
310
256
        self.check_command = config["checker"]
311
 
    
312
257
    def start(self):
313
258
        """Start this client's checker and timeout hooks"""
314
 
        self.started = True
315
259
        # Schedule a new checker to be started an 'interval' from now,
316
260
        # and every interval from then on.
317
261
        self.checker_initiator_tag = gobject.timeout_add\
323
267
        self.stop_initiator_tag = gobject.timeout_add\
324
268
                                  (self._timeout_milliseconds,
325
269
                                   self.stop)
326
 
        # Emit D-Bus signal
327
 
        self.StateChanged(True)
328
 
    
329
 
    @dbus.service.signal(interface, signature="b")
330
 
    def StateChanged(self, started):
331
 
        "D-Bus signal"
332
 
        pass
333
 
    
334
270
    def stop(self):
335
 
        """Stop this client."""
336
 
        if getattr(self, "started", False):
 
271
        """Stop this client.
 
272
        The possibility that a client might be restarted is left open,
 
273
        but not currently used."""
 
274
        # If this client doesn't have a secret, it is already stopped.
 
275
        if hasattr(self, "secret") and self.secret:
337
276
            logger.info(u"Stopping client %s", self.name)
 
277
            self.secret = None
338
278
        else:
339
279
            return False
340
280
        if getattr(self, "stop_initiator_tag", False):
346
286
        self.stop_checker()
347
287
        if self.stop_hook:
348
288
            self.stop_hook(self)
349
 
        # Emit D-Bus signal
350
 
        self.StateChanged(False)
351
289
        # Do not run this again if called by a gobject.timeout_add
352
290
        return False
353
 
    # D-Bus variant
354
 
    Stop = dbus.service.method(interface)(stop)
355
 
    
356
291
    def __del__(self):
357
292
        self.stop_hook = None
358
293
        self.stop()
359
 
    
360
294
    def checker_callback(self, pid, condition):
361
295
        """The checker has completed, so take appropriate actions."""
 
296
        now = datetime.datetime.now()
362
297
        self.checker_callback_tag = None
363
298
        self.checker = None
364
299
        if os.WIFEXITED(condition) \
365
300
               and (os.WEXITSTATUS(condition) == 0):
366
301
            logger.info(u"Checker for %(name)s succeeded",
367
302
                        vars(self))
368
 
            # Emit D-Bus signal
369
 
            self.CheckerCompleted(True)
370
 
            self.bump_timeout()
 
303
            self.last_checked_ok = now
 
304
            gobject.source_remove(self.stop_initiator_tag)
 
305
            self.stop_initiator_tag = gobject.timeout_add\
 
306
                                      (self._timeout_milliseconds,
 
307
                                       self.stop)
371
308
        elif not os.WIFEXITED(condition):
372
309
            logger.warning(u"Checker for %(name)s crashed?",
373
310
                           vars(self))
374
 
            # Emit D-Bus signal
375
 
            self.CheckerCompleted(False)
376
311
        else:
377
312
            logger.info(u"Checker for %(name)s failed",
378
313
                        vars(self))
379
 
            # Emit D-Bus signal
380
 
            self.CheckerCompleted(False)
381
 
    
382
 
    @dbus.service.signal(interface, signature="b")
383
 
    def CheckerCompleted(self, success):
384
 
        "D-Bus signal"
385
 
        pass
386
 
    
387
 
    def bump_timeout(self):
388
 
        """Bump up the timeout for this client.
389
 
        This should only be called when the client has been seen,
390
 
        alive and well.
391
 
        """
392
 
        self.last_checked_ok = datetime.datetime.now()
393
 
        gobject.source_remove(self.stop_initiator_tag)
394
 
        self.stop_initiator_tag = gobject.timeout_add\
395
 
            (self._timeout_milliseconds, self.stop)
396
 
    # D-Bus variant
397
 
    bumpTimeout = dbus.service.method(interface)(bump_timeout)
398
 
    
399
314
    def start_checker(self):
400
315
        """Start a new checker subprocess if one is not running.
401
316
        If a checker already exists, leave it running and do
436
351
                self.checker_callback_tag = gobject.child_watch_add\
437
352
                                            (self.checker.pid,
438
353
                                             self.checker_callback)
439
 
                # Emit D-Bus signal
440
 
                self.CheckerStarted(command)
441
354
            except OSError, error:
442
355
                logger.error(u"Failed to start subprocess: %s",
443
356
                             error)
444
357
        # Re-run this periodically if run by gobject.timeout_add
445
358
        return True
446
 
    
447
 
    @dbus.service.signal(interface, signature="s")
448
 
    def CheckerStarted(self, command):
449
 
        pass
450
 
    
451
 
    @dbus.service.method(interface, out_signature="b")
452
 
    def checkerIsRunning(self):
453
 
        "D-Bus getter method"
454
 
        return self.checker is not None
455
 
    
456
359
    def stop_checker(self):
457
360
        """Force the checker process, if any, to stop."""
458
361
        if self.checker_callback_tag:
470
373
            if error.errno != errno.ESRCH: # No such process
471
374
                raise
472
375
        self.checker = None
473
 
    # D-Bus variant
474
 
    StopChecker = dbus.service.method(interface)(stop_checker)
475
 
    
476
376
    def still_valid(self):
477
377
        """Has the timeout not yet passed for this client?"""
478
 
        if not self.started:
479
 
            return False
480
378
        now = datetime.datetime.now()
481
379
        if self.last_checked_ok is None:
482
380
            return now < (self.created + self.timeout)
483
381
        else:
484
382
            return now < (self.last_checked_ok + self.timeout)
485
 
    # D-Bus variant
486
 
    stillValid = dbus.service.method(interface, out_signature="b")\
487
 
        (still_valid)
488
 
    
489
 
    del interface
490
383
 
491
384
 
492
385
def peer_certificate(session):
522
415
                    (crt, ctypes.byref(datum),
523
416
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
524
417
    # Verify the self signature in the key
525
 
    crtverify = ctypes.c_uint()
 
418
    crtverify = ctypes.c_uint();
526
419
    gnutls.library.functions.gnutls_openpgp_crt_verify_self\
527
420
        (crt, 0, ctypes.byref(crtverify))
528
421
    if crtverify.value != 0:
529
422
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
530
423
        raise gnutls.errors.CertificateSecurityError("Verify failed")
531
424
    # New buffer for the fingerprint
532
 
    buf = ctypes.create_string_buffer(20)
533
 
    buf_len = ctypes.c_size_t()
 
425
    buffer = ctypes.create_string_buffer(20)
 
426
    buffer_length = ctypes.c_size_t()
534
427
    # Get the fingerprint from the certificate into the buffer
535
428
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
536
 
        (crt, ctypes.byref(buf), ctypes.byref(buf_len))
 
429
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
537
430
    # Deinit the certificate
538
431
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
539
432
    # Convert the buffer to a Python bytestring
540
 
    fpr = ctypes.string_at(buf, buf_len.value)
 
433
    fpr = ctypes.string_at(buffer, buffer_length.value)
541
434
    # Convert the bytestring to hexadecimal notation
542
435
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
543
436
    return hex_fpr
544
437
 
545
438
 
546
 
class TCP_handler(SocketServer.BaseRequestHandler, object):
 
439
class tcp_handler(SocketServer.BaseRequestHandler, object):
547
440
    """A TCP request handler class.
548
441
    Instantiated by IPv6_TCPServer for each request to handle it.
549
442
    Note: This will run in its own forked process."""
550
443
    
551
444
    def handle(self):
552
445
        logger.info(u"TCP connection from: %s",
553
 
                    unicode(self.client_address))
 
446
                     unicode(self.client_address))
554
447
        session = gnutls.connection.ClientSession\
555
448
                  (self.request, gnutls.connection.X509Credentials())
556
449
        
571
464
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
572
465
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
573
466
        #                "+DHE-DSS"))
574
 
        # Use a fallback default, since this MUST be set.
575
 
        priority = self.server.settings.get("priority", "NORMAL")
 
467
        priority = "NORMAL"             # Fallback default, since this
 
468
                                        # MUST be set.
 
469
        if self.server.settings["priority"]:
 
470
            priority = self.server.settings["priority"]
576
471
        gnutls.library.functions.gnutls_priority_set_direct\
577
 
            (session._c_object, priority, None)
 
472
            (session._c_object, priority, None);
578
473
        
579
474
        try:
580
475
            session.handshake()
608
503
                           vars(client))
609
504
            session.bye()
610
505
            return
611
 
        ## This won't work here, since we're in a fork.
612
 
        # client.bump_timeout()
613
506
        sent_size = 0
614
507
        while sent_size < len(client.secret):
615
508
            sent = session.send(client.secret[sent_size:])
620
513
        session.bye()
621
514
 
622
515
 
623
 
class IPv6_TCPServer(SocketServer.ForkingMixIn,
624
 
                     SocketServer.TCPServer, object):
 
516
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
625
517
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
626
518
    Attributes:
627
519
        settings:       Server settings
637
529
            self.clients = kwargs["clients"]
638
530
            del kwargs["clients"]
639
531
        self.enabled = False
640
 
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
 
532
        return super(type(self), self).__init__(*args, **kwargs)
641
533
    def server_bind(self):
642
534
        """This overrides the normal server_bind() function
643
535
        to bind to an interface if one was specified, and also NOT to
672
564
#                                            if_nametoindex
673
565
#                                            (self.settings
674
566
#                                             ["interface"]))
675
 
            return super(IPv6_TCPServer, self).server_bind()
 
567
            return super(type(self), self).server_bind()
676
568
    def server_activate(self):
677
569
        if self.enabled:
678
 
            return super(IPv6_TCPServer, self).server_activate()
 
570
            return super(type(self), self).server_activate()
679
571
    def enable(self):
680
572
        self.enabled = True
681
573
 
699
591
    timevalue = datetime.timedelta(0)
700
592
    for s in interval.split():
701
593
        try:
702
 
            suffix = unicode(s[-1])
703
 
            value = int(s[:-1])
 
594
            suffix=unicode(s[-1])
 
595
            value=int(s[:-1])
704
596
            if suffix == u"d":
705
597
                delta = datetime.timedelta(value)
706
598
            elif suffix == u"s":
746
638
    """Call the C function if_nametoindex(), or equivalent"""
747
639
    global if_nametoindex
748
640
    try:
 
641
        if "ctypes.util" not in sys.modules:
 
642
            import ctypes.util
749
643
        if_nametoindex = ctypes.cdll.LoadLibrary\
750
644
            (ctypes.util.find_library("c")).if_nametoindex
751
645
    except (OSError, AttributeError):
756
650
        def if_nametoindex(interface):
757
651
            "Get an interface index the hard way, i.e. using fcntl()"
758
652
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
759
 
            with closing(socket.socket()) as s:
760
 
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
761
 
                                    struct.pack("16s16x", interface))
 
653
            s = socket.socket()
 
654
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
655
                                struct.pack("16s16x", interface))
 
656
            s.close()
762
657
            interface_index = struct.unpack("I", ifreq[16:20])[0]
763
658
            return interface_index
764
659
    return if_nametoindex(interface)
788
683
 
789
684
 
790
685
def main():
 
686
    global main_loop_started
 
687
    main_loop_started = False
 
688
    
791
689
    parser = OptionParser(version = "%%prog %s" % version)
792
690
    parser.add_option("-i", "--interface", type="string",
793
691
                      metavar="IF", help="Bind to interface IF")
808
706
                      default="/etc/mandos", metavar="DIR",
809
707
                      help="Directory to search for configuration"
810
708
                      " files")
811
 
    options = parser.parse_args()[0]
 
709
    (options, args) = parser.parse_args()
812
710
    
813
711
    if options.check:
814
712
        import doctest
871
769
    clients = Set()
872
770
    tcp_server = IPv6_TCPServer((server_settings["address"],
873
771
                                 server_settings["port"]),
874
 
                                TCP_handler,
 
772
                                tcp_handler,
875
773
                                settings=server_settings,
876
774
                                clients=clients)
877
775
    pidfilename = "/var/run/mandos.pid"
905
803
    
906
804
    global service
907
805
    service = AvahiService(name = server_settings["servicename"],
908
 
                           servicetype = "_mandos._tcp", )
 
806
                           type = "_mandos._tcp", );
909
807
    if server_settings["interface"]:
910
808
        service.interface = if_nametoindex\
911
809
                            (server_settings["interface"])
954
852
        pidfile.write(str(pid) + "\n")
955
853
        pidfile.close()
956
854
        del pidfile
957
 
    except IOError:
 
855
    except IOError, err:
958
856
        logger.error(u"Could not write to file %r with PID %d",
959
857
                     pidfilename, pid)
960
858
    except NameError:
1012
910
                             (*args[2:], **kwargs) or True)
1013
911
        
1014
912
        logger.debug(u"Starting main loop")
 
913
        main_loop_started = True
1015
914
        main_loop.run()
1016
915
    except AvahiError, error:
1017
916
        logger.critical(u"AvahiError: %s" + unicode(error))