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
use_dbus: bool(); Whether to provide D-Bus interface and signals
205
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
73
_timeout: Real variable for 'timeout'
74
_interval: Real variable for 'interval'
75
_timeout_milliseconds: Used by gobject.timeout_add()
76
_interval_milliseconds: - '' -
207
def timeout_milliseconds(self):
208
"Return the 'timeout' attribute in milliseconds"
209
return ((self.timeout.days * 24 * 60 * 60 * 1000)
210
+ (self.timeout.seconds * 1000)
211
+ (self.timeout.microseconds // 1000))
213
def interval_milliseconds(self):
214
"Return the 'interval' attribute in milliseconds"
215
return ((self.interval.days * 24 * 60 * 60 * 1000)
216
+ (self.interval.seconds * 1000)
217
+ (self.interval.microseconds // 1000))
219
def __init__(self, name = None, disable_hook=None, config=None,
221
"""Note: the 'checker' key in 'config' sets the
222
'checker_command' attribute and *not* the 'checker'
78
def _set_timeout(self, timeout):
79
"Setter function for 'timeout' attribute"
80
self._timeout = timeout
81
self._timeout_milliseconds = ((self.timeout.days
82
* 24 * 60 * 60 * 1000)
83
+ (self.timeout.seconds * 1000)
84
+ (self.timeout.microseconds
86
timeout = property(lambda self: self._timeout,
89
def _set_interval(self, interval):
90
"Setter function for 'interval' attribute"
91
self._interval = interval
92
self._interval_milliseconds = ((self.interval.days
93
* 24 * 60 * 60 * 1000)
94
+ (self.interval.seconds
96
+ (self.interval.microseconds
98
interval = property(lambda self: self._interval,
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):
227
logger.debug(u"Creating client %r", self.name)
228
self.use_dbus = use_dbus
230
self.dbus_object_path = (dbus.ObjectPath
232
+ self.name.replace(".", "_")))
233
dbus.service.Object.__init__(self, bus,
234
self.dbus_object_path)
235
# Uppercase and remove spaces from fingerprint for later
236
# comparison purposes with return value from the fingerprint()
238
self.fingerprint = (config["fingerprint"].upper()
240
logger.debug(u" Fingerprint: %s", self.fingerprint)
241
if "secret" in config:
242
self.secret = config["secret"].decode(u"base64")
243
elif "secfile" in config:
244
with closing(open(os.path.expanduser
246
(config["secfile"])))) as secfile:
247
self.secret = secfile.read()
249
raise TypeError(u"No secret or secfile for client %s"
251
self.host = config.get("host", "")
252
self.created = datetime.datetime.utcnow()
254
self.last_enabled = None
255
self.last_checked_ok = None
256
self.timeout = string_to_delta(config["timeout"])
257
self.interval = string_to_delta(config["interval"])
258
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
259
130
self.checker = None
260
131
self.checker_initiator_tag = None
261
self.disable_initiator_tag = None
132
self.stop_initiator_tag = None
262
133
self.checker_callback_tag = None
263
self.checker_command = config["checker"]
266
"""Start this client's checker and timeout hooks"""
267
self.last_enabled = datetime.datetime.utcnow()
134
self.check_command = checker
136
"""Start this clients checker and timeout hooks"""
268
137
# Schedule a new checker to be started an 'interval' from now,
269
138
# and every interval from then on.
270
self.checker_initiator_tag = (gobject.timeout_add
271
(self.interval_milliseconds(),
139
self.checker_initiator_tag = gobject.timeout_add\
140
(self._interval_milliseconds,
273
142
# Also start a new checker *right now*.
274
143
self.start_checker()
275
# Schedule a disable() when 'timeout' has passed
276
self.disable_initiator_tag = (gobject.timeout_add
277
(self.timeout_milliseconds(),
282
self.PropertyChanged(dbus.String(u"enabled"),
283
dbus.Boolean(True, variant_level=1))
284
self.PropertyChanged(dbus.String(u"last_enabled"),
285
(_datetime_to_dbus(self.last_enabled,
289
"""Disable this client."""
290
if not getattr(self, "enabled", False):
292
logger.info(u"Disabling client %s", self.name)
293
if getattr(self, "disable_initiator_tag", False):
294
gobject.source_remove(self.disable_initiator_tag)
295
self.disable_initiator_tag = None
296
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:
297
159
gobject.source_remove(self.checker_initiator_tag)
298
160
self.checker_initiator_tag = None
299
161
self.stop_checker()
300
if self.disable_hook:
301
self.disable_hook(self)
305
self.PropertyChanged(dbus.String(u"enabled"),
306
dbus.Boolean(False, variant_level=1))
307
164
# Do not run this again if called by a gobject.timeout_add
310
166
def __del__(self):
311
self.disable_hook = None
314
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):
315
178
"""The checker has completed, so take appropriate actions."""
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\
188
(self._timeout_milliseconds,
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"
316
198
self.checker_callback_tag = None
320
self.PropertyChanged(dbus.String(u"checker_running"),
321
dbus.Boolean(False, variant_level=1))
322
if (os.WIFEXITED(condition)
323
and (os.WEXITSTATUS(condition) == 0)):
324
logger.info(u"Checker for %(name)s succeeded",
328
self.CheckerCompleted(dbus.Boolean(True),
329
dbus.UInt16(condition),
330
dbus.String(command))
332
elif not os.WIFEXITED(condition):
333
logger.warning(u"Checker for %(name)s crashed?",
337
self.CheckerCompleted(dbus.Boolean(False),
338
dbus.UInt16(condition),
339
dbus.String(command))
341
logger.info(u"Checker for %(name)s failed",
345
self.CheckerCompleted(dbus.Boolean(False),
346
dbus.UInt16(condition),
347
dbus.String(command))
349
def bump_timeout(self):
350
"""Bump up the timeout for this client.
351
This should only be called when the client has been seen,
354
self.last_checked_ok = datetime.datetime.utcnow()
355
gobject.source_remove(self.disable_initiator_tag)
356
self.disable_initiator_tag = (gobject.timeout_add
357
(self.timeout_milliseconds(),
361
self.PropertyChanged(
362
dbus.String(u"last_checked_ok"),
363
(_datetime_to_dbus(self.last_checked_ok,
366
199
def start_checker(self):
367
200
"""Start a new checker subprocess if one is not running.
368
201
If a checker already exists, leave it running and do
370
# The reason for not killing a running checker is that if we
371
# did that, then if a checker (for some reason) started
372
# running slowly and taking more than 'interval' time, the
373
# client would inevitably timeout, since no checker would get
374
# a chance to run to completion. If we instead leave running
375
# checkers alone, the checker would have to take more time
376
# than 'timeout' for the client to be declared invalid, which
377
# is as it should be.
378
203
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
380
# In case checker_command has exactly one % operator
381
command = self.checker_command % self.host
208
command = self.check_command % self.fqdn
382
209
except TypeError:
383
# Escape attributes for the shell
384
210
escaped_attrs = dict((key, re.escape(str(val)))
386
212
vars(self).iteritems())
388
command = self.checker_command % escaped_attrs
389
except TypeError, error:
390
logger.error(u'Could not format string "%s":'
391
u' %s', self.checker_command, error)
392
return True # Try again later
213
command = self.check_command % escaped_attrs
394
logger.info(u"Starting checker %r for %s",
396
# We don't need to redirect stdout and stderr, since
397
# in normal mode, that is already done by daemon(),
398
# and in debug mode we don't want to. (Stdin is
399
# always replaced by /dev/null.)
400
self.checker = subprocess.Popen(command,
405
self.CheckerStarted(command)
406
self.PropertyChanged(
407
dbus.String("checker_running"),
408
dbus.Boolean(True, variant_level=1))
409
self.checker_callback_tag = (gobject.child_watch_add
411
self.checker_callback,
413
except OSError, error:
414
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"
416
227
# Re-run this periodically if run by gobject.timeout_add
419
229
def stop_checker(self):
420
230
"""Force the checker process, if any, to stop."""
421
if self.checker_callback_tag:
422
gobject.source_remove(self.checker_callback_tag)
423
self.checker_callback_tag = None
424
if getattr(self, "checker", None) is None:
231
if not hasattr(self, "checker") or self.checker is None:
426
logger.debug(u"Stopping checker for %(name)s", vars(self))
428
os.kill(self.checker.pid, signal.SIGTERM)
430
#if self.checker.poll() is None:
431
# os.kill(self.checker.pid, signal.SIGKILL)
432
except OSError, error:
433
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)
435
238
self.checker = None
437
self.PropertyChanged(dbus.String(u"checker_running"),
438
dbus.Boolean(False, variant_level=1))
440
def still_valid(self):
239
def still_valid(self, now=None):
441
240
"""Has the timeout not yet passed for this client?"""
442
if not getattr(self, "enabled", False):
444
now = datetime.datetime.utcnow()
445
if self.last_checked_ok is None:
242
now = datetime.datetime.now()
243
if self.last_seen is None:
446
244
return now < (self.created + self.timeout)
448
return now < (self.last_checked_ok + self.timeout)
450
## D-Bus methods & signals
451
_interface = u"org.mandos_system.Mandos.Client"
453
# BumpTimeout - method
454
BumpTimeout = dbus.service.method(_interface)(bump_timeout)
455
BumpTimeout.__name__ = "BumpTimeout"
457
# CheckerCompleted - signal
458
@dbus.service.signal(_interface, signature="bqs")
459
def CheckerCompleted(self, success, condition, command):
463
# CheckerStarted - signal
464
@dbus.service.signal(_interface, signature="s")
465
def CheckerStarted(self, command):
469
# GetAllProperties - method
470
@dbus.service.method(_interface, out_signature="a{sv}")
471
def GetAllProperties(self):
473
return dbus.Dictionary({
475
dbus.String(self.name, variant_level=1),
476
dbus.String("fingerprint"):
477
dbus.String(self.fingerprint, variant_level=1),
479
dbus.String(self.host, variant_level=1),
480
dbus.String("created"):
481
_datetime_to_dbus(self.created, variant_level=1),
482
dbus.String("last_enabled"):
483
(_datetime_to_dbus(self.last_enabled,
485
if self.last_enabled is not None
486
else dbus.Boolean(False, variant_level=1)),
487
dbus.String("enabled"):
488
dbus.Boolean(self.enabled, variant_level=1),
489
dbus.String("last_checked_ok"):
490
(_datetime_to_dbus(self.last_checked_ok,
492
if self.last_checked_ok is not None
493
else dbus.Boolean (False, variant_level=1)),
494
dbus.String("timeout"):
495
dbus.UInt64(self.timeout_milliseconds(),
497
dbus.String("interval"):
498
dbus.UInt64(self.interval_milliseconds(),
500
dbus.String("checker"):
501
dbus.String(self.checker_command,
503
dbus.String("checker_running"):
504
dbus.Boolean(self.checker is not None,
508
# IsStillValid - method
509
IsStillValid = (dbus.service.method(_interface, out_signature="b")
511
IsStillValid.__name__ = "IsStillValid"
513
# PropertyChanged - signal
514
@dbus.service.signal(_interface, signature="sv")
515
def PropertyChanged(self, property, value):
519
# SetChecker - method
520
@dbus.service.method(_interface, in_signature="s")
521
def SetChecker(self, checker):
522
"D-Bus setter method"
523
self.checker_command = checker
525
self.PropertyChanged(dbus.String(u"checker"),
526
dbus.String(self.checker_command,
530
@dbus.service.method(_interface, in_signature="s")
531
def SetHost(self, host):
532
"D-Bus setter method"
535
self.PropertyChanged(dbus.String(u"host"),
536
dbus.String(self.host, variant_level=1))
538
# SetInterval - method
539
@dbus.service.method(_interface, in_signature="t")
540
def SetInterval(self, milliseconds):
541
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
543
self.PropertyChanged(dbus.String(u"interval"),
544
(dbus.UInt64(self.interval_milliseconds(),
548
@dbus.service.method(_interface, in_signature="ay",
550
def SetSecret(self, secret):
551
"D-Bus setter method"
552
self.secret = str(secret)
554
# SetTimeout - method
555
@dbus.service.method(_interface, in_signature="t")
556
def SetTimeout(self, milliseconds):
557
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
559
self.PropertyChanged(dbus.String(u"timeout"),
560
(dbus.UInt64(self.timeout_milliseconds(),
564
Enable = dbus.service.method(_interface)(enable)
565
Enable.__name__ = "Enable"
567
# StartChecker - method
568
@dbus.service.method(_interface)
569
def StartChecker(self):
574
@dbus.service.method(_interface)
579
# StopChecker - method
580
StopChecker = dbus.service.method(_interface)(stop_checker)
581
StopChecker.__name__ = "StopChecker"
246
return now < (self.last_seen + self.timeout)
586
249
def peer_certificate(session):
587
"Return the peer's OpenPGP certificate as a bytestring"
588
250
# If not an OpenPGP certificate...
589
if (gnutls.library.functions
590
.gnutls_certificate_type_get(session._c_object)
591
!= 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:
592
254
# ...do the normal thing
593
255
return session.peer_certificate
594
256
list_size = ctypes.c_uint()
595
cert_list = (gnutls.library.functions
596
.gnutls_certificate_get_peers
597
(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))
598
259
if list_size.value == 0:
600
261
cert = cert_list[0]
604
265
def fingerprint(openpgp):
605
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
606
# New GnuTLS "datum" with the OpenPGP public key
607
datum = (gnutls.library.types
608
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
611
ctypes.c_uint(len(openpgp))))
612
266
# New empty GnuTLS certificate
613
267
crt = gnutls.library.types.gnutls_openpgp_crt_t()
614
(gnutls.library.functions
615
.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)))
616
275
# Import the OpenPGP public key into the certificate
617
(gnutls.library.functions
618
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
619
gnutls.library.constants
620
.GNUTLS_OPENPGP_FMT_RAW))
621
# Verify the self signature in the key
622
crtverify = ctypes.c_uint()
623
(gnutls.library.functions
624
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
625
if crtverify.value != 0:
626
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
627
raise gnutls.errors.CertificateSecurityError("Verify failed")
276
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
279
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
628
280
# New buffer for the fingerprint
629
buf = ctypes.create_string_buffer(20)
630
buf_len = ctypes.c_size_t()
281
buffer = ctypes.create_string_buffer(20)
282
buffer_length = ctypes.c_size_t()
631
283
# Get the fingerprint from the certificate into the buffer
632
(gnutls.library.functions
633
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
634
ctypes.byref(buf_len)))
284
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
635
286
# Deinit the certificate
636
287
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
637
288
# Convert the buffer to a Python bytestring
638
fpr = ctypes.string_at(buf, buf_len.value)
289
fpr = ctypes.string_at(buffer, buffer_length.value)
639
290
# Convert the bytestring to hexadecimal notation
640
291
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
644
class TCP_handler(SocketServer.BaseRequestHandler, object):
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
645
296
"""A TCP request handler class.
646
297
Instantiated by IPv6_TCPServer for each request to handle it.
647
298
Note: This will run in its own forked process."""
649
300
def handle(self):
650
logger.info(u"TCP connection from: %s",
651
unicode(self.client_address))
652
session = (gnutls.connection
653
.ClientSession(self.request,
657
line = self.request.makefile().readline()
658
logger.debug(u"Protocol version: %r", line)
660
if int(line.strip().split()[0]) > 1:
662
except (ValueError, IndexError, RuntimeError), error:
663
logger.error(u"Unknown protocol version: %s", error)
666
# Note: gnutls.connection.X509Credentials is really a generic
667
# GnuTLS certificate credentials object so long as no X.509
668
# keys are added to it. Therefore, we can use it here despite
669
# 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,
671
311
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
672
312
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
674
# Use a fallback default, since this MUST be set.
675
priority = self.server.settings.get("priority", "NORMAL")
676
(gnutls.library.functions
677
.gnutls_priority_set_direct(session._c_object,
314
priority = "SECURE256"
316
gnutls.library.functions.gnutls_priority_set_direct\
317
(session._c_object, priority, None);
681
320
session.handshake()
682
321
except gnutls.errors.GNUTLSError, error:
683
logger.warning(u"Handshake failed: %s", error)
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
684
324
# Do not run session.bye() here: the session is not
685
325
# established. Just abandon the request.
688
328
fpr = fingerprint(peer_certificate(session))
689
329
except (TypeError, gnutls.errors.GNUTLSError), error:
690
logger.warning(u"Bad certificate: %s", error)
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
693
logger.debug(u"Fingerprint: %s", fpr)
694
for c in self.server.clients:
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
695
338
if c.fingerprint == fpr:
699
logger.warning(u"Client not found for fingerprint: %s",
703
341
# Have to check if client.still_valid(), since it is possible
704
342
# that the client timed out while establishing the GnuTLS
706
if not client.still_valid():
707
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)
711
## This won't work here, since we're in a fork.
712
# client.bump_timeout()
714
355
while sent_size < len(client.secret):
715
356
sent = session.send(client.secret[sent_size:])
716
logger.debug(u"Sent: %d, remaining: %d",
717
sent, len(client.secret)
718
- (sent_size + sent))
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
719
361
sent_size += sent
723
class IPv6_TCPServer(SocketServer.ForkingMixIn,
724
SocketServer.TCPServer, object):
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
725
366
"""IPv6 TCP server. Accepts 'None' as address and/or port.
727
settings: Server settings
368
options: Command line options
728
369
clients: Set() of Client objects
729
enabled: Boolean; whether this server is activated yet
731
371
address_family = socket.AF_INET6
732
372
def __init__(self, *args, **kwargs):
733
if "settings" in kwargs:
734
self.settings = kwargs["settings"]
735
del kwargs["settings"]
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
736
376
if "clients" in kwargs:
737
377
self.clients = kwargs["clients"]
738
378
del kwargs["clients"]
740
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
379
return super(type(self), self).__init__(*args, **kwargs)
741
380
def server_bind(self):
742
381
"""This overrides the normal server_bind() function
743
382
to bind to an interface if one was specified, and also NOT to
744
383
bind to an address or port if they were not specified."""
745
if self.settings["interface"]:
746
# 25 is from /usr/include/asm-i486/socket.h
747
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
749
389
self.socket.setsockopt(socket.SOL_SOCKET,
751
self.settings["interface"])
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
752
392
except socket.error, error:
753
393
if error[0] == errno.EPERM:
754
logger.error(u"No permission to"
755
u" bind to interface %s",
756
self.settings["interface"])
394
sys.stderr.write(u"Warning: No permission to" \
395
u" bind to interface %s\n"
396
% self.options.interface)
759
399
# Only bind(2) the socket if we really need to.
793
421
datetime.timedelta(1)
794
422
>>> string_to_delta(u'1w')
795
423
datetime.timedelta(7)
796
>>> string_to_delta('5m 30s')
797
datetime.timedelta(0, 330)
799
timevalue = datetime.timedelta(0)
800
for s in interval.split():
802
suffix = unicode(s[-1])
805
delta = datetime.timedelta(value)
807
delta = datetime.timedelta(0, value)
809
delta = datetime.timedelta(0, 0, 0, 0, value)
811
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
813
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
816
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:
822
479
def server_state_changed(state):
823
"""Derived from the Avahi example code"""
480
"""From the Avahi server example code"""
824
481
if state == avahi.SERVER_COLLISION:
825
logger.error(u"Zeroconf server name collision")
482
sys.stderr.write(u"WARNING: Server name collision\n")
827
484
elif state == avahi.SERVER_RUNNING:
831
488
def entry_group_state_changed(state, error):
832
"""Derived from the Avahi example code"""
833
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)
835
495
if state == avahi.ENTRY_GROUP_ESTABLISHED:
836
logger.debug(u"Zeroconf service established.")
497
sys.stderr.write(u"Service established.\n")
837
498
elif state == avahi.ENTRY_GROUP_COLLISION:
838
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"
840
513
elif state == avahi.ENTRY_GROUP_FAILURE:
841
logger.critical(u"Avahi: Error in group state changed %s",
843
raise AvahiGroupError(u"State changed: %s" % unicode(error))
514
sys.stderr.write(u"Error in group state changed %s\n"
845
520
def if_nametoindex(interface):
846
"""Call the C function if_nametoindex(), or equivalent"""
847
global if_nametoindex
521
"""Call the C function if_nametoindex()"""
849
if_nametoindex = (ctypes.cdll.LoadLibrary
850
(ctypes.util.find_library("c"))
523
libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
return libc.if_nametoindex(interface)
852
525
except (OSError, AttributeError):
853
526
if "struct" not in sys.modules:
855
528
if "fcntl" not in sys.modules:
857
def if_nametoindex(interface):
858
"Get an interface index the hard way, i.e. using fcntl()"
859
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
860
with closing(socket.socket()) as s:
861
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
862
struct.pack("16s16x", interface))
863
interface_index = struct.unpack("I", ifreq[16:20])[0]
864
return interface_index
865
return if_nametoindex(interface)
868
def daemon(nochdir = False, noclose = False):
869
"""See daemon(3). Standard BSD Unix function.
870
This should really exist as os.daemon, but it doesn't (yet)."""
879
# Close all standard open file descriptors
880
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
881
if not stat.S_ISCHR(os.fstat(null).st_mode):
882
raise OSError(errno.ENODEV,
883
"/dev/null not a character device")
884
os.dup2(null, sys.stdin.fileno())
885
os.dup2(null, sys.stdout.fileno())
886
os.dup2(null, sys.stderr.fileno())
892
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()
893
541
parser.add_option("-i", "--interface", type="string",
894
metavar="IF", help="Bind to interface IF")
895
parser.add_option("-a", "--address", type="string",
896
help="Address to listen for requests on")
897
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,
898
557
help="Port number to receive requests on")
899
parser.add_option("--check", action="store_true",
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")
564
parser.add_option("--check", action="store_true", default=False,
900
565
help="Run self-test")
901
parser.add_option("--debug", action="store_true",
902
help="Debug mode; run in foreground and log to"
904
parser.add_option("--priority", type="string", help="GnuTLS"
905
" priority string (see GnuTLS documentation)")
906
parser.add_option("--servicename", type="string", metavar="NAME",
907
help="Zeroconf service name")
908
parser.add_option("--configdir", type="string",
909
default="/etc/mandos", metavar="DIR",
910
help="Directory to search for configuration"
912
parser.add_option("--no-dbus", action="store_false",
914
help="Do not provide D-Bus system bus"
916
options = parser.parse_args()[0]
566
parser.add_option("--debug", action="store_true", default=False,
568
(options, args) = parser.parse_args()
918
570
if options.check:
920
572
doctest.testmod()
923
# Default values for config file for server-global settings
924
server_defaults = { "interface": "",
929
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
930
"servicename": "Mandos",
934
# Parse config file for server-global settings
935
server_config = ConfigParser.SafeConfigParser(server_defaults)
937
server_config.read(os.path.join(options.configdir, "mandos.conf"))
938
# Convert the SafeConfigParser object to a dict
939
server_settings = server_config.defaults()
940
# Use getboolean on the boolean config options
941
server_settings["debug"] = (server_config.getboolean
942
("DEFAULT", "debug"))
943
server_settings["use_dbus"] = (server_config.getboolean
944
("DEFAULT", "use_dbus"))
947
# Override the settings from the config file with command line
949
for option in ("interface", "address", "port", "debug",
950
"priority", "servicename", "configdir",
952
value = getattr(options, option)
953
if value is not None:
954
server_settings[option] = value
956
# Now we have our good server settings in "server_settings"
959
debug = server_settings["debug"]
960
use_dbus = server_settings["use_dbus"]
963
syslogger.setLevel(logging.WARNING)
964
console.setLevel(logging.WARNING)
966
if server_settings["servicename"] != "Mandos":
967
syslogger.setFormatter(logging.Formatter
968
('Mandos (%s): %%(levelname)s:'
970
% server_settings["servicename"]))
972
# Parse config file with clients
973
client_defaults = { "timeout": "1h",
975
"checker": "fping -q -- %(host)s",
978
client_config = ConfigParser.SafeConfigParser(client_defaults)
979
client_config.read(os.path.join(server_settings["configdir"],
983
tcp_server = IPv6_TCPServer((server_settings["address"],
984
server_settings["port"]),
986
settings=server_settings,
988
pidfilename = "/var/run/mandos.pid"
990
pidfile = open(pidfilename, "w")
991
except IOError, error:
992
logger.error("Could not open file %r", pidfilename)
995
uid = pwd.getpwnam("_mandos").pw_uid
998
uid = pwd.getpwnam("mandos").pw_uid
1001
uid = pwd.getpwnam("nobody").pw_uid
1005
gid = pwd.getpwnam("_mandos").pw_gid
1008
gid = pwd.getpwnam("mandos").pw_gid
1011
gid = pwd.getpwnam("nogroup").pw_gid
1017
except OSError, error:
1018
if error[0] != errno.EPERM:
1022
service = AvahiService(name = server_settings["servicename"],
1023
servicetype = "_mandos._tcp", )
1024
if server_settings["interface"]:
1025
service.interface = (if_nametoindex
1026
(server_settings["interface"]))
1031
# 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
1032
592
DBusGMainLoop(set_as_default=True )
1033
593
main_loop = gobject.MainLoop()
1034
594
bus = dbus.SystemBus()
1035
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1036
avahi.DBUS_PATH_SERVER),
1037
avahi.DBUS_INTERFACE_SERVER)
595
server = dbus.Interface(
596
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
597
avahi.DBUS_INTERFACE_SERVER )
1038
598
# End of Avahi example code
1040
bus_name = dbus.service.BusName(u"org.mandos-system.Mandos",
1043
clients.update(Set(Client(name = section,
1045
= dict(client_config.items(section)),
1046
use_dbus = use_dbus)
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\
1047
614
for section in client_config.sections()))
1049
logger.warning(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]
1052
# Redirect stdin so all checkers get /dev/null
1053
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1054
os.dup2(null, sys.stdin.fileno())
1058
# No console logging
1059
logger.removeHandler(console)
1060
# Close all input and output, do double fork, etc.
1065
pidfile.write(str(pid) + "\n")
1069
logger.error(u"Could not write to file %r with PID %d",
1072
# "pidfile" was never created
1077
"Cleanup function; run on exit"
1079
# From the Avahi example code
1080
if not group is None:
1083
# End of Avahi example code
1086
client = clients.pop()
1087
client.disable_hook = None
1090
atexit.register(cleanup)
1093
signal.signal(signal.SIGINT, signal.SIG_IGN)
1094
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1095
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1098
class MandosServer(dbus.service.Object):
1099
"""A D-Bus proxy object"""
1101
dbus.service.Object.__init__(self, bus,
1103
_interface = u"org.mandos_system.Mandos"
1105
@dbus.service.signal(_interface, signature="oa{sv}")
1106
def ClientAdded(self, objpath, properties):
1110
@dbus.service.signal(_interface, signature="o")
1111
def ClientRemoved(self, objpath):
1115
@dbus.service.method(_interface, out_signature="ao")
1116
def GetAllClients(self):
1117
return dbus.Array(c.dbus_object_path for c in clients)
1119
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1120
def GetAllClientsWithProperties(self):
1121
return dbus.Dictionary(
1122
((c.dbus_object_path, c.GetAllProperties())
1126
@dbus.service.method(_interface, in_signature="o")
1127
def RemoveClient(self, object_path):
1129
if c.dbus_object_path == object_path:
1131
# Don't signal anything except ClientRemoved
1135
self.ClientRemoved(object_path)
1138
@dbus.service.method(_interface)
1144
mandos_server = MandosServer()
1146
for client in clients:
1149
mandos_server.ClientAdded(client.dbus_object_path,
1150
client.GetAllProperties())
1154
tcp_server.server_activate()
1156
# Find out what port we got
1157
service.port = tcp_server.socket.getsockname()[1]
1158
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1159
u" scope_id %d" % tcp_server.socket.getsockname())
1161
#service.interface = tcp_server.socket.getsockname()[3]
1164
# From the Avahi example code
1165
server.connect_to_signal("StateChanged", server_state_changed)
1167
server_state_changed(server.GetState())
1168
except dbus.exceptions.DBusException, error:
1169
logger.critical(u"DBusException: %s", error)
1171
# End of Avahi example code
1173
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1174
lambda *args, **kwargs:
1175
(tcp_server.handle_request
1176
(*args[2:], **kwargs) or True))
1178
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:],
1180
except AvahiError, error:
1181
logger.critical(u"AvahiError: %s", error)
1183
641
except KeyboardInterrupt:
1187
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