=== modified file 'TODO'
--- TODO 2011-10-22 00:46:35 +0000
+++ TODO 2011-11-09 17:16:03 +0000
@@ -41,8 +41,7 @@
* mandos (server)
** TODO [#B] Log level :BUGS:
-** TODO Persistent state :BUGS:
- /var/lib/mandos/*
+
*** TODO /etc/mandos/clients.d/*.conf
Watch this directory and add/remove/update clients?
** TODO [#C] config for TXT record
@@ -66,7 +65,6 @@
http://0pointer.de/blog/projects/systemd.html
http://wiki.debian.org/systemd
** TODO Separate logging logic to own object
-** TODO make clients to a dict!
** TODO [#A] Limit approval_delay to max gnutls/tls timeout value
** TODO [#B] break the wait on approval_delay if connection dies
** TODO Generate Client.runtime_expansions from client options + extra
=== modified file 'mandos'
--- mandos 2011-10-22 00:46:35 +0000
+++ mandos 2011-11-09 17:16:03 +0000
@@ -63,6 +63,7 @@
import cPickle as pickle
import multiprocessing
import types
+import hashlib
import dbus
import dbus.service
@@ -73,6 +74,7 @@
import ctypes.util
import xml.dom.minidom
import inspect
+import Crypto.Cipher.AES
try:
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
@@ -86,6 +88,8 @@
version = "1.4.1"
logger = logging.getLogger()
+stored_state_path = "/var/lib/mandos/clients.pickle"
+
syslogger = (logging.handlers.SysLogHandler
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
address = str("/dev/log")))
@@ -295,8 +299,9 @@
instance %(name)s can be used in the command.
checker_initiator_tag: a gobject event source tag, or None
created: datetime.datetime(); (UTC) object creation
+ client_structure: Object describing what attributes a client has
+ and is used for storing the client at exit
current_checker_command: string; current running checker_command
- disable_hook: If set, called by disable() as disable_hook(self)
disable_initiator_tag: a gobject event source tag, or None
enabled: bool()
fingerprint: string (40 or 32 hexadecimal digits); used to
@@ -305,6 +310,9 @@
interval: datetime.timedelta(); How often to start a new checker
last_approval_request: datetime.datetime(); (UTC) or None
last_checked_ok: datetime.datetime(); (UTC) or None
+ last_checker_status: integer between 0 and 255 reflecting exit status
+ of last checker. -1 reflect crashed checker,
+ or None.
last_enabled: datetime.datetime(); (UTC)
name: string; from the config file, used in log messages and
D-Bus identifiers
@@ -337,7 +345,7 @@
def approval_delay_milliseconds(self):
return _timedelta_to_milliseconds(self.approval_delay)
- def __init__(self, name = None, disable_hook=None, config=None):
+ def __init__(self, name = None, config=None):
"""Note: the 'checker' key in 'config' sets the
'checker_command' attribute and *not* the 'checker'
attribute."""
@@ -363,23 +371,22 @@
% self.name)
self.host = config.get("host", "")
self.created = datetime.datetime.utcnow()
- self.enabled = False
+ self.enabled = True
self.last_approval_request = None
- self.last_enabled = None
+ self.last_enabled = datetime.datetime.utcnow()
self.last_checked_ok = None
+ self.last_checker_status = None
self.timeout = string_to_delta(config["timeout"])
self.extended_timeout = string_to_delta(config
["extended_timeout"])
self.interval = string_to_delta(config["interval"])
- self.disable_hook = disable_hook
self.checker = None
self.checker_initiator_tag = None
self.disable_initiator_tag = None
- self.expires = None
+ self.expires = datetime.datetime.utcnow() + self.timeout
self.checker_callback_tag = None
self.checker_command = config["checker"]
self.current_checker_command = None
- self.last_connect = None
self._approved = None
self.approved_by_default = config.get("approved_by_default",
True)
@@ -391,11 +398,19 @@
self.changedstate = (multiprocessing_manager
.Condition(multiprocessing_manager
.Lock()))
+ self.client_structure = [attr for attr in self.__dict__.iterkeys() if not attr.startswith("_")]
+ self.client_structure.append("client_structure")
+
+
+ for name, t in inspect.getmembers(type(self),
+ lambda obj: isinstance(obj, property)):
+ if not name.startswith("_"):
+ self.client_structure.append(name)
+ # Send notice to process children that client state has changed
def send_changedstate(self):
- self.changedstate.acquire()
- self.changedstate.notify_all()
- self.changedstate.release()
+ with self.changedstate:
+ self.changedstate.notify_all()
def enable(self):
"""Start this client's checker and timeout hooks"""
@@ -403,20 +418,10 @@
# Already enabled
return
self.send_changedstate()
- # Schedule a new checker to be started an 'interval' from now,
- # and every interval from then on.
- self.checker_initiator_tag = (gobject.timeout_add
- (self.interval_milliseconds(),
- self.start_checker))
- # Schedule a disable() when 'timeout' has passed
self.expires = datetime.datetime.utcnow() + self.timeout
- self.disable_initiator_tag = (gobject.timeout_add
- (self.timeout_milliseconds(),
- self.disable))
self.enabled = True
self.last_enabled = datetime.datetime.utcnow()
- # Also start a new checker *right now*.
- self.start_checker()
+ self.init_checker()
def disable(self, quiet=True):
"""Disable this client."""
@@ -434,23 +439,34 @@
gobject.source_remove(self.checker_initiator_tag)
self.checker_initiator_tag = None
self.stop_checker()
- if self.disable_hook:
- self.disable_hook(self)
self.enabled = False
# Do not run this again if called by a gobject.timeout_add
return False
def __del__(self):
- self.disable_hook = None
self.disable()
-
+
+ def init_checker(self):
+ # Schedule a new checker to be started an 'interval' from now,
+ # and every interval from then on.
+ self.checker_initiator_tag = (gobject.timeout_add
+ (self.interval_milliseconds(),
+ self.start_checker))
+ # Schedule a disable() when 'timeout' has passed
+ self.disable_initiator_tag = (gobject.timeout_add
+ (self.timeout_milliseconds(),
+ self.disable))
+ # Also start a new checker *right now*.
+ self.start_checker()
+
+
def checker_callback(self, pid, condition, command):
"""The checker has completed, so take appropriate actions."""
self.checker_callback_tag = None
self.checker = None
if os.WIFEXITED(condition):
- exitstatus = os.WEXITSTATUS(condition)
- if exitstatus == 0:
+ self.last_checker_status = os.WEXITSTATUS(condition)
+ if self.last_checker_status == 0:
logger.info("Checker for %(name)s succeeded",
vars(self))
self.checked_ok()
@@ -458,6 +474,7 @@
logger.info("Checker for %(name)s failed",
vars(self))
else:
+ self.last_checker_status = -1
logger.warning("Checker for %(name)s crashed?",
vars(self))
@@ -574,6 +591,52 @@
raise
self.checker = None
+ # Encrypts a client secret and stores it in a varible encrypted_secret
+ def encrypt_secret(self, key):
+ # Encryption-key need to be of a specific size, so we hash inputed key
+ hasheng = hashlib.sha256()
+ hasheng.update(key)
+ encryptionkey = hasheng.digest()
+
+ # Create validation hash so we know at decryption if it was sucessful
+ hasheng = hashlib.sha256()
+ hasheng.update(self.secret)
+ validationhash = hasheng.digest()
+
+ # Encrypt secret
+ iv = os.urandom(Crypto.Cipher.AES.block_size)
+ ciphereng = Crypto.Cipher.AES.new(encryptionkey,
+ Crypto.Cipher.AES.MODE_CFB, iv)
+ ciphertext = ciphereng.encrypt(validationhash+self.secret)
+ self.encrypted_secret = (ciphertext, iv)
+
+ # Decrypt a encrypted client secret
+ def decrypt_secret(self, key):
+ # Decryption-key need to be of a specific size, so we hash inputed key
+ hasheng = hashlib.sha256()
+ hasheng.update(key)
+ encryptionkey = hasheng.digest()
+
+ # Decrypt encrypted secret
+ ciphertext, iv = self.encrypted_secret
+ ciphereng = Crypto.Cipher.AES.new(encryptionkey,
+ Crypto.Cipher.AES.MODE_CFB, iv)
+ plain = ciphereng.decrypt(ciphertext)
+
+ # Validate decrypted secret to know if it was succesful
+ hasheng = hashlib.sha256()
+ validationhash = plain[:hasheng.digest_size]
+ secret = plain[hasheng.digest_size:]
+ hasheng.update(secret)
+
+ # if validation fails, we use key as new secret. Otherwhise, we use
+ # the decrypted secret
+ if hasheng.digest() == validationhash:
+ self.secret = secret
+ else:
+ self.secret = key
+ del self.encrypted_secret
+
def dbus_service_property(dbus_interface, signature="v",
access="readwrite", byte_arrays=False):
@@ -880,9 +943,10 @@
# dbus.service.Object doesn't use super(), so we can't either.
def __init__(self, bus = None, *args, **kwargs):
+ self.bus = bus
+ Client.__init__(self, *args, **kwargs)
+
self._approvals_pending = 0
- self.bus = bus
- Client.__init__(self, *args, **kwargs)
# Only now, when this client is initialized, can it show up on
# the D-Bus
client_object_name = unicode(self.name).translate(
@@ -1630,7 +1694,7 @@
self.enabled = False
self.clients = clients
if self.clients is None:
- self.clients = set()
+ self.clients = {}
self.use_dbus = use_dbus
self.gnutls_priority = gnutls_priority
IPv6_TCPServer.__init__(self, server_address,
@@ -1683,7 +1747,7 @@
fpr = request[1]
address = request[2]
- for c in self.clients:
+ for c in self.clients.itervalues():
if c.fingerprint == fpr:
client = c
break
@@ -1857,6 +1921,10 @@
" system bus interface")
parser.add_argument("--no-ipv6", action="store_false",
dest="use_ipv6", help="Do not use IPv6")
+ parser.add_argument("--no-restore", action="store_false",
+ dest="restore", help="Do not restore stored state",
+ default=True)
+
options = parser.parse_args()
if options.check:
@@ -1897,7 +1965,7 @@
# options, if set.
for option in ("interface", "address", "port", "debug",
"priority", "servicename", "configdir",
- "use_dbus", "use_ipv6", "debuglevel"):
+ "use_dbus", "use_ipv6", "debuglevel", "restore"):
value = getattr(options, option)
if value is not None:
server_settings[option] = value
@@ -2044,23 +2112,102 @@
if use_dbus:
client_class = functools.partial(ClientDBusTransitional,
bus = bus)
- def client_config_items(config, section):
- special_settings = {
- "approved_by_default":
- lambda: config.getboolean(section,
- "approved_by_default"),
- }
- for name, value in config.items(section):
+
+ special_settings = {
+ # Some settings need to be accessd by special methods;
+ # booleans need .getboolean(), etc. Here is a list of them:
+ "approved_by_default":
+ lambda section:
+ client_config.getboolean(section, "approved_by_default"),
+ }
+ # Construct a new dict of client settings of this form:
+ # { client_name: {setting_name: value, ...}, ...}
+ # with exceptions for any special settings as defined above
+ client_settings = dict((clientname,
+ dict((setting,
+ (value if setting not in special_settings
+ else special_settings[setting](clientname)))
+ for setting, value in client_config.items(clientname)))
+ for clientname in client_config.sections())
+
+ old_client_settings = {}
+ clients_data = []
+
+ # Get client data and settings from last running state.
+ if server_settings["restore"]:
+ try:
+ with open(stored_state_path, "rb") as stored_state:
+ clients_data, old_client_settings = pickle.load(stored_state)
+ os.remove(stored_state_path)
+ except IOError as e:
+ logger.warning("Could not load persistant state: {0}".format(e))
+ if e.errno != errno.ENOENT:
+ raise
+
+ for client in clients_data:
+ client_name = client["name"]
+
+ # Decide which value to use after restoring saved state.
+ # We have three different values: Old config file,
+ # new config file, and saved state.
+ # New config value takes precedence if it differs from old
+ # config value, otherwise use saved state.
+ for name, value in client_settings[client_name].items():
try:
- yield (name, special_settings[name]())
+ # For each value in new config, check if it differs
+ # from the old config value (Except for the "secret"
+ # attribute)
+ if name != "secret" and value != old_client_settings[client_name][name]:
+ setattr(client, name, value)
except KeyError:
- yield (name, value)
+ pass
+
+ # Clients who has passed its expire date, can still be enabled if its
+ # last checker was sucessful. Clients who checkers failed before we
+ # stored it state is asumed to had failed checker during downtime.
+ if client["enabled"] and client["last_checked_ok"]:
+ if ((datetime.datetime.utcnow() - client["last_checked_ok"])
+ > client["interval"]):
+ if client["last_checker_status"] != 0:
+ client["enabled"] = False
+ else:
+ client["expires"] = datetime.datetime.utcnow() + client["timeout"]
+
+ client["changedstate"] = (multiprocessing_manager
+ .Condition(multiprocessing_manager
+ .Lock()))
+ if use_dbus:
+ new_client = ClientDBusTransitional.__new__(ClientDBusTransitional)
+ tcp_server.clients[client_name] = new_client
+ new_client.bus = bus
+ for name, value in client.iteritems():
+ setattr(new_client, name, value)
+ client_object_name = unicode(client_name).translate(
+ {ord("."): ord("_"),
+ ord("-"): ord("_")})
+ new_client.dbus_object_path = (dbus.ObjectPath
+ ("/clients/" + client_object_name))
+ DBusObjectWithProperties.__init__(new_client,
+ new_client.bus,
+ new_client.dbus_object_path)
+ else:
+ tcp_server.clients[client_name] = Client.__new__(Client)
+ for name, value in client.iteritems():
+ setattr(tcp_server.clients[client_name], name, value)
+
+ tcp_server.clients[client_name].decrypt_secret(
+ client_settings[client_name]["secret"])
+
+ # Create/remove clients based on new changes made to config
+ for clientname in set(old_client_settings) - set(client_settings):
+ del tcp_server.clients[clientname]
+ for clientname in set(client_settings) - set(old_client_settings):
+ tcp_server.clients[clientname] = (client_class(name = clientname,
+ config =
+ client_settings
+ [clientname]))
- tcp_server.clients.update(set(
- client_class(name = section,
- config= dict(client_config_items(
- client_config, section)))
- for section in client_config.sections()))
+
if not tcp_server.clients:
logger.warning("No clients defined")
@@ -2109,7 +2256,8 @@
def GetAllClients(self):
"D-Bus method"
return dbus.Array(c.dbus_object_path
- for c in tcp_server.clients)
+ for c in
+ tcp_server.clients.itervalues())
@dbus.service.method(_interface,
out_signature="a{oa{sv}}")
@@ -2117,15 +2265,15 @@
"D-Bus method"
return dbus.Dictionary(
((c.dbus_object_path, c.GetAll(""))
- for c in tcp_server.clients),
+ for c in tcp_server.clients.itervalues()),
signature="oa{sv}")
@dbus.service.method(_interface, in_signature="o")
def RemoveClient(self, object_path):
"D-Bus method"
- for c in tcp_server.clients:
+ for c in tcp_server.clients.itervalues():
if c.dbus_object_path == object_path:
- tcp_server.clients.remove(c)
+ del tcp_server.clients[c.name]
c.remove_from_connection()
# Don't signal anything except ClientRemoved
c.disable(quiet=True)
@@ -2145,11 +2293,44 @@
service.cleanup()
multiprocessing.active_children()
+ if not (tcp_server.clients or client_settings):
+ return
+
+ # Store client before exiting. Secrets are encrypted with key based
+ # on what config file has. If config file is removed/edited, old
+ # secret will thus be unrecovable.
+ clients = []
+ for client in tcp_server.clients.itervalues():
+ client.encrypt_secret(client_settings[client.name]["secret"])
+
+ client_dict = {}
+
+ # A list of attributes that will not be stored when shuting down.
+ exclude = set(("bus", "changedstate", "secret"))
+ for name, typ in inspect.getmembers(dbus.service.Object):
+ exclude.add(name)
+
+ client_dict["encrypted_secret"] = client.encrypted_secret
+ for attr in client.client_structure:
+ if attr not in exclude:
+ client_dict[attr] = getattr(client, attr)
+
+ clients.append(client_dict)
+ del client_settings[client.name]["secret"]
+
+ try:
+ with os.fdopen(os.open(stored_state_path, os.O_CREAT|os.O_WRONLY|os.O_TRUNC, 0600), "wb") as stored_state:
+ pickle.dump((clients, client_settings), stored_state)
+ except IOError as e:
+ logger.warning("Could not save persistant state: {0}".format(e))
+ if e.errno != errno.ENOENT:
+ raise
+
+ # Delete all clients, and settings from config
while tcp_server.clients:
- client = tcp_server.clients.pop()
+ name, client = tcp_server.clients.popitem()
if use_dbus:
client.remove_from_connection()
- client.disable_hook = None
# Don't signal anything except ClientRemoved
client.disable(quiet=True)
if use_dbus:
@@ -2157,14 +2338,18 @@
mandos_dbus_service.ClientRemoved(client
.dbus_object_path,
client.name)
+ client_settings.clear()
atexit.register(cleanup)
- for client in tcp_server.clients:
+ for client in tcp_server.clients.itervalues():
if use_dbus:
# Emit D-Bus signal
mandos_dbus_service.ClientAdded(client.dbus_object_path)
- client.enable()
+ # Need to initiate checking of clients
+ if client.enabled:
+ client.init_checker()
+
tcp_server.enable()
tcp_server.server_activate()
=== modified file 'mandos-options.xml'
--- mandos-options.xml 2009-02-13 05:38:21 +0000
+++ mandos-options.xml 2011-11-09 17:16:03 +0000
@@ -85,5 +85,10 @@
to the server if this option is turned off. Only
advanced users should consider changing this option.
+
+
+ This option controls whether the server will restore any state from
+ last time it ran. Default is to try restore last state.
+
=== modified file 'mandos.xml'
--- mandos.xml 2011-10-22 00:46:35 +0000
+++ mandos.xml 2011-11-09 17:16:03 +0000
@@ -94,6 +94,8 @@
+
+ &COMMANDNAME;
@@ -275,6 +277,13 @@
+
+
+
+
+
+
+