52
22
from sets import Set
57
import logging.handlers
59
from contextlib import closing
65
28
from dbus.mainloop.glib import DBusGMainLoop
71
logger = logging.Logger('mandos')
72
syslogger = (logging.handlers.SysLogHandler
73
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
address = "/dev/log"))
75
syslogger.setFormatter(logging.Formatter
76
('Mandos: %(levelname)s: %(message)s'))
77
logger.addHandler(syslogger)
79
console = logging.StreamHandler()
80
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
82
logger.addHandler(console)
84
class AvahiError(Exception):
85
def __init__(self, value, *args, **kwargs):
87
super(AvahiError, self).__init__(value, *args, **kwargs)
88
def __unicode__(self):
89
return unicode(repr(self.value))
91
class AvahiServiceError(AvahiError):
94
class AvahiGroupError(AvahiError):
98
class AvahiService(object):
99
"""An Avahi (Zeroconf) service.
101
interface: integer; avahi.IF_UNSPEC or an interface index.
102
Used to optionally bind to the specified interface.
103
name: string; Example: 'Mandos'
104
type: string; Example: '_mandos._tcp'.
105
See <http://www.dns-sd.org/ServiceTypes.html>
106
port: integer; what port to announce
107
TXT: list of strings; TXT record for the service
108
domain: string; Domain to publish on, default to .local if empty.
109
host: string; Host to publish records for, default is localhost
110
max_renames: integer; maximum number of renames
111
rename_count: integer; counter so we only rename after collisions
112
a sensible number of times
114
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
servicetype = None, port = None, TXT = None,
116
domain = "", host = "", max_renames = 32768):
117
self.interface = interface
119
self.type = servicetype
121
self.TXT = TXT if TXT is not None else []
124
self.rename_count = 0
125
self.max_renames = max_renames
127
"""Derived from the Avahi example code"""
128
if self.rename_count >= self.max_renames:
129
logger.critical(u"No suitable Zeroconf service name found"
130
u" after %i retries, exiting.",
132
raise AvahiServiceError(u"Too many renames")
133
self.name = server.GetAlternativeServiceName(self.name)
134
logger.info(u"Changing Zeroconf service name to %r ...",
136
syslogger.setFormatter(logging.Formatter
137
('Mandos (%s): %%(levelname)s:'
138
' %%(message)s' % self.name))
141
self.rename_count += 1
143
"""Derived from the Avahi example code"""
144
if group is not None:
147
"""Derived from the Avahi example code"""
150
group = dbus.Interface(bus.get_object
152
server.EntryGroupNew()),
153
avahi.DBUS_INTERFACE_ENTRY_GROUP)
154
group.connect_to_signal('StateChanged',
155
entry_group_state_changed)
156
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
157
service.name, service.type)
159
self.interface, # interface
160
avahi.PROTO_INET6, # protocol
161
dbus.UInt32(0), # flags
162
self.name, self.type,
163
self.domain, self.host,
164
dbus.UInt16(self.port),
165
avahi.string_array_to_txt_array(self.TXT))
168
# From the Avahi example code:
169
group = None # our entry group
31
# This variable is used to optionally bind to a specified interface.
32
# It is a global variable to fit in with the other variables from the
33
# Avahi server example code.
34
serviceInterface = avahi.IF_UNSPEC
35
# From the Avahi server example code:
36
serviceName = "Mandos"
37
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
38
servicePort = None # Not known at startup
39
serviceTXT = [] # TXT record for the service
40
domain = "" # Domain to publish on, default to .local
41
host = "" # Host to publish records for, default to localhost
42
group = None #our entry group
43
rename_count = 12 # Counter so we only rename after collisions a
44
# sensible number of times
170
45
# End of Avahi example code
173
def _datetime_to_dbus(dt, variant_level=0):
174
"""Convert a UTC datetime.datetime() to a D-Bus type."""
175
return dbus.String(dt.isoformat(), variant_level=variant_level)
178
class Client(dbus.service.Object):
179
49
"""A representation of a client host served by this server.
181
name: string; from the config file, used in log messages
51
name: string; from the config file, used in log messages
182
52
fingerprint: string (40 or 32 hexadecimal digits); used to
183
53
uniquely identify the client
184
secret: bytestring; sent verbatim (over TLS) to client
185
host: string; available for use by the checker command
186
created: datetime.datetime(); (UTC) object creation
187
last_enabled: datetime.datetime(); (UTC)
189
last_checked_ok: datetime.datetime(); (UTC) or None
190
timeout: datetime.timedelta(); How long from last_checked_ok
191
until this client is invalid
192
interval: datetime.timedelta(); How often to start a new checker
193
disable_hook: If set, called by disable() as disable_hook(self)
194
checker: subprocess.Popen(); a running checker process used
195
to see if the client lives.
196
'None' if no process is running.
54
secret: bytestring; sent verbatim (over TLS) to client
55
fqdn: string (FQDN); available for use by the checker command
56
created: datetime.datetime()
57
last_seen: datetime.datetime() or None if not yet seen
58
timeout: datetime.timedelta(); How long from last_seen until
59
this client is invalid
60
interval: datetime.timedelta(); How often to start a new checker
61
stop_hook: If set, called by stop() as stop_hook(self)
62
checker: subprocess.Popen(); a running checker process used
63
to see if the client lives.
64
Is None if no process is running.
197
65
checker_initiator_tag: a gobject event source tag, or None
198
disable_initiator_tag: - '' -
66
stop_initiator_tag: - '' -
199
67
checker_callback_tag: - '' -
200
68
checker_command: string; External command which is run to check if
201
client lives. %() expansions are done at
69
client lives. %()s expansions are done at
202
70
runtime with vars(self) as dict, so that for
203
71
instance %(name)s can be used in the command.
204
dbus_object_path: dbus.ObjectPath
205
72
Private attibutes:
206
73
_timeout: Real variable for 'timeout'
207
74
_interval: Real variable for 'interval'
208
_timeout_milliseconds: Used when calling gobject.timeout_add()
75
_timeout_milliseconds: Used by gobject.timeout_add()
209
76
_interval_milliseconds: - '' -
211
78
def _set_timeout(self, timeout):
212
"Setter function for the 'timeout' attribute"
79
"Setter function for 'timeout' attribute"
213
80
self._timeout = timeout
214
81
self._timeout_milliseconds = ((self.timeout.days
215
82
* 24 * 60 * 60 * 1000)
216
83
+ (self.timeout.seconds * 1000)
217
84
+ (self.timeout.microseconds
220
self.PropertyChanged(dbus.String(u"timeout"),
221
(dbus.UInt64(self._timeout_milliseconds,
223
timeout = property(lambda self: self._timeout, _set_timeout)
86
timeout = property(lambda self: self._timeout,
226
89
def _set_interval(self, interval):
227
"Setter function for the 'interval' attribute"
90
"Setter function for 'interval' attribute"
228
91
self._interval = interval
229
92
self._interval_milliseconds = ((self.interval.days
230
93
* 24 * 60 * 60 * 1000)
233
96
+ (self.interval.microseconds
236
self.PropertyChanged(dbus.String(u"interval"),
237
(dbus.UInt64(self._interval_milliseconds,
239
interval = property(lambda self: self._interval, _set_interval)
98
interval = property(lambda self: self._interval,
240
100
del _set_interval
242
def __init__(self, name = None, disable_hook=None, config=None):
243
"""Note: the 'checker' key in 'config' sets the
244
'checker_command' attribute and *not* the 'checker'
246
self.dbus_object_path = (dbus.ObjectPath
248
+ name.replace(".", "_")))
249
dbus.service.Object.__init__(self, bus,
250
self.dbus_object_path)
101
def __init__(self, name=None, options=None, stop_hook=None,
102
fingerprint=None, secret=None, secfile=None, fqdn=None,
103
timeout=None, interval=-1, checker=None):
254
logger.debug(u"Creating client %r", self.name)
255
# Uppercase and remove spaces from fingerprint for later
256
# comparison purposes with return value from the fingerprint()
258
self.fingerprint = (config["fingerprint"].upper()
260
logger.debug(u" Fingerprint: %s", self.fingerprint)
261
if "secret" in config:
262
self.secret = config["secret"].decode(u"base64")
263
elif "secfile" in config:
264
with closing(open(os.path.expanduser
266
(config["secfile"])))) as secfile:
267
self.secret = secfile.read()
269
raise TypeError(u"No secret or secfile for client %s"
271
self.host = config.get("host", "")
272
self.created = datetime.datetime.utcnow()
274
self.last_enabled = None
275
self.last_checked_ok = None
276
self.timeout = string_to_delta(config["timeout"])
277
self.interval = string_to_delta(config["interval"])
278
self.disable_hook = disable_hook
105
# Uppercase and remove spaces from fingerprint
106
# for later comparison purposes with return value of
107
# the fingerprint() function
108
self.fingerprint = fingerprint.upper().replace(u" ", u"")
110
self.secret = secret.decode(u"base64")
113
self.secret = sf.read()
116
raise RuntimeError(u"No secret or secfile for client %s"
118
self.fqdn = fqdn # string
119
self.created = datetime.datetime.now()
120
self.last_seen = None
122
timeout = options.timeout
123
self.timeout = timeout
125
interval = options.interval
127
interval = string_to_delta(interval)
128
self.interval = interval
129
self.stop_hook = stop_hook
279
130
self.checker = None
280
131
self.checker_initiator_tag = None
281
self.disable_initiator_tag = None
132
self.stop_initiator_tag = None
282
133
self.checker_callback_tag = None
283
self.checker_command = config["checker"]
286
"""Start this client's checker and timeout hooks"""
287
self.last_enabled = datetime.datetime.utcnow()
134
self.check_command = checker
136
"""Start this clients checker and timeout hooks"""
288
137
# Schedule a new checker to be started an 'interval' from now,
289
138
# and every interval from then on.
290
self.checker_initiator_tag = (gobject.timeout_add
291
(self._interval_milliseconds,
139
self.checker_initiator_tag = gobject.timeout_add\
140
(self._interval_milliseconds,
293
142
# Also start a new checker *right now*.
294
143
self.start_checker()
295
# Schedule a disable() when 'timeout' has passed
296
self.disable_initiator_tag = (gobject.timeout_add
297
(self._timeout_milliseconds,
301
self.PropertyChanged(dbus.String(u"enabled"),
302
dbus.Boolean(True, variant_level=1))
303
self.PropertyChanged(dbus.String(u"last_enabled"),
304
(_datetime_to_dbus(self.last_enabled,
308
"""Disable this client."""
309
if not getattr(self, "enabled", False):
311
logger.info(u"Disabling client %s", self.name)
312
if getattr(self, "disable_initiator_tag", False):
313
gobject.source_remove(self.disable_initiator_tag)
314
self.disable_initiator_tag = None
315
if getattr(self, "checker_initiator_tag", False):
144
# Schedule a stop() when 'timeout' has passed
145
self.stop_initiator_tag = gobject.timeout_add\
146
(self._timeout_milliseconds,
150
The possibility that this client might be restarted is left
151
open, but not currently used."""
153
sys.stderr.write(u"Stopping client %s\n" % self.name)
155
if self.stop_initiator_tag:
156
gobject.source_remove(self.stop_initiator_tag)
157
self.stop_initiator_tag = None
158
if self.checker_initiator_tag:
316
159
gobject.source_remove(self.checker_initiator_tag)
317
160
self.checker_initiator_tag = None
318
161
self.stop_checker()
319
if self.disable_hook:
320
self.disable_hook(self)
323
self.PropertyChanged(dbus.String(u"enabled"),
324
dbus.Boolean(False, variant_level=1))
325
164
# Do not run this again if called by a gobject.timeout_add
328
166
def __del__(self):
329
self.disable_hook = None
332
def checker_callback(self, pid, condition, command):
167
# Some code duplication here and in stop()
168
if hasattr(self, "stop_initiator_tag") \
169
and self.stop_initiator_tag:
170
gobject.source_remove(self.stop_initiator_tag)
171
self.stop_initiator_tag = None
172
if hasattr(self, "checker_initiator_tag") \
173
and self.checker_initiator_tag:
174
gobject.source_remove(self.checker_initiator_tag)
175
self.checker_initiator_tag = None
177
def checker_callback(self, pid, condition):
333
178
"""The checker has completed, so take appropriate actions."""
334
self.checker_callback_tag = None
337
self.PropertyChanged(dbus.String(u"checker_running"),
338
dbus.Boolean(False, variant_level=1))
339
if (os.WIFEXITED(condition)
340
and (os.WEXITSTATUS(condition) == 0)):
341
logger.info(u"Checker for %(name)s succeeded",
344
self.CheckerCompleted(dbus.Boolean(True),
345
dbus.UInt16(condition),
346
dbus.String(command))
348
elif not os.WIFEXITED(condition):
349
logger.warning(u"Checker for %(name)s crashed?",
352
self.CheckerCompleted(dbus.Boolean(False),
353
dbus.UInt16(condition),
354
dbus.String(command))
356
logger.info(u"Checker for %(name)s failed",
359
self.CheckerCompleted(dbus.Boolean(False),
360
dbus.UInt16(condition),
361
dbus.String(command))
363
def bump_timeout(self):
364
"""Bump up the timeout for this client.
365
This should only be called when the client has been seen,
368
self.last_checked_ok = datetime.datetime.utcnow()
369
gobject.source_remove(self.disable_initiator_tag)
370
self.disable_initiator_tag = (gobject.timeout_add
179
now = datetime.datetime.now()
180
if os.WIFEXITED(condition) \
181
and (os.WEXITSTATUS(condition) == 0):
183
sys.stderr.write(u"Checker for %(name)s succeeded\n"
186
gobject.source_remove(self.stop_initiator_tag)
187
self.stop_initiator_tag = gobject.timeout_add\
371
188
(self._timeout_milliseconds,
373
self.PropertyChanged(dbus.String(u"last_checked_ok"),
374
(_datetime_to_dbus(self.last_checked_ok,
191
if not os.WIFEXITED(condition):
192
sys.stderr.write(u"Checker for %(name)s crashed?\n"
195
sys.stderr.write(u"Checker for %(name)s failed\n"
198
self.checker_callback_tag = None
377
199
def start_checker(self):
378
200
"""Start a new checker subprocess if one is not running.
379
201
If a checker already exists, leave it running and do
381
# The reason for not killing a running checker is that if we
382
# did that, then if a checker (for some reason) started
383
# running slowly and taking more than 'interval' time, the
384
# client would inevitably timeout, since no checker would get
385
# a chance to run to completion. If we instead leave running
386
# checkers alone, the checker would have to take more time
387
# than 'timeout' for the client to be declared invalid, which
388
# is as it should be.
389
203
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
391
# In case checker_command has exactly one % operator
392
command = self.checker_command % self.host
208
command = self.check_command % self.fqdn
393
209
except TypeError:
394
# Escape attributes for the shell
395
210
escaped_attrs = dict((key, re.escape(str(val)))
397
212
vars(self).iteritems())
399
command = self.checker_command % escaped_attrs
400
except TypeError, error:
401
logger.error(u'Could not format string "%s":'
402
u' %s', self.checker_command, error)
403
return True # Try again later
213
command = self.check_command % escaped_attrs
405
logger.info(u"Starting checker %r for %s",
407
# We don't need to redirect stdout and stderr, since
408
# in normal mode, that is already done by daemon(),
409
# and in debug mode we don't want to. (Stdin is
410
# always replaced by /dev/null.)
411
self.checker = subprocess.Popen(command,
415
self.CheckerStarted(command)
416
self.PropertyChanged(dbus.String("checker_running"),
417
dbus.Boolean(True, variant_level=1))
418
self.checker_callback_tag = (gobject.child_watch_add
420
self.checker_callback,
422
except OSError, error:
423
logger.error(u"Failed to start subprocess: %s",
215
self.checker = subprocess.\
217
stdout=subprocess.PIPE,
218
close_fds=True, shell=True,
220
self.checker_callback_tag = gobject.\
221
child_watch_add(self.checker.pid,
224
except subprocess.OSError, error:
225
sys.stderr.write(u"Failed to start subprocess: %s\n"
425
227
# Re-run this periodically if run by gobject.timeout_add
428
229
def stop_checker(self):
429
230
"""Force the checker process, if any, to stop."""
430
if self.checker_callback_tag:
431
gobject.source_remove(self.checker_callback_tag)
432
self.checker_callback_tag = None
433
if getattr(self, "checker", None) is None:
231
if not hasattr(self, "checker") or self.checker is None:
435
logger.debug(u"Stopping checker for %(name)s", vars(self))
437
os.kill(self.checker.pid, signal.SIGTERM)
439
#if self.checker.poll() is None:
440
# os.kill(self.checker.pid, signal.SIGKILL)
441
except OSError, error:
442
if error.errno != errno.ESRCH: # No such process
233
gobject.source_remove(self.checker_callback_tag)
234
self.checker_callback_tag = None
235
os.kill(self.checker.pid, signal.SIGTERM)
236
if self.checker.poll() is None:
237
os.kill(self.checker.pid, signal.SIGKILL)
444
238
self.checker = None
445
self.PropertyChanged(dbus.String(u"checker_running"),
446
dbus.Boolean(False, variant_level=1))
448
def still_valid(self):
239
def still_valid(self, now=None):
449
240
"""Has the timeout not yet passed for this client?"""
450
if not getattr(self, "enabled", False):
452
now = datetime.datetime.utcnow()
453
if self.last_checked_ok is None:
242
now = datetime.datetime.now()
243
if self.last_seen is None:
454
244
return now < (self.created + self.timeout)
456
return now < (self.last_checked_ok + self.timeout)
458
## D-Bus methods & signals
459
_interface = u"org.mandos_system.Mandos.Client"
461
# BumpTimeout - method
462
BumpTimeout = dbus.service.method(_interface)(bump_timeout)
463
BumpTimeout.__name__ = "BumpTimeout"
465
# CheckerCompleted - signal
466
@dbus.service.signal(_interface, signature="bqs")
467
def CheckerCompleted(self, success, condition, command):
471
# CheckerStarted - signal
472
@dbus.service.signal(_interface, signature="s")
473
def CheckerStarted(self, command):
477
# GetAllProperties - method
478
@dbus.service.method(_interface, out_signature="a{sv}")
479
def GetAllProperties(self):
481
return dbus.Dictionary({
483
dbus.String(self.name, variant_level=1),
484
dbus.String("fingerprint"):
485
dbus.String(self.fingerprint, variant_level=1),
487
dbus.String(self.host, variant_level=1),
488
dbus.String("created"):
489
_datetime_to_dbus(self.created, variant_level=1),
490
dbus.String("last_enabled"):
491
(_datetime_to_dbus(self.last_enabled,
493
if self.last_enabled is not None
494
else dbus.Boolean(False, variant_level=1)),
495
dbus.String("enabled"):
496
dbus.Boolean(self.enabled, variant_level=1),
497
dbus.String("last_checked_ok"):
498
(_datetime_to_dbus(self.last_checked_ok,
500
if self.last_checked_ok is not None
501
else dbus.Boolean (False, variant_level=1)),
502
dbus.String("timeout"):
503
dbus.UInt64(self._timeout_milliseconds,
505
dbus.String("interval"):
506
dbus.UInt64(self._interval_milliseconds,
508
dbus.String("checker"):
509
dbus.String(self.checker_command,
511
dbus.String("checker_running"):
512
dbus.Boolean(self.checker is not None,
516
# IsStillValid - method
517
IsStillValid = (dbus.service.method(_interface, out_signature="b")
519
IsStillValid.__name__ = "IsStillValid"
521
# PropertyChanged - signal
522
@dbus.service.signal(_interface, signature="sv")
523
def PropertyChanged(self, property, value):
527
# SetChecker - method
528
@dbus.service.method(_interface, in_signature="s")
529
def SetChecker(self, checker):
530
"D-Bus setter method"
531
self.checker_command = checker
534
@dbus.service.method(_interface, in_signature="s")
535
def SetHost(self, host):
536
"D-Bus setter method"
539
# SetInterval - method
540
@dbus.service.method(_interface, in_signature="t")
541
def SetInterval(self, milliseconds):
542
self.interval = datetime.timdeelta(0, 0, 0, milliseconds)
545
@dbus.service.method(_interface, in_signature="ay",
547
def SetSecret(self, secret):
548
"D-Bus setter method"
549
self.secret = str(secret)
551
# SetTimeout - method
552
@dbus.service.method(_interface, in_signature="t")
553
def SetTimeout(self, milliseconds):
554
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
557
Enable = dbus.service.method(_interface)(enable)
558
Enable.__name__ = "Enable"
560
# StartChecker - method
561
@dbus.service.method(_interface)
562
def StartChecker(self):
567
@dbus.service.method(_interface)
572
# StopChecker - method
573
StopChecker = dbus.service.method(_interface)(stop_checker)
574
StopChecker.__name__ = "StopChecker"
246
return now < (self.last_seen + self.timeout)
579
249
def peer_certificate(session):
580
"Return the peer's OpenPGP certificate as a bytestring"
581
250
# If not an OpenPGP certificate...
582
if (gnutls.library.functions
583
.gnutls_certificate_type_get(session._c_object)
584
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
251
if gnutls.library.functions.gnutls_certificate_type_get\
252
(session._c_object) \
253
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
585
254
# ...do the normal thing
586
255
return session.peer_certificate
587
256
list_size = ctypes.c_uint()
588
cert_list = (gnutls.library.functions
589
.gnutls_certificate_get_peers
590
(session._c_object, ctypes.byref(list_size)))
257
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
258
(session._c_object, ctypes.byref(list_size))
591
259
if list_size.value == 0:
593
261
cert = cert_list[0]
597
265
def fingerprint(openpgp):
598
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
599
# New GnuTLS "datum" with the OpenPGP public key
600
datum = (gnutls.library.types
601
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
604
ctypes.c_uint(len(openpgp))))
605
266
# New empty GnuTLS certificate
606
267
crt = gnutls.library.types.gnutls_openpgp_crt_t()
607
(gnutls.library.functions
608
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
268
gnutls.library.functions.gnutls_openpgp_crt_init\
270
# New GnuTLS "datum" with the OpenPGP public key
271
datum = gnutls.library.types.gnutls_datum_t\
272
(ctypes.cast(ctypes.c_char_p(openpgp),
273
ctypes.POINTER(ctypes.c_ubyte)),
274
ctypes.c_uint(len(openpgp)))
609
275
# Import the OpenPGP public key into the certificate
610
(gnutls.library.functions
611
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
612
gnutls.library.constants
613
.GNUTLS_OPENPGP_FMT_RAW))
614
# Verify the self signature in the key
615
crtverify = ctypes.c_uint()
616
(gnutls.library.functions
617
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
618
if crtverify.value != 0:
619
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
620
raise gnutls.errors.CertificateSecurityError("Verify failed")
276
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
279
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
621
280
# New buffer for the fingerprint
622
buf = ctypes.create_string_buffer(20)
623
buf_len = ctypes.c_size_t()
281
buffer = ctypes.create_string_buffer(20)
282
buffer_length = ctypes.c_size_t()
624
283
# Get the fingerprint from the certificate into the buffer
625
(gnutls.library.functions
626
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
627
ctypes.byref(buf_len)))
284
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
628
286
# Deinit the certificate
629
287
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
630
288
# Convert the buffer to a Python bytestring
631
fpr = ctypes.string_at(buf, buf_len.value)
289
fpr = ctypes.string_at(buffer, buffer_length.value)
632
290
# Convert the bytestring to hexadecimal notation
633
291
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
637
class TCP_handler(SocketServer.BaseRequestHandler, object):
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
638
296
"""A TCP request handler class.
639
297
Instantiated by IPv6_TCPServer for each request to handle it.
640
298
Note: This will run in its own forked process."""
642
300
def handle(self):
643
logger.info(u"TCP connection from: %s",
644
unicode(self.client_address))
645
session = (gnutls.connection
646
.ClientSession(self.request,
650
line = self.request.makefile().readline()
651
logger.debug(u"Protocol version: %r", line)
653
if int(line.strip().split()[0]) > 1:
655
except (ValueError, IndexError, RuntimeError), error:
656
logger.error(u"Unknown protocol version: %s", error)
659
# Note: gnutls.connection.X509Credentials is really a generic
660
# GnuTLS certificate credentials object so long as no X.509
661
# keys are added to it. Therefore, we can use it here despite
662
# using OpenPGP certificates.
302
sys.stderr.write(u"TCP request came\n")
303
sys.stderr.write(u"Request: %s\n" % self.request)
304
sys.stderr.write(u"Client Address: %s\n"
305
% unicode(self.client_address))
306
sys.stderr.write(u"Server: %s\n" % self.server)
307
session = gnutls.connection.ClientSession(self.request,
664
311
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
665
312
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
667
# Use a fallback default, since this MUST be set.
668
priority = self.server.settings.get("priority", "NORMAL")
669
(gnutls.library.functions
670
.gnutls_priority_set_direct(session._c_object,
314
priority = "SECURE256"
316
gnutls.library.functions.gnutls_priority_set_direct\
317
(session._c_object, priority, None);
674
320
session.handshake()
675
321
except gnutls.errors.GNUTLSError, error:
676
logger.warning(u"Handshake failed: %s", error)
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
677
324
# Do not run session.bye() here: the session is not
678
325
# established. Just abandon the request.
681
328
fpr = fingerprint(peer_certificate(session))
682
329
except (TypeError, gnutls.errors.GNUTLSError), error:
683
logger.warning(u"Bad certificate: %s", error)
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
686
logger.debug(u"Fingerprint: %s", fpr)
687
for c in self.server.clients:
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
688
338
if c.fingerprint == fpr:
692
logger.warning(u"Client not found for fingerprint: %s",
696
341
# Have to check if client.still_valid(), since it is possible
697
342
# that the client timed out while establishing the GnuTLS
699
if not client.still_valid():
700
logger.warning(u"Client %(name)s is invalid",
344
if (not client) or (not client.still_valid()):
347
sys.stderr.write(u"Client %(name)s is invalid\n"
350
sys.stderr.write(u"Client not found for "
351
u"fingerprint: %s\n" % fpr)
704
## This won't work here, since we're in a fork.
705
# client.bump_timeout()
707
355
while sent_size < len(client.secret):
708
356
sent = session.send(client.secret[sent_size:])
709
logger.debug(u"Sent: %d, remaining: %d",
710
sent, len(client.secret)
711
- (sent_size + sent))
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
712
361
sent_size += sent
716
class IPv6_TCPServer(SocketServer.ForkingMixIn,
717
SocketServer.TCPServer, object):
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
718
366
"""IPv6 TCP server. Accepts 'None' as address and/or port.
720
settings: Server settings
368
options: Command line options
721
369
clients: Set() of Client objects
722
enabled: Boolean; whether this server is activated yet
724
371
address_family = socket.AF_INET6
725
372
def __init__(self, *args, **kwargs):
726
if "settings" in kwargs:
727
self.settings = kwargs["settings"]
728
del kwargs["settings"]
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
729
376
if "clients" in kwargs:
730
377
self.clients = kwargs["clients"]
731
378
del kwargs["clients"]
733
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
379
return super(type(self), self).__init__(*args, **kwargs)
734
380
def server_bind(self):
735
381
"""This overrides the normal server_bind() function
736
382
to bind to an interface if one was specified, and also NOT to
737
383
bind to an address or port if they were not specified."""
738
if self.settings["interface"]:
739
# 25 is from /usr/include/asm-i486/socket.h
740
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
384
if self.options.interface:
385
if not hasattr(socket, "SO_BINDTODEVICE"):
386
# From /usr/include/asm-i486/socket.h
387
socket.SO_BINDTODEVICE = 25
742
389
self.socket.setsockopt(socket.SOL_SOCKET,
744
self.settings["interface"])
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
745
392
except socket.error, error:
746
393
if error[0] == errno.EPERM:
747
logger.error(u"No permission to"
748
u" bind to interface %s",
749
self.settings["interface"])
394
sys.stderr.write(u"Warning: No permission to" \
395
u" bind to interface %s\n"
396
% self.options.interface)
752
399
# Only bind(2) the socket if we really need to.
786
421
datetime.timedelta(1)
787
422
>>> string_to_delta(u'1w')
788
423
datetime.timedelta(7)
789
>>> string_to_delta('5m 30s')
790
datetime.timedelta(0, 330)
792
timevalue = datetime.timedelta(0)
793
for s in interval.split():
795
suffix = unicode(s[-1])
798
delta = datetime.timedelta(value)
800
delta = datetime.timedelta(0, value)
802
delta = datetime.timedelta(0, 0, 0, 0, value)
804
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
806
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
809
except (ValueError, IndexError):
426
suffix=unicode(interval[-1])
427
value=int(interval[:-1])
429
delta = datetime.timedelta(value)
431
delta = datetime.timedelta(0, value)
433
delta = datetime.timedelta(0, 0, 0, 0, value)
435
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
437
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
440
except (ValueError, IndexError):
446
"""From the Avahi server example code"""
447
global group, serviceName, serviceType, servicePort, serviceTXT, \
450
group = dbus.Interface(
451
bus.get_object( avahi.DBUS_NAME,
452
server.EntryGroupNew()),
453
avahi.DBUS_INTERFACE_ENTRY_GROUP)
454
group.connect_to_signal('StateChanged',
455
entry_group_state_changed)
457
sys.stderr.write(u"Adding service '%s' of type '%s' ...\n"
458
% (serviceName, serviceType))
461
serviceInterface, # interface
462
avahi.PROTO_INET6, # protocol
463
dbus.UInt32(0), # flags
464
serviceName, serviceType,
466
dbus.UInt16(servicePort),
467
avahi.string_array_to_txt_array(serviceTXT))
471
def remove_service():
472
"""From the Avahi server example code"""
475
if not group is None:
815
479
def server_state_changed(state):
816
"""Derived from the Avahi example code"""
480
"""From the Avahi server example code"""
817
481
if state == avahi.SERVER_COLLISION:
818
logger.error(u"Zeroconf server name collision")
482
sys.stderr.write(u"WARNING: Server name collision\n")
820
484
elif state == avahi.SERVER_RUNNING:
824
488
def entry_group_state_changed(state, error):
825
"""Derived from the Avahi example code"""
826
logger.debug(u"Avahi state change: %i", state)
489
"""From the Avahi server example code"""
490
global serviceName, server, rename_count
493
sys.stderr.write(u"state change: %i\n" % state)
828
495
if state == avahi.ENTRY_GROUP_ESTABLISHED:
829
logger.debug(u"Zeroconf service established.")
497
sys.stderr.write(u"Service established.\n")
830
498
elif state == avahi.ENTRY_GROUP_COLLISION:
831
logger.warning(u"Zeroconf service name collision.")
500
rename_count = rename_count - 1
502
name = server.GetAlternativeServiceName(name)
503
sys.stderr.write(u"WARNING: Service name collision, "
504
u"changing name to '%s' ...\n" % name)
509
sys.stderr.write(u"ERROR: No suitable service name found "
510
u"after %i retries, exiting.\n"
833
513
elif state == avahi.ENTRY_GROUP_FAILURE:
834
logger.critical(u"Avahi: Error in group state changed %s",
836
raise AvahiGroupError(u"State changed: %s" % unicode(error))
514
sys.stderr.write(u"Error in group state changed %s\n"
838
520
def if_nametoindex(interface):
839
"""Call the C function if_nametoindex(), or equivalent"""
840
global if_nametoindex
521
"""Call the C function if_nametoindex()"""
842
if_nametoindex = (ctypes.cdll.LoadLibrary
843
(ctypes.util.find_library("c"))
523
libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
return libc.if_nametoindex(interface)
845
525
except (OSError, AttributeError):
846
526
if "struct" not in sys.modules:
848
528
if "fcntl" not in sys.modules:
850
def if_nametoindex(interface):
851
"Get an interface index the hard way, i.e. using fcntl()"
852
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
853
with closing(socket.socket()) as s:
854
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
855
struct.pack("16s16x", interface))
856
interface_index = struct.unpack("I", ifreq[16:20])[0]
857
return interface_index
858
return if_nametoindex(interface)
861
def daemon(nochdir = False, noclose = False):
862
"""See daemon(3). Standard BSD Unix function.
863
This should really exist as os.daemon, but it doesn't (yet)."""
872
# Close all standard open file descriptors
873
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
874
if not stat.S_ISCHR(os.fstat(null).st_mode):
875
raise OSError(errno.ENODEV,
876
"/dev/null not a character device")
877
os.dup2(null, sys.stdin.fileno())
878
os.dup2(null, sys.stdout.fileno())
879
os.dup2(null, sys.stderr.fileno())
885
parser = OptionParser(version = "%%prog %s" % version)
530
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
532
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
533
struct.pack("16s16x", interface))
535
interface_index = struct.unpack("I", ifreq[16:20])[0]
536
return interface_index
539
if __name__ == '__main__':
540
parser = OptionParser()
886
541
parser.add_option("-i", "--interface", type="string",
887
metavar="IF", help="Bind to interface IF")
888
parser.add_option("-a", "--address", type="string",
889
help="Address to listen for requests on")
890
parser.add_option("-p", "--port", type="int",
542
default=None, metavar="IF",
543
help="Bind to interface IF")
544
parser.add_option("--cert", type="string", default="cert.pem",
546
help="Public key certificate PEM file to use")
547
parser.add_option("--key", type="string", default="key.pem",
549
help="Private key PEM file to use")
550
parser.add_option("--ca", type="string", default="ca.pem",
552
help="Certificate Authority certificate PEM file to use")
553
parser.add_option("--crl", type="string", default="crl.pem",
555
help="Certificate Revokation List PEM file to use")
556
parser.add_option("-p", "--port", type="int", default=None,
891
557
help="Port number to receive requests on")
558
parser.add_option("--timeout", type="string", # Parsed later
560
help="Amount of downtime allowed for clients")
561
parser.add_option("--interval", type="string", # Parsed later
563
help="How often to check that a client is up")
892
564
parser.add_option("--check", action="store_true", default=False,
893
565
help="Run self-test")
894
parser.add_option("--debug", action="store_true",
895
help="Debug mode; run in foreground and log to"
897
parser.add_option("--priority", type="string", help="GnuTLS"
898
" priority string (see GnuTLS documentation)")
899
parser.add_option("--servicename", type="string", metavar="NAME",
900
help="Zeroconf service name")
901
parser.add_option("--configdir", type="string",
902
default="/etc/mandos", metavar="DIR",
903
help="Directory to search for configuration"
905
options = parser.parse_args()[0]
566
parser.add_option("--debug", action="store_true", default=False,
568
(options, args) = parser.parse_args()
907
570
if options.check:
909
572
doctest.testmod()
912
# Default values for config file for server-global settings
913
server_defaults = { "interface": "",
918
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
919
"servicename": "Mandos",
922
# Parse config file for server-global settings
923
server_config = ConfigParser.SafeConfigParser(server_defaults)
925
server_config.read(os.path.join(options.configdir, "mandos.conf"))
926
# Convert the SafeConfigParser object to a dict
927
server_settings = server_config.defaults()
928
# Use getboolean on the boolean config option
929
server_settings["debug"] = (server_config.getboolean
930
("DEFAULT", "debug"))
933
# Override the settings from the config file with command line
935
for option in ("interface", "address", "port", "debug",
936
"priority", "servicename", "configdir"):
937
value = getattr(options, option)
938
if value is not None:
939
server_settings[option] = value
941
# Now we have our good server settings in "server_settings"
943
debug = server_settings["debug"]
946
syslogger.setLevel(logging.WARNING)
947
console.setLevel(logging.WARNING)
949
if server_settings["servicename"] != "Mandos":
950
syslogger.setFormatter(logging.Formatter
951
('Mandos (%s): %%(levelname)s:'
953
% server_settings["servicename"]))
955
# Parse config file with clients
956
client_defaults = { "timeout": "1h",
958
"checker": "fping -q -- %(host)s",
961
client_config = ConfigParser.SafeConfigParser(client_defaults)
962
client_config.read(os.path.join(server_settings["configdir"],
966
tcp_server = IPv6_TCPServer((server_settings["address"],
967
server_settings["port"]),
969
settings=server_settings,
971
pidfilename = "/var/run/mandos.pid"
973
pidfile = open(pidfilename, "w")
974
except IOError, error:
975
logger.error("Could not open file %r", pidfilename)
978
uid = pwd.getpwnam("_mandos").pw_uid
981
uid = pwd.getpwnam("mandos").pw_uid
984
uid = pwd.getpwnam("nobody").pw_uid
988
gid = pwd.getpwnam("_mandos").pw_gid
991
gid = pwd.getpwnam("mandos").pw_gid
994
gid = pwd.getpwnam("nogroup").pw_gid
1000
except OSError, error:
1001
if error[0] != errno.EPERM:
1005
service = AvahiService(name = server_settings["servicename"],
1006
servicetype = "_mandos._tcp", )
1007
if server_settings["interface"]:
1008
service.interface = (if_nametoindex
1009
(server_settings["interface"]))
1014
# From the Avahi example code
575
# Parse the time arguments
577
options.timeout = string_to_delta(options.timeout)
579
parser.error("option --timeout: Unparseable time")
581
options.interval = string_to_delta(options.interval)
583
parser.error("option --interval: Unparseable time")
586
defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
587
client_config = ConfigParser.SafeConfigParser(defaults)
588
#client_config.readfp(open("secrets.conf"), "secrets.conf")
589
client_config.read("mandos-clients.conf")
591
# From the Avahi server example code
1015
592
DBusGMainLoop(set_as_default=True )
1016
593
main_loop = gobject.MainLoop()
1017
594
bus = dbus.SystemBus()
1018
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1019
avahi.DBUS_PATH_SERVER),
1020
avahi.DBUS_INTERFACE_SERVER)
595
server = dbus.Interface(
596
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
597
avahi.DBUS_INTERFACE_SERVER )
1021
598
# End of Avahi example code
1022
bus_name = dbus.service.BusName(u"org.mandos-system.Mandos", bus)
1024
clients.update(Set(Client(name = section,
1026
= dict(client_config.items(section)))
600
debug = options.debug
603
def remove_from_clients(client):
604
clients.remove(client)
607
sys.stderr.write(u"No clients left, exiting\n")
610
clients.update(Set(Client(name=section, options=options,
611
stop_hook = remove_from_clients,
612
**(dict(client_config\
1027
614
for section in client_config.sections()))
1029
logger.critical(u"No clients defined")
615
for client in clients:
618
tcp_server = IPv6_TCPServer((None, options.port),
622
# Find out what random port we got
623
servicePort = tcp_server.socket.getsockname()[1]
1033
# Redirect stdin so all checkers get /dev/null
1034
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1035
os.dup2(null, sys.stdin.fileno())
1039
# No console logging
1040
logger.removeHandler(console)
1041
# Close all input and output, do double fork, etc.
1046
pidfile.write(str(pid) + "\n")
1050
logger.error(u"Could not write to file %r with PID %d",
1053
# "pidfile" was never created
1058
"Cleanup function; run on exit"
1060
# From the Avahi example code
1061
if not group is None:
1064
# End of Avahi example code
1067
client = clients.pop()
1068
client.disable_hook = None
1071
atexit.register(cleanup)
1074
signal.signal(signal.SIGINT, signal.SIG_IGN)
1075
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1076
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1078
class MandosServer(dbus.service.Object):
1079
"""A D-Bus proxy object"""
1081
dbus.service.Object.__init__(self, bus,
1083
_interface = u"org.mandos_system.Mandos"
1085
@dbus.service.signal(_interface, signature="oa{sv}")
1086
def ClientAdded(self, objpath, properties):
1090
@dbus.service.signal(_interface, signature="o")
1091
def ClientRemoved(self, objpath):
1095
@dbus.service.method(_interface, out_signature="ao")
1096
def GetAllClients(self):
1097
return dbus.Array(c.dbus_object_path for c in clients)
1099
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1100
def GetAllClientsWithProperties(self):
1101
return dbus.Dictionary(
1102
((c.dbus_object_path, c.GetAllProperties())
1106
@dbus.service.method(_interface, in_signature="o")
1107
def RemoveClient(self, object_path):
1109
if c.dbus_object_path == object_path:
1117
mandos_server = MandosServer()
1119
for client in clients:
1121
mandos_server.ClientAdded(client.dbus_object_path,
1122
client.GetAllProperties())
1126
tcp_server.server_activate()
1128
# Find out what port we got
1129
service.port = tcp_server.socket.getsockname()[1]
1130
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1131
u" scope_id %d" % tcp_server.socket.getsockname())
1133
#service.interface = tcp_server.socket.getsockname()[3]
1136
# From the Avahi example code
1137
server.connect_to_signal("StateChanged", server_state_changed)
1139
server_state_changed(server.GetState())
1140
except dbus.exceptions.DBusException, error:
1141
logger.critical(u"DBusException: %s", error)
1143
# End of Avahi example code
1145
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1146
lambda *args, **kwargs:
1147
(tcp_server.handle_request
1148
(*args[2:], **kwargs) or True))
1150
logger.debug(u"Starting main loop")
625
sys.stderr.write(u"Now listening on port %d\n" % servicePort)
627
if options.interface is not None:
628
serviceInterface = if_nametoindex(options.interface)
630
# From the Avahi server example code
631
server.connect_to_signal("StateChanged", server_state_changed)
632
server_state_changed(server.GetState())
633
# End of Avahi example code
635
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
636
lambda *args, **kwargs:
637
tcp_server.handle_request(*args[2:],
1152
except AvahiError, error:
1153
logger.critical(u"AvahiError: %s", error)
1155
641
except KeyboardInterrupt:
1159
if __name__ == '__main__':
646
# From the Avahi server example code
647
if not group is None:
649
# End of Avahi example code
651
for client in clients:
652
client.stop_hook = None