22
52
from sets import Set
57
import logging.handlers
59
from contextlib import closing
28
65
from dbus.mainloop.glib import DBusGMainLoop
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
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
45
170
# 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):
49
179
"""A representation of a client host served by this server.
51
name: string; from the config file, used in log messages
181
name: string; from the config file, used in log messages and
52
183
fingerprint: string (40 or 32 hexadecimal digits); used to
53
184
uniquely identify the client
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.
185
secret: bytestring; sent verbatim (over TLS) to client
186
host: string; available for use by the checker command
187
created: datetime.datetime(); (UTC) object creation
188
last_enabled: datetime.datetime(); (UTC)
190
last_checked_ok: datetime.datetime(); (UTC) or None
191
timeout: datetime.timedelta(); How long from last_checked_ok
192
until this client is invalid
193
interval: datetime.timedelta(); How often to start a new checker
194
disable_hook: If set, called by disable() as disable_hook(self)
195
checker: subprocess.Popen(); a running checker process used
196
to see if the client lives.
197
'None' if no process is running.
65
198
checker_initiator_tag: a gobject event source tag, or None
66
stop_initiator_tag: - '' -
199
disable_initiator_tag: - '' -
67
200
checker_callback_tag: - '' -
68
201
checker_command: string; External command which is run to check if
69
client lives. %()s expansions are done at
202
client lives. %() expansions are done at
70
203
runtime with vars(self) as dict, so that for
71
204
instance %(name)s can be used in the command.
73
_timeout: Real variable for 'timeout'
74
_interval: Real variable for 'interval'
75
_timeout_milliseconds: Used by gobject.timeout_add()
76
_interval_milliseconds: - '' -
205
use_dbus: bool(); Whether to provide D-Bus interface and signals
206
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
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):
208
def timeout_milliseconds(self):
209
"Return the 'timeout' attribute in milliseconds"
210
return ((self.timeout.days * 24 * 60 * 60 * 1000)
211
+ (self.timeout.seconds * 1000)
212
+ (self.timeout.microseconds // 1000))
214
def interval_milliseconds(self):
215
"Return the 'interval' attribute in milliseconds"
216
return ((self.interval.days * 24 * 60 * 60 * 1000)
217
+ (self.interval.seconds * 1000)
218
+ (self.interval.microseconds // 1000))
220
def __init__(self, name = None, disable_hook=None, config=None,
222
"""Note: the 'checker' key in 'config' sets the
223
'checker_command' attribute and *not* the 'checker'
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
228
logger.debug(u"Creating client %r", self.name)
229
self.use_dbus = False # During __init__
230
# Uppercase and remove spaces from fingerprint for later
231
# comparison purposes with return value from the fingerprint()
233
self.fingerprint = (config["fingerprint"].upper()
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
236
if "secret" in config:
237
self.secret = config["secret"].decode(u"base64")
238
elif "secfile" in config:
239
with closing(open(os.path.expanduser
241
(config["secfile"])))) as secfile:
242
self.secret = secfile.read()
244
raise TypeError(u"No secret or secfile for client %s"
246
self.host = config.get("host", "")
247
self.created = datetime.datetime.utcnow()
249
self.last_enabled = None
250
self.last_checked_ok = None
251
self.timeout = string_to_delta(config["timeout"])
252
self.interval = string_to_delta(config["interval"])
253
self.disable_hook = disable_hook
130
254
self.checker = None
131
255
self.checker_initiator_tag = None
132
self.stop_initiator_tag = None
256
self.disable_initiator_tag = None
133
257
self.checker_callback_tag = None
134
self.check_command = checker
136
"""Start this clients checker and timeout hooks"""
258
self.checker_command = config["checker"]
259
self.last_connect = None
260
# Only now, when this client is initialized, can it show up on
262
self.use_dbus = use_dbus
264
self.dbus_object_path = (dbus.ObjectPath
266
+ self.name.replace(".", "_")))
267
dbus.service.Object.__init__(self, bus,
268
self.dbus_object_path)
271
"""Start this client's checker and timeout hooks"""
272
self.last_enabled = datetime.datetime.utcnow()
137
273
# Schedule a new checker to be started an 'interval' from now,
138
274
# and every interval from then on.
139
self.checker_initiator_tag = gobject.timeout_add\
140
(self._interval_milliseconds,
275
self.checker_initiator_tag = (gobject.timeout_add
276
(self.interval_milliseconds(),
142
278
# Also start a new checker *right now*.
143
279
self.start_checker()
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:
280
# Schedule a disable() when 'timeout' has passed
281
self.disable_initiator_tag = (gobject.timeout_add
282
(self.timeout_milliseconds(),
287
self.PropertyChanged(dbus.String(u"enabled"),
288
dbus.Boolean(True, variant_level=1))
289
self.PropertyChanged(dbus.String(u"last_enabled"),
290
(_datetime_to_dbus(self.last_enabled,
294
"""Disable this client."""
295
if not getattr(self, "enabled", False):
297
logger.info(u"Disabling client %s", self.name)
298
if getattr(self, "disable_initiator_tag", False):
299
gobject.source_remove(self.disable_initiator_tag)
300
self.disable_initiator_tag = None
301
if getattr(self, "checker_initiator_tag", False):
159
302
gobject.source_remove(self.checker_initiator_tag)
160
303
self.checker_initiator_tag = None
161
304
self.stop_checker()
305
if self.disable_hook:
306
self.disable_hook(self)
310
self.PropertyChanged(dbus.String(u"enabled"),
311
dbus.Boolean(False, variant_level=1))
164
312
# Do not run this again if called by a gobject.timeout_add
166
315
def __del__(self):
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):
316
self.disable_hook = None
319
def checker_callback(self, pid, condition, command):
178
320
"""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"
321
self.checker_callback_tag = None
325
self.PropertyChanged(dbus.String(u"checker_running"),
326
dbus.Boolean(False, variant_level=1))
327
if os.WIFEXITED(condition):
328
exitstatus = os.WEXITSTATUS(condition)
330
logger.info(u"Checker for %(name)s succeeded",
195
sys.stderr.write(u"Checker for %(name)s failed\n"
198
self.checker_callback_tag = None
334
logger.info(u"Checker for %(name)s failed",
338
self.CheckerCompleted(dbus.Int16(exitstatus),
339
dbus.Int64(condition),
340
dbus.String(command))
342
logger.warning(u"Checker for %(name)s crashed?",
346
self.CheckerCompleted(dbus.Int16(-1),
347
dbus.Int64(condition),
348
dbus.String(command))
350
def checked_ok(self):
351
"""Bump up the timeout for this client.
352
This should only be called when the client has been seen,
355
self.last_checked_ok = datetime.datetime.utcnow()
356
gobject.source_remove(self.disable_initiator_tag)
357
self.disable_initiator_tag = (gobject.timeout_add
358
(self.timeout_milliseconds(),
362
self.PropertyChanged(
363
dbus.String(u"last_checked_ok"),
364
(_datetime_to_dbus(self.last_checked_ok,
199
367
def start_checker(self):
200
368
"""Start a new checker subprocess if one is not running.
201
369
If a checker already exists, leave it running and do
371
# The reason for not killing a running checker is that if we
372
# did that, then if a checker (for some reason) started
373
# running slowly and taking more than 'interval' time, the
374
# client would inevitably timeout, since no checker would get
375
# a chance to run to completion. If we instead leave running
376
# checkers alone, the checker would have to take more time
377
# than 'timeout' for the client to be declared invalid, which
378
# is as it should be.
203
379
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
208
command = self.check_command % self.fqdn
381
# In case checker_command has exactly one % operator
382
command = self.checker_command % self.host
209
383
except TypeError:
384
# Escape attributes for the shell
210
385
escaped_attrs = dict((key, re.escape(str(val)))
212
387
vars(self).iteritems())
213
command = self.check_command % escaped_attrs
389
command = self.checker_command % escaped_attrs
390
except TypeError, error:
391
logger.error(u'Could not format string "%s":'
392
u' %s', self.checker_command, error)
393
return True # Try again later
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"
395
logger.info(u"Starting checker %r for %s",
397
# We don't need to redirect stdout and stderr, since
398
# in normal mode, that is already done by daemon(),
399
# and in debug mode we don't want to. (Stdin is
400
# always replaced by /dev/null.)
401
self.checker = subprocess.Popen(command,
406
self.CheckerStarted(command)
407
self.PropertyChanged(
408
dbus.String("checker_running"),
409
dbus.Boolean(True, variant_level=1))
410
self.checker_callback_tag = (gobject.child_watch_add
412
self.checker_callback,
414
except OSError, error:
415
logger.error(u"Failed to start subprocess: %s",
227
417
# Re-run this periodically if run by gobject.timeout_add
229
420
def stop_checker(self):
230
421
"""Force the checker process, if any, to stop."""
231
if not hasattr(self, "checker") or self.checker is None:
422
if self.checker_callback_tag:
423
gobject.source_remove(self.checker_callback_tag)
424
self.checker_callback_tag = None
425
if getattr(self, "checker", None) is None:
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)
427
logger.debug(u"Stopping checker for %(name)s", vars(self))
429
os.kill(self.checker.pid, signal.SIGTERM)
431
#if self.checker.poll() is None:
432
# os.kill(self.checker.pid, signal.SIGKILL)
433
except OSError, error:
434
if error.errno != errno.ESRCH: # No such process
238
436
self.checker = None
239
def still_valid(self, now=None):
438
self.PropertyChanged(dbus.String(u"checker_running"),
439
dbus.Boolean(False, variant_level=1))
441
def still_valid(self):
240
442
"""Has the timeout not yet passed for this client?"""
242
now = datetime.datetime.now()
243
if self.last_seen is None:
443
if not getattr(self, "enabled", False):
445
now = datetime.datetime.utcnow()
446
if self.last_checked_ok is None:
244
447
return now < (self.created + self.timeout)
246
return now < (self.last_seen + self.timeout)
449
return now < (self.last_checked_ok + self.timeout)
451
## D-Bus methods & signals
452
_interface = u"se.bsnet.fukt.Mandos.Client"
455
CheckedOK = dbus.service.method(_interface)(checked_ok)
456
CheckedOK.__name__ = "CheckedOK"
458
# CheckerCompleted - signal
459
@dbus.service.signal(_interface, signature="nxs")
460
def CheckerCompleted(self, exitcode, waitstatus, command):
464
# CheckerStarted - signal
465
@dbus.service.signal(_interface, signature="s")
466
def CheckerStarted(self, command):
470
# GetAllProperties - method
471
@dbus.service.method(_interface, out_signature="a{sv}")
472
def GetAllProperties(self):
474
return dbus.Dictionary({
476
dbus.String(self.name, variant_level=1),
477
dbus.String("fingerprint"):
478
dbus.String(self.fingerprint, variant_level=1),
480
dbus.String(self.host, variant_level=1),
481
dbus.String("created"):
482
_datetime_to_dbus(self.created, variant_level=1),
483
dbus.String("last_enabled"):
484
(_datetime_to_dbus(self.last_enabled,
486
if self.last_enabled is not None
487
else dbus.Boolean(False, variant_level=1)),
488
dbus.String("enabled"):
489
dbus.Boolean(self.enabled, variant_level=1),
490
dbus.String("last_checked_ok"):
491
(_datetime_to_dbus(self.last_checked_ok,
493
if self.last_checked_ok is not None
494
else dbus.Boolean (False, variant_level=1)),
495
dbus.String("timeout"):
496
dbus.UInt64(self.timeout_milliseconds(),
498
dbus.String("interval"):
499
dbus.UInt64(self.interval_milliseconds(),
501
dbus.String("checker"):
502
dbus.String(self.checker_command,
504
dbus.String("checker_running"):
505
dbus.Boolean(self.checker is not None,
507
dbus.String("object_path"):
508
dbus.ObjectPath(self.dbus_object_path,
512
# IsStillValid - method
513
IsStillValid = (dbus.service.method(_interface, out_signature="b")
515
IsStillValid.__name__ = "IsStillValid"
517
# PropertyChanged - signal
518
@dbus.service.signal(_interface, signature="sv")
519
def PropertyChanged(self, property, value):
523
# SetChecker - method
524
@dbus.service.method(_interface, in_signature="s")
525
def SetChecker(self, checker):
526
"D-Bus setter method"
527
self.checker_command = checker
529
self.PropertyChanged(dbus.String(u"checker"),
530
dbus.String(self.checker_command,
534
@dbus.service.method(_interface, in_signature="s")
535
def SetHost(self, host):
536
"D-Bus setter method"
539
self.PropertyChanged(dbus.String(u"host"),
540
dbus.String(self.host, variant_level=1))
542
# SetInterval - method
543
@dbus.service.method(_interface, in_signature="t")
544
def SetInterval(self, milliseconds):
545
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
547
self.PropertyChanged(dbus.String(u"interval"),
548
(dbus.UInt64(self.interval_milliseconds(),
552
@dbus.service.method(_interface, in_signature="ay",
554
def SetSecret(self, secret):
555
"D-Bus setter method"
556
self.secret = str(secret)
558
# SetTimeout - method
559
@dbus.service.method(_interface, in_signature="t")
560
def SetTimeout(self, milliseconds):
561
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
563
self.PropertyChanged(dbus.String(u"timeout"),
564
(dbus.UInt64(self.timeout_milliseconds(),
568
Enable = dbus.service.method(_interface)(enable)
569
Enable.__name__ = "Enable"
571
# StartChecker - method
572
@dbus.service.method(_interface)
573
def StartChecker(self):
578
@dbus.service.method(_interface)
583
# StopChecker - method
584
StopChecker = dbus.service.method(_interface)(stop_checker)
585
StopChecker.__name__ = "StopChecker"
249
590
def peer_certificate(session):
591
"Return the peer's OpenPGP certificate as a bytestring"
250
592
# If not an OpenPGP certificate...
251
if gnutls.library.functions.gnutls_certificate_type_get\
252
(session._c_object) \
253
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
593
if (gnutls.library.functions
594
.gnutls_certificate_type_get(session._c_object)
595
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
254
596
# ...do the normal thing
255
597
return session.peer_certificate
256
list_size = ctypes.c_uint()
257
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
258
(session._c_object, ctypes.byref(list_size))
598
list_size = ctypes.c_uint(1)
599
cert_list = (gnutls.library.functions
600
.gnutls_certificate_get_peers
601
(session._c_object, ctypes.byref(list_size)))
602
if not bool(cert_list) and list_size.value != 0:
603
raise gnutls.errors.GNUTLSError("error getting peer"
259
605
if list_size.value == 0:
261
607
cert = cert_list[0]
265
611
def fingerprint(openpgp):
612
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
613
# New GnuTLS "datum" with the OpenPGP public key
614
datum = (gnutls.library.types
615
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
618
ctypes.c_uint(len(openpgp))))
266
619
# New empty GnuTLS certificate
267
620
crt = gnutls.library.types.gnutls_openpgp_crt_t()
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)))
621
(gnutls.library.functions
622
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
275
623
# Import the OpenPGP public key into the certificate
276
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
279
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
624
(gnutls.library.functions
625
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
626
gnutls.library.constants
627
.GNUTLS_OPENPGP_FMT_RAW))
628
# Verify the self signature in the key
629
crtverify = ctypes.c_uint()
630
(gnutls.library.functions
631
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
632
if crtverify.value != 0:
633
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
634
raise gnutls.errors.CertificateSecurityError("Verify failed")
280
635
# New buffer for the fingerprint
281
buffer = ctypes.create_string_buffer(20)
282
buffer_length = ctypes.c_size_t()
636
buf = ctypes.create_string_buffer(20)
637
buf_len = ctypes.c_size_t()
283
638
# Get the fingerprint from the certificate into the buffer
284
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
639
(gnutls.library.functions
640
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
641
ctypes.byref(buf_len)))
286
642
# Deinit the certificate
287
643
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
288
644
# Convert the buffer to a Python bytestring
289
fpr = ctypes.string_at(buffer, buffer_length.value)
645
fpr = ctypes.string_at(buf, buf_len.value)
290
646
# Convert the bytestring to hexadecimal notation
291
647
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
651
class TCP_handler(SocketServer.BaseRequestHandler, object):
296
652
"""A TCP request handler class.
297
653
Instantiated by IPv6_TCPServer for each request to handle it.
298
654
Note: This will run in its own forked process."""
300
656
def handle(self):
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,
657
logger.info(u"TCP connection from: %s",
658
unicode(self.client_address))
659
session = (gnutls.connection
660
.ClientSession(self.request,
664
line = self.request.makefile().readline()
665
logger.debug(u"Protocol version: %r", line)
667
if int(line.strip().split()[0]) > 1:
669
except (ValueError, IndexError, RuntimeError), error:
670
logger.error(u"Unknown protocol version: %s", error)
673
# Note: gnutls.connection.X509Credentials is really a generic
674
# GnuTLS certificate credentials object so long as no X.509
675
# keys are added to it. Therefore, we can use it here despite
676
# using OpenPGP certificates.
311
678
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
312
679
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
314
priority = "SECURE256"
316
gnutls.library.functions.gnutls_priority_set_direct\
317
(session._c_object, priority, None);
681
# Use a fallback default, since this MUST be set.
682
priority = self.server.settings.get("priority", "NORMAL")
683
(gnutls.library.functions
684
.gnutls_priority_set_direct(session._c_object,
320
688
session.handshake()
321
689
except gnutls.errors.GNUTLSError, error:
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
690
logger.warning(u"Handshake failed: %s", error)
324
691
# Do not run session.bye() here: the session is not
325
692
# established. Just abandon the request.
694
logger.debug(u"Handshake succeeded")
328
696
fpr = fingerprint(peer_certificate(session))
329
697
except (TypeError, gnutls.errors.GNUTLSError), error:
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
698
logger.warning(u"Bad certificate: %s", error)
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
701
logger.debug(u"Fingerprint: %s", fpr)
703
for c in self.server.clients:
338
704
if c.fingerprint == fpr:
708
logger.warning(u"Client not found for fingerprint: %s",
341
712
# Have to check if client.still_valid(), since it is possible
342
713
# that the client timed out while establishing the GnuTLS
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)
715
if not client.still_valid():
716
logger.warning(u"Client %(name)s is invalid",
720
## This won't work here, since we're in a fork.
721
# client.checked_ok()
355
723
while sent_size < len(client.secret):
356
724
sent = session.send(client.secret[sent_size:])
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
725
logger.debug(u"Sent: %d, remaining: %d",
726
sent, len(client.secret)
727
- (sent_size + sent))
361
728
sent_size += sent
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
732
class IPv6_TCPServer(SocketServer.ForkingMixIn,
733
SocketServer.TCPServer, object):
366
734
"""IPv6 TCP server. Accepts 'None' as address and/or port.
368
options: Command line options
736
settings: Server settings
369
737
clients: Set() of Client objects
738
enabled: Boolean; whether this server is activated yet
371
740
address_family = socket.AF_INET6
372
741
def __init__(self, *args, **kwargs):
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
742
if "settings" in kwargs:
743
self.settings = kwargs["settings"]
744
del kwargs["settings"]
376
745
if "clients" in kwargs:
377
746
self.clients = kwargs["clients"]
378
747
del kwargs["clients"]
379
return super(type(self), self).__init__(*args, **kwargs)
749
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
380
750
def server_bind(self):
381
751
"""This overrides the normal server_bind() function
382
752
to bind to an interface if one was specified, and also NOT to
383
753
bind to an address or port if they were not specified."""
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
754
if self.settings["interface"]:
755
# 25 is from /usr/include/asm-i486/socket.h
756
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
389
758
self.socket.setsockopt(socket.SOL_SOCKET,
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
760
self.settings["interface"])
392
761
except socket.error, error:
393
762
if error[0] == errno.EPERM:
394
sys.stderr.write(u"Warning: No permission to" \
395
u" bind to interface %s\n"
396
% self.options.interface)
763
logger.error(u"No permission to"
764
u" bind to interface %s",
765
self.settings["interface"])
399
768
# Only bind(2) the socket if we really need to.
421
802
datetime.timedelta(1)
422
803
>>> string_to_delta(u'1w')
423
804
datetime.timedelta(7)
805
>>> string_to_delta('5m 30s')
806
datetime.timedelta(0, 330)
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)
808
timevalue = datetime.timedelta(0)
809
for s in interval.split():
811
suffix = unicode(s[-1])
814
delta = datetime.timedelta(value)
816
delta = datetime.timedelta(0, value)
818
delta = datetime.timedelta(0, 0, 0, 0, value)
820
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
822
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
825
except (ValueError, IndexError):
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:
479
831
def server_state_changed(state):
480
"""From the Avahi server example code"""
832
"""Derived from the Avahi example code"""
481
833
if state == avahi.SERVER_COLLISION:
482
sys.stderr.write(u"WARNING: Server name collision\n")
834
logger.error(u"Zeroconf server name collision")
484
836
elif state == avahi.SERVER_RUNNING:
488
840
def entry_group_state_changed(state, error):
489
"""From the Avahi server example code"""
490
global serviceName, server, rename_count
493
sys.stderr.write(u"state change: %i\n" % state)
841
"""Derived from the Avahi example code"""
842
logger.debug(u"Avahi state change: %i", state)
495
844
if state == avahi.ENTRY_GROUP_ESTABLISHED:
497
sys.stderr.write(u"Service established.\n")
845
logger.debug(u"Zeroconf service established.")
498
846
elif state == avahi.ENTRY_GROUP_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"
847
logger.warning(u"Zeroconf service name collision.")
513
849
elif state == avahi.ENTRY_GROUP_FAILURE:
514
sys.stderr.write(u"Error in group state changed %s\n"
850
logger.critical(u"Avahi: Error in group state changed %s",
852
raise AvahiGroupError(u"State changed: %s" % unicode(error))
520
854
def if_nametoindex(interface):
521
"""Call the C function if_nametoindex()"""
855
"""Call the C function if_nametoindex(), or equivalent"""
856
global if_nametoindex
523
libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
return libc.if_nametoindex(interface)
858
if_nametoindex = (ctypes.cdll.LoadLibrary
859
(ctypes.util.find_library("c"))
525
861
except (OSError, AttributeError):
526
862
if "struct" not in sys.modules:
528
864
if "fcntl" not in sys.modules:
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()
866
def if_nametoindex(interface):
867
"Get an interface index the hard way, i.e. using fcntl()"
868
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
869
with closing(socket.socket()) as s:
870
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
871
struct.pack("16s16x", interface))
872
interface_index = struct.unpack("I", ifreq[16:20])[0]
873
return interface_index
874
return if_nametoindex(interface)
877
def daemon(nochdir = False, noclose = False):
878
"""See daemon(3). Standard BSD Unix function.
879
This should really exist as os.daemon, but it doesn't (yet)."""
888
# Close all standard open file descriptors
889
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
890
if not stat.S_ISCHR(os.fstat(null).st_mode):
891
raise OSError(errno.ENODEV,
892
"/dev/null not a character device")
893
os.dup2(null, sys.stdin.fileno())
894
os.dup2(null, sys.stdout.fileno())
895
os.dup2(null, sys.stderr.fileno())
901
parser = optparse.OptionParser(version = "%%prog %s" % version)
541
902
parser.add_option("-i", "--interface", type="string",
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,
903
metavar="IF", help="Bind to interface IF")
904
parser.add_option("-a", "--address", type="string",
905
help="Address to listen for requests on")
906
parser.add_option("-p", "--port", type="int",
557
907
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")
564
parser.add_option("--check", action="store_true", default=False,
908
parser.add_option("--check", action="store_true",
565
909
help="Run self-test")
566
parser.add_option("--debug", action="store_true", default=False,
568
(options, args) = parser.parse_args()
910
parser.add_option("--debug", action="store_true",
911
help="Debug mode; run in foreground and log to"
913
parser.add_option("--priority", type="string", help="GnuTLS"
914
" priority string (see GnuTLS documentation)")
915
parser.add_option("--servicename", type="string", metavar="NAME",
916
help="Zeroconf service name")
917
parser.add_option("--configdir", type="string",
918
default="/etc/mandos", metavar="DIR",
919
help="Directory to search for configuration"
921
parser.add_option("--no-dbus", action="store_false",
923
help="Do not provide D-Bus system bus"
925
options = parser.parse_args()[0]
570
927
if options.check:
572
929
doctest.testmod()
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
932
# Default values for config file for server-global settings
933
server_defaults = { "interface": "",
938
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
939
"servicename": "Mandos",
943
# Parse config file for server-global settings
944
server_config = ConfigParser.SafeConfigParser(server_defaults)
946
server_config.read(os.path.join(options.configdir, "mandos.conf"))
947
# Convert the SafeConfigParser object to a dict
948
server_settings = server_config.defaults()
949
# Use the appropriate methods on the non-string config options
950
server_settings["debug"] = server_config.getboolean("DEFAULT",
952
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
954
if server_settings["port"]:
955
server_settings["port"] = server_config.getint("DEFAULT",
959
# Override the settings from the config file with command line
961
for option in ("interface", "address", "port", "debug",
962
"priority", "servicename", "configdir",
964
value = getattr(options, option)
965
if value is not None:
966
server_settings[option] = value
968
# Now we have our good server settings in "server_settings"
971
debug = server_settings["debug"]
972
use_dbus = server_settings["use_dbus"]
974
def sigsegvhandler(signum, frame):
975
raise RuntimeError('Segmentation fault')
978
syslogger.setLevel(logging.WARNING)
979
console.setLevel(logging.WARNING)
981
signal.signal(signal.SIGSEGV, sigsegvhandler)
983
if server_settings["servicename"] != "Mandos":
984
syslogger.setFormatter(logging.Formatter
985
('Mandos (%s): %%(levelname)s:'
987
% server_settings["servicename"]))
989
# Parse config file with clients
990
client_defaults = { "timeout": "1h",
992
"checker": "fping -q -- %%(host)s",
995
client_config = ConfigParser.SafeConfigParser(client_defaults)
996
client_config.read(os.path.join(server_settings["configdir"],
1000
tcp_server = IPv6_TCPServer((server_settings["address"],
1001
server_settings["port"]),
1003
settings=server_settings,
1005
pidfilename = "/var/run/mandos.pid"
1007
pidfile = open(pidfilename, "w")
1008
except IOError, error:
1009
logger.error("Could not open file %r", pidfilename)
1012
uid = pwd.getpwnam("_mandos").pw_uid
1013
gid = pwd.getpwnam("_mandos").pw_gid
1016
uid = pwd.getpwnam("mandos").pw_uid
1017
gid = pwd.getpwnam("mandos").pw_gid
1020
uid = pwd.getpwnam("nobody").pw_uid
1021
gid = pwd.getpwnam("nogroup").pw_gid
1028
except OSError, error:
1029
if error[0] != errno.EPERM:
1032
# Enable all possible GnuTLS debugging
1034
# "Use a log level over 10 to enable all debugging options."
1036
gnutls.library.functions.gnutls_global_set_log_level(11)
1038
@gnutls.library.types.gnutls_log_func
1039
def debug_gnutls(level, string):
1040
logger.debug("GnuTLS: %s", string[:-1])
1042
(gnutls.library.functions
1043
.gnutls_global_set_log_function(debug_gnutls))
1046
service = AvahiService(name = server_settings["servicename"],
1047
servicetype = "_mandos._tcp", )
1048
if server_settings["interface"]:
1049
service.interface = (if_nametoindex
1050
(server_settings["interface"]))
1055
# From the Avahi example code
592
1056
DBusGMainLoop(set_as_default=True )
593
1057
main_loop = gobject.MainLoop()
594
1058
bus = dbus.SystemBus()
595
server = dbus.Interface(
596
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
597
avahi.DBUS_INTERFACE_SERVER )
1059
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1060
avahi.DBUS_PATH_SERVER),
1061
avahi.DBUS_INTERFACE_SERVER)
598
1062
# End of Avahi example code
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\
1064
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1066
clients.update(Set(Client(name = section,
1068
= dict(client_config.items(section)),
1069
use_dbus = use_dbus)
614
1070
for section in client_config.sections()))
1072
logger.warning(u"No clients defined")
1075
# Redirect stdin so all checkers get /dev/null
1076
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1077
os.dup2(null, sys.stdin.fileno())
1081
# No console logging
1082
logger.removeHandler(console)
1083
# Close all input and output, do double fork, etc.
1088
pidfile.write(str(pid) + "\n")
1092
logger.error(u"Could not write to file %r with PID %d",
1095
# "pidfile" was never created
1100
"Cleanup function; run on exit"
1102
# From the Avahi example code
1103
if not group is None:
1106
# End of Avahi example code
1109
client = clients.pop()
1110
client.disable_hook = None
1113
atexit.register(cleanup)
1116
signal.signal(signal.SIGINT, signal.SIG_IGN)
1117
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1118
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1121
class MandosServer(dbus.service.Object):
1122
"""A D-Bus proxy object"""
1124
dbus.service.Object.__init__(self, bus, "/")
1125
_interface = u"se.bsnet.fukt.Mandos"
1127
@dbus.service.signal(_interface, signature="oa{sv}")
1128
def ClientAdded(self, objpath, properties):
1132
@dbus.service.signal(_interface, signature="os")
1133
def ClientRemoved(self, objpath, name):
1137
@dbus.service.method(_interface, out_signature="ao")
1138
def GetAllClients(self):
1140
return dbus.Array(c.dbus_object_path for c in clients)
1142
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1143
def GetAllClientsWithProperties(self):
1145
return dbus.Dictionary(
1146
((c.dbus_object_path, c.GetAllProperties())
1150
@dbus.service.method(_interface, in_signature="o")
1151
def RemoveClient(self, object_path):
1154
if c.dbus_object_path == object_path:
1156
# Don't signal anything except ClientRemoved
1160
self.ClientRemoved(object_path, c.name)
1166
mandos_server = MandosServer()
615
1168
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]
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:],
1171
mandos_server.ClientAdded(client.dbus_object_path,
1172
client.GetAllProperties())
1176
tcp_server.server_activate()
1178
# Find out what port we got
1179
service.port = tcp_server.socket.getsockname()[1]
1180
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1181
u" scope_id %d" % tcp_server.socket.getsockname())
1183
#service.interface = tcp_server.socket.getsockname()[3]
1186
# From the Avahi example code
1187
server.connect_to_signal("StateChanged", server_state_changed)
1189
server_state_changed(server.GetState())
1190
except dbus.exceptions.DBusException, error:
1191
logger.critical(u"DBusException: %s", error)
1193
# End of Avahi example code
1195
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1196
lambda *args, **kwargs:
1197
(tcp_server.handle_request
1198
(*args[2:], **kwargs) or True))
1200
logger.debug(u"Starting main loop")
1202
except AvahiError, error:
1203
logger.critical(u"AvahiError: %s", error)
641
1205
except KeyboardInterrupt:
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
1209
if __name__ == '__main__':