/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: 2011-10-22 00:46:35 UTC
  • mto: This revision was merged to the branch mainline in revision 519.
  • Revision ID: teddy@recompile.se-20111022004635-3hey47pvet46yccr
* Makefile (run-server): Remove obsolete warning.
* mandos: Clean up and demystify logging.  Add time stamp to console
          debug log messages.
  (AvahiServiceToSyslog): New; only to adjust syslog format.
  (AvahiService.rename): Don't touch syslog logger.
* mandos.xml (BUGS): Remove note about timestamps on console log.

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