45
15
import gnutls.library.functions
46
16
import gnutls.library.constants
47
17
import gnutls.library.types
48
import ConfigParser as configparser
57
import logging.handlers
63
import cPickle as pickle
64
import multiprocessing
70
31
from dbus.mainloop.glib import DBusGMainLoop
73
import xml.dom.minidom
77
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
78
except AttributeError:
80
from IN import SO_BINDTODEVICE
82
SO_BINDTODEVICE = None
87
#logger = logging.getLogger('mandos')
35
import logging.handlers
88
37
logger = logging.Logger('mandos')
89
syslogger = (logging.handlers.SysLogHandler
90
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
91
address = str("/dev/log")))
92
syslogger.setFormatter(logging.Formatter
93
('Mandos [%(process)d]: %(levelname)s:'
38
syslogger = logging.handlers.SysLogHandler\
39
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
40
syslogger.setFormatter(logging.Formatter\
41
('%(levelname)s: %(message)s'))
95
42
logger.addHandler(syslogger)
97
console = logging.StreamHandler()
98
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
101
logger.addHandler(console)
103
class AvahiError(Exception):
104
def __init__(self, value, *args, **kwargs):
106
super(AvahiError, self).__init__(value, *args, **kwargs)
107
def __unicode__(self):
108
return unicode(repr(self.value))
110
class AvahiServiceError(AvahiError):
113
class AvahiGroupError(AvahiError):
117
class AvahiService(object):
118
"""An Avahi (Zeroconf) service.
121
interface: integer; avahi.IF_UNSPEC or an interface index.
122
Used to optionally bind to the specified interface.
123
name: string; Example: 'Mandos'
124
type: string; Example: '_mandos._tcp'.
125
See <http://www.dns-sd.org/ServiceTypes.html>
126
port: integer; what port to announce
127
TXT: list of strings; TXT record for the service
128
domain: string; Domain to publish on, default to .local if empty.
129
host: string; Host to publish records for, default is localhost
130
max_renames: integer; maximum number of renames
131
rename_count: integer; counter so we only rename after collisions
132
a sensible number of times
133
group: D-Bus Entry Group
135
bus: dbus.SystemBus()
137
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
138
servicetype = None, port = None, TXT = None,
139
domain = "", host = "", max_renames = 32768,
140
protocol = avahi.PROTO_UNSPEC, bus = None):
141
self.interface = interface
143
self.type = servicetype
145
self.TXT = TXT if TXT is not None else []
148
self.rename_count = 0
149
self.max_renames = max_renames
150
self.protocol = protocol
151
self.group = None # our entry group
154
self.entry_group_state_changed_match = None
156
"""Derived from the Avahi example code"""
157
if self.rename_count >= self.max_renames:
158
logger.critical("No suitable Zeroconf service name found"
159
" after %i retries, exiting.",
161
raise AvahiServiceError("Too many renames")
162
self.name = unicode(self.server.GetAlternativeServiceName(self.name))
163
logger.info("Changing Zeroconf service name to %r ...",
165
syslogger.setFormatter(logging.Formatter
166
('Mandos (%s) [%%(process)d]:'
167
' %%(levelname)s: %%(message)s'
172
except dbus.exceptions.DBusException as error:
173
logger.critical("DBusException: %s", error)
176
self.rename_count += 1
178
"""Derived from the Avahi example code"""
179
if self.entry_group_state_changed_match is not None:
180
self.entry_group_state_changed_match.remove()
181
self.entry_group_state_changed_match = None
182
if self.group is not None:
185
"""Derived from the Avahi example code"""
187
if self.group is None:
188
self.group = dbus.Interface(
189
self.bus.get_object(avahi.DBUS_NAME,
190
self.server.EntryGroupNew()),
191
avahi.DBUS_INTERFACE_ENTRY_GROUP)
192
self.entry_group_state_changed_match = (
193
self.group.connect_to_signal(
194
'StateChanged', self .entry_group_state_changed))
195
logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
196
self.name, self.type)
197
self.group.AddService(
200
dbus.UInt32(0), # flags
201
self.name, self.type,
202
self.domain, self.host,
203
dbus.UInt16(self.port),
204
avahi.string_array_to_txt_array(self.TXT))
206
def entry_group_state_changed(self, state, error):
207
"""Derived from the Avahi example code"""
208
logger.debug("Avahi entry group state change: %i", state)
210
if state == avahi.ENTRY_GROUP_ESTABLISHED:
211
logger.debug("Zeroconf service established.")
212
elif state == avahi.ENTRY_GROUP_COLLISION:
213
logger.info("Zeroconf service name collision.")
215
elif state == avahi.ENTRY_GROUP_FAILURE:
216
logger.critical("Avahi: Error in group state changed %s",
218
raise AvahiGroupError("State changed: %s"
221
"""Derived from the Avahi example code"""
222
if self.group is not None:
225
except (dbus.exceptions.UnknownMethodException,
226
dbus.exceptions.DBusException) as e:
230
def server_state_changed(self, state, error=None):
231
"""Derived from the Avahi example code"""
232
logger.debug("Avahi server state change: %i", state)
233
bad_states = { avahi.SERVER_INVALID:
234
"Zeroconf server invalid",
235
avahi.SERVER_REGISTERING: None,
236
avahi.SERVER_COLLISION:
237
"Zeroconf server name collision",
238
avahi.SERVER_FAILURE:
239
"Zeroconf server failure" }
240
if state in bad_states:
241
if bad_states[state] is not None:
243
logger.error(bad_states[state])
245
logger.error(bad_states[state] + ": %r", error)
247
elif state == avahi.SERVER_RUNNING:
251
logger.debug("Unknown state: %r", state)
253
logger.debug("Unknown state: %r: %r", state, error)
255
"""Derived from the Avahi example code"""
256
if self.server is None:
257
self.server = dbus.Interface(
258
self.bus.get_object(avahi.DBUS_NAME,
259
avahi.DBUS_PATH_SERVER,
260
follow_name_owner_changes=True),
261
avahi.DBUS_INTERFACE_SERVER)
262
self.server.connect_to_signal("StateChanged",
263
self.server_state_changed)
264
self.server_state_changed(self.server.GetState())
45
# This variable is used to optionally bind to a specified interface.
46
# It is a global variable to fit in with the other variables from the
47
# Avahi server example code.
48
serviceInterface = avahi.IF_UNSPEC
49
# From the Avahi server example code:
50
serviceName = "Mandos"
51
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
52
servicePort = None # Not known at startup
53
serviceTXT = [] # TXT record for the service
54
domain = "" # Domain to publish on, default to .local
55
host = "" # Host to publish records for, default to localhost
56
group = None #our entry group
57
rename_count = 12 # Counter so we only rename after collisions a
58
# sensible number of times
59
# End of Avahi example code
267
62
class Client(object):
268
63
"""A representation of a client host served by this server.
271
_approved: bool(); 'None' if not yet approved/disapproved
272
approval_delay: datetime.timedelta(); Time to wait for approval
273
approval_duration: datetime.timedelta(); Duration of one approval
274
checker: subprocess.Popen(); a running checker process used
275
to see if the client lives.
276
'None' if no process is running.
277
checker_callback_tag: a gobject event source tag, or None
278
checker_command: string; External command which is run to check
279
if client lives. %() expansions are done at
65
name: string; from the config file, used in log messages
66
fingerprint: string (40 or 32 hexadecimal digits); used to
67
uniquely identify the client
68
secret: bytestring; sent verbatim (over TLS) to client
69
fqdn: string (FQDN); available for use by the checker command
70
created: datetime.datetime()
71
last_seen: datetime.datetime() or None if not yet seen
72
timeout: datetime.timedelta(); How long from last_seen until
73
this client is invalid
74
interval: datetime.timedelta(); How often to start a new checker
75
stop_hook: If set, called by stop() as stop_hook(self)
76
checker: subprocess.Popen(); a running checker process used
77
to see if the client lives.
78
Is None if no process is running.
79
checker_initiator_tag: a gobject event source tag, or None
80
stop_initiator_tag: - '' -
81
checker_callback_tag: - '' -
82
checker_command: string; External command which is run to check if
83
client lives. %()s expansions are done at
280
84
runtime with vars(self) as dict, so that for
281
85
instance %(name)s can be used in the command.
282
checker_initiator_tag: a gobject event source tag, or None
283
created: datetime.datetime(); (UTC) object creation
284
current_checker_command: string; current running checker_command
285
disable_hook: If set, called by disable() as disable_hook(self)
286
disable_initiator_tag: a gobject event source tag, or None
288
fingerprint: string (40 or 32 hexadecimal digits); used to
289
uniquely identify the client
290
host: string; available for use by the checker command
291
interval: datetime.timedelta(); How often to start a new checker
292
last_approval_request: datetime.datetime(); (UTC) or None
293
last_checked_ok: datetime.datetime(); (UTC) or None
294
last_enabled: datetime.datetime(); (UTC)
295
name: string; from the config file, used in log messages and
297
secret: bytestring; sent verbatim (over TLS) to client
298
timeout: datetime.timedelta(); How long from last_checked_ok
299
until this client is disabled
300
runtime_expansions: Allowed attributes for runtime expansion.
87
_timeout: Real variable for 'timeout'
88
_interval: Real variable for 'interval'
89
_timeout_milliseconds: Used by gobject.timeout_add()
90
_interval_milliseconds: - '' -
303
runtime_expansions = ("approval_delay", "approval_duration",
304
"created", "enabled", "fingerprint",
305
"host", "interval", "last_checked_ok",
306
"last_enabled", "name", "timeout")
309
def _timedelta_to_milliseconds(td):
310
"Convert a datetime.timedelta() to milliseconds"
311
return ((td.days * 24 * 60 * 60 * 1000)
312
+ (td.seconds * 1000)
313
+ (td.microseconds // 1000))
315
def timeout_milliseconds(self):
316
"Return the 'timeout' attribute in milliseconds"
317
return self._timedelta_to_milliseconds(self.timeout)
319
def interval_milliseconds(self):
320
"Return the 'interval' attribute in milliseconds"
321
return self._timedelta_to_milliseconds(self.interval)
323
def approval_delay_milliseconds(self):
324
return self._timedelta_to_milliseconds(self.approval_delay)
326
def __init__(self, name = None, disable_hook=None, config=None):
327
"""Note: the 'checker' key in 'config' sets the
328
'checker_command' attribute and *not* the 'checker'
92
def _set_timeout(self, timeout):
93
"Setter function for 'timeout' attribute"
94
self._timeout = timeout
95
self._timeout_milliseconds = ((self.timeout.days
96
* 24 * 60 * 60 * 1000)
97
+ (self.timeout.seconds * 1000)
98
+ (self.timeout.microseconds
100
timeout = property(lambda self: self._timeout,
103
def _set_interval(self, interval):
104
"Setter function for 'interval' attribute"
105
self._interval = interval
106
self._interval_milliseconds = ((self.interval.days
107
* 24 * 60 * 60 * 1000)
108
+ (self.interval.seconds
110
+ (self.interval.microseconds
112
interval = property(lambda self: self._interval,
115
def __init__(self, name=None, options=None, stop_hook=None,
116
fingerprint=None, secret=None, secfile=None,
117
fqdn=None, timeout=None, interval=-1, checker=None):
333
logger.debug("Creating client %r", self.name)
334
# Uppercase and remove spaces from fingerprint for later
335
# comparison purposes with return value from the fingerprint()
337
self.fingerprint = (config["fingerprint"].upper()
339
logger.debug(" Fingerprint: %s", self.fingerprint)
340
if "secret" in config:
341
self.secret = config["secret"].decode("base64")
342
elif "secfile" in config:
343
with open(os.path.expanduser(os.path.expandvars
344
(config["secfile"])),
346
self.secret = secfile.read()
348
raise TypeError("No secret or secfile for client %s"
350
self.host = config.get("host", "")
351
self.created = datetime.datetime.utcnow()
353
self.last_approval_request = None
354
self.last_enabled = None
355
self.last_checked_ok = None
356
self.timeout = string_to_delta(config["timeout"])
357
self.interval = string_to_delta(config["interval"])
358
self.disable_hook = disable_hook
119
# Uppercase and remove spaces from fingerprint
120
# for later comparison purposes with return value of
121
# the fingerprint() function
122
self.fingerprint = fingerprint.upper().replace(u" ", u"")
124
self.secret = secret.decode(u"base64")
127
self.secret = sf.read()
130
raise RuntimeError(u"No secret or secfile for client %s"
132
self.fqdn = fqdn # string
133
self.created = datetime.datetime.now()
134
self.last_seen = None
136
self.timeout = options.timeout
138
self.timeout = string_to_delta(timeout)
140
self.interval = options.interval
142
self.interval = string_to_delta(interval)
143
self.stop_hook = stop_hook
359
144
self.checker = None
360
145
self.checker_initiator_tag = None
361
self.disable_initiator_tag = None
146
self.stop_initiator_tag = None
362
147
self.checker_callback_tag = None
363
self.checker_command = config["checker"]
364
self.current_checker_command = None
365
self.last_connect = None
366
self._approved = None
367
self.approved_by_default = config.get("approved_by_default",
369
self.approvals_pending = 0
370
self.approval_delay = string_to_delta(
371
config["approval_delay"])
372
self.approval_duration = string_to_delta(
373
config["approval_duration"])
374
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
376
def send_changedstate(self):
377
self.changedstate.acquire()
378
self.changedstate.notify_all()
379
self.changedstate.release()
382
"""Start this client's checker and timeout hooks"""
383
if getattr(self, "enabled", False):
386
self.send_changedstate()
387
self.last_enabled = datetime.datetime.utcnow()
148
self.check_command = checker
150
"""Start this clients checker and timeout hooks"""
388
151
# Schedule a new checker to be started an 'interval' from now,
389
152
# and every interval from then on.
390
self.checker_initiator_tag = (gobject.timeout_add
391
(self.interval_milliseconds(),
393
# Schedule a disable() when 'timeout' has passed
394
self.disable_initiator_tag = (gobject.timeout_add
395
(self.timeout_milliseconds(),
153
self.checker_initiator_tag = gobject.timeout_add\
154
(self._interval_milliseconds,
398
156
# Also start a new checker *right now*.
399
157
self.start_checker()
401
def disable(self, quiet=True):
402
"""Disable this client."""
403
if not getattr(self, "enabled", False):
406
self.send_changedstate()
408
logger.info("Disabling client %s", self.name)
409
if getattr(self, "disable_initiator_tag", False):
410
gobject.source_remove(self.disable_initiator_tag)
411
self.disable_initiator_tag = None
412
if getattr(self, "checker_initiator_tag", False):
158
# Schedule a stop() when 'timeout' has passed
159
self.stop_initiator_tag = gobject.timeout_add\
160
(self._timeout_milliseconds,
164
The possibility that this client might be restarted is left
165
open, but not currently used."""
166
logger.debug(u"Stopping client %s", self.name)
168
if self.stop_initiator_tag:
169
gobject.source_remove(self.stop_initiator_tag)
170
self.stop_initiator_tag = None
171
if self.checker_initiator_tag:
413
172
gobject.source_remove(self.checker_initiator_tag)
414
173
self.checker_initiator_tag = None
415
174
self.stop_checker()
416
if self.disable_hook:
417
self.disable_hook(self)
419
177
# Do not run this again if called by a gobject.timeout_add
422
179
def __del__(self):
423
self.disable_hook = None
426
def checker_callback(self, pid, condition, command):
180
# Some code duplication here and in stop()
181
if hasattr(self, "stop_initiator_tag") \
182
and self.stop_initiator_tag:
183
gobject.source_remove(self.stop_initiator_tag)
184
self.stop_initiator_tag = None
185
if hasattr(self, "checker_initiator_tag") \
186
and self.checker_initiator_tag:
187
gobject.source_remove(self.checker_initiator_tag)
188
self.checker_initiator_tag = None
190
def checker_callback(self, pid, condition):
427
191
"""The checker has completed, so take appropriate actions."""
428
self.checker_callback_tag = None
430
if os.WIFEXITED(condition):
431
exitstatus = os.WEXITSTATUS(condition)
433
logger.info("Checker for %(name)s succeeded",
437
logger.info("Checker for %(name)s failed",
440
logger.warning("Checker for %(name)s crashed?",
192
now = datetime.datetime.now()
193
if os.WIFEXITED(condition) \
194
and (os.WEXITSTATUS(condition) == 0):
195
logger.debug(u"Checker for %(name)s succeeded",
198
gobject.source_remove(self.stop_initiator_tag)
199
self.stop_initiator_tag = gobject.timeout_add\
200
(self._timeout_milliseconds,
202
elif not os.WIFEXITED(condition):
203
logger.warning(u"Checker for %(name)s crashed?",
443
def checked_ok(self):
444
"""Bump up the timeout for this client.
446
This should only be called when the client has been seen,
449
self.last_checked_ok = datetime.datetime.utcnow()
450
gobject.source_remove(self.disable_initiator_tag)
451
self.disable_initiator_tag = (gobject.timeout_add
452
(self.timeout_milliseconds(),
455
def need_approval(self):
456
self.last_approval_request = datetime.datetime.utcnow()
206
logger.debug(u"Checker for %(name)s failed",
209
self.checker_callback_tag = None
458
210
def start_checker(self):
459
211
"""Start a new checker subprocess if one is not running.
461
212
If a checker already exists, leave it running and do
463
# The reason for not killing a running checker is that if we
464
# did that, then if a checker (for some reason) started
465
# running slowly and taking more than 'interval' time, the
466
# client would inevitably timeout, since no checker would get
467
# a chance to run to completion. If we instead leave running
468
# checkers alone, the checker would have to take more time
469
# than 'timeout' for the client to be disabled, which is as it
472
# If a checker exists, make sure it is not a zombie
474
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
475
except (AttributeError, OSError) as error:
476
if (isinstance(error, OSError)
477
and error.errno != errno.ECHILD):
481
logger.warning("Checker was a zombie")
482
gobject.source_remove(self.checker_callback_tag)
483
self.checker_callback(pid, status,
484
self.current_checker_command)
485
# Start a new checker if needed
486
214
if self.checker is None:
488
# In case checker_command has exactly one % operator
489
command = self.checker_command % self.host
216
command = self.check_command % self.fqdn
490
217
except TypeError:
491
# Escape attributes for the shell
492
escaped_attrs = dict(
494
re.escape(unicode(str(getattr(self, attr, "")),
498
self.runtime_expansions)
218
escaped_attrs = dict((key, re.escape(str(val)))
220
vars(self).iteritems())
501
command = self.checker_command % escaped_attrs
502
except TypeError as error:
503
logger.error('Could not format string "%s":'
504
' %s', self.checker_command, error)
222
command = self.check_command % escaped_attrs
223
except TypeError, error:
224
logger.critical(u'Could not format string "%s":'
225
u' %s', self.check_command, error)
505
226
return True # Try again later
506
self.current_checker_command = command
508
logger.info("Starting checker %r for %s",
510
# We don't need to redirect stdout and stderr, since
511
# in normal mode, that is already done by daemon(),
512
# and in debug mode we don't want to. (Stdin is
513
# always replaced by /dev/null.)
514
self.checker = subprocess.Popen(command,
517
self.checker_callback_tag = (gobject.child_watch_add
519
self.checker_callback,
521
# The checker may have completed before the gobject
522
# watch was added. Check for this.
523
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
525
gobject.source_remove(self.checker_callback_tag)
526
self.checker_callback(pid, status, command)
527
except OSError as error:
528
logger.error("Failed to start subprocess: %s",
228
logger.debug(u"Starting checker %r for %s",
230
self.checker = subprocess.\
232
close_fds=True, shell=True,
234
self.checker_callback_tag = gobject.child_watch_add\
236
self.checker_callback)
237
except subprocess.OSError, error:
238
logger.error(u"Failed to start subprocess: %s",
530
240
# Re-run this periodically if run by gobject.timeout_add
533
242
def stop_checker(self):
534
243
"""Force the checker process, if any, to stop."""
535
if self.checker_callback_tag:
536
gobject.source_remove(self.checker_callback_tag)
537
self.checker_callback_tag = None
538
if getattr(self, "checker", None) is None:
244
if not hasattr(self, "checker") or self.checker is None:
540
logger.debug("Stopping checker for %(name)s", vars(self))
542
os.kill(self.checker.pid, signal.SIGTERM)
544
#if self.checker.poll() is None:
545
# os.kill(self.checker.pid, signal.SIGKILL)
546
except OSError as error:
547
if error.errno != errno.ESRCH: # No such process
551
def dbus_service_property(dbus_interface, signature="v",
552
access="readwrite", byte_arrays=False):
553
"""Decorators for marking methods of a DBusObjectWithProperties to
554
become properties on the D-Bus.
556
The decorated method will be called with no arguments by "Get"
557
and with one argument by "Set".
559
The parameters, where they are supported, are the same as
560
dbus.service.method, except there is only "signature", since the
561
type from Get() and the type sent to Set() is the same.
563
# Encoding deeply encoded byte arrays is not supported yet by the
564
# "Set" method, so we fail early here:
565
if byte_arrays and signature != "ay":
566
raise ValueError("Byte arrays not supported for non-'ay'"
567
" signature %r" % signature)
569
func._dbus_is_property = True
570
func._dbus_interface = dbus_interface
571
func._dbus_signature = signature
572
func._dbus_access = access
573
func._dbus_name = func.__name__
574
if func._dbus_name.endswith("_dbus_property"):
575
func._dbus_name = func._dbus_name[:-14]
576
func._dbus_get_args_options = {'byte_arrays': byte_arrays }
581
class DBusPropertyException(dbus.exceptions.DBusException):
582
"""A base class for D-Bus property-related exceptions
584
def __unicode__(self):
585
return unicode(str(self))
588
class DBusPropertyAccessException(DBusPropertyException):
589
"""A property's access permissions disallows an operation.
594
class DBusPropertyNotFound(DBusPropertyException):
595
"""An attempt was made to access a non-existing property.
600
class DBusObjectWithProperties(dbus.service.Object):
601
"""A D-Bus object with properties.
603
Classes inheriting from this can use the dbus_service_property
604
decorator to expose methods as D-Bus properties. It exposes the
605
standard Get(), Set(), and GetAll() methods on the D-Bus.
609
def _is_dbus_property(obj):
610
return getattr(obj, "_dbus_is_property", False)
612
def _get_all_dbus_properties(self):
613
"""Returns a generator of (name, attribute) pairs
615
return ((prop._dbus_name, prop)
617
inspect.getmembers(self, self._is_dbus_property))
619
def _get_dbus_property(self, interface_name, property_name):
620
"""Returns a bound method if one exists which is a D-Bus
621
property with the specified name and interface.
623
for name in (property_name,
624
property_name + "_dbus_property"):
625
prop = getattr(self, name, None)
627
or not self._is_dbus_property(prop)
628
or prop._dbus_name != property_name
629
or (interface_name and prop._dbus_interface
630
and interface_name != prop._dbus_interface)):
634
raise DBusPropertyNotFound(self.dbus_object_path + ":"
635
+ interface_name + "."
638
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss",
640
def Get(self, interface_name, property_name):
641
"""Standard D-Bus property Get() method, see D-Bus standard.
643
prop = self._get_dbus_property(interface_name, property_name)
644
if prop._dbus_access == "write":
645
raise DBusPropertyAccessException(property_name)
647
if not hasattr(value, "variant_level"):
649
return type(value)(value, variant_level=value.variant_level+1)
651
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv")
652
def Set(self, interface_name, property_name, value):
653
"""Standard D-Bus property Set() method, see D-Bus standard.
655
prop = self._get_dbus_property(interface_name, property_name)
656
if prop._dbus_access == "read":
657
raise DBusPropertyAccessException(property_name)
658
if prop._dbus_get_args_options["byte_arrays"]:
659
# The byte_arrays option is not supported yet on
660
# signatures other than "ay".
661
if prop._dbus_signature != "ay":
663
value = dbus.ByteArray(''.join(unichr(byte)
667
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s",
668
out_signature="a{sv}")
669
def GetAll(self, interface_name):
670
"""Standard D-Bus property GetAll() method, see D-Bus
673
Note: Will not include properties with access="write".
676
for name, prop in self._get_all_dbus_properties():
678
and interface_name != prop._dbus_interface):
679
# Interface non-empty but did not match
681
# Ignore write-only properties
682
if prop._dbus_access == "write":
685
if not hasattr(value, "variant_level"):
688
all[name] = type(value)(value, variant_level=
689
value.variant_level+1)
690
return dbus.Dictionary(all, signature="sv")
692
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
694
path_keyword='object_path',
695
connection_keyword='connection')
696
def Introspect(self, object_path, connection):
697
"""Standard D-Bus method, overloaded to insert property tags.
699
xmlstring = dbus.service.Object.Introspect(self, object_path,
702
document = xml.dom.minidom.parseString(xmlstring)
703
def make_tag(document, name, prop):
704
e = document.createElement("property")
705
e.setAttribute("name", name)
706
e.setAttribute("type", prop._dbus_signature)
707
e.setAttribute("access", prop._dbus_access)
709
for if_tag in document.getElementsByTagName("interface"):
710
for tag in (make_tag(document, name, prop)
712
in self._get_all_dbus_properties()
713
if prop._dbus_interface
714
== if_tag.getAttribute("name")):
715
if_tag.appendChild(tag)
716
# Add the names to the return values for the
717
# "org.freedesktop.DBus.Properties" methods
718
if (if_tag.getAttribute("name")
719
== "org.freedesktop.DBus.Properties"):
720
for cn in if_tag.getElementsByTagName("method"):
721
if cn.getAttribute("name") == "Get":
722
for arg in cn.getElementsByTagName("arg"):
723
if (arg.getAttribute("direction")
725
arg.setAttribute("name", "value")
726
elif cn.getAttribute("name") == "GetAll":
727
for arg in cn.getElementsByTagName("arg"):
728
if (arg.getAttribute("direction")
730
arg.setAttribute("name", "props")
731
xmlstring = document.toxml("utf-8")
733
except (AttributeError, xml.dom.DOMException,
734
xml.parsers.expat.ExpatError) as error:
735
logger.error("Failed to override Introspection method",
740
class ClientDBus(Client, DBusObjectWithProperties):
741
"""A Client class using D-Bus
744
dbus_object_path: dbus.ObjectPath
745
bus: dbus.SystemBus()
748
runtime_expansions = (Client.runtime_expansions
749
+ ("dbus_object_path",))
751
# dbus.service.Object doesn't use super(), so we can't either.
753
def __init__(self, bus = None, *args, **kwargs):
754
self._approvals_pending = 0
756
Client.__init__(self, *args, **kwargs)
757
# Only now, when this client is initialized, can it show up on
759
client_object_name = unicode(self.name).translate(
762
self.dbus_object_path = (dbus.ObjectPath
763
("/clients/" + client_object_name))
764
DBusObjectWithProperties.__init__(self, self.bus,
765
self.dbus_object_path)
767
def _get_approvals_pending(self):
768
return self._approvals_pending
769
def _set_approvals_pending(self, value):
770
old_value = self._approvals_pending
771
self._approvals_pending = value
773
if (hasattr(self, "dbus_object_path")
774
and bval is not bool(old_value)):
775
dbus_bool = dbus.Boolean(bval, variant_level=1)
776
self.PropertyChanged(dbus.String("ApprovalPending"),
779
approvals_pending = property(_get_approvals_pending,
780
_set_approvals_pending)
781
del _get_approvals_pending, _set_approvals_pending
784
def _datetime_to_dbus(dt, variant_level=0):
785
"""Convert a UTC datetime.datetime() to a D-Bus type."""
786
return dbus.String(dt.isoformat(),
787
variant_level=variant_level)
790
oldstate = getattr(self, "enabled", False)
791
r = Client.enable(self)
792
if oldstate != self.enabled:
794
self.PropertyChanged(dbus.String("Enabled"),
795
dbus.Boolean(True, variant_level=1))
796
self.PropertyChanged(
797
dbus.String("LastEnabled"),
798
self._datetime_to_dbus(self.last_enabled,
802
def disable(self, quiet = False):
803
oldstate = getattr(self, "enabled", False)
804
r = Client.disable(self, quiet=quiet)
805
if not quiet and oldstate != self.enabled:
807
self.PropertyChanged(dbus.String("Enabled"),
808
dbus.Boolean(False, variant_level=1))
811
def __del__(self, *args, **kwargs):
813
self.remove_from_connection()
816
if hasattr(DBusObjectWithProperties, "__del__"):
817
DBusObjectWithProperties.__del__(self, *args, **kwargs)
818
Client.__del__(self, *args, **kwargs)
820
def checker_callback(self, pid, condition, command,
246
gobject.source_remove(self.checker_callback_tag)
822
247
self.checker_callback_tag = None
248
os.kill(self.checker.pid, signal.SIGTERM)
249
if self.checker.poll() is None:
250
os.kill(self.checker.pid, signal.SIGKILL)
823
251
self.checker = None
825
self.PropertyChanged(dbus.String("CheckerRunning"),
826
dbus.Boolean(False, variant_level=1))
827
if os.WIFEXITED(condition):
828
exitstatus = os.WEXITSTATUS(condition)
830
self.CheckerCompleted(dbus.Int16(exitstatus),
831
dbus.Int64(condition),
832
dbus.String(command))
835
self.CheckerCompleted(dbus.Int16(-1),
836
dbus.Int64(condition),
837
dbus.String(command))
839
return Client.checker_callback(self, pid, condition, command,
842
def checked_ok(self, *args, **kwargs):
843
Client.checked_ok(self, *args, **kwargs)
845
self.PropertyChanged(
846
dbus.String("LastCheckedOK"),
847
(self._datetime_to_dbus(self.last_checked_ok,
850
def need_approval(self, *args, **kwargs):
851
r = Client.need_approval(self, *args, **kwargs)
853
self.PropertyChanged(
854
dbus.String("LastApprovalRequest"),
855
(self._datetime_to_dbus(self.last_approval_request,
859
def start_checker(self, *args, **kwargs):
860
old_checker = self.checker
861
if self.checker is not None:
862
old_checker_pid = self.checker.pid
864
old_checker_pid = None
865
r = Client.start_checker(self, *args, **kwargs)
866
# Only if new checker process was started
867
if (self.checker is not None
868
and old_checker_pid != self.checker.pid):
870
self.CheckerStarted(self.current_checker_command)
871
self.PropertyChanged(
872
dbus.String("CheckerRunning"),
873
dbus.Boolean(True, variant_level=1))
876
def stop_checker(self, *args, **kwargs):
877
old_checker = getattr(self, "checker", None)
878
r = Client.stop_checker(self, *args, **kwargs)
879
if (old_checker is not None
880
and getattr(self, "checker", None) is None):
881
self.PropertyChanged(dbus.String("CheckerRunning"),
882
dbus.Boolean(False, variant_level=1))
885
def _reset_approved(self):
886
self._approved = None
889
def approve(self, value=True):
890
self.send_changedstate()
891
self._approved = value
892
gobject.timeout_add(self._timedelta_to_milliseconds
893
(self.approval_duration),
894
self._reset_approved)
897
## D-Bus methods, signals & properties
898
_interface = "se.bsnet.fukt.Mandos.Client"
902
# CheckerCompleted - signal
903
@dbus.service.signal(_interface, signature="nxs")
904
def CheckerCompleted(self, exitcode, waitstatus, command):
908
# CheckerStarted - signal
909
@dbus.service.signal(_interface, signature="s")
910
def CheckerStarted(self, command):
914
# PropertyChanged - signal
915
@dbus.service.signal(_interface, signature="sv")
916
def PropertyChanged(self, property, value):
921
@dbus.service.signal(_interface)
924
Is sent after a successful transfer of secret from the Mandos
925
server to mandos-client
930
@dbus.service.signal(_interface, signature="s")
931
def Rejected(self, reason):
935
# NeedApproval - signal
936
@dbus.service.signal(_interface, signature="tb")
937
def NeedApproval(self, timeout, default):
939
return self.need_approval()
944
@dbus.service.method(_interface, in_signature="b")
945
def Approve(self, value):
949
@dbus.service.method(_interface)
954
@dbus.service.method(_interface)
959
# StartChecker - method
960
@dbus.service.method(_interface)
961
def StartChecker(self):
966
@dbus.service.method(_interface)
971
# StopChecker - method
972
@dbus.service.method(_interface)
973
def StopChecker(self):
978
# ApprovalPending - property
979
@dbus_service_property(_interface, signature="b", access="read")
980
def ApprovalPending_dbus_property(self):
981
return dbus.Boolean(bool(self.approvals_pending))
983
# ApprovedByDefault - property
984
@dbus_service_property(_interface, signature="b",
986
def ApprovedByDefault_dbus_property(self, value=None):
987
if value is None: # get
988
return dbus.Boolean(self.approved_by_default)
989
self.approved_by_default = bool(value)
991
self.PropertyChanged(dbus.String("ApprovedByDefault"),
992
dbus.Boolean(value, variant_level=1))
994
# ApprovalDelay - property
995
@dbus_service_property(_interface, signature="t",
997
def ApprovalDelay_dbus_property(self, value=None):
998
if value is None: # get
999
return dbus.UInt64(self.approval_delay_milliseconds())
1000
self.approval_delay = datetime.timedelta(0, 0, 0, value)
1002
self.PropertyChanged(dbus.String("ApprovalDelay"),
1003
dbus.UInt64(value, variant_level=1))
1005
# ApprovalDuration - property
1006
@dbus_service_property(_interface, signature="t",
1008
def ApprovalDuration_dbus_property(self, value=None):
1009
if value is None: # get
1010
return dbus.UInt64(self._timedelta_to_milliseconds(
1011
self.approval_duration))
1012
self.approval_duration = datetime.timedelta(0, 0, 0, value)
1014
self.PropertyChanged(dbus.String("ApprovalDuration"),
1015
dbus.UInt64(value, variant_level=1))
1018
@dbus_service_property(_interface, signature="s", access="read")
1019
def Name_dbus_property(self):
1020
return dbus.String(self.name)
1022
# Fingerprint - property
1023
@dbus_service_property(_interface, signature="s", access="read")
1024
def Fingerprint_dbus_property(self):
1025
return dbus.String(self.fingerprint)
1028
@dbus_service_property(_interface, signature="s",
1030
def Host_dbus_property(self, value=None):
1031
if value is None: # get
1032
return dbus.String(self.host)
1035
self.PropertyChanged(dbus.String("Host"),
1036
dbus.String(value, variant_level=1))
1038
# Created - property
1039
@dbus_service_property(_interface, signature="s", access="read")
1040
def Created_dbus_property(self):
1041
return dbus.String(self._datetime_to_dbus(self.created))
1043
# LastEnabled - property
1044
@dbus_service_property(_interface, signature="s", access="read")
1045
def LastEnabled_dbus_property(self):
1046
if self.last_enabled is None:
1047
return dbus.String("")
1048
return dbus.String(self._datetime_to_dbus(self.last_enabled))
1050
# Enabled - property
1051
@dbus_service_property(_interface, signature="b",
1053
def Enabled_dbus_property(self, value=None):
1054
if value is None: # get
1055
return dbus.Boolean(self.enabled)
1061
# LastCheckedOK - property
1062
@dbus_service_property(_interface, signature="s",
1064
def LastCheckedOK_dbus_property(self, value=None):
1065
if value is not None:
1068
if self.last_checked_ok is None:
1069
return dbus.String("")
1070
return dbus.String(self._datetime_to_dbus(self
1073
# LastApprovalRequest - property
1074
@dbus_service_property(_interface, signature="s", access="read")
1075
def LastApprovalRequest_dbus_property(self):
1076
if self.last_approval_request is None:
1077
return dbus.String("")
1078
return dbus.String(self.
1079
_datetime_to_dbus(self
1080
.last_approval_request))
1082
# Timeout - property
1083
@dbus_service_property(_interface, signature="t",
1085
def Timeout_dbus_property(self, value=None):
1086
if value is None: # get
1087
return dbus.UInt64(self.timeout_milliseconds())
1088
self.timeout = datetime.timedelta(0, 0, 0, value)
1090
self.PropertyChanged(dbus.String("Timeout"),
1091
dbus.UInt64(value, variant_level=1))
1092
if getattr(self, "disable_initiator_tag", None) is None:
1094
# Reschedule timeout
1095
gobject.source_remove(self.disable_initiator_tag)
1096
self.disable_initiator_tag = None
1097
time_to_die = (self.
1098
_timedelta_to_milliseconds((self
1103
if time_to_die <= 0:
1104
# The timeout has passed
1107
self.disable_initiator_tag = (gobject.timeout_add
1108
(time_to_die, self.disable))
1110
# Interval - property
1111
@dbus_service_property(_interface, signature="t",
1113
def Interval_dbus_property(self, value=None):
1114
if value is None: # get
1115
return dbus.UInt64(self.interval_milliseconds())
1116
self.interval = datetime.timedelta(0, 0, 0, value)
1118
self.PropertyChanged(dbus.String("Interval"),
1119
dbus.UInt64(value, variant_level=1))
1120
if getattr(self, "checker_initiator_tag", None) is None:
1122
# Reschedule checker run
1123
gobject.source_remove(self.checker_initiator_tag)
1124
self.checker_initiator_tag = (gobject.timeout_add
1125
(value, self.start_checker))
1126
self.start_checker() # Start one now, too
1128
# Checker - property
1129
@dbus_service_property(_interface, signature="s",
1131
def Checker_dbus_property(self, value=None):
1132
if value is None: # get
1133
return dbus.String(self.checker_command)
1134
self.checker_command = value
1136
self.PropertyChanged(dbus.String("Checker"),
1137
dbus.String(self.checker_command,
1140
# CheckerRunning - property
1141
@dbus_service_property(_interface, signature="b",
1143
def CheckerRunning_dbus_property(self, value=None):
1144
if value is None: # get
1145
return dbus.Boolean(self.checker is not None)
1147
self.start_checker()
1151
# ObjectPath - property
1152
@dbus_service_property(_interface, signature="o", access="read")
1153
def ObjectPath_dbus_property(self):
1154
return self.dbus_object_path # is already a dbus.ObjectPath
1157
@dbus_service_property(_interface, signature="ay",
1158
access="write", byte_arrays=True)
1159
def Secret_dbus_property(self, value):
1160
self.secret = str(value)
1165
class ProxyClient(object):
1166
def __init__(self, child_pipe, fpr, address):
1167
self._pipe = child_pipe
1168
self._pipe.send(('init', fpr, address))
1169
if not self._pipe.recv():
1172
def __getattribute__(self, name):
1173
if(name == '_pipe'):
1174
return super(ProxyClient, self).__getattribute__(name)
1175
self._pipe.send(('getattr', name))
1176
data = self._pipe.recv()
1177
if data[0] == 'data':
1179
if data[0] == 'function':
1180
def func(*args, **kwargs):
1181
self._pipe.send(('funcall', name, args, kwargs))
1182
return self._pipe.recv()[1]
1185
def __setattr__(self, name, value):
1186
if(name == '_pipe'):
1187
return super(ProxyClient, self).__setattr__(name, value)
1188
self._pipe.send(('setattr', name, value))
1191
class ClientHandler(socketserver.BaseRequestHandler, object):
1192
"""A class to handle client connections.
1194
Instantiated once for each connection to handle it.
252
def still_valid(self, now=None):
253
"""Has the timeout not yet passed for this client?"""
255
now = datetime.datetime.now()
256
if self.last_seen is None:
257
return now < (self.created + self.timeout)
259
return now < (self.last_seen + self.timeout)
262
def peer_certificate(session):
263
"Return an OpenPGP data packet string for the peer's certificate"
264
# If not an OpenPGP certificate...
265
if gnutls.library.functions.gnutls_certificate_type_get\
266
(session._c_object) \
267
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
268
# ...do the normal thing
269
return session.peer_certificate
270
list_size = ctypes.c_uint()
271
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
272
(session._c_object, ctypes.byref(list_size))
273
if list_size.value == 0:
276
return ctypes.string_at(cert.data, cert.size)
279
def fingerprint(openpgp):
280
"Convert an OpenPGP data string to a hexdigit fingerprint string"
281
# New empty GnuTLS certificate
282
crt = gnutls.library.types.gnutls_openpgp_crt_t()
283
gnutls.library.functions.gnutls_openpgp_crt_init\
285
# New GnuTLS "datum" with the OpenPGP public key
286
datum = gnutls.library.types.gnutls_datum_t\
287
(ctypes.cast(ctypes.c_char_p(openpgp),
288
ctypes.POINTER(ctypes.c_ubyte)),
289
ctypes.c_uint(len(openpgp)))
290
# Import the OpenPGP public key into the certificate
291
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
294
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
295
# New buffer for the fingerprint
296
buffer = ctypes.create_string_buffer(20)
297
buffer_length = ctypes.c_size_t()
298
# Get the fingerprint from the certificate into the buffer
299
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
300
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
301
# Deinit the certificate
302
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
303
# Convert the buffer to a Python bytestring
304
fpr = ctypes.string_at(buffer, buffer_length.value)
305
# Convert the bytestring to hexadecimal notation
306
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
310
class tcp_handler(SocketServer.BaseRequestHandler, object):
311
"""A TCP request handler class.
312
Instantiated by IPv6_TCPServer for each request to handle it.
1195
313
Note: This will run in its own forked process."""
1197
315
def handle(self):
1198
with contextlib.closing(self.server.child_pipe) as child_pipe:
1199
logger.info("TCP connection from: %s",
1200
unicode(self.client_address))
1201
logger.debug("Pipe FD: %d",
1202
self.server.child_pipe.fileno())
1204
session = (gnutls.connection
1205
.ClientSession(self.request,
1207
.X509Credentials()))
1209
# Note: gnutls.connection.X509Credentials is really a
1210
# generic GnuTLS certificate credentials object so long as
1211
# no X.509 keys are added to it. Therefore, we can use it
1212
# here despite using OpenPGP certificates.
1214
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1215
# "+AES-256-CBC", "+SHA1",
1216
# "+COMP-NULL", "+CTYPE-OPENPGP",
1218
# Use a fallback default, since this MUST be set.
1219
priority = self.server.gnutls_priority
1220
if priority is None:
1222
(gnutls.library.functions
1223
.gnutls_priority_set_direct(session._c_object,
1226
# Start communication using the Mandos protocol
1227
# Get protocol number
1228
line = self.request.makefile().readline()
1229
logger.debug("Protocol version: %r", line)
1231
if int(line.strip().split()[0]) > 1:
1233
except (ValueError, IndexError, RuntimeError) as error:
1234
logger.error("Unknown protocol version: %s", error)
1237
# Start GnuTLS connection
1240
except gnutls.errors.GNUTLSError as error:
1241
logger.warning("Handshake failed: %s", error)
1242
# Do not run session.bye() here: the session is not
1243
# established. Just abandon the request.
1245
logger.debug("Handshake succeeded")
1247
approval_required = False
1250
fpr = self.fingerprint(self.peer_certificate
1253
gnutls.errors.GNUTLSError) as error:
1254
logger.warning("Bad certificate: %s", error)
1256
logger.debug("Fingerprint: %s", fpr)
1259
client = ProxyClient(child_pipe, fpr,
1260
self.client_address)
1264
if client.approval_delay:
1265
delay = client.approval_delay
1266
client.approvals_pending += 1
1267
approval_required = True
1270
if not client.enabled:
1271
logger.info("Client %s is disabled",
1273
if self.server.use_dbus:
1275
client.Rejected("Disabled")
1278
if client._approved or not client.approval_delay:
1279
#We are approved or approval is disabled
1281
elif client._approved is None:
1282
logger.info("Client %s needs approval",
1284
if self.server.use_dbus:
1286
client.NeedApproval(
1287
client.approval_delay_milliseconds(),
1288
client.approved_by_default)
1290
logger.warning("Client %s was not approved",
1292
if self.server.use_dbus:
1294
client.Rejected("Denied")
1297
#wait until timeout or approved
1298
#x = float(client._timedelta_to_milliseconds(delay))
1299
time = datetime.datetime.now()
1300
client.changedstate.acquire()
1301
client.changedstate.wait(float(client._timedelta_to_milliseconds(delay) / 1000))
1302
client.changedstate.release()
1303
time2 = datetime.datetime.now()
1304
if (time2 - time) >= delay:
1305
if not client.approved_by_default:
1306
logger.warning("Client %s timed out while"
1307
" waiting for approval",
1309
if self.server.use_dbus:
1311
client.Rejected("Approval timed out")
1316
delay -= time2 - time
1319
while sent_size < len(client.secret):
1321
sent = session.send(client.secret[sent_size:])
1322
except gnutls.errors.GNUTLSError as error:
1323
logger.warning("gnutls send failed")
1325
logger.debug("Sent: %d, remaining: %d",
1326
sent, len(client.secret)
1327
- (sent_size + sent))
1330
logger.info("Sending secret to %s", client.name)
1331
# bump the timeout as if seen
1333
if self.server.use_dbus:
1338
if approval_required:
1339
client.approvals_pending -= 1
1342
except gnutls.errors.GNUTLSError as error:
1343
logger.warning("GnuTLS bye failed")
1346
def peer_certificate(session):
1347
"Return the peer's OpenPGP certificate as a bytestring"
1348
# If not an OpenPGP certificate...
1349
if (gnutls.library.functions
1350
.gnutls_certificate_type_get(session._c_object)
1351
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1352
# ...do the normal thing
1353
return session.peer_certificate
1354
list_size = ctypes.c_uint(1)
1355
cert_list = (gnutls.library.functions
1356
.gnutls_certificate_get_peers
1357
(session._c_object, ctypes.byref(list_size)))
1358
if not bool(cert_list) and list_size.value != 0:
1359
raise gnutls.errors.GNUTLSError("error getting peer"
1361
if list_size.value == 0:
1364
return ctypes.string_at(cert.data, cert.size)
1367
def fingerprint(openpgp):
1368
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
1369
# New GnuTLS "datum" with the OpenPGP public key
1370
datum = (gnutls.library.types
1371
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1374
ctypes.c_uint(len(openpgp))))
1375
# New empty GnuTLS certificate
1376
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1377
(gnutls.library.functions
1378
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
1379
# Import the OpenPGP public key into the certificate
1380
(gnutls.library.functions
1381
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1382
gnutls.library.constants
1383
.GNUTLS_OPENPGP_FMT_RAW))
1384
# Verify the self signature in the key
1385
crtverify = ctypes.c_uint()
1386
(gnutls.library.functions
1387
.gnutls_openpgp_crt_verify_self(crt, 0,
1388
ctypes.byref(crtverify)))
1389
if crtverify.value != 0:
1390
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1391
raise (gnutls.errors.CertificateSecurityError
1393
# New buffer for the fingerprint
1394
buf = ctypes.create_string_buffer(20)
1395
buf_len = ctypes.c_size_t()
1396
# Get the fingerprint from the certificate into the buffer
1397
(gnutls.library.functions
1398
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1399
ctypes.byref(buf_len)))
1400
# Deinit the certificate
1401
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1402
# Convert the buffer to a Python bytestring
1403
fpr = ctypes.string_at(buf, buf_len.value)
1404
# Convert the bytestring to hexadecimal notation
1405
hex_fpr = ''.join("%02X" % ord(char) for char in fpr)
1409
class MultiprocessingMixIn(object):
1410
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
1411
def sub_process_main(self, request, address):
1413
self.finish_request(request, address)
1415
self.handle_error(request, address)
1416
self.close_request(request)
1418
def process_request(self, request, address):
1419
"""Start a new process to process the request."""
1420
multiprocessing.Process(target = self.sub_process_main,
1421
args = (request, address)).start()
1423
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
1424
""" adds a pipe to the MixIn """
1425
def process_request(self, request, client_address):
1426
"""Overrides and wraps the original process_request().
1428
This function creates a new pipe in self.pipe
1430
parent_pipe, self.child_pipe = multiprocessing.Pipe()
1432
super(MultiprocessingMixInWithPipe,
1433
self).process_request(request, client_address)
1434
self.child_pipe.close()
1435
self.add_pipe(parent_pipe)
1437
def add_pipe(self, parent_pipe):
1438
"""Dummy function; override as necessary"""
1439
raise NotImplementedError
1441
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1442
socketserver.TCPServer, object):
1443
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
316
logger.debug(u"TCP connection from: %s",
317
unicode(self.client_address))
318
session = gnutls.connection.ClientSession(self.request,
322
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
323
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
325
priority = "SECURE256"
327
gnutls.library.functions.gnutls_priority_set_direct\
328
(session._c_object, priority, None);
332
except gnutls.errors.GNUTLSError, error:
333
logger.debug(u"Handshake failed: %s", error)
334
# Do not run session.bye() here: the session is not
335
# established. Just abandon the request.
338
fpr = fingerprint(peer_certificate(session))
339
except (TypeError, gnutls.errors.GNUTLSError), error:
340
logger.debug(u"Bad certificate: %s", error)
343
logger.debug(u"Fingerprint: %s", fpr)
346
if c.fingerprint == fpr:
349
# Have to check if client.still_valid(), since it is possible
350
# that the client timed out while establishing the GnuTLS
352
if (not client) or (not client.still_valid()):
354
logger.debug(u"Client %(name)s is invalid",
357
logger.debug(u"Client not found for fingerprint: %s",
362
while sent_size < len(client.secret):
363
sent = session.send(client.secret[sent_size:])
364
logger.debug(u"Sent: %d, remaining: %d",
365
sent, len(client.secret)
366
- (sent_size + sent))
371
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
372
"""IPv6 TCP server. Accepts 'None' as address and/or port.
1446
enabled: Boolean; whether this server is activated yet
1447
interface: None or a network interface name (string)
1448
use_ipv6: Boolean; to use IPv6 or not
374
options: Command line options
375
clients: Set() of Client objects
1450
def __init__(self, server_address, RequestHandlerClass,
1451
interface=None, use_ipv6=True):
1452
self.interface = interface
1454
self.address_family = socket.AF_INET6
1455
socketserver.TCPServer.__init__(self, server_address,
1456
RequestHandlerClass)
377
address_family = socket.AF_INET6
378
def __init__(self, *args, **kwargs):
379
if "options" in kwargs:
380
self.options = kwargs["options"]
381
del kwargs["options"]
382
if "clients" in kwargs:
383
self.clients = kwargs["clients"]
384
del kwargs["clients"]
385
return super(type(self), self).__init__(*args, **kwargs)
1457
386
def server_bind(self):
1458
387
"""This overrides the normal server_bind() function
1459
388
to bind to an interface if one was specified, and also NOT to
1460
389
bind to an address or port if they were not specified."""
1461
if self.interface is not None:
1462
if SO_BINDTODEVICE is None:
1463
logger.error("SO_BINDTODEVICE does not exist;"
1464
" cannot bind to interface %s",
1468
self.socket.setsockopt(socket.SOL_SOCKET,
1472
except socket.error as error:
1473
if error[0] == errno.EPERM:
1474
logger.error("No permission to"
1475
" bind to interface %s",
1477
elif error[0] == errno.ENOPROTOOPT:
1478
logger.error("SO_BINDTODEVICE not available;"
1479
" cannot bind to interface %s",
390
if self.options.interface:
391
if not hasattr(socket, "SO_BINDTODEVICE"):
392
# From /usr/include/asm-i486/socket.h
393
socket.SO_BINDTODEVICE = 25
395
self.socket.setsockopt(socket.SOL_SOCKET,
396
socket.SO_BINDTODEVICE,
397
self.options.interface)
398
except socket.error, error:
399
if error[0] == errno.EPERM:
400
logger.warning(u"No permission to"
401
u" bind to interface %s",
402
self.options.interface)
1483
405
# Only bind(2) the socket if we really need to.
1484
406
if self.server_address[0] or self.server_address[1]:
1485
407
if not self.server_address[0]:
1486
if self.address_family == socket.AF_INET6:
1487
any_address = "::" # in6addr_any
1489
any_address = socket.INADDR_ANY
1490
self.server_address = (any_address,
409
self.server_address = (in6addr_any,
1491
410
self.server_address[1])
1492
elif not self.server_address[1]:
411
elif self.server_address[1] is None:
1493
412
self.server_address = (self.server_address[0],
1495
# if self.interface:
1496
# self.server_address = (self.server_address[0],
1501
return socketserver.TCPServer.server_bind(self)
1504
class MandosServer(IPv6_TCPServer):
1508
clients: set of Client objects
1509
gnutls_priority GnuTLS priority string
1510
use_dbus: Boolean; to emit D-Bus signals or not
1512
Assumes a gobject.MainLoop event loop.
1514
def __init__(self, server_address, RequestHandlerClass,
1515
interface=None, use_ipv6=True, clients=None,
1516
gnutls_priority=None, use_dbus=True):
1517
self.enabled = False
1518
self.clients = clients
1519
if self.clients is None:
1520
self.clients = set()
1521
self.use_dbus = use_dbus
1522
self.gnutls_priority = gnutls_priority
1523
IPv6_TCPServer.__init__(self, server_address,
1524
RequestHandlerClass,
1525
interface = interface,
1526
use_ipv6 = use_ipv6)
1527
def server_activate(self):
1529
return socketserver.TCPServer.server_activate(self)
1532
def add_pipe(self, parent_pipe):
1533
# Call "handle_ipc" for both data and EOF events
1534
gobject.io_add_watch(parent_pipe.fileno(),
1535
gobject.IO_IN | gobject.IO_HUP,
1536
functools.partial(self.handle_ipc,
1537
parent_pipe = parent_pipe))
1539
def handle_ipc(self, source, condition, parent_pipe=None,
1540
client_object=None):
1542
gobject.IO_IN: "IN", # There is data to read.
1543
gobject.IO_OUT: "OUT", # Data can be written (without
1545
gobject.IO_PRI: "PRI", # There is urgent data to read.
1546
gobject.IO_ERR: "ERR", # Error condition.
1547
gobject.IO_HUP: "HUP" # Hung up (the connection has been
1548
# broken, usually for pipes and
1551
conditions_string = ' | '.join(name
1553
condition_names.iteritems()
1554
if cond & condition)
1555
# error or the other end of multiprocessing.Pipe has closed
1556
if condition & (gobject.IO_ERR | condition & gobject.IO_HUP):
1559
# Read a request from the child
1560
request = parent_pipe.recv()
1561
command = request[0]
1563
if command == 'init':
1565
address = request[2]
1567
for c in self.clients:
1568
if c.fingerprint == fpr:
1572
logger.info("Client not found for fingerprint: %s, ad"
1573
"dress: %s", fpr, address)
1576
mandos_dbus_service.ClientNotFound(fpr, address[0])
1577
parent_pipe.send(False)
1580
gobject.io_add_watch(parent_pipe.fileno(),
1581
gobject.IO_IN | gobject.IO_HUP,
1582
functools.partial(self.handle_ipc,
1583
parent_pipe = parent_pipe,
1584
client_object = client))
1585
parent_pipe.send(True)
1586
# remove the old hook in favor of the new above hook on same fileno
1588
if command == 'funcall':
1589
funcname = request[1]
1593
parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs)))
1595
if command == 'getattr':
1596
attrname = request[1]
1597
if callable(client_object.__getattribute__(attrname)):
1598
parent_pipe.send(('function',))
1600
parent_pipe.send(('data', client_object.__getattribute__(attrname)))
1602
if command == 'setattr':
1603
attrname = request[1]
1605
setattr(client_object, attrname, value)
414
return super(type(self), self).server_bind()
1610
417
def string_to_delta(interval):
1611
418
"""Parse a string and return a datetime.timedelta
1613
420
>>> string_to_delta('7d')
1614
421
datetime.timedelta(7)
1615
422
>>> string_to_delta('60s')
1700
##################################################################
1701
# Parsing of options, both command line and config file
1703
parser = argparse.ArgumentParser()
1704
parser.add_argument("-v", "--version", action="version",
1705
version = "%%(prog)s %s" % version,
1706
help="show version number and exit")
1707
parser.add_argument("-i", "--interface", metavar="IF",
1708
help="Bind to interface IF")
1709
parser.add_argument("-a", "--address",
1710
help="Address to listen for requests on")
1711
parser.add_argument("-p", "--port", type=int,
1712
help="Port number to receive requests on")
1713
parser.add_argument("--check", action="store_true",
1714
help="Run self-test")
1715
parser.add_argument("--debug", action="store_true",
1716
help="Debug mode; run in foreground and log"
1718
parser.add_argument("--debuglevel", metavar="LEVEL",
1719
help="Debug level for stdout output")
1720
parser.add_argument("--priority", help="GnuTLS"
1721
" priority string (see GnuTLS documentation)")
1722
parser.add_argument("--servicename",
1723
metavar="NAME", help="Zeroconf service name")
1724
parser.add_argument("--configdir",
1725
default="/etc/mandos", metavar="DIR",
1726
help="Directory to search for configuration"
1728
parser.add_argument("--no-dbus", action="store_false",
1729
dest="use_dbus", help="Do not provide D-Bus"
1730
" system bus interface")
1731
parser.add_argument("--no-ipv6", action="store_false",
1732
dest="use_ipv6", help="Do not use IPv6")
1733
options = parser.parse_args()
561
def killme(status = 0):
562
logger.debug("Stopping server with exit status %d", status)
564
if main_loop_started:
570
if __name__ == '__main__':
572
main_loop_started = False
573
parser = OptionParser()
574
parser.add_option("-i", "--interface", type="string",
575
default=None, metavar="IF",
576
help="Bind to interface IF")
577
parser.add_option("-p", "--port", type="int", default=None,
578
help="Port number to receive requests on")
579
parser.add_option("--timeout", type="string", # Parsed later
581
help="Amount of downtime allowed for clients")
582
parser.add_option("--interval", type="string", # Parsed later
584
help="How often to check that a client is up")
585
parser.add_option("--check", action="store_true", default=False,
586
help="Run self-test")
587
parser.add_option("--debug", action="store_true", default=False,
589
(options, args) = parser.parse_args()
1735
591
if options.check:
1737
593
doctest.testmod()
1740
# Default values for config file for server-global settings
1741
server_defaults = { "interface": "",
1746
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1747
"servicename": "Mandos",
1753
# Parse config file for server-global settings
1754
server_config = configparser.SafeConfigParser(server_defaults)
1756
server_config.read(os.path.join(options.configdir,
1758
# Convert the SafeConfigParser object to a dict
1759
server_settings = server_config.defaults()
1760
# Use the appropriate methods on the non-string config options
1761
for option in ("debug", "use_dbus", "use_ipv6"):
1762
server_settings[option] = server_config.getboolean("DEFAULT",
1764
if server_settings["port"]:
1765
server_settings["port"] = server_config.getint("DEFAULT",
1769
# Override the settings from the config file with command line
1771
for option in ("interface", "address", "port", "debug",
1772
"priority", "servicename", "configdir",
1773
"use_dbus", "use_ipv6", "debuglevel"):
1774
value = getattr(options, option)
1775
if value is not None:
1776
server_settings[option] = value
1778
# Force all strings to be unicode
1779
for option in server_settings.keys():
1780
if type(server_settings[option]) is str:
1781
server_settings[option] = unicode(server_settings[option])
1782
# Now we have our good server settings in "server_settings"
1784
##################################################################
1787
debug = server_settings["debug"]
1788
debuglevel = server_settings["debuglevel"]
1789
use_dbus = server_settings["use_dbus"]
1790
use_ipv6 = server_settings["use_ipv6"]
1792
if server_settings["servicename"] != "Mandos":
1793
syslogger.setFormatter(logging.Formatter
1794
('Mandos (%s) [%%(process)d]:'
1795
' %%(levelname)s: %%(message)s'
1796
% server_settings["servicename"]))
1798
# Parse config file with clients
1799
client_defaults = { "timeout": "1h",
1801
"checker": "fping -q -- %%(host)s",
1803
"approval_delay": "0s",
1804
"approval_duration": "1s",
1806
client_config = configparser.SafeConfigParser(client_defaults)
1807
client_config.read(os.path.join(server_settings["configdir"],
1810
global mandos_dbus_service
1811
mandos_dbus_service = None
1813
tcp_server = MandosServer((server_settings["address"],
1814
server_settings["port"]),
1816
interface=(server_settings["interface"]
1820
server_settings["priority"],
1823
pidfilename = "/var/run/mandos.pid"
1825
pidfile = open(pidfilename, "w")
1827
logger.error("Could not open file %r", pidfilename)
1830
uid = pwd.getpwnam("_mandos").pw_uid
1831
gid = pwd.getpwnam("_mandos").pw_gid
1834
uid = pwd.getpwnam("mandos").pw_uid
1835
gid = pwd.getpwnam("mandos").pw_gid
1838
uid = pwd.getpwnam("nobody").pw_uid
1839
gid = pwd.getpwnam("nobody").pw_gid
1846
except OSError as error:
1847
if error[0] != errno.EPERM:
1850
if not debug and not debuglevel:
1851
syslogger.setLevel(logging.WARNING)
1852
console.setLevel(logging.WARNING)
1854
level = getattr(logging, debuglevel.upper())
1855
syslogger.setLevel(level)
1856
console.setLevel(level)
1859
# Enable all possible GnuTLS debugging
1861
# "Use a log level over 10 to enable all debugging options."
1863
gnutls.library.functions.gnutls_global_set_log_level(11)
1865
@gnutls.library.types.gnutls_log_func
1866
def debug_gnutls(level, string):
1867
logger.debug("GnuTLS: %s", string[:-1])
1869
(gnutls.library.functions
1870
.gnutls_global_set_log_function(debug_gnutls))
1872
# Redirect stdin so all checkers get /dev/null
1873
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1874
os.dup2(null, sys.stdin.fileno())
1878
# No console logging
1879
logger.removeHandler(console)
1881
# Need to fork before connecting to D-Bus
1883
# Close all input and output, do double fork, etc.
1887
# From the Avahi example code
596
# Parse the time arguments
598
options.timeout = string_to_delta(options.timeout)
600
parser.error("option --timeout: Unparseable time")
602
options.interval = string_to_delta(options.interval)
604
parser.error("option --interval: Unparseable time")
607
defaults = { "checker": "fping -q -- %%(fqdn)s" }
608
client_config = ConfigParser.SafeConfigParser(defaults)
609
#client_config.readfp(open("secrets.conf"), "secrets.conf")
610
client_config.read("mandos-clients.conf")
612
# From the Avahi server example code
1888
613
DBusGMainLoop(set_as_default=True )
1889
614
main_loop = gobject.MainLoop()
1890
615
bus = dbus.SystemBus()
616
server = dbus.Interface(
617
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
618
avahi.DBUS_INTERFACE_SERVER )
1891
619
# End of Avahi example code
1894
bus_name = dbus.service.BusName("se.bsnet.fukt.Mandos",
1895
bus, do_not_queue=True)
1896
except dbus.exceptions.NameExistsException as e:
1897
logger.error(unicode(e) + ", disabling D-Bus")
1899
server_settings["use_dbus"] = False
1900
tcp_server.use_dbus = False
1901
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1902
service = AvahiService(name = server_settings["servicename"],
1903
servicetype = "_mandos._tcp",
1904
protocol = protocol, bus = bus)
1905
if server_settings["interface"]:
1906
service.interface = (if_nametoindex
1907
(str(server_settings["interface"])))
1909
global multiprocessing_manager
1910
multiprocessing_manager = multiprocessing.Manager()
1912
client_class = Client
1914
client_class = functools.partial(ClientDBus, bus = bus)
1915
def client_config_items(config, section):
1916
special_settings = {
1917
"approved_by_default":
1918
lambda: config.getboolean(section,
1919
"approved_by_default"),
1921
for name, value in config.items(section):
1923
yield (name, special_settings[name]())
1927
tcp_server.clients.update(set(
1928
client_class(name = section,
1929
config= dict(client_config_items(
1930
client_config, section)))
1931
for section in client_config.sections()))
1932
if not tcp_server.clients:
1933
logger.warning("No clients defined")
621
debug = options.debug
624
console = logging.StreamHandler()
625
# console.setLevel(logging.DEBUG)
626
console.setFormatter(logging.Formatter\
627
('%(levelname)s: %(message)s'))
628
logger.addHandler(console)
632
def remove_from_clients(client):
633
clients.remove(client)
635
logger.debug(u"No clients left, exiting")
638
clients.update(Set(Client(name=section, options=options,
639
stop_hook = remove_from_clients,
640
**(dict(client_config\
642
for section in client_config.sections()))
1939
pidfile.write(str(pid) + "\n".encode("utf-8"))
1942
logger.error("Could not write to file %r with PID %d",
1945
# "pidfile" was never created
1949
signal.signal(signal.SIGINT, signal.SIG_IGN)
1951
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1952
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1955
class MandosDBusService(dbus.service.Object):
1956
"""A D-Bus proxy object"""
1958
dbus.service.Object.__init__(self, bus, "/")
1959
_interface = "se.bsnet.fukt.Mandos"
1961
@dbus.service.signal(_interface, signature="o")
1962
def ClientAdded(self, objpath):
1966
@dbus.service.signal(_interface, signature="ss")
1967
def ClientNotFound(self, fingerprint, address):
1971
@dbus.service.signal(_interface, signature="os")
1972
def ClientRemoved(self, objpath, name):
1976
@dbus.service.method(_interface, out_signature="ao")
1977
def GetAllClients(self):
1979
return dbus.Array(c.dbus_object_path
1980
for c in tcp_server.clients)
1982
@dbus.service.method(_interface,
1983
out_signature="a{oa{sv}}")
1984
def GetAllClientsWithProperties(self):
1986
return dbus.Dictionary(
1987
((c.dbus_object_path, c.GetAll(""))
1988
for c in tcp_server.clients),
1991
@dbus.service.method(_interface, in_signature="o")
1992
def RemoveClient(self, object_path):
1994
for c in tcp_server.clients:
1995
if c.dbus_object_path == object_path:
1996
tcp_server.clients.remove(c)
1997
c.remove_from_connection()
1998
# Don't signal anything except ClientRemoved
1999
c.disable(quiet=True)
2001
self.ClientRemoved(object_path, c.name)
2003
raise KeyError(object_path)
2007
mandos_dbus_service = MandosDBusService()
2010
648
"Cleanup function; run on exit"
650
# From the Avahi server example code
651
if not group is None:
654
# End of Avahi example code
2013
while tcp_server.clients:
2014
client = tcp_server.clients.pop()
2016
client.remove_from_connection()
2017
client.disable_hook = None
2018
# Don't signal anything except ClientRemoved
2019
client.disable(quiet=True)
2022
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
656
for client in clients:
657
client.stop_hook = None
2025
660
atexit.register(cleanup)
2027
for client in tcp_server.clients:
2030
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2034
tcp_server.server_activate()
2036
# Find out what port we got
2037
service.port = tcp_server.socket.getsockname()[1]
2039
logger.info("Now listening on address %r, port %d,"
2040
" flowinfo %d, scope_id %d"
2041
% tcp_server.socket.getsockname())
2043
logger.info("Now listening on address %r, port %d"
2044
% tcp_server.socket.getsockname())
2046
#service.interface = tcp_server.socket.getsockname()[3]
2049
# From the Avahi example code
2052
except dbus.exceptions.DBusException as error:
2053
logger.critical("DBusException: %s", error)
2056
# End of Avahi example code
2058
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2059
lambda *args, **kwargs:
2060
(tcp_server.handle_request
2061
(*args[2:], **kwargs) or True))
2063
logger.debug("Starting main loop")
663
signal.signal(signal.SIGINT, signal.SIG_IGN)
664
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
665
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
667
for client in clients:
670
tcp_server = IPv6_TCPServer((None, options.port),
674
# Find out what random port we got
675
servicePort = tcp_server.socket.getsockname()[1]
676
logger.debug(u"Now listening on port %d", servicePort)
678
if options.interface is not None:
679
serviceInterface = if_nametoindex(options.interface)
681
# From the Avahi server example code
682
server.connect_to_signal("StateChanged", server_state_changed)
684
server_state_changed(server.GetState())
685
except dbus.exceptions.DBusException, error:
686
logger.critical(u"DBusException: %s", error)
688
# End of Avahi example code
690
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
691
lambda *args, **kwargs:
692
tcp_server.handle_request(*args[2:],
695
main_loop_started = True
2065
except AvahiError as error:
2066
logger.critical("AvahiError: %s", error)
2069
697
except KeyboardInterrupt:
2071
print("", file=sys.stderr)
2072
logger.debug("Server received KeyboardInterrupt")
2073
logger.debug("Server exiting")
2074
# Must run before the D-Bus bus name gets deregistered
2077
if __name__ == '__main__':