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 and
51
name: string; from the config file, used in log messages
183
52
fingerprint: string (40 or 32 hexadecimal digits); used to
184
53
uniquely identify the client
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.
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.
198
65
checker_initiator_tag: a gobject event source tag, or None
199
disable_initiator_tag: - '' -
66
stop_initiator_tag: - '' -
200
67
checker_callback_tag: - '' -
201
68
checker_command: string; External command which is run to check if
202
client lives. %() expansions are done at
69
client lives. %()s expansions are done at
203
70
runtime with vars(self) as dict, so that for
204
71
instance %(name)s can be used in the command.
205
use_dbus: bool(); Whether to provide D-Bus interface and signals
206
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: - '' -
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'
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):
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
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
254
130
self.checker = None
255
131
self.checker_initiator_tag = None
256
self.disable_initiator_tag = None
132
self.stop_initiator_tag = None
257
133
self.checker_callback_tag = None
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()
134
self.check_command = checker
136
"""Start this clients checker and timeout hooks"""
273
137
# Schedule a new checker to be started an 'interval' from now,
274
138
# and every interval from then on.
275
self.checker_initiator_tag = (gobject.timeout_add
276
(self.interval_milliseconds(),
139
self.checker_initiator_tag = gobject.timeout_add\
140
(self._interval_milliseconds,
278
142
# Also start a new checker *right now*.
279
143
self.start_checker()
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):
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:
302
159
gobject.source_remove(self.checker_initiator_tag)
303
160
self.checker_initiator_tag = None
304
161
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))
312
164
# Do not run this again if called by a gobject.timeout_add
315
166
def __del__(self):
316
self.disable_hook = None
319
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):
320
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"
321
198
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",
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,
367
199
def start_checker(self):
368
200
"""Start a new checker subprocess if one is not running.
369
201
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.
379
203
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
381
# In case checker_command has exactly one % operator
382
command = self.checker_command % self.host
208
command = self.check_command % self.fqdn
383
209
except TypeError:
384
# Escape attributes for the shell
385
210
escaped_attrs = dict((key, re.escape(str(val)))
387
212
vars(self).iteritems())
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
213
command = self.check_command % escaped_attrs
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",
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"
417
227
# Re-run this periodically if run by gobject.timeout_add
420
229
def stop_checker(self):
421
230
"""Force the checker process, if any, to stop."""
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:
231
if not hasattr(self, "checker") or self.checker is None:
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
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)
436
238
self.checker = None
438
self.PropertyChanged(dbus.String(u"checker_running"),
439
dbus.Boolean(False, variant_level=1))
441
def still_valid(self):
239
def still_valid(self, now=None):
442
240
"""Has the timeout not yet passed for this client?"""
443
if not getattr(self, "enabled", False):
445
now = datetime.datetime.utcnow()
446
if self.last_checked_ok is None:
242
now = datetime.datetime.now()
243
if self.last_seen is None:
447
244
return now < (self.created + 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"
246
return now < (self.last_seen + self.timeout)
590
249
def peer_certificate(session):
591
"Return the peer's OpenPGP certificate as a bytestring"
592
250
# If not an OpenPGP certificate...
593
if (gnutls.library.functions
594
.gnutls_certificate_type_get(session._c_object)
595
!= 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:
596
254
# ...do the normal thing
597
255
return session.peer_certificate
598
256
list_size = ctypes.c_uint()
599
cert_list = (gnutls.library.functions
600
.gnutls_certificate_get_peers
601
(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))
602
259
if list_size.value == 0:
604
261
cert = cert_list[0]
608
265
def fingerprint(openpgp):
609
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
610
# New GnuTLS "datum" with the OpenPGP public key
611
datum = (gnutls.library.types
612
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
615
ctypes.c_uint(len(openpgp))))
616
266
# New empty GnuTLS certificate
617
267
crt = gnutls.library.types.gnutls_openpgp_crt_t()
618
(gnutls.library.functions
619
.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)))
620
275
# Import the OpenPGP public key into the certificate
621
(gnutls.library.functions
622
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
623
gnutls.library.constants
624
.GNUTLS_OPENPGP_FMT_RAW))
625
# Verify the self signature in the key
626
crtverify = ctypes.c_uint()
627
(gnutls.library.functions
628
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
629
if crtverify.value != 0:
630
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
631
raise gnutls.errors.CertificateSecurityError("Verify failed")
276
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
279
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
632
280
# New buffer for the fingerprint
633
buf = ctypes.create_string_buffer(20)
634
buf_len = ctypes.c_size_t()
281
buffer = ctypes.create_string_buffer(20)
282
buffer_length = ctypes.c_size_t()
635
283
# Get the fingerprint from the certificate into the buffer
636
(gnutls.library.functions
637
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
638
ctypes.byref(buf_len)))
284
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
639
286
# Deinit the certificate
640
287
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
641
288
# Convert the buffer to a Python bytestring
642
fpr = ctypes.string_at(buf, buf_len.value)
289
fpr = ctypes.string_at(buffer, buffer_length.value)
643
290
# Convert the bytestring to hexadecimal notation
644
291
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
648
class TCP_handler(SocketServer.BaseRequestHandler, object):
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
649
296
"""A TCP request handler class.
650
297
Instantiated by IPv6_TCPServer for each request to handle it.
651
298
Note: This will run in its own forked process."""
653
300
def handle(self):
654
logger.info(u"TCP connection from: %s",
655
unicode(self.client_address))
656
session = (gnutls.connection
657
.ClientSession(self.request,
661
line = self.request.makefile().readline()
662
logger.debug(u"Protocol version: %r", line)
664
if int(line.strip().split()[0]) > 1:
666
except (ValueError, IndexError, RuntimeError), error:
667
logger.error(u"Unknown protocol version: %s", error)
670
# Note: gnutls.connection.X509Credentials is really a generic
671
# GnuTLS certificate credentials object so long as no X.509
672
# keys are added to it. Therefore, we can use it here despite
673
# 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,
675
311
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
676
312
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
678
# Use a fallback default, since this MUST be set.
679
priority = self.server.settings.get("priority", "NORMAL")
680
(gnutls.library.functions
681
.gnutls_priority_set_direct(session._c_object,
314
priority = "SECURE256"
316
gnutls.library.functions.gnutls_priority_set_direct\
317
(session._c_object, priority, None);
685
320
session.handshake()
686
321
except gnutls.errors.GNUTLSError, error:
687
logger.warning(u"Handshake failed: %s", error)
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
688
324
# Do not run session.bye() here: the session is not
689
325
# established. Just abandon the request.
692
328
fpr = fingerprint(peer_certificate(session))
693
329
except (TypeError, gnutls.errors.GNUTLSError), error:
694
logger.warning(u"Bad certificate: %s", error)
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
697
logger.debug(u"Fingerprint: %s", fpr)
698
for c in self.server.clients:
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
699
338
if c.fingerprint == fpr:
703
logger.warning(u"Client not found for fingerprint: %s",
707
341
# Have to check if client.still_valid(), since it is possible
708
342
# that the client timed out while establishing the GnuTLS
710
if not client.still_valid():
711
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)
715
## This won't work here, since we're in a fork.
716
# client.checked_ok()
718
355
while sent_size < len(client.secret):
719
356
sent = session.send(client.secret[sent_size:])
720
logger.debug(u"Sent: %d, remaining: %d",
721
sent, len(client.secret)
722
- (sent_size + sent))
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
723
361
sent_size += sent
727
class IPv6_TCPServer(SocketServer.ForkingMixIn,
728
SocketServer.TCPServer, object):
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
729
366
"""IPv6 TCP server. Accepts 'None' as address and/or port.
731
settings: Server settings
368
options: Command line options
732
369
clients: Set() of Client objects
733
enabled: Boolean; whether this server is activated yet
735
371
address_family = socket.AF_INET6
736
372
def __init__(self, *args, **kwargs):
737
if "settings" in kwargs:
738
self.settings = kwargs["settings"]
739
del kwargs["settings"]
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
740
376
if "clients" in kwargs:
741
377
self.clients = kwargs["clients"]
742
378
del kwargs["clients"]
744
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
379
return super(type(self), self).__init__(*args, **kwargs)
745
380
def server_bind(self):
746
381
"""This overrides the normal server_bind() function
747
382
to bind to an interface if one was specified, and also NOT to
748
383
bind to an address or port if they were not specified."""
749
if self.settings["interface"]:
750
# 25 is from /usr/include/asm-i486/socket.h
751
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
753
389
self.socket.setsockopt(socket.SOL_SOCKET,
755
self.settings["interface"])
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
756
392
except socket.error, error:
757
393
if error[0] == errno.EPERM:
758
logger.error(u"No permission to"
759
u" bind to interface %s",
760
self.settings["interface"])
394
sys.stderr.write(u"Warning: No permission to" \
395
u" bind to interface %s\n"
396
% self.options.interface)
763
399
# Only bind(2) the socket if we really need to.
797
421
datetime.timedelta(1)
798
422
>>> string_to_delta(u'1w')
799
423
datetime.timedelta(7)
800
>>> string_to_delta('5m 30s')
801
datetime.timedelta(0, 330)
803
timevalue = datetime.timedelta(0)
804
for s in interval.split():
806
suffix = unicode(s[-1])
809
delta = datetime.timedelta(value)
811
delta = datetime.timedelta(0, value)
813
delta = datetime.timedelta(0, 0, 0, 0, value)
815
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
817
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
820
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:
826
479
def server_state_changed(state):
827
"""Derived from the Avahi example code"""
480
"""From the Avahi server example code"""
828
481
if state == avahi.SERVER_COLLISION:
829
logger.error(u"Zeroconf server name collision")
482
sys.stderr.write(u"WARNING: Server name collision\n")
831
484
elif state == avahi.SERVER_RUNNING:
835
488
def entry_group_state_changed(state, error):
836
"""Derived from the Avahi example code"""
837
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)
839
495
if state == avahi.ENTRY_GROUP_ESTABLISHED:
840
logger.debug(u"Zeroconf service established.")
497
sys.stderr.write(u"Service established.\n")
841
498
elif state == avahi.ENTRY_GROUP_COLLISION:
842
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"
844
513
elif state == avahi.ENTRY_GROUP_FAILURE:
845
logger.critical(u"Avahi: Error in group state changed %s",
847
raise AvahiGroupError(u"State changed: %s" % unicode(error))
514
sys.stderr.write(u"Error in group state changed %s\n"
849
520
def if_nametoindex(interface):
850
"""Call the C function if_nametoindex(), or equivalent"""
851
global if_nametoindex
521
"""Call the C function if_nametoindex()"""
853
if_nametoindex = (ctypes.cdll.LoadLibrary
854
(ctypes.util.find_library("c"))
523
libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
return libc.if_nametoindex(interface)
856
525
except (OSError, AttributeError):
857
526
if "struct" not in sys.modules:
859
528
if "fcntl" not in sys.modules:
861
def if_nametoindex(interface):
862
"Get an interface index the hard way, i.e. using fcntl()"
863
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
864
with closing(socket.socket()) as s:
865
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
866
struct.pack("16s16x", interface))
867
interface_index = struct.unpack("I", ifreq[16:20])[0]
868
return interface_index
869
return if_nametoindex(interface)
872
def daemon(nochdir = False, noclose = False):
873
"""See daemon(3). Standard BSD Unix function.
874
This should really exist as os.daemon, but it doesn't (yet)."""
883
# Close all standard open file descriptors
884
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
885
if not stat.S_ISCHR(os.fstat(null).st_mode):
886
raise OSError(errno.ENODEV,
887
"/dev/null not a character device")
888
os.dup2(null, sys.stdin.fileno())
889
os.dup2(null, sys.stdout.fileno())
890
os.dup2(null, sys.stderr.fileno())
896
parser = optparse.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()
897
541
parser.add_option("-i", "--interface", type="string",
898
metavar="IF", help="Bind to interface IF")
899
parser.add_option("-a", "--address", type="string",
900
help="Address to listen for requests on")
901
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,
902
557
help="Port number to receive requests on")
903
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,
904
565
help="Run self-test")
905
parser.add_option("--debug", action="store_true",
906
help="Debug mode; run in foreground and log to"
908
parser.add_option("--priority", type="string", help="GnuTLS"
909
" priority string (see GnuTLS documentation)")
910
parser.add_option("--servicename", type="string", metavar="NAME",
911
help="Zeroconf service name")
912
parser.add_option("--configdir", type="string",
913
default="/etc/mandos", metavar="DIR",
914
help="Directory to search for configuration"
916
parser.add_option("--no-dbus", action="store_false",
918
help="Do not provide D-Bus system bus"
920
options = parser.parse_args()[0]
566
parser.add_option("--debug", action="store_true", default=False,
568
(options, args) = parser.parse_args()
922
570
if options.check:
924
572
doctest.testmod()
927
# Default values for config file for server-global settings
928
server_defaults = { "interface": "",
933
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
934
"servicename": "Mandos",
938
# Parse config file for server-global settings
939
server_config = ConfigParser.SafeConfigParser(server_defaults)
941
server_config.read(os.path.join(options.configdir, "mandos.conf"))
942
# Convert the SafeConfigParser object to a dict
943
server_settings = server_config.defaults()
944
# Use the appropriate methods on the non-string config options
945
server_settings["debug"] = server_config.getboolean("DEFAULT",
947
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
949
if server_settings["port"]:
950
server_settings["port"] = server_config.getint("DEFAULT",
954
# Override the settings from the config file with command line
956
for option in ("interface", "address", "port", "debug",
957
"priority", "servicename", "configdir",
959
value = getattr(options, option)
960
if value is not None:
961
server_settings[option] = value
963
# Now we have our good server settings in "server_settings"
966
debug = server_settings["debug"]
967
use_dbus = server_settings["use_dbus"]
970
syslogger.setLevel(logging.WARNING)
971
console.setLevel(logging.WARNING)
973
if server_settings["servicename"] != "Mandos":
974
syslogger.setFormatter(logging.Formatter
975
('Mandos (%s): %%(levelname)s:'
977
% server_settings["servicename"]))
979
# Parse config file with clients
980
client_defaults = { "timeout": "1h",
982
"checker": "fping -q -- %%(host)s",
985
client_config = ConfigParser.SafeConfigParser(client_defaults)
986
client_config.read(os.path.join(server_settings["configdir"],
990
tcp_server = IPv6_TCPServer((server_settings["address"],
991
server_settings["port"]),
993
settings=server_settings,
995
pidfilename = "/var/run/mandos.pid"
997
pidfile = open(pidfilename, "w")
998
except IOError, error:
999
logger.error("Could not open file %r", pidfilename)
1002
uid = pwd.getpwnam("_mandos").pw_uid
1003
gid = pwd.getpwnam("_mandos").pw_gid
1006
uid = pwd.getpwnam("mandos").pw_uid
1007
gid = pwd.getpwnam("mandos").pw_gid
1010
uid = pwd.getpwnam("nobody").pw_uid
1011
gid = pwd.getpwnam("nogroup").pw_gid
1018
except OSError, error:
1019
if error[0] != errno.EPERM:
1023
service = AvahiService(name = server_settings["servicename"],
1024
servicetype = "_mandos._tcp", )
1025
if server_settings["interface"]:
1026
service.interface = (if_nametoindex
1027
(server_settings["interface"]))
1032
# 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
1033
592
DBusGMainLoop(set_as_default=True )
1034
593
main_loop = gobject.MainLoop()
1035
594
bus = dbus.SystemBus()
1036
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1037
avahi.DBUS_PATH_SERVER),
1038
avahi.DBUS_INTERFACE_SERVER)
595
server = dbus.Interface(
596
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
597
avahi.DBUS_INTERFACE_SERVER )
1039
598
# End of Avahi example code
1041
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
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, "/")
1102
_interface = u"se.bsnet.fukt.Mandos"
1104
@dbus.service.signal(_interface, signature="oa{sv}")
1105
def ClientAdded(self, objpath, properties):
1109
@dbus.service.signal(_interface, signature="os")
1110
def ClientRemoved(self, objpath, name):
1114
@dbus.service.method(_interface, out_signature="ao")
1115
def GetAllClients(self):
1116
return dbus.Array(c.dbus_object_path for c in clients)
1118
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1119
def GetAllClientsWithProperties(self):
1120
return dbus.Dictionary(
1121
((c.dbus_object_path, c.GetAllProperties())
1125
@dbus.service.method(_interface, in_signature="o")
1126
def RemoveClient(self, object_path):
1128
if c.dbus_object_path == object_path:
1130
# Don't signal anything except ClientRemoved
1134
self.ClientRemoved(object_path, c.name)
1140
mandos_server = MandosServer()
1142
for client in clients:
1145
mandos_server.ClientAdded(client.dbus_object_path,
1146
client.GetAllProperties())
1150
tcp_server.server_activate()
1152
# Find out what port we got
1153
service.port = tcp_server.socket.getsockname()[1]
1154
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1155
u" scope_id %d" % tcp_server.socket.getsockname())
1157
#service.interface = tcp_server.socket.getsockname()[3]
1160
# From the Avahi example code
1161
server.connect_to_signal("StateChanged", server_state_changed)
1163
server_state_changed(server.GetState())
1164
except dbus.exceptions.DBusException, error:
1165
logger.critical(u"DBusException: %s", error)
1167
# End of Avahi example code
1169
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1170
lambda *args, **kwargs:
1171
(tcp_server.handle_request
1172
(*args[2:], **kwargs) or True))
1174
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:],
1176
except AvahiError, error:
1177
logger.critical(u"AvahiError: %s", error)
1179
641
except KeyboardInterrupt:
1183
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