/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: 2015-07-20 03:03:33 UTC
  • Revision ID: teddy@recompile.se-20150720030333-203m2aeblypcsfte
Bug fix for GnuTLS 3: be compatible with old 2048-bit DSA keys.

The mandos-keygen program in Mandos version 1.6.0 and older generated
2048-bit DSA keys, and when GnuTLS uses these it has trouble
connecting using the Mandos default priority string.  This was
previously fixed in Mandos 1.6.2, but the bug reappeared when using
GnuTLS 3, so the default priority string has to change again; this
time also the Mandos client has to change its default, so now the
server and the client should use the same default priority string:

SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256

* mandos (main/server_defaults): Changed default priority string.
* mandos-options.xml (/section/para[id="priority_compat"]): Removed.
  (/section/para[id="priority"]): Changed default priority string.
* mandos.conf ([DEFAULT]/priority): - '' -
* mandos.conf.xml (OPTIONS/priority): Refer to the id "priority"
                                      instead of "priority_compat".
* mandos.xml (OPTIONS/--priority): - '' -
* plugins.d/mandos-client.c (main): Changed default priority string.

Show diffs side-by-side

added added

removed removed

Lines of Context:
36
36
 
37
37
from future_builtins import *
38
38
 
39
 
import SocketServer as socketserver
 
39
try:
 
40
    import SocketServer as socketserver
 
41
except ImportError:
 
42
    import socketserver
40
43
import socket
41
44
import argparse
42
45
import datetime
47
50
import gnutls.library.functions
48
51
import gnutls.library.constants
49
52
import gnutls.library.types
50
 
import ConfigParser as configparser
 
53
try:
 
54
    import ConfigParser as configparser
 
55
except ImportError:
 
56
    import configparser
51
57
import sys
52
58
import re
53
59
import os
62
68
import struct
63
69
import fcntl
64
70
import functools
65
 
import cPickle as pickle
 
71
try:
 
72
    import cPickle as pickle
 
73
except ImportError:
 
74
    import pickle
66
75
import multiprocessing
67
76
import types
68
77
import binascii
69
78
import tempfile
70
79
import itertools
71
80
import collections
 
81
import codecs
72
82
 
73
83
import dbus
74
84
import dbus.service
75
 
import gobject
 
85
try:
 
86
    import gobject
 
87
except ImportError:
 
88
    from gi.repository import GObject as gobject
76
89
import avahi
77
90
from dbus.mainloop.glib import DBusGMainLoop
78
91
import ctypes
382
395
                    logger.error(bad_states[state] + ": %r", error)
383
396
            self.cleanup()
384
397
        elif state == avahi.SERVER_RUNNING:
385
 
            self.add()
 
398
            try:
 
399
                self.add()
 
400
            except dbus.exceptions.DBusException as error:
 
401
                if (error.get_dbus_name()
 
402
                    == "org.freedesktop.Avahi.CollisionError"):
 
403
                    logger.info("Local Zeroconf service name"
 
404
                                " collision.")
 
405
                    return self.rename(remove=False)
 
406
                else:
 
407
                    logger.critical("D-Bus Exception", exc_info=error)
 
408
                    self.cleanup()
 
409
                    os._exit(1)
386
410
        else:
387
411
            if error is None:
388
412
                logger.debug("Unknown state: %r", state)
411
435
            .format(self.name)))
412
436
        return ret
413
437
 
 
438
def call_pipe(connection,       # : multiprocessing.Connection
 
439
              func, *args, **kwargs):
 
440
    """This function is meant to be called by multiprocessing.Process
 
441
    
 
442
    This function runs func(*args, **kwargs), and writes the resulting
 
443
    return value on the provided multiprocessing.Connection.
 
444
    """
 
445
    connection.send(func(*args, **kwargs))
 
446
    connection.close()
414
447
 
415
448
class Client(object):
416
449
    """A representation of a client host served by this server.
443
476
    last_checker_status: integer between 0 and 255 reflecting exit
444
477
                         status of last checker. -1 reflects crashed
445
478
                         checker, -2 means no checker completed yet.
 
479
    last_checker_signal: The signal which killed the last checker, if
 
480
                         last_checker_status is -1
446
481
    last_enabled: datetime.datetime(); (UTC) or None
447
482
    name:       string; from the config file, used in log messages and
448
483
                        D-Bus identifiers
622
657
        # Also start a new checker *right now*.
623
658
        self.start_checker()
624
659
    
625
 
    def checker_callback(self, pid, condition, command):
 
660
    def checker_callback(self, source, condition, connection,
 
661
                         command):
626
662
        """The checker has completed, so take appropriate actions."""
627
663
        self.checker_callback_tag = None
628
664
        self.checker = None
629
 
        if os.WIFEXITED(condition):
630
 
            self.last_checker_status = os.WEXITSTATUS(condition)
 
665
        # Read return code from connection (see call_pipe)
 
666
        returncode = connection.recv()
 
667
        connection.close()
 
668
        
 
669
        if returncode >= 0:
 
670
            self.last_checker_status = returncode
 
671
            self.last_checker_signal = None
631
672
            if self.last_checker_status == 0:
632
673
                logger.info("Checker for %(name)s succeeded",
633
674
                            vars(self))
636
677
                logger.info("Checker for %(name)s failed", vars(self))
637
678
        else:
638
679
            self.last_checker_status = -1
 
680
            self.last_checker_signal = -returncode
639
681
            logger.warning("Checker for %(name)s crashed?",
640
682
                           vars(self))
 
683
        return False
641
684
    
642
685
    def checked_ok(self):
643
686
        """Assert that the client has been seen, alive and well."""
644
687
        self.last_checked_ok = datetime.datetime.utcnow()
645
688
        self.last_checker_status = 0
 
689
        self.last_checker_signal = None
646
690
        self.bump_timeout()
647
691
    
648
692
    def bump_timeout(self, timeout=None):
674
718
        # than 'timeout' for the client to be disabled, which is as it
675
719
        # should be.
676
720
        
677
 
        # If a checker exists, make sure it is not a zombie
678
 
        try:
679
 
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
680
 
        except AttributeError:
681
 
            pass
682
 
        except OSError as error:
683
 
            if error.errno != errno.ECHILD:
684
 
                raise
685
 
        else:
686
 
            if pid:
687
 
                logger.warning("Checker was a zombie")
688
 
                gobject.source_remove(self.checker_callback_tag)
689
 
                self.checker_callback(pid, status,
690
 
                                      self.current_checker_command)
 
721
        if self.checker is not None and not self.checker.is_alive():
 
722
            logger.warning("Checker was not alive; joining")
 
723
            self.checker.join()
 
724
            self.checker = None
691
725
        # Start a new checker if needed
692
726
        if self.checker is None:
693
727
            # Escape attributes for the shell
702
736
                             exc_info=error)
703
737
                return True     # Try again later
704
738
            self.current_checker_command = command
705
 
            try:
706
 
                logger.info("Starting checker %r for %s", command,
707
 
                            self.name)
708
 
                # We don't need to redirect stdout and stderr, since
709
 
                # in normal mode, that is already done by daemon(),
710
 
                # and in debug mode we don't want to.  (Stdin is
711
 
                # always replaced by /dev/null.)
712
 
                # The exception is when not debugging but nevertheless
713
 
                # running in the foreground; use the previously
714
 
                # created wnull.
715
 
                popen_args = {}
716
 
                if (not self.server_settings["debug"]
717
 
                    and self.server_settings["foreground"]):
718
 
                    popen_args.update({"stdout": wnull,
719
 
                                       "stderr": wnull })
720
 
                self.checker = subprocess.Popen(command,
721
 
                                                close_fds=True,
722
 
                                                shell=True,
723
 
                                                cwd="/",
724
 
                                                **popen_args)
725
 
            except OSError as error:
726
 
                logger.error("Failed to start subprocess",
727
 
                             exc_info=error)
728
 
                return True
729
 
            self.checker_callback_tag = gobject.child_watch_add(
730
 
                self.checker.pid, self.checker_callback, data=command)
731
 
            # The checker may have completed before the gobject
732
 
            # watch was added.  Check for this.
733
 
            try:
734
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
735
 
            except OSError as error:
736
 
                if error.errno == errno.ECHILD:
737
 
                    # This should never happen
738
 
                    logger.error("Child process vanished",
739
 
                                 exc_info=error)
740
 
                    return True
741
 
                raise
742
 
            if pid:
743
 
                gobject.source_remove(self.checker_callback_tag)
744
 
                self.checker_callback(pid, status, command)
 
739
            logger.info("Starting checker %r for %s", command,
 
740
                        self.name)
 
741
            # We don't need to redirect stdout and stderr, since
 
742
            # in normal mode, that is already done by daemon(),
 
743
            # and in debug mode we don't want to.  (Stdin is
 
744
            # always replaced by /dev/null.)
 
745
            # The exception is when not debugging but nevertheless
 
746
            # running in the foreground; use the previously
 
747
            # created wnull.
 
748
            popen_args = { "close_fds": True,
 
749
                           "shell": True,
 
750
                           "cwd": "/" }
 
751
            if (not self.server_settings["debug"]
 
752
                and self.server_settings["foreground"]):
 
753
                popen_args.update({"stdout": wnull,
 
754
                                   "stderr": wnull })
 
755
            pipe = multiprocessing.Pipe(duplex = False)
 
756
            self.checker = multiprocessing.Process(
 
757
                target = call_pipe,
 
758
                args = (pipe[1], subprocess.call, command),
 
759
                kwargs = popen_args)
 
760
            self.checker.start()
 
761
            self.checker_callback_tag = gobject.io_add_watch(
 
762
                pipe[0].fileno(), gobject.IO_IN,
 
763
                self.checker_callback, pipe[0], command)
745
764
        # Re-run this periodically if run by gobject.timeout_add
746
765
        return True
747
766
    
753
772
        if getattr(self, "checker", None) is None:
754
773
            return
755
774
        logger.debug("Stopping checker for %(name)s", vars(self))
756
 
        try:
757
 
            self.checker.terminate()
758
 
            #time.sleep(0.5)
759
 
            #if self.checker.poll() is None:
760
 
            #    self.checker.kill()
761
 
        except OSError as error:
762
 
            if error.errno != errno.ESRCH: # No such process
763
 
                raise
 
775
        self.checker.terminate()
764
776
        self.checker = None
765
777
 
766
778
 
1098
1110
                interface_names.add(alt_interface)
1099
1111
                # Is this a D-Bus signal?
1100
1112
                if getattr(attribute, "_dbus_is_signal", False):
1101
 
                    # Extract the original non-method undecorated
1102
 
                    # function by black magic
1103
 
                    nonmethod_func = (dict(
1104
 
                        zip(attribute.func_code.co_freevars,
1105
 
                            attribute.__closure__))
1106
 
                                      ["func"].cell_contents)
 
1113
                    if sys.version_info.major == 2:
 
1114
                        # Extract the original non-method undecorated
 
1115
                        # function by black magic
 
1116
                        nonmethod_func = (dict(
 
1117
                            zip(attribute.func_code.co_freevars,
 
1118
                                attribute.__closure__))
 
1119
                                          ["func"].cell_contents)
 
1120
                    else:
 
1121
                        nonmethod_func = attribute
1107
1122
                    # Create a new, but exactly alike, function
1108
1123
                    # object, and decorate it to be a new D-Bus signal
1109
1124
                    # with the alternate D-Bus interface name
 
1125
                    if sys.version_info.major == 2:
 
1126
                        new_function = types.FunctionType(
 
1127
                            nonmethod_func.func_code,
 
1128
                            nonmethod_func.func_globals,
 
1129
                            nonmethod_func.func_name,
 
1130
                            nonmethod_func.func_defaults,
 
1131
                            nonmethod_func.func_closure)
 
1132
                    else:
 
1133
                        new_function = types.FunctionType(
 
1134
                            nonmethod_func.__code__,
 
1135
                            nonmethod_func.__globals__,
 
1136
                            nonmethod_func.__name__,
 
1137
                            nonmethod_func.__defaults__,
 
1138
                            nonmethod_func.__closure__)
1110
1139
                    new_function = (dbus.service.signal(
1111
 
                        alt_interface, attribute._dbus_signature)
1112
 
                                    (types.FunctionType(
1113
 
                                        nonmethod_func.func_code,
1114
 
                                        nonmethod_func.func_globals,
1115
 
                                        nonmethod_func.func_name,
1116
 
                                        nonmethod_func.func_defaults,
1117
 
                                        nonmethod_func.func_closure)))
 
1140
                        alt_interface,
 
1141
                        attribute._dbus_signature)(new_function))
1118
1142
                    # Copy annotations, if any
1119
1143
                    try:
1120
1144
                        new_function._dbus_annotations = dict(
1343
1367
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1344
1368
        Client.__del__(self, *args, **kwargs)
1345
1369
    
1346
 
    def checker_callback(self, pid, condition, command,
1347
 
                         *args, **kwargs):
1348
 
        self.checker_callback_tag = None
1349
 
        self.checker = None
1350
 
        if os.WIFEXITED(condition):
1351
 
            exitstatus = os.WEXITSTATUS(condition)
 
1370
    def checker_callback(self, source, condition,
 
1371
                         connection, command, *args, **kwargs):
 
1372
        ret = Client.checker_callback(self, source, condition,
 
1373
                                      connection, command, *args,
 
1374
                                      **kwargs)
 
1375
        exitstatus = self.last_checker_status
 
1376
        if exitstatus >= 0:
1352
1377
            # Emit D-Bus signal
1353
1378
            self.CheckerCompleted(dbus.Int16(exitstatus),
1354
 
                                  dbus.Int64(condition),
 
1379
                                  dbus.Int64(0),
1355
1380
                                  dbus.String(command))
1356
1381
        else:
1357
1382
            # Emit D-Bus signal
1358
1383
            self.CheckerCompleted(dbus.Int16(-1),
1359
 
                                  dbus.Int64(condition),
 
1384
                                  dbus.Int64(
 
1385
                                      self.last_checker_signal),
1360
1386
                                  dbus.String(command))
1361
 
        
1362
 
        return Client.checker_callback(self, pid, condition, command,
1363
 
                                       *args, **kwargs)
 
1387
        return ret
1364
1388
    
1365
1389
    def start_checker(self, *args, **kwargs):
1366
1390
        old_checker_pid = getattr(self.checker, "pid", None)
1659
1683
        self._pipe = child_pipe
1660
1684
        self._pipe.send(('init', fpr, address))
1661
1685
        if not self._pipe.recv():
1662
 
            raise KeyError()
 
1686
            raise KeyError(fpr)
1663
1687
    
1664
1688
    def __getattribute__(self, name):
1665
1689
        if name == '_pipe':
2128
2152
        
2129
2153
        if command == 'getattr':
2130
2154
            attrname = request[1]
2131
 
            if callable(client_object.__getattribute__(attrname)):
 
2155
            if isinstance(client_object.__getattribute__(attrname),
 
2156
                          collections.Callable):
2132
2157
                parent_pipe.send(('function', ))
2133
2158
            else:
2134
2159
                parent_pipe.send((
2169
2194
    # avoid excessive use of external libraries.
2170
2195
    
2171
2196
    # New type for defining tokens, syntax, and semantics all-in-one
2172
 
    Token = collections.namedtuple("Token",
2173
 
                                   ("regexp", # To match token; if
2174
 
                                              # "value" is not None,
2175
 
                                              # must have a "group"
2176
 
                                              # containing digits
2177
 
                                    "value",  # datetime.timedelta or
2178
 
                                              # None
2179
 
                                    "followers")) # Tokens valid after
2180
 
                                                  # this token
2181
2197
    Token = collections.namedtuple("Token", (
2182
2198
        "regexp",  # To match token; if "value" is not None, must have
2183
2199
                   # a "group" containing digits
2218
2234
    # Define starting values
2219
2235
    value = datetime.timedelta() # Value so far
2220
2236
    found_token = None
2221
 
    followers = frozenset((token_duration,)) # Following valid tokens
 
2237
    followers = frozenset((token_duration, )) # Following valid tokens
2222
2238
    s = duration                # String left to parse
2223
2239
    # Loop until end token is found
2224
2240
    while found_token is not token_end:
2241
2257
                break
2242
2258
        else:
2243
2259
            # No currently valid tokens were found
2244
 
            raise ValueError("Invalid RFC 3339 duration")
 
2260
            raise ValueError("Invalid RFC 3339 duration: {!r}"
 
2261
                             .format(duration))
2245
2262
    # End token found
2246
2263
    return value
2247
2264
 
2380
2397
                        "debug": "False",
2381
2398
                        "priority":
2382
2399
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2383
 
                        ":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2400
                        ":+SIGN-DSA-SHA256",
2384
2401
                        "servicename": "Mandos",
2385
2402
                        "use_dbus": "True",
2386
2403
                        "use_ipv6": "True",
2498
2515
            pidfilename = "/var/run/mandos.pid"
2499
2516
        pidfile = None
2500
2517
        try:
2501
 
            pidfile = open(pidfilename, "w")
 
2518
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
2502
2519
        except IOError as e:
2503
2520
            logger.error("Could not open file %r", pidfilename,
2504
2521
                         exc_info=e)
2563
2580
            old_bus_name = dbus.service.BusName(
2564
2581
                "se.bsnet.fukt.Mandos", bus,
2565
2582
                do_not_queue=True)
2566
 
        except dbus.exceptions.NameExistsException as e:
 
2583
        except dbus.exceptions.DBusException as e:
2567
2584
            logger.error("Disabling D-Bus:", exc_info=e)
2568
2585
            use_dbus = False
2569
2586
            server_settings["use_dbus"] = False
2642
2659
                    pass
2643
2660
            
2644
2661
            # Clients who has passed its expire date can still be
2645
 
            # enabled if its last checker was successful.  Clients
 
2662
            # enabled if its last checker was successful.  A Client
2646
2663
            # whose checker succeeded before we stored its state is
2647
2664
            # assumed to have successfully run all checkers during
2648
2665
            # downtime.
2700
2717
    
2701
2718
    if not foreground:
2702
2719
        if pidfile is not None:
 
2720
            pid = os.getpid()
2703
2721
            try:
2704
2722
                with pidfile:
2705
 
                    pid = os.getpid()
2706
 
                    pidfile.write("{}\n".format(pid).encode("utf-8"))
 
2723
                    print(pid, file=pidfile)
2707
2724
            except IOError:
2708
2725
                logger.error("Could not write to file %r with PID %d",
2709
2726
                             pidfilename, pid)