88
82
except ImportError:
89
83
SO_BINDTODEVICE = None
92
stored_state_file = "clients.pickle"
94
logger = logging.getLogger()
88
#logger = logging.getLogger('mandos')
89
logger = logging.Logger('mandos')
95
90
syslogger = (logging.handlers.SysLogHandler
96
91
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
97
92
address = str("/dev/log")))
100
if_nametoindex = (ctypes.cdll.LoadLibrary
101
(ctypes.util.find_library("c"))
103
except (OSError, AttributeError):
104
def if_nametoindex(interface):
105
"Get an interface index the hard way, i.e. using fcntl()"
106
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
107
with contextlib.closing(socket.socket()) as s:
108
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
109
struct.pack(str("16s16x"),
111
interface_index = struct.unpack(str("I"),
113
return interface_index
116
def initlogger(debug, level=logging.WARNING):
117
"""init logger and add loglevel"""
119
syslogger.setFormatter(logging.Formatter
120
('Mandos [%(process)d]: %(levelname)s:'
122
logger.addHandler(syslogger)
125
console = logging.StreamHandler()
126
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
130
logger.addHandler(console)
131
logger.setLevel(level)
134
class PGPError(Exception):
135
"""Exception if encryption/decryption fails"""
139
class PGPEngine(object):
140
"""A simple class for OpenPGP symmetric encryption & decryption"""
142
self.gnupg = GnuPGInterface.GnuPG()
143
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
144
self.gnupg = GnuPGInterface.GnuPG()
145
self.gnupg.options.meta_interactive = False
146
self.gnupg.options.homedir = self.tempdir
147
self.gnupg.options.extra_args.extend(['--force-mdc',
154
def __exit__ (self, exc_type, exc_value, traceback):
162
if self.tempdir is not None:
163
# Delete contents of tempdir
164
for root, dirs, files in os.walk(self.tempdir,
166
for filename in files:
167
os.remove(os.path.join(root, filename))
169
os.rmdir(os.path.join(root, dirname))
171
os.rmdir(self.tempdir)
174
def password_encode(self, password):
175
# Passphrase can not be empty and can not contain newlines or
176
# NUL bytes. So we prefix it and hex encode it.
177
return b"mandos" + binascii.hexlify(password)
179
def encrypt(self, data, password):
180
self.gnupg.passphrase = self.password_encode(password)
181
with open(os.devnull, "w") as devnull:
183
proc = self.gnupg.run(['--symmetric'],
184
create_fhs=['stdin', 'stdout'],
185
attach_fhs={'stderr': devnull})
186
with contextlib.closing(proc.handles['stdin']) as f:
188
with contextlib.closing(proc.handles['stdout']) as f:
189
ciphertext = f.read()
193
self.gnupg.passphrase = None
196
def decrypt(self, data, password):
197
self.gnupg.passphrase = self.password_encode(password)
198
with open(os.devnull, "w") as devnull:
200
proc = self.gnupg.run(['--decrypt'],
201
create_fhs=['stdin', 'stdout'],
202
attach_fhs={'stderr': devnull})
203
with contextlib.closing(proc.handles['stdin']) as f:
205
with contextlib.closing(proc.handles['stdout']) as f:
206
decrypted_plaintext = f.read()
210
self.gnupg.passphrase = None
211
return decrypted_plaintext
93
syslogger.setFormatter(logging.Formatter
94
('Mandos [%(process)d]: %(levelname)s:'
96
logger.addHandler(syslogger)
98
console = logging.StreamHandler()
99
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
102
logger.addHandler(console)
214
104
class AvahiError(Exception):
215
105
def __init__(self, value, *args, **kwargs):
445
315
"created", "enabled", "fingerprint",
446
316
"host", "interval", "last_checked_ok",
447
317
"last_enabled", "name", "timeout")
448
client_defaults = { "timeout": "5m",
449
"extended_timeout": "15m",
451
"checker": "fping -q -- %%(host)s",
453
"approval_delay": "0s",
454
"approval_duration": "1s",
455
"approved_by_default": "True",
459
319
def timeout_milliseconds(self):
460
320
"Return the 'timeout' attribute in milliseconds"
461
return timedelta_to_milliseconds(self.timeout)
321
return _timedelta_to_milliseconds(self.timeout)
463
323
def extended_timeout_milliseconds(self):
464
324
"Return the 'extended_timeout' attribute in milliseconds"
465
return timedelta_to_milliseconds(self.extended_timeout)
325
return _timedelta_to_milliseconds(self.extended_timeout)
467
327
def interval_milliseconds(self):
468
328
"Return the 'interval' attribute in milliseconds"
469
return timedelta_to_milliseconds(self.interval)
329
return _timedelta_to_milliseconds(self.interval)
471
331
def approval_delay_milliseconds(self):
472
return timedelta_to_milliseconds(self.approval_delay)
475
def config_parser(config):
476
"""Construct a new dict of client settings of this form:
477
{ client_name: {setting_name: value, ...}, ...}
478
with exceptions for any special settings as defined above.
479
NOTE: Must be a pure function. Must return the same result
480
value given the same arguments.
483
for client_name in config.sections():
484
section = dict(config.items(client_name))
485
client = settings[client_name] = {}
487
client["host"] = section["host"]
488
# Reformat values from string types to Python types
489
client["approved_by_default"] = config.getboolean(
490
client_name, "approved_by_default")
491
client["enabled"] = config.getboolean(client_name,
494
client["fingerprint"] = (section["fingerprint"].upper()
496
if "secret" in section:
497
client["secret"] = section["secret"].decode("base64")
498
elif "secfile" in section:
499
with open(os.path.expanduser(os.path.expandvars
500
(section["secfile"])),
502
client["secret"] = secfile.read()
504
raise TypeError("No secret or secfile for section {0}"
506
client["timeout"] = string_to_delta(section["timeout"])
507
client["extended_timeout"] = string_to_delta(
508
section["extended_timeout"])
509
client["interval"] = string_to_delta(section["interval"])
510
client["approval_delay"] = string_to_delta(
511
section["approval_delay"])
512
client["approval_duration"] = string_to_delta(
513
section["approval_duration"])
514
client["checker_command"] = section["checker"]
515
client["last_approval_request"] = None
516
client["last_checked_ok"] = None
517
client["last_checker_status"] = -2
521
def __init__(self, settings, name = None):
332
return _timedelta_to_milliseconds(self.approval_delay)
334
def __init__(self, name = None, disable_hook=None, config=None):
335
"""Note: the 'checker' key in 'config' sets the
336
'checker_command' attribute and *not* the 'checker'
523
# adding all client settings
524
for setting, value in settings.iteritems():
525
setattr(self, setting, value)
528
if not hasattr(self, "last_enabled"):
529
self.last_enabled = datetime.datetime.utcnow()
530
if not hasattr(self, "expires"):
531
self.expires = (datetime.datetime.utcnow()
534
self.last_enabled = None
537
341
logger.debug("Creating client %r", self.name)
538
342
# Uppercase and remove spaces from fingerprint for later
539
343
# comparison purposes with return value from the fingerprint()
345
self.fingerprint = (config["fingerprint"].upper()
541
347
logger.debug(" Fingerprint: %s", self.fingerprint)
542
self.created = settings.get("created",
543
datetime.datetime.utcnow())
545
# attributes specific for this server instance
348
if "secret" in config:
349
self.secret = config["secret"].decode("base64")
350
elif "secfile" in config:
351
with open(os.path.expanduser(os.path.expandvars
352
(config["secfile"])),
354
self.secret = secfile.read()
356
raise TypeError("No secret or secfile for client %s"
358
self.host = config.get("host", "")
359
self.created = datetime.datetime.utcnow()
361
self.last_approval_request = None
362
self.last_enabled = None
363
self.last_checked_ok = None
364
self.timeout = string_to_delta(config["timeout"])
365
self.extended_timeout = string_to_delta(config
366
["extended_timeout"])
367
self.interval = string_to_delta(config["interval"])
368
self.disable_hook = disable_hook
546
369
self.checker = None
547
370
self.checker_initiator_tag = None
548
371
self.disable_initiator_tag = None
549
373
self.checker_callback_tag = None
374
self.checker_command = config["checker"]
550
375
self.current_checker_command = None
376
self.last_connect = None
377
self._approved = None
378
self.approved_by_default = config.get("approved_by_default",
552
380
self.approvals_pending = 0
381
self.approval_delay = string_to_delta(
382
config["approval_delay"])
383
self.approval_duration = string_to_delta(
384
config["approval_duration"])
553
385
self.changedstate = (multiprocessing_manager
554
386
.Condition(multiprocessing_manager
556
self.client_structure = [attr for attr in
557
self.__dict__.iterkeys()
558
if not attr.startswith("_")]
559
self.client_structure.append("client_structure")
561
for name, t in inspect.getmembers(type(self),
565
if not name.startswith("_"):
566
self.client_structure.append(name)
568
# Send notice to process children that client state has changed
569
389
def send_changedstate(self):
570
with self.changedstate:
571
self.changedstate.notify_all()
390
self.changedstate.acquire()
391
self.changedstate.notify_all()
392
self.changedstate.release()
573
394
def enable(self):
574
395
"""Start this client's checker and timeout hooks"""
575
396
if getattr(self, "enabled", False):
576
397
# Already enabled
399
self.send_changedstate()
400
# Schedule a new checker to be started an 'interval' from now,
401
# and every interval from then on.
402
self.checker_initiator_tag = (gobject.timeout_add
403
(self.interval_milliseconds(),
405
# Schedule a disable() when 'timeout' has passed
578
406
self.expires = datetime.datetime.utcnow() + self.timeout
407
self.disable_initiator_tag = (gobject.timeout_add
408
(self.timeout_milliseconds(),
579
410
self.enabled = True
580
411
self.last_enabled = datetime.datetime.utcnow()
582
self.send_changedstate()
412
# Also start a new checker *right now*.
584
415
def disable(self, quiet=True):
585
416
"""Disable this client."""
586
417
if not getattr(self, "enabled", False):
420
self.send_changedstate()
589
422
logger.info("Disabling client %s", self.name)
590
if getattr(self, "disable_initiator_tag", None) is not None:
423
if getattr(self, "disable_initiator_tag", False):
591
424
gobject.source_remove(self.disable_initiator_tag)
592
425
self.disable_initiator_tag = None
593
426
self.expires = None
594
if getattr(self, "checker_initiator_tag", None) is not None:
427
if getattr(self, "checker_initiator_tag", False):
595
428
gobject.source_remove(self.checker_initiator_tag)
596
429
self.checker_initiator_tag = None
597
430
self.stop_checker()
431
if self.disable_hook:
432
self.disable_hook(self)
598
433
self.enabled = False
600
self.send_changedstate()
601
434
# Do not run this again if called by a gobject.timeout_add
604
437
def __del__(self):
438
self.disable_hook = None
607
def init_checker(self):
608
# Schedule a new checker to be started an 'interval' from now,
609
# and every interval from then on.
610
if self.checker_initiator_tag is not None:
611
gobject.source_remove(self.checker_initiator_tag)
612
self.checker_initiator_tag = (gobject.timeout_add
613
(self.interval_milliseconds(),
615
# Schedule a disable() when 'timeout' has passed
616
if self.disable_initiator_tag is not None:
617
gobject.source_remove(self.disable_initiator_tag)
618
self.disable_initiator_tag = (gobject.timeout_add
619
(self.timeout_milliseconds(),
621
# Also start a new checker *right now*.
624
441
def checker_callback(self, pid, condition, command):
625
442
"""The checker has completed, so take appropriate actions."""
626
443
self.checker_callback_tag = None
627
444
self.checker = None
628
445
if os.WIFEXITED(condition):
629
self.last_checker_status = os.WEXITSTATUS(condition)
630
if self.last_checker_status == 0:
446
exitstatus = os.WEXITSTATUS(condition)
631
448
logger.info("Checker for %(name)s succeeded",
633
450
self.checked_ok()
1030
761
return dbus.String(dt.isoformat(),
1031
762
variant_level=variant_level)
1034
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1035
"""A class decorator; applied to a subclass of
1036
dbus.service.Object, it will add alternate D-Bus attributes with
1037
interface names according to the "alt_interface_names" mapping.
1040
@alternate_dbus_names({"org.example.Interface":
1041
"net.example.AlternateInterface"})
1042
class SampleDBusObject(dbus.service.Object):
1043
@dbus.service.method("org.example.Interface")
1044
def SampleDBusMethod():
1047
The above "SampleDBusMethod" on "SampleDBusObject" will be
1048
reachable via two interfaces: "org.example.Interface" and
1049
"net.example.AlternateInterface", the latter of which will have
1050
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1051
"true", unless "deprecate" is passed with a False value.
1053
This works for methods and signals, and also for D-Bus properties
1054
(from DBusObjectWithProperties) and interfaces (from the
1055
dbus_interface_annotations decorator).
764
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
766
"""Applied to an empty subclass of a D-Bus object, this metaclass
767
will add additional D-Bus attributes matching a certain pattern.
1058
for orig_interface_name, alt_interface_name in (
1059
alt_interface_names.iteritems()):
1061
interface_names = set()
1062
# Go though all attributes of the class
1063
for attrname, attribute in inspect.getmembers(cls):
769
def __new__(mcs, name, bases, attr):
770
# Go through all the base classes which could have D-Bus
771
# methods, signals, or properties in them
772
for base in (b for b in bases
773
if issubclass(b, dbus.service.Object)):
774
# Go though all attributes of the base class
775
for attrname, attribute in inspect.getmembers(base):
1064
776
# Ignore non-D-Bus attributes, and D-Bus attributes
1065
777
# with the wrong interface name
1066
778
if (not hasattr(attribute, "_dbus_interface")
1067
779
or not attribute._dbus_interface
1068
.startswith(orig_interface_name)):
780
.startswith("se.recompile.Mandos")):
1070
782
# Create an alternate D-Bus interface name based on
1071
783
# the current name
1072
784
alt_interface = (attribute._dbus_interface
1073
.replace(orig_interface_name,
1074
alt_interface_name))
1075
interface_names.add(alt_interface)
785
.replace("se.recompile.Mandos",
786
"se.bsnet.fukt.Mandos"))
1076
787
# Is this a D-Bus signal?
1077
788
if getattr(attribute, "_dbus_is_signal", False):
1078
789
# Extract the original non-method function by
1268
925
checker is not None)
1269
926
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1270
927
"LastCheckedOK")
1271
last_checker_status = notifychangeproperty(dbus.Int16,
1272
"LastCheckerStatus")
1273
928
last_approval_request = notifychangeproperty(
1274
929
datetime_to_dbus, "LastApprovalRequest")
1275
930
approved_by_default = notifychangeproperty(dbus.Boolean,
1276
931
"ApprovedByDefault")
1277
approval_delay = notifychangeproperty(dbus.UInt64,
932
approval_delay = notifychangeproperty(dbus.UInt16,
1278
933
"ApprovalDelay",
1280
timedelta_to_milliseconds)
935
_timedelta_to_milliseconds)
1281
936
approval_duration = notifychangeproperty(
1282
dbus.UInt64, "ApprovalDuration",
1283
type_func = timedelta_to_milliseconds)
937
dbus.UInt16, "ApprovalDuration",
938
type_func = _timedelta_to_milliseconds)
1284
939
host = notifychangeproperty(dbus.String, "Host")
1285
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
940
timeout = notifychangeproperty(dbus.UInt16, "Timeout",
1287
timedelta_to_milliseconds)
942
_timedelta_to_milliseconds)
1288
943
extended_timeout = notifychangeproperty(
1289
dbus.UInt64, "ExtendedTimeout",
1290
type_func = timedelta_to_milliseconds)
1291
interval = notifychangeproperty(dbus.UInt64,
944
dbus.UInt16, "ExtendedTimeout",
945
type_func = _timedelta_to_milliseconds)
946
interval = notifychangeproperty(dbus.UInt16,
1294
timedelta_to_milliseconds)
949
_timedelta_to_milliseconds)
1295
950
checker_command = notifychangeproperty(dbus.String, "Checker")
1297
952
del notifychangeproperty
2358
2035
client_class = Client
2360
client_class = functools.partial(ClientDBus, bus = bus)
2362
client_settings = Client.config_parser(client_config)
2363
old_client_settings = {}
2366
# Get client data and settings from last running state.
2367
if server_settings["restore"]:
2369
with open(stored_state_path, "rb") as stored_state:
2370
clients_data, old_client_settings = (pickle.load
2372
os.remove(stored_state_path)
2373
except IOError as e:
2374
if e.errno == errno.ENOENT:
2375
logger.warning("Could not load persistent state: {0}"
2376
.format(os.strerror(e.errno)))
2378
logger.critical("Could not load persistent state:",
2381
except EOFError as e:
2382
logger.warning("Could not load persistent state: "
2383
"EOFError:", exc_info=e)
2385
with PGPEngine() as pgp:
2386
for client_name, client in clients_data.iteritems():
2387
# Decide which value to use after restoring saved state.
2388
# We have three different values: Old config file,
2389
# new config file, and saved state.
2390
# New config value takes precedence if it differs from old
2391
# config value, otherwise use saved state.
2392
for name, value in client_settings[client_name].items():
2394
# For each value in new config, check if it
2395
# differs from the old config value (Except for
2396
# the "secret" attribute)
2397
if (name != "secret" and
2398
value != old_client_settings[client_name]
2400
client[name] = value
2404
# Clients who has passed its expire date can still be
2405
# enabled if its last checker was successful. Clients
2406
# whose checker succeeded before we stored its state is
2407
# assumed to have successfully run all checkers during
2409
if client["enabled"]:
2410
if datetime.datetime.utcnow() >= client["expires"]:
2411
if not client["last_checked_ok"]:
2413
"disabling client {0} - Client never "
2414
"performed a successful checker"
2415
.format(client_name))
2416
client["enabled"] = False
2417
elif client["last_checker_status"] != 0:
2419
"disabling client {0} - Client "
2420
"last checker failed with error code {1}"
2421
.format(client_name,
2422
client["last_checker_status"]))
2423
client["enabled"] = False
2425
client["expires"] = (datetime.datetime
2427
+ client["timeout"])
2428
logger.debug("Last checker succeeded,"
2429
" keeping {0} enabled"
2430
.format(client_name))
2037
client_class = functools.partial(ClientDBusTransitional,
2039
def client_config_items(config, section):
2040
special_settings = {
2041
"approved_by_default":
2042
lambda: config.getboolean(section,
2043
"approved_by_default"),
2045
for name, value in config.items(section):
2432
client["secret"] = (
2433
pgp.decrypt(client["encrypted_secret"],
2434
client_settings[client_name]
2437
# If decryption fails, we use secret from new settings
2438
logger.debug("Failed to decrypt {0} old secret"
2439
.format(client_name))
2440
client["secret"] = (
2441
client_settings[client_name]["secret"])
2443
# Add/remove clients based on new changes made to config
2444
for client_name in (set(old_client_settings)
2445
- set(client_settings)):
2446
del clients_data[client_name]
2447
for client_name in (set(client_settings)
2448
- set(old_client_settings)):
2449
clients_data[client_name] = client_settings[client_name]
2451
# Create all client objects
2452
for client_name, client in clients_data.iteritems():
2453
tcp_server.clients[client_name] = client_class(
2454
name = client_name, settings = client)
2047
yield (name, special_settings[name]())
2051
tcp_server.clients.update(set(
2052
client_class(name = section,
2053
config= dict(client_config_items(
2054
client_config, section)))
2055
for section in client_config.sections()))
2456
2056
if not tcp_server.clients:
2457
2057
logger.warning("No clients defined")
2539
mandos_dbus_service = MandosDBusService()
2131
class MandosDBusServiceTransitional(MandosDBusService):
2132
__metaclass__ = AlternateDBusNamesMetaclass
2133
mandos_dbus_service = MandosDBusServiceTransitional()
2542
2136
"Cleanup function; run on exit"
2543
2137
service.cleanup()
2545
2139
multiprocessing.active_children()
2546
if not (tcp_server.clients or client_settings):
2549
# Store client before exiting. Secrets are encrypted with key
2550
# based on what config file has. If config file is
2551
# removed/edited, old secret will thus be unrecovable.
2553
with PGPEngine() as pgp:
2554
for client in tcp_server.clients.itervalues():
2555
key = client_settings[client.name]["secret"]
2556
client.encrypted_secret = pgp.encrypt(client.secret,
2560
# A list of attributes that can not be pickled
2562
exclude = set(("bus", "changedstate", "secret",
2564
for name, typ in (inspect.getmembers
2565
(dbus.service.Object)):
2568
client_dict["encrypted_secret"] = (client
2570
for attr in client.client_structure:
2571
if attr not in exclude:
2572
client_dict[attr] = getattr(client, attr)
2574
clients[client.name] = client_dict
2575
del client_settings[client.name]["secret"]
2578
with (tempfile.NamedTemporaryFile
2579
(mode='wb', suffix=".pickle", prefix='clients-',
2580
dir=os.path.dirname(stored_state_path),
2581
delete=False)) as stored_state:
2582
pickle.dump((clients, client_settings), stored_state)
2583
tempname=stored_state.name
2584
os.rename(tempname, stored_state_path)
2585
except (IOError, OSError) as e:
2591
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2592
logger.warning("Could not save persistent state: {0}"
2593
.format(os.strerror(e.errno)))
2595
logger.warning("Could not save persistent state:",
2599
# Delete all clients, and settings from config
2600
2140
while tcp_server.clients:
2601
name, client = tcp_server.clients.popitem()
2141
client = tcp_server.clients.pop()
2603
2143
client.remove_from_connection()
2144
client.disable_hook = None
2604
2145
# Don't signal anything except ClientRemoved
2605
2146
client.disable(quiet=True)