/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: Björn Påhlsson
  • Date: 2011-11-09 17:16:03 UTC
  • mfrom: (518.1.1 mandos-persistent)
  • Revision ID: belorn@fukt.bsnet.se-20111109171603-srz21uoclpldp5ve
merge persistent state

Show diffs side-by-side

added added

removed removed

Lines of Context:
63
63
import cPickle as pickle
64
64
import multiprocessing
65
65
import types
 
66
import hashlib
66
67
 
67
68
import dbus
68
69
import dbus.service
73
74
import ctypes.util
74
75
import xml.dom.minidom
75
76
import inspect
 
77
import Crypto.Cipher.AES
76
78
 
77
79
try:
78
80
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
86
88
version = "1.4.1"
87
89
 
88
90
logger = logging.getLogger()
 
91
stored_state_path = "/var/lib/mandos/clients.pickle"
 
92
 
89
93
syslogger = (logging.handlers.SysLogHandler
90
94
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
91
95
              address = str("/dev/log")))
295
299
                     instance %(name)s can be used in the command.
296
300
    checker_initiator_tag: a gobject event source tag, or None
297
301
    created:    datetime.datetime(); (UTC) object creation
 
302
    client_structure: Object describing what attributes a client has
 
303
                      and is used for storing the client at exit
298
304
    current_checker_command: string; current running checker_command
299
 
    disable_hook:  If set, called by disable() as disable_hook(self)
300
305
    disable_initiator_tag: a gobject event source tag, or None
301
306
    enabled:    bool()
302
307
    fingerprint: string (40 or 32 hexadecimal digits); used to
305
310
    interval:   datetime.timedelta(); How often to start a new checker
306
311
    last_approval_request: datetime.datetime(); (UTC) or None
307
312
    last_checked_ok: datetime.datetime(); (UTC) or None
 
313
    last_checker_status: integer between 0 and 255 reflecting exit status
 
314
                         of last checker. -1 reflect crashed checker,
 
315
                         or None.
308
316
    last_enabled: datetime.datetime(); (UTC)
309
317
    name:       string; from the config file, used in log messages and
310
318
                        D-Bus identifiers
337
345
    def approval_delay_milliseconds(self):
338
346
        return _timedelta_to_milliseconds(self.approval_delay)
339
347
    
340
 
    def __init__(self, name = None, disable_hook=None, config=None):
 
348
    def __init__(self, name = None, config=None):
341
349
        """Note: the 'checker' key in 'config' sets the
342
350
        'checker_command' attribute and *not* the 'checker'
343
351
        attribute."""
363
371
                            % self.name)
364
372
        self.host = config.get("host", "")
365
373
        self.created = datetime.datetime.utcnow()
366
 
        self.enabled = False
 
374
        self.enabled = True
367
375
        self.last_approval_request = None
368
 
        self.last_enabled = None
 
376
        self.last_enabled = datetime.datetime.utcnow()
369
377
        self.last_checked_ok = None
 
378
        self.last_checker_status = None
370
379
        self.timeout = string_to_delta(config["timeout"])
371
380
        self.extended_timeout = string_to_delta(config
372
381
                                                ["extended_timeout"])
373
382
        self.interval = string_to_delta(config["interval"])
374
 
        self.disable_hook = disable_hook
375
383
        self.checker = None
376
384
        self.checker_initiator_tag = None
377
385
        self.disable_initiator_tag = None
378
 
        self.expires = None
 
386
        self.expires = datetime.datetime.utcnow() + self.timeout
379
387
        self.checker_callback_tag = None
380
388
        self.checker_command = config["checker"]
381
389
        self.current_checker_command = None
382
 
        self.last_connect = None
383
390
        self._approved = None
384
391
        self.approved_by_default = config.get("approved_by_default",
385
392
                                              True)
391
398
        self.changedstate = (multiprocessing_manager
392
399
                             .Condition(multiprocessing_manager
393
400
                                        .Lock()))
 
401
        self.client_structure = [attr for attr in self.__dict__.iterkeys() if not attr.startswith("_")]
 
402
        self.client_structure.append("client_structure")
 
403
 
 
404
 
 
405
        for name, t in inspect.getmembers(type(self),
 
406
                                          lambda obj: isinstance(obj, property)):
 
407
            if not name.startswith("_"):
 
408
                self.client_structure.append(name)
394
409
    
 
410
    # Send notice to process children that client state has changed
395
411
    def send_changedstate(self):
396
 
        self.changedstate.acquire()
397
 
        self.changedstate.notify_all()
398
 
        self.changedstate.release()
 
412
        with self.changedstate:
 
413
            self.changedstate.notify_all()
399
414
    
400
415
    def enable(self):
401
416
        """Start this client's checker and timeout hooks"""
403
418
            # Already enabled
404
419
            return
405
420
        self.send_changedstate()
406
 
        # Schedule a new checker to be started an 'interval' from now,
407
 
        # and every interval from then on.
408
 
        self.checker_initiator_tag = (gobject.timeout_add
409
 
                                      (self.interval_milliseconds(),
410
 
                                       self.start_checker))
411
 
        # Schedule a disable() when 'timeout' has passed
412
421
        self.expires = datetime.datetime.utcnow() + self.timeout
413
 
        self.disable_initiator_tag = (gobject.timeout_add
414
 
                                   (self.timeout_milliseconds(),
415
 
                                    self.disable))
416
422
        self.enabled = True
417
423
        self.last_enabled = datetime.datetime.utcnow()
418
 
        # Also start a new checker *right now*.
419
 
        self.start_checker()
 
424
        self.init_checker()
420
425
    
421
426
    def disable(self, quiet=True):
422
427
        """Disable this client."""
434
439
            gobject.source_remove(self.checker_initiator_tag)
435
440
            self.checker_initiator_tag = None
436
441
        self.stop_checker()
437
 
        if self.disable_hook:
438
 
            self.disable_hook(self)
439
442
        self.enabled = False
440
443
        # Do not run this again if called by a gobject.timeout_add
441
444
        return False
442
445
    
443
446
    def __del__(self):
444
 
        self.disable_hook = None
445
447
        self.disable()
446
 
    
 
448
 
 
449
    def init_checker(self):
 
450
        # Schedule a new checker to be started an 'interval' from now,
 
451
        # and every interval from then on.
 
452
        self.checker_initiator_tag = (gobject.timeout_add
 
453
                                      (self.interval_milliseconds(),
 
454
                                       self.start_checker))
 
455
        # Schedule a disable() when 'timeout' has passed
 
456
        self.disable_initiator_tag = (gobject.timeout_add
 
457
                                   (self.timeout_milliseconds(),
 
458
                                    self.disable))
 
459
        # Also start a new checker *right now*.
 
460
        self.start_checker()
 
461
 
 
462
        
447
463
    def checker_callback(self, pid, condition, command):
448
464
        """The checker has completed, so take appropriate actions."""
449
465
        self.checker_callback_tag = None
450
466
        self.checker = None
451
467
        if os.WIFEXITED(condition):
452
 
            exitstatus = os.WEXITSTATUS(condition)
453
 
            if exitstatus == 0:
 
468
            self.last_checker_status =  os.WEXITSTATUS(condition)
 
469
            if self.last_checker_status == 0:
454
470
                logger.info("Checker for %(name)s succeeded",
455
471
                            vars(self))
456
472
                self.checked_ok()
458
474
                logger.info("Checker for %(name)s failed",
459
475
                            vars(self))
460
476
        else:
 
477
            self.last_checker_status = -1
461
478
            logger.warning("Checker for %(name)s crashed?",
462
479
                           vars(self))
463
480
    
574
591
                raise
575
592
        self.checker = None
576
593
 
 
594
    # Encrypts a client secret and stores it in a varible encrypted_secret
 
595
    def encrypt_secret(self, key):
 
596
        # Encryption-key need to be of a specific size, so we hash inputed key
 
597
        hasheng = hashlib.sha256()
 
598
        hasheng.update(key)
 
599
        encryptionkey = hasheng.digest()
 
600
 
 
601
        # Create validation hash so we know at decryption if it was sucessful
 
602
        hasheng = hashlib.sha256()
 
603
        hasheng.update(self.secret)
 
604
        validationhash = hasheng.digest()
 
605
 
 
606
        # Encrypt secret
 
607
        iv = os.urandom(Crypto.Cipher.AES.block_size)
 
608
        ciphereng = Crypto.Cipher.AES.new(encryptionkey,
 
609
                                        Crypto.Cipher.AES.MODE_CFB, iv)
 
610
        ciphertext = ciphereng.encrypt(validationhash+self.secret)
 
611
        self.encrypted_secret = (ciphertext, iv)
 
612
 
 
613
    # Decrypt a encrypted client secret
 
614
    def decrypt_secret(self, key):
 
615
        # Decryption-key need to be of a specific size, so we hash inputed key
 
616
        hasheng = hashlib.sha256()
 
617
        hasheng.update(key)
 
618
        encryptionkey = hasheng.digest()
 
619
 
 
620
        # Decrypt encrypted secret
 
621
        ciphertext, iv = self.encrypted_secret
 
622
        ciphereng = Crypto.Cipher.AES.new(encryptionkey,
 
623
                                        Crypto.Cipher.AES.MODE_CFB, iv)
 
624
        plain = ciphereng.decrypt(ciphertext)
 
625
 
 
626
        # Validate decrypted secret to know if it was succesful
 
627
        hasheng = hashlib.sha256()
 
628
        validationhash = plain[:hasheng.digest_size]
 
629
        secret = plain[hasheng.digest_size:]
 
630
        hasheng.update(secret)
 
631
 
 
632
        # if validation fails, we use key as new secret. Otherwhise, we use
 
633
        # the decrypted secret
 
634
        if hasheng.digest() == validationhash:
 
635
            self.secret = secret
 
636
        else:
 
637
            self.secret = key
 
638
        del self.encrypted_secret
 
639
 
577
640
 
578
641
def dbus_service_property(dbus_interface, signature="v",
579
642
                          access="readwrite", byte_arrays=False):
880
943
    # dbus.service.Object doesn't use super(), so we can't either.
881
944
    
882
945
    def __init__(self, bus = None, *args, **kwargs):
 
946
        self.bus = bus
 
947
        Client.__init__(self, *args, **kwargs)
 
948
 
883
949
        self._approvals_pending = 0
884
 
        self.bus = bus
885
 
        Client.__init__(self, *args, **kwargs)
886
950
        # Only now, when this client is initialized, can it show up on
887
951
        # the D-Bus
888
952
        client_object_name = unicode(self.name).translate(
1630
1694
        self.enabled = False
1631
1695
        self.clients = clients
1632
1696
        if self.clients is None:
1633
 
            self.clients = set()
 
1697
            self.clients = {}
1634
1698
        self.use_dbus = use_dbus
1635
1699
        self.gnutls_priority = gnutls_priority
1636
1700
        IPv6_TCPServer.__init__(self, server_address,
1683
1747
            fpr = request[1]
1684
1748
            address = request[2]
1685
1749
            
1686
 
            for c in self.clients:
 
1750
            for c in self.clients.itervalues():
1687
1751
                if c.fingerprint == fpr:
1688
1752
                    client = c
1689
1753
                    break
1857
1921
                        " system bus interface")
1858
1922
    parser.add_argument("--no-ipv6", action="store_false",
1859
1923
                        dest="use_ipv6", help="Do not use IPv6")
 
1924
    parser.add_argument("--no-restore", action="store_false",
 
1925
                        dest="restore", help="Do not restore stored state",
 
1926
                        default=True)
 
1927
 
1860
1928
    options = parser.parse_args()
1861
1929
    
1862
1930
    if options.check:
1897
1965
    # options, if set.
1898
1966
    for option in ("interface", "address", "port", "debug",
1899
1967
                   "priority", "servicename", "configdir",
1900
 
                   "use_dbus", "use_ipv6", "debuglevel"):
 
1968
                   "use_dbus", "use_ipv6", "debuglevel", "restore"):
1901
1969
        value = getattr(options, option)
1902
1970
        if value is not None:
1903
1971
            server_settings[option] = value
2044
2112
    if use_dbus:
2045
2113
        client_class = functools.partial(ClientDBusTransitional,
2046
2114
                                         bus = bus)
2047
 
    def client_config_items(config, section):
2048
 
        special_settings = {
2049
 
            "approved_by_default":
2050
 
                lambda: config.getboolean(section,
2051
 
                                          "approved_by_default"),
2052
 
            }
2053
 
        for name, value in config.items(section):
 
2115
    
 
2116
    special_settings = {
 
2117
        # Some settings need to be accessd by special methods;
 
2118
        # booleans need .getboolean(), etc.  Here is a list of them:
 
2119
        "approved_by_default":
 
2120
            lambda section:
 
2121
            client_config.getboolean(section, "approved_by_default"),
 
2122
        }
 
2123
    # Construct a new dict of client settings of this form:
 
2124
    # { client_name: {setting_name: value, ...}, ...}
 
2125
    # with exceptions for any special settings as defined above
 
2126
    client_settings = dict((clientname,
 
2127
                           dict((setting,
 
2128
                                 (value if setting not in special_settings
 
2129
                                  else special_settings[setting](clientname)))
 
2130
                                for setting, value in client_config.items(clientname)))
 
2131
                          for clientname in client_config.sections())
 
2132
    
 
2133
    old_client_settings = {}
 
2134
    clients_data = []
 
2135
 
 
2136
    # Get client data and settings from last running state. 
 
2137
    if server_settings["restore"]:
 
2138
        try:
 
2139
            with open(stored_state_path, "rb") as stored_state:
 
2140
                clients_data, old_client_settings = pickle.load(stored_state)
 
2141
            os.remove(stored_state_path)
 
2142
        except IOError as e:
 
2143
            logger.warning("Could not load persistant state: {0}".format(e))
 
2144
            if e.errno != errno.ENOENT:
 
2145
                raise
 
2146
 
 
2147
    for client in clients_data:
 
2148
        client_name = client["name"]
 
2149
        
 
2150
        # Decide which value to use after restoring saved state.
 
2151
        # We have three different values: Old config file,
 
2152
        # new config file, and saved state.
 
2153
        # New config value takes precedence if it differs from old
 
2154
        # config value, otherwise use saved state.
 
2155
        for name, value in client_settings[client_name].items():
2054
2156
            try:
2055
 
                yield (name, special_settings[name]())
 
2157
                # For each value in new config, check if it differs
 
2158
                # from the old config value (Except for the "secret"
 
2159
                # attribute)
 
2160
                if name != "secret" and value != old_client_settings[client_name][name]:
 
2161
                    setattr(client, name, value)
2056
2162
            except KeyError:
2057
 
                yield (name, value)
 
2163
                pass
 
2164
 
 
2165
        # Clients who has passed its expire date, can still be enabled if its
 
2166
        # last checker was sucessful. Clients who checkers failed before we
 
2167
        # stored it state is asumed to had failed checker during downtime.
 
2168
        if client["enabled"] and client["last_checked_ok"]:
 
2169
            if ((datetime.datetime.utcnow() - client["last_checked_ok"])
 
2170
                > client["interval"]):
 
2171
                if client["last_checker_status"] != 0:
 
2172
                    client["enabled"] = False
 
2173
                else:
 
2174
                    client["expires"] = datetime.datetime.utcnow() + client["timeout"]
 
2175
 
 
2176
        client["changedstate"] = (multiprocessing_manager
 
2177
                                  .Condition(multiprocessing_manager
 
2178
                                             .Lock()))
 
2179
        if use_dbus:
 
2180
            new_client = ClientDBusTransitional.__new__(ClientDBusTransitional)
 
2181
            tcp_server.clients[client_name] = new_client
 
2182
            new_client.bus = bus
 
2183
            for name, value in client.iteritems():
 
2184
                setattr(new_client, name, value)
 
2185
            client_object_name = unicode(client_name).translate(
 
2186
                {ord("."): ord("_"),
 
2187
                 ord("-"): ord("_")})
 
2188
            new_client.dbus_object_path = (dbus.ObjectPath
 
2189
                                     ("/clients/" + client_object_name))
 
2190
            DBusObjectWithProperties.__init__(new_client,
 
2191
                                              new_client.bus,
 
2192
                                              new_client.dbus_object_path)
 
2193
        else:
 
2194
            tcp_server.clients[client_name] = Client.__new__(Client)
 
2195
            for name, value in client.iteritems():
 
2196
                setattr(tcp_server.clients[client_name], name, value)
 
2197
                
 
2198
        tcp_server.clients[client_name].decrypt_secret(
 
2199
            client_settings[client_name]["secret"])            
 
2200
        
 
2201
    # Create/remove clients based on new changes made to config
 
2202
    for clientname in set(old_client_settings) - set(client_settings):
 
2203
        del tcp_server.clients[clientname]
 
2204
    for clientname in set(client_settings) - set(old_client_settings):
 
2205
        tcp_server.clients[clientname] = (client_class(name = clientname,
 
2206
                                                       config =
 
2207
                                                       client_settings
 
2208
                                                       [clientname]))
2058
2209
    
2059
 
    tcp_server.clients.update(set(
2060
 
            client_class(name = section,
2061
 
                         config= dict(client_config_items(
2062
 
                        client_config, section)))
2063
 
            for section in client_config.sections()))
 
2210
 
2064
2211
    if not tcp_server.clients:
2065
2212
        logger.warning("No clients defined")
2066
2213
        
2109
2256
            def GetAllClients(self):
2110
2257
                "D-Bus method"
2111
2258
                return dbus.Array(c.dbus_object_path
2112
 
                                  for c in tcp_server.clients)
 
2259
                                  for c in
 
2260
                                  tcp_server.clients.itervalues())
2113
2261
            
2114
2262
            @dbus.service.method(_interface,
2115
2263
                                 out_signature="a{oa{sv}}")
2117
2265
                "D-Bus method"
2118
2266
                return dbus.Dictionary(
2119
2267
                    ((c.dbus_object_path, c.GetAll(""))
2120
 
                     for c in tcp_server.clients),
 
2268
                     for c in tcp_server.clients.itervalues()),
2121
2269
                    signature="oa{sv}")
2122
2270
            
2123
2271
            @dbus.service.method(_interface, in_signature="o")
2124
2272
            def RemoveClient(self, object_path):
2125
2273
                "D-Bus method"
2126
 
                for c in tcp_server.clients:
 
2274
                for c in tcp_server.clients.itervalues():
2127
2275
                    if c.dbus_object_path == object_path:
2128
 
                        tcp_server.clients.remove(c)
 
2276
                        del tcp_server.clients[c.name]
2129
2277
                        c.remove_from_connection()
2130
2278
                        # Don't signal anything except ClientRemoved
2131
2279
                        c.disable(quiet=True)
2145
2293
        service.cleanup()
2146
2294
        
2147
2295
        multiprocessing.active_children()
 
2296
        if not (tcp_server.clients or client_settings):
 
2297
            return
 
2298
 
 
2299
        # Store client before exiting. Secrets are encrypted with key based
 
2300
        # on what config file has. If config file is removed/edited, old
 
2301
        # secret will thus be unrecovable.
 
2302
        clients = []
 
2303
        for client in tcp_server.clients.itervalues():
 
2304
            client.encrypt_secret(client_settings[client.name]["secret"])
 
2305
 
 
2306
            client_dict = {}
 
2307
 
 
2308
            # A list of attributes that will not be stored when shuting down.
 
2309
            exclude = set(("bus", "changedstate", "secret"))            
 
2310
            for name, typ in inspect.getmembers(dbus.service.Object):
 
2311
                exclude.add(name)
 
2312
                
 
2313
            client_dict["encrypted_secret"] = client.encrypted_secret
 
2314
            for attr in client.client_structure:
 
2315
                if attr not in exclude:
 
2316
                    client_dict[attr] = getattr(client, attr)
 
2317
 
 
2318
            clients.append(client_dict) 
 
2319
            del client_settings[client.name]["secret"]
 
2320
            
 
2321
        try:
 
2322
            with os.fdopen(os.open(stored_state_path, os.O_CREAT|os.O_WRONLY|os.O_TRUNC, 0600), "wb") as stored_state:
 
2323
                pickle.dump((clients, client_settings), stored_state)
 
2324
        except IOError as e:
 
2325
            logger.warning("Could not save persistant state: {0}".format(e))
 
2326
            if e.errno != errno.ENOENT:
 
2327
                raise
 
2328
 
 
2329
        # Delete all clients, and settings from config
2148
2330
        while tcp_server.clients:
2149
 
            client = tcp_server.clients.pop()
 
2331
            name, client = tcp_server.clients.popitem()
2150
2332
            if use_dbus:
2151
2333
                client.remove_from_connection()
2152
 
            client.disable_hook = None
2153
2334
            # Don't signal anything except ClientRemoved
2154
2335
            client.disable(quiet=True)
2155
2336
            if use_dbus:
2157
2338
                mandos_dbus_service.ClientRemoved(client
2158
2339
                                                  .dbus_object_path,
2159
2340
                                                  client.name)
 
2341
        client_settings.clear()
2160
2342
    
2161
2343
    atexit.register(cleanup)
2162
2344
    
2163
 
    for client in tcp_server.clients:
 
2345
    for client in tcp_server.clients.itervalues():
2164
2346
        if use_dbus:
2165
2347
            # Emit D-Bus signal
2166
2348
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
2167
 
        client.enable()
 
2349
        # Need to initiate checking of clients
 
2350
        if client.enabled:
 
2351
            client.init_checker()
 
2352
 
2168
2353
    
2169
2354
    tcp_server.enable()
2170
2355
    tcp_server.server_activate()