81
88
except ImportError:
82
89
SO_BINDTODEVICE = None
87
#logger = logging.getLogger('mandos')
88
logger = logging.Logger('mandos')
92
stored_state_file = "clients.pickle"
94
logger = logging.getLogger()
89
95
syslogger = (logging.handlers.SysLogHandler
90
96
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
91
97
address = str("/dev/log")))
92
syslogger.setFormatter(logging.Formatter
93
('Mandos [%(process)d]: %(levelname)s:'
95
logger.addHandler(syslogger)
97
console = logging.StreamHandler()
98
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
101
logger.addHandler(console)
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
103
214
class AvahiError(Exception):
104
215
def __init__(self, value, *args, **kwargs):
210
328
elif state == avahi.ENTRY_GROUP_FAILURE:
211
329
logger.critical("Avahi: Error in group state changed %s",
213
raise AvahiGroupError("State changed: %s"
331
raise AvahiGroupError("State changed: {0!s}"
215
334
def cleanup(self):
216
335
"""Derived from the Avahi example code"""
217
336
if self.group is not None:
339
except (dbus.exceptions.UnknownMethodException,
340
dbus.exceptions.DBusException):
219
342
self.group = None
220
def server_state_changed(self, state):
345
def server_state_changed(self, state, error=None):
221
346
"""Derived from the Avahi example code"""
222
347
logger.debug("Avahi server state change: %i", state)
223
if state == avahi.SERVER_COLLISION:
224
logger.error("Zeroconf server name collision")
348
bad_states = { avahi.SERVER_INVALID:
349
"Zeroconf server invalid",
350
avahi.SERVER_REGISTERING: None,
351
avahi.SERVER_COLLISION:
352
"Zeroconf server name collision",
353
avahi.SERVER_FAILURE:
354
"Zeroconf server failure" }
355
if state in bad_states:
356
if bad_states[state] is not None:
358
logger.error(bad_states[state])
360
logger.error(bad_states[state] + ": %r", error)
226
362
elif state == avahi.SERVER_RUNNING:
366
logger.debug("Unknown state: %r", state)
368
logger.debug("Unknown state: %r: %r", state, error)
228
370
def activate(self):
229
371
"""Derived from the Avahi example code"""
230
372
if self.server is None:
231
373
self.server = dbus.Interface(
232
374
self.bus.get_object(avahi.DBUS_NAME,
233
avahi.DBUS_PATH_SERVER),
375
avahi.DBUS_PATH_SERVER,
376
follow_name_owner_changes=True),
234
377
avahi.DBUS_INTERFACE_SERVER)
235
378
self.server.connect_to_signal("StateChanged",
236
379
self.server_state_changed)
237
380
self.server_state_changed(self.server.GetState())
383
class AvahiServiceToSyslog(AvahiService):
385
"""Add the new name to the syslog messages"""
386
ret = AvahiService.rename(self)
387
syslogger.setFormatter(logging.Formatter
388
('Mandos ({0}) [%(process)d]:'
389
' %(levelname)s: %(message)s'
394
def timedelta_to_milliseconds(td):
395
"Convert a datetime.timedelta() to milliseconds"
396
return ((td.days * 24 * 60 * 60 * 1000)
397
+ (td.seconds * 1000)
398
+ (td.microseconds // 1000))
240
401
class Client(object):
241
402
"""A representation of a client host served by this server.
244
_approved: bool(); 'None' if not yet approved/disapproved
405
approved: bool(); 'None' if not yet approved/disapproved
245
406
approval_delay: datetime.timedelta(); Time to wait for approval
246
407
approval_duration: datetime.timedelta(); Duration of one approval
247
408
checker: subprocess.Popen(); a running checker process used
264
426
interval: datetime.timedelta(); How often to start a new checker
265
427
last_approval_request: datetime.datetime(); (UTC) or None
266
428
last_checked_ok: datetime.datetime(); (UTC) or None
267
last_enabled: datetime.datetime(); (UTC)
429
last_checker_status: integer between 0 and 255 reflecting exit
430
status of last checker. -1 reflects crashed
431
checker, -2 means no checker completed yet.
432
last_enabled: datetime.datetime(); (UTC) or None
268
433
name: string; from the config file, used in log messages and
269
434
D-Bus identifiers
270
435
secret: bytestring; sent verbatim (over TLS) to client
271
436
timeout: datetime.timedelta(); How long from last_checked_ok
272
437
until this client is disabled
438
extended_timeout: extra long timeout when secret has been sent
273
439
runtime_expansions: Allowed attributes for runtime expansion.
440
expires: datetime.datetime(); time (UTC) when a client will be
276
444
runtime_expansions = ("approval_delay", "approval_duration",
277
"created", "enabled", "fingerprint",
278
"host", "interval", "last_checked_ok",
445
"created", "enabled", "expires",
446
"fingerprint", "host", "interval",
447
"last_approval_request", "last_checked_ok",
279
448
"last_enabled", "name", "timeout")
282
def _timedelta_to_milliseconds(td):
283
"Convert a datetime.timedelta() to milliseconds"
284
return ((td.days * 24 * 60 * 60 * 1000)
285
+ (td.seconds * 1000)
286
+ (td.microseconds // 1000))
449
client_defaults = { "timeout": "5m",
450
"extended_timeout": "15m",
452
"checker": "fping -q -- %%(host)s",
454
"approval_delay": "0s",
455
"approval_duration": "1s",
456
"approved_by_default": "True",
288
460
def timeout_milliseconds(self):
289
461
"Return the 'timeout' attribute in milliseconds"
290
return self._timedelta_to_milliseconds(self.timeout)
462
return timedelta_to_milliseconds(self.timeout)
464
def extended_timeout_milliseconds(self):
465
"Return the 'extended_timeout' attribute in milliseconds"
466
return timedelta_to_milliseconds(self.extended_timeout)
292
468
def interval_milliseconds(self):
293
469
"Return the 'interval' attribute in milliseconds"
294
return self._timedelta_to_milliseconds(self.interval)
470
return timedelta_to_milliseconds(self.interval)
296
472
def approval_delay_milliseconds(self):
297
return self._timedelta_to_milliseconds(self.approval_delay)
299
def __init__(self, name = None, disable_hook=None, config=None):
300
"""Note: the 'checker' key in 'config' sets the
301
'checker_command' attribute and *not* the 'checker'
473
return timedelta_to_milliseconds(self.approval_delay)
476
def config_parser(config):
477
"""Construct a new dict of client settings of this form:
478
{ client_name: {setting_name: value, ...}, ...}
479
with exceptions for any special settings as defined above.
480
NOTE: Must be a pure function. Must return the same result
481
value given the same arguments.
484
for client_name in config.sections():
485
section = dict(config.items(client_name))
486
client = settings[client_name] = {}
488
client["host"] = section["host"]
489
# Reformat values from string types to Python types
490
client["approved_by_default"] = config.getboolean(
491
client_name, "approved_by_default")
492
client["enabled"] = config.getboolean(client_name,
495
client["fingerprint"] = (section["fingerprint"].upper()
497
if "secret" in section:
498
client["secret"] = section["secret"].decode("base64")
499
elif "secfile" in section:
500
with open(os.path.expanduser(os.path.expandvars
501
(section["secfile"])),
503
client["secret"] = secfile.read()
505
raise TypeError("No secret or secfile for section {0}"
507
client["timeout"] = string_to_delta(section["timeout"])
508
client["extended_timeout"] = string_to_delta(
509
section["extended_timeout"])
510
client["interval"] = string_to_delta(section["interval"])
511
client["approval_delay"] = string_to_delta(
512
section["approval_delay"])
513
client["approval_duration"] = string_to_delta(
514
section["approval_duration"])
515
client["checker_command"] = section["checker"]
516
client["last_approval_request"] = None
517
client["last_checked_ok"] = None
518
client["last_checker_status"] = -2
522
def __init__(self, settings, name = None):
524
# adding all client settings
525
for setting, value in settings.iteritems():
526
setattr(self, setting, value)
529
if not hasattr(self, "last_enabled"):
530
self.last_enabled = datetime.datetime.utcnow()
531
if not hasattr(self, "expires"):
532
self.expires = (datetime.datetime.utcnow()
535
self.last_enabled = None
306
538
logger.debug("Creating client %r", self.name)
307
539
# Uppercase and remove spaces from fingerprint for later
308
540
# comparison purposes with return value from the fingerprint()
310
self.fingerprint = (config["fingerprint"].upper()
312
542
logger.debug(" Fingerprint: %s", self.fingerprint)
313
if "secret" in config:
314
self.secret = config["secret"].decode("base64")
315
elif "secfile" in config:
316
with open(os.path.expanduser(os.path.expandvars
317
(config["secfile"])),
319
self.secret = secfile.read()
321
raise TypeError("No secret or secfile for client %s"
323
self.host = config.get("host", "")
324
self.created = datetime.datetime.utcnow()
326
self.last_approval_request = None
327
self.last_enabled = None
328
self.last_checked_ok = None
329
self.timeout = string_to_delta(config["timeout"])
330
self.interval = string_to_delta(config["interval"])
331
self.disable_hook = disable_hook
543
self.created = settings.get("created",
544
datetime.datetime.utcnow())
546
# attributes specific for this server instance
332
547
self.checker = None
333
548
self.checker_initiator_tag = None
334
549
self.disable_initiator_tag = None
335
550
self.checker_callback_tag = None
336
self.checker_command = config["checker"]
337
551
self.current_checker_command = None
338
self.last_connect = None
339
self._approved = None
340
self.approved_by_default = config.get("approved_by_default",
342
553
self.approvals_pending = 0
343
self.approval_delay = string_to_delta(
344
config["approval_delay"])
345
self.approval_duration = string_to_delta(
346
config["approval_duration"])
347
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
554
self.changedstate = (multiprocessing_manager
555
.Condition(multiprocessing_manager
557
self.client_structure = [attr for attr in
558
self.__dict__.iterkeys()
559
if not attr.startswith("_")]
560
self.client_structure.append("client_structure")
562
for name, t in inspect.getmembers(type(self),
566
if not name.startswith("_"):
567
self.client_structure.append(name)
569
# Send notice to process children that client state has changed
349
570
def send_changedstate(self):
350
self.changedstate.acquire()
351
self.changedstate.notify_all()
352
self.changedstate.release()
571
with self.changedstate:
572
self.changedstate.notify_all()
354
574
def enable(self):
355
575
"""Start this client's checker and timeout hooks"""
356
576
if getattr(self, "enabled", False):
357
577
# Already enabled
359
self.send_changedstate()
579
self.expires = datetime.datetime.utcnow() + self.timeout
360
581
self.last_enabled = datetime.datetime.utcnow()
583
self.send_changedstate()
585
def disable(self, quiet=True):
586
"""Disable this client."""
587
if not getattr(self, "enabled", False):
590
logger.info("Disabling client %s", self.name)
591
if getattr(self, "disable_initiator_tag", None) is not None:
592
gobject.source_remove(self.disable_initiator_tag)
593
self.disable_initiator_tag = None
595
if getattr(self, "checker_initiator_tag", None) is not None:
596
gobject.source_remove(self.checker_initiator_tag)
597
self.checker_initiator_tag = None
601
self.send_changedstate()
602
# Do not run this again if called by a gobject.timeout_add
608
def init_checker(self):
361
609
# Schedule a new checker to be started an 'interval' from now,
362
610
# and every interval from then on.
611
if self.checker_initiator_tag is not None:
612
gobject.source_remove(self.checker_initiator_tag)
363
613
self.checker_initiator_tag = (gobject.timeout_add
364
614
(self.interval_milliseconds(),
365
615
self.start_checker))
366
616
# Schedule a disable() when 'timeout' has passed
617
if self.disable_initiator_tag is not None:
618
gobject.source_remove(self.disable_initiator_tag)
367
619
self.disable_initiator_tag = (gobject.timeout_add
368
620
(self.timeout_milliseconds(),
371
622
# Also start a new checker *right now*.
372
623
self.start_checker()
374
def disable(self, quiet=True):
375
"""Disable this client."""
376
if not getattr(self, "enabled", False):
379
self.send_changedstate()
381
logger.info("Disabling client %s", self.name)
382
if getattr(self, "disable_initiator_tag", False):
383
gobject.source_remove(self.disable_initiator_tag)
384
self.disable_initiator_tag = None
385
if getattr(self, "checker_initiator_tag", False):
386
gobject.source_remove(self.checker_initiator_tag)
387
self.checker_initiator_tag = None
389
if self.disable_hook:
390
self.disable_hook(self)
392
# Do not run this again if called by a gobject.timeout_add
396
self.disable_hook = None
399
625
def checker_callback(self, pid, condition, command):
400
626
"""The checker has completed, so take appropriate actions."""
401
627
self.checker_callback_tag = None
402
628
self.checker = None
403
629
if os.WIFEXITED(condition):
404
exitstatus = os.WEXITSTATUS(condition)
630
self.last_checker_status = os.WEXITSTATUS(condition)
631
if self.last_checker_status == 0:
406
632
logger.info("Checker for %(name)s succeeded",
408
634
self.checked_ok()
573
837
class DBusObjectWithProperties(dbus.service.Object):
574
838
"""A D-Bus object with properties.
576
840
Classes inheriting from this can use the dbus_service_property
577
841
decorator to expose methods as D-Bus properties. It exposes the
578
842
standard Get(), Set(), and GetAll() methods on the D-Bus.
582
def _is_dbus_property(obj):
583
return getattr(obj, "_dbus_is_property", False)
846
def _is_dbus_thing(thing):
847
"""Returns a function testing if an attribute is a D-Bus thing
849
If called like _is_dbus_thing("method") it returns a function
850
suitable for use as predicate to inspect.getmembers().
852
return lambda obj: getattr(obj, "_dbus_is_{0}".format(thing),
585
def _get_all_dbus_properties(self):
855
def _get_all_dbus_things(self, thing):
586
856
"""Returns a generator of (name, attribute) pairs
588
return ((prop._dbus_name, prop)
590
inspect.getmembers(self, self._is_dbus_property))
858
return ((getattr(athing.__get__(self), "_dbus_name",
860
athing.__get__(self))
861
for cls in self.__class__.__mro__
863
inspect.getmembers(cls,
864
self._is_dbus_thing(thing)))
592
866
def _get_dbus_property(self, interface_name, property_name):
593
867
"""Returns a bound method if one exists which is a D-Bus
594
868
property with the specified name and interface.
596
for name in (property_name,
597
property_name + "_dbus_property"):
598
prop = getattr(self, name, None)
600
or not self._is_dbus_property(prop)
601
or prop._dbus_name != property_name
602
or (interface_name and prop._dbus_interface
603
and interface_name != prop._dbus_interface)):
870
for cls in self.__class__.__mro__:
871
for name, value in (inspect.getmembers
873
self._is_dbus_thing("property"))):
874
if (value._dbus_name == property_name
875
and value._dbus_interface == interface_name):
876
return value.__get__(self)
606
878
# No such property
607
879
raise DBusPropertyNotFound(self.dbus_object_path + ":"
608
880
+ interface_name + "."
704
1010
xmlstring = document.toxml("utf-8")
705
1011
document.unlink()
706
1012
except (AttributeError, xml.dom.DOMException,
707
xml.parsers.expat.ExpatError), error:
1013
xml.parsers.expat.ExpatError) as error:
708
1014
logger.error("Failed to override Introspection method",
710
1016
return xmlstring
1019
def datetime_to_dbus(dt, variant_level=0):
1020
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1022
return dbus.String("", variant_level = variant_level)
1023
return dbus.String(dt.isoformat(),
1024
variant_level=variant_level)
1027
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1028
"""A class decorator; applied to a subclass of
1029
dbus.service.Object, it will add alternate D-Bus attributes with
1030
interface names according to the "alt_interface_names" mapping.
1033
@alternate_dbus_interfaces({"org.example.Interface":
1034
"net.example.AlternateInterface"})
1035
class SampleDBusObject(dbus.service.Object):
1036
@dbus.service.method("org.example.Interface")
1037
def SampleDBusMethod():
1040
The above "SampleDBusMethod" on "SampleDBusObject" will be
1041
reachable via two interfaces: "org.example.Interface" and
1042
"net.example.AlternateInterface", the latter of which will have
1043
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1044
"true", unless "deprecate" is passed with a False value.
1046
This works for methods and signals, and also for D-Bus properties
1047
(from DBusObjectWithProperties) and interfaces (from the
1048
dbus_interface_annotations decorator).
1051
for orig_interface_name, alt_interface_name in (
1052
alt_interface_names.iteritems()):
1054
interface_names = set()
1055
# Go though all attributes of the class
1056
for attrname, attribute in inspect.getmembers(cls):
1057
# Ignore non-D-Bus attributes, and D-Bus attributes
1058
# with the wrong interface name
1059
if (not hasattr(attribute, "_dbus_interface")
1060
or not attribute._dbus_interface
1061
.startswith(orig_interface_name)):
1063
# Create an alternate D-Bus interface name based on
1065
alt_interface = (attribute._dbus_interface
1066
.replace(orig_interface_name,
1067
alt_interface_name))
1068
interface_names.add(alt_interface)
1069
# Is this a D-Bus signal?
1070
if getattr(attribute, "_dbus_is_signal", False):
1071
# Extract the original non-method function by
1073
nonmethod_func = (dict(
1074
zip(attribute.func_code.co_freevars,
1075
attribute.__closure__))["func"]
1077
# Create a new, but exactly alike, function
1078
# object, and decorate it to be a new D-Bus signal
1079
# with the alternate D-Bus interface name
1080
new_function = (dbus.service.signal
1082
attribute._dbus_signature)
1083
(types.FunctionType(
1084
nonmethod_func.func_code,
1085
nonmethod_func.func_globals,
1086
nonmethod_func.func_name,
1087
nonmethod_func.func_defaults,
1088
nonmethod_func.func_closure)))
1089
# Copy annotations, if any
1091
new_function._dbus_annotations = (
1092
dict(attribute._dbus_annotations))
1093
except AttributeError:
1095
# Define a creator of a function to call both the
1096
# original and alternate functions, so both the
1097
# original and alternate signals gets sent when
1098
# the function is called
1099
def fixscope(func1, func2):
1100
"""This function is a scope container to pass
1101
func1 and func2 to the "call_both" function
1102
outside of its arguments"""
1103
def call_both(*args, **kwargs):
1104
"""This function will emit two D-Bus
1105
signals by calling func1 and func2"""
1106
func1(*args, **kwargs)
1107
func2(*args, **kwargs)
1109
# Create the "call_both" function and add it to
1111
attr[attrname] = fixscope(attribute, new_function)
1112
# Is this a D-Bus method?
1113
elif getattr(attribute, "_dbus_is_method", False):
1114
# Create a new, but exactly alike, function
1115
# object. Decorate it to be a new D-Bus method
1116
# with the alternate D-Bus interface name. Add it
1118
attr[attrname] = (dbus.service.method
1120
attribute._dbus_in_signature,
1121
attribute._dbus_out_signature)
1123
(attribute.func_code,
1124
attribute.func_globals,
1125
attribute.func_name,
1126
attribute.func_defaults,
1127
attribute.func_closure)))
1128
# Copy annotations, if any
1130
attr[attrname]._dbus_annotations = (
1131
dict(attribute._dbus_annotations))
1132
except AttributeError:
1134
# Is this a D-Bus property?
1135
elif getattr(attribute, "_dbus_is_property", False):
1136
# Create a new, but exactly alike, function
1137
# object, and decorate it to be a new D-Bus
1138
# property with the alternate D-Bus interface
1139
# name. Add it to the class.
1140
attr[attrname] = (dbus_service_property
1142
attribute._dbus_signature,
1143
attribute._dbus_access,
1145
._dbus_get_args_options
1148
(attribute.func_code,
1149
attribute.func_globals,
1150
attribute.func_name,
1151
attribute.func_defaults,
1152
attribute.func_closure)))
1153
# Copy annotations, if any
1155
attr[attrname]._dbus_annotations = (
1156
dict(attribute._dbus_annotations))
1157
except AttributeError:
1159
# Is this a D-Bus interface?
1160
elif getattr(attribute, "_dbus_is_interface", False):
1161
# Create a new, but exactly alike, function
1162
# object. Decorate it to be a new D-Bus interface
1163
# with the alternate D-Bus interface name. Add it
1165
attr[attrname] = (dbus_interface_annotations
1168
(attribute.func_code,
1169
attribute.func_globals,
1170
attribute.func_name,
1171
attribute.func_defaults,
1172
attribute.func_closure)))
1174
# Deprecate all alternate interfaces
1175
iname="_AlternateDBusNames_interface_annotation{0}"
1176
for interface_name in interface_names:
1177
@dbus_interface_annotations(interface_name)
1179
return { "org.freedesktop.DBus.Deprecated":
1181
# Find an unused name
1182
for aname in (iname.format(i)
1183
for i in itertools.count()):
1184
if aname not in attr:
1188
# Replace the class with a new subclass of it with
1189
# methods, signals, etc. as created above.
1190
cls = type(b"{0}Alternate".format(cls.__name__),
1196
@alternate_dbus_interfaces({"se.recompile.Mandos":
1197
"se.bsnet.fukt.Mandos"})
713
1198
class ClientDBus(Client, DBusObjectWithProperties):
714
1199
"""A Client class using D-Bus
736
1220
("/clients/" + client_object_name))
737
1221
DBusObjectWithProperties.__init__(self, self.bus,
738
1222
self.dbus_object_path)
740
def _get_approvals_pending(self):
741
return self._approvals_pending
742
def _set_approvals_pending(self, value):
743
old_value = self._approvals_pending
744
self._approvals_pending = value
746
if (hasattr(self, "dbus_object_path")
747
and bval is not bool(old_value)):
748
dbus_bool = dbus.Boolean(bval, variant_level=1)
749
self.PropertyChanged(dbus.String("ApprovalPending"),
752
approvals_pending = property(_get_approvals_pending,
753
_set_approvals_pending)
754
del _get_approvals_pending, _set_approvals_pending
757
def _datetime_to_dbus(dt, variant_level=0):
758
"""Convert a UTC datetime.datetime() to a D-Bus type."""
759
return dbus.String(dt.isoformat(),
760
variant_level=variant_level)
763
oldstate = getattr(self, "enabled", False)
764
r = Client.enable(self)
765
if oldstate != self.enabled:
767
self.PropertyChanged(dbus.String("Enabled"),
768
dbus.Boolean(True, variant_level=1))
769
self.PropertyChanged(
770
dbus.String("LastEnabled"),
771
self._datetime_to_dbus(self.last_enabled,
775
def disable(self, quiet = False):
776
oldstate = getattr(self, "enabled", False)
777
r = Client.disable(self, quiet=quiet)
778
if not quiet and oldstate != self.enabled:
780
self.PropertyChanged(dbus.String("Enabled"),
781
dbus.Boolean(False, variant_level=1))
1224
def notifychangeproperty(transform_func,
1225
dbus_name, type_func=lambda x: x,
1227
""" Modify a variable so that it's a property which announces
1228
its changes to DBus.
1230
transform_fun: Function that takes a value and a variant_level
1231
and transforms it to a D-Bus type.
1232
dbus_name: D-Bus name of the variable
1233
type_func: Function that transform the value before sending it
1234
to the D-Bus. Default: no transform
1235
variant_level: D-Bus variant level. Default: 1
1237
attrname = "_{0}".format(dbus_name)
1238
def setter(self, value):
1239
if hasattr(self, "dbus_object_path"):
1240
if (not hasattr(self, attrname) or
1241
type_func(getattr(self, attrname, None))
1242
!= type_func(value)):
1243
dbus_value = transform_func(type_func(value),
1246
self.PropertyChanged(dbus.String(dbus_name),
1248
setattr(self, attrname, value)
1250
return property(lambda self: getattr(self, attrname), setter)
1252
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1253
approvals_pending = notifychangeproperty(dbus.Boolean,
1256
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1257
last_enabled = notifychangeproperty(datetime_to_dbus,
1259
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
1260
type_func = lambda checker:
1261
checker is not None)
1262
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1264
last_checker_status = notifychangeproperty(dbus.Int16,
1265
"LastCheckerStatus")
1266
last_approval_request = notifychangeproperty(
1267
datetime_to_dbus, "LastApprovalRequest")
1268
approved_by_default = notifychangeproperty(dbus.Boolean,
1269
"ApprovedByDefault")
1270
approval_delay = notifychangeproperty(dbus.UInt64,
1273
timedelta_to_milliseconds)
1274
approval_duration = notifychangeproperty(
1275
dbus.UInt64, "ApprovalDuration",
1276
type_func = timedelta_to_milliseconds)
1277
host = notifychangeproperty(dbus.String, "Host")
1278
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
1280
timedelta_to_milliseconds)
1281
extended_timeout = notifychangeproperty(
1282
dbus.UInt64, "ExtendedTimeout",
1283
type_func = timedelta_to_milliseconds)
1284
interval = notifychangeproperty(dbus.UInt64,
1287
timedelta_to_milliseconds)
1288
checker_command = notifychangeproperty(dbus.String, "Checker")
1290
del notifychangeproperty
784
1292
def __del__(self, *args, **kwargs):
1487
1999
def __init__(self, server_address, RequestHandlerClass,
1488
2000
interface=None, use_ipv6=True, clients=None,
1489
gnutls_priority=None, use_dbus=True):
2001
gnutls_priority=None, use_dbus=True, socketfd=None):
1490
2002
self.enabled = False
1491
2003
self.clients = clients
1492
2004
if self.clients is None:
1493
self.clients = set()
1494
2006
self.use_dbus = use_dbus
1495
2007
self.gnutls_priority = gnutls_priority
1496
2008
IPv6_TCPServer.__init__(self, server_address,
1497
2009
RequestHandlerClass,
1498
2010
interface = interface,
1499
use_ipv6 = use_ipv6)
2011
use_ipv6 = use_ipv6,
2012
socketfd = socketfd)
1500
2013
def server_activate(self):
1501
2014
if self.enabled:
1502
2015
return socketserver.TCPServer.server_activate(self)
1503
2017
def enable(self):
1504
2018
self.enabled = True
1505
def add_pipe(self, parent_pipe):
2020
def add_pipe(self, parent_pipe, proc):
1506
2021
# Call "handle_ipc" for both data and EOF events
1507
2022
gobject.io_add_watch(parent_pipe.fileno(),
1508
2023
gobject.IO_IN | gobject.IO_HUP,
1509
2024
functools.partial(self.handle_ipc,
1510
parent_pipe = parent_pipe))
1512
2029
def handle_ipc(self, source, condition, parent_pipe=None,
1513
client_object=None):
1515
gobject.IO_IN: "IN", # There is data to read.
1516
gobject.IO_OUT: "OUT", # Data can be written (without
1518
gobject.IO_PRI: "PRI", # There is urgent data to read.
1519
gobject.IO_ERR: "ERR", # Error condition.
1520
gobject.IO_HUP: "HUP" # Hung up (the connection has been
1521
# broken, usually for pipes and
1524
conditions_string = ' | '.join(name
1526
condition_names.iteritems()
1527
if cond & condition)
1528
# error or the other end of multiprocessing.Pipe has closed
1529
if condition & (gobject.IO_ERR | condition & gobject.IO_HUP):
2030
proc = None, client_object=None):
2031
# error, or the other end of multiprocessing.Pipe has closed
2032
if condition & (gobject.IO_ERR | gobject.IO_HUP):
2033
# Wait for other process to exit
1532
2037
# Read a request from the child
1673
2163
##################################################################
1674
2164
# Parsing of options, both command line and config file
1676
parser = optparse.OptionParser(version = "%%prog %s" % version)
1677
parser.add_option("-i", "--interface", type="string",
1678
metavar="IF", help="Bind to interface IF")
1679
parser.add_option("-a", "--address", type="string",
1680
help="Address to listen for requests on")
1681
parser.add_option("-p", "--port", type="int",
1682
help="Port number to receive requests on")
1683
parser.add_option("--check", action="store_true",
1684
help="Run self-test")
1685
parser.add_option("--debug", action="store_true",
1686
help="Debug mode; run in foreground and log to"
1688
parser.add_option("--debuglevel", type="string", metavar="LEVEL",
1689
help="Debug level for stdout output")
1690
parser.add_option("--priority", type="string", help="GnuTLS"
1691
" priority string (see GnuTLS documentation)")
1692
parser.add_option("--servicename", type="string",
1693
metavar="NAME", help="Zeroconf service name")
1694
parser.add_option("--configdir", type="string",
1695
default="/etc/mandos", metavar="DIR",
1696
help="Directory to search for configuration"
1698
parser.add_option("--no-dbus", action="store_false",
1699
dest="use_dbus", help="Do not provide D-Bus"
1700
" system bus interface")
1701
parser.add_option("--no-ipv6", action="store_false",
1702
dest="use_ipv6", help="Do not use IPv6")
1703
options = parser.parse_args()[0]
2166
parser = argparse.ArgumentParser()
2167
parser.add_argument("-v", "--version", action="version",
2168
version = "%(prog)s {0}".format(version),
2169
help="show version number and exit")
2170
parser.add_argument("-i", "--interface", metavar="IF",
2171
help="Bind to interface IF")
2172
parser.add_argument("-a", "--address",
2173
help="Address to listen for requests on")
2174
parser.add_argument("-p", "--port", type=int,
2175
help="Port number to receive requests on")
2176
parser.add_argument("--check", action="store_true",
2177
help="Run self-test")
2178
parser.add_argument("--debug", action="store_true",
2179
help="Debug mode; run in foreground and log"
2181
parser.add_argument("--debuglevel", metavar="LEVEL",
2182
help="Debug level for stdout output")
2183
parser.add_argument("--priority", help="GnuTLS"
2184
" priority string (see GnuTLS documentation)")
2185
parser.add_argument("--servicename",
2186
metavar="NAME", help="Zeroconf service name")
2187
parser.add_argument("--configdir",
2188
default="/etc/mandos", metavar="DIR",
2189
help="Directory to search for configuration"
2191
parser.add_argument("--no-dbus", action="store_false",
2192
dest="use_dbus", help="Do not provide D-Bus"
2193
" system bus interface")
2194
parser.add_argument("--no-ipv6", action="store_false",
2195
dest="use_ipv6", help="Do not use IPv6")
2196
parser.add_argument("--no-restore", action="store_false",
2197
dest="restore", help="Do not restore stored"
2199
parser.add_argument("--socket", type=int,
2200
help="Specify a file descriptor to a network"
2201
" socket to use instead of creating one")
2202
parser.add_argument("--statedir", metavar="DIR",
2203
help="Directory to save/restore state in")
2205
options = parser.parse_args()
1705
2207
if options.check:
1840
2352
.gnutls_global_set_log_function(debug_gnutls))
1842
2354
# Redirect stdin so all checkers get /dev/null
1843
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
2355
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
1844
2356
os.dup2(null, sys.stdin.fileno())
1848
# No console logging
1849
logger.removeHandler(console)
1851
2360
# Need to fork before connecting to D-Bus
1853
2362
# Close all input and output, do double fork, etc.
2365
gobject.threads_init()
1856
2367
global main_loop
1857
2368
# From the Avahi example code
1858
DBusGMainLoop(set_as_default=True )
2369
DBusGMainLoop(set_as_default=True)
1859
2370
main_loop = gobject.MainLoop()
1860
2371
bus = dbus.SystemBus()
1861
2372
# End of Avahi example code
1864
bus_name = dbus.service.BusName("se.bsnet.fukt.Mandos",
2375
bus_name = dbus.service.BusName("se.recompile.Mandos",
1865
2376
bus, do_not_queue=True)
1866
except dbus.exceptions.NameExistsException, e:
1867
logger.error(unicode(e) + ", disabling D-Bus")
2377
old_bus_name = (dbus.service.BusName
2378
("se.bsnet.fukt.Mandos", bus,
2380
except dbus.exceptions.NameExistsException as e:
2381
logger.error("Disabling D-Bus:", exc_info=e)
1868
2382
use_dbus = False
1869
2383
server_settings["use_dbus"] = False
1870
2384
tcp_server.use_dbus = False
1871
2385
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1872
service = AvahiService(name = server_settings["servicename"],
1873
servicetype = "_mandos._tcp",
1874
protocol = protocol, bus = bus)
2386
service = AvahiServiceToSyslog(name =
2387
server_settings["servicename"],
2388
servicetype = "_mandos._tcp",
2389
protocol = protocol, bus = bus)
1875
2390
if server_settings["interface"]:
1876
2391
service.interface = (if_nametoindex
1877
2392
(str(server_settings["interface"])))
1882
2397
client_class = Client
1884
2399
client_class = functools.partial(ClientDBus, bus = bus)
1885
def client_config_items(config, section):
1886
special_settings = {
1887
"approved_by_default":
1888
lambda: config.getboolean(section,
1889
"approved_by_default"),
1891
for name, value in config.items(section):
2401
client_settings = Client.config_parser(client_config)
2402
old_client_settings = {}
2405
# Get client data and settings from last running state.
2406
if server_settings["restore"]:
2408
with open(stored_state_path, "rb") as stored_state:
2409
clients_data, old_client_settings = (pickle.load
2411
os.remove(stored_state_path)
2412
except IOError as e:
2413
if e.errno == errno.ENOENT:
2414
logger.warning("Could not load persistent state: {0}"
2415
.format(os.strerror(e.errno)))
2417
logger.critical("Could not load persistent state:",
2420
except EOFError as e:
2421
logger.warning("Could not load persistent state: "
2422
"EOFError:", exc_info=e)
2424
with PGPEngine() as pgp:
2425
for client_name, client in clients_data.iteritems():
2426
# Decide which value to use after restoring saved state.
2427
# We have three different values: Old config file,
2428
# new config file, and saved state.
2429
# New config value takes precedence if it differs from old
2430
# config value, otherwise use saved state.
2431
for name, value in client_settings[client_name].items():
2433
# For each value in new config, check if it
2434
# differs from the old config value (Except for
2435
# the "secret" attribute)
2436
if (name != "secret" and
2437
value != old_client_settings[client_name]
2439
client[name] = value
2443
# Clients who has passed its expire date can still be
2444
# enabled if its last checker was successful. Clients
2445
# whose checker succeeded before we stored its state is
2446
# assumed to have successfully run all checkers during
2448
if client["enabled"]:
2449
if datetime.datetime.utcnow() >= client["expires"]:
2450
if not client["last_checked_ok"]:
2452
"disabling client {0} - Client never "
2453
"performed a successful checker"
2454
.format(client_name))
2455
client["enabled"] = False
2456
elif client["last_checker_status"] != 0:
2458
"disabling client {0} - Client "
2459
"last checker failed with error code {1}"
2460
.format(client_name,
2461
client["last_checker_status"]))
2462
client["enabled"] = False
2464
client["expires"] = (datetime.datetime
2466
+ client["timeout"])
2467
logger.debug("Last checker succeeded,"
2468
" keeping {0} enabled"
2469
.format(client_name))
1893
yield (name, special_settings[name]())
1897
tcp_server.clients.update(set(
1898
client_class(name = section,
1899
config= dict(client_config_items(
1900
client_config, section)))
1901
for section in client_config.sections()))
2471
client["secret"] = (
2472
pgp.decrypt(client["encrypted_secret"],
2473
client_settings[client_name]
2476
# If decryption fails, we use secret from new settings
2477
logger.debug("Failed to decrypt {0} old secret"
2478
.format(client_name))
2479
client["secret"] = (
2480
client_settings[client_name]["secret"])
2482
# Add/remove clients based on new changes made to config
2483
for client_name in (set(old_client_settings)
2484
- set(client_settings)):
2485
del clients_data[client_name]
2486
for client_name in (set(client_settings)
2487
- set(old_client_settings)):
2488
clients_data[client_name] = client_settings[client_name]
2490
# Create all client objects
2491
for client_name, client in clients_data.iteritems():
2492
tcp_server.clients[client_name] = client_class(
2493
name = client_name, settings = client)
1902
2495
if not tcp_server.clients:
1903
2496
logger.warning("No clients defined")
1980
2580
"Cleanup function; run on exit"
1981
2581
service.cleanup()
2583
multiprocessing.active_children()
2584
if not (tcp_server.clients or client_settings):
2587
# Store client before exiting. Secrets are encrypted with key
2588
# based on what config file has. If config file is
2589
# removed/edited, old secret will thus be unrecovable.
2591
with PGPEngine() as pgp:
2592
for client in tcp_server.clients.itervalues():
2593
key = client_settings[client.name]["secret"]
2594
client.encrypted_secret = pgp.encrypt(client.secret,
2598
# A list of attributes that can not be pickled
2600
exclude = set(("bus", "changedstate", "secret",
2602
for name, typ in (inspect.getmembers
2603
(dbus.service.Object)):
2606
client_dict["encrypted_secret"] = (client
2608
for attr in client.client_structure:
2609
if attr not in exclude:
2610
client_dict[attr] = getattr(client, attr)
2612
clients[client.name] = client_dict
2613
del client_settings[client.name]["secret"]
2616
with (tempfile.NamedTemporaryFile
2617
(mode='wb', suffix=".pickle", prefix='clients-',
2618
dir=os.path.dirname(stored_state_path),
2619
delete=False)) as stored_state:
2620
pickle.dump((clients, client_settings), stored_state)
2621
tempname=stored_state.name
2622
os.rename(tempname, stored_state_path)
2623
except (IOError, OSError) as e:
2629
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
2630
logger.warning("Could not save persistent state: {0}"
2631
.format(os.strerror(e.errno)))
2633
logger.warning("Could not save persistent state:",
2637
# Delete all clients, and settings from config
1983
2638
while tcp_server.clients:
1984
client = tcp_server.clients.pop()
2639
name, client = tcp_server.clients.popitem()
1986
2641
client.remove_from_connection()
1987
client.disable_hook = None
1988
2642
# Don't signal anything except ClientRemoved
1989
2643
client.disable(quiet=True)
1991
2645
# Emit D-Bus signal
1992
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
2646
mandos_dbus_service.ClientRemoved(client
2649
client_settings.clear()
1995
2651
atexit.register(cleanup)
1997
for client in tcp_server.clients:
2653
for client in tcp_server.clients.itervalues():
1999
2655
# Emit D-Bus signal
2000
2656
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2657
# Need to initiate checking of clients
2659
client.init_checker()
2003
2661
tcp_server.enable()
2004
2662
tcp_server.server_activate()