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 [%(process)d]: %(levelname)s:'
78
logger.addHandler(syslogger)
80
console = logging.StreamHandler()
81
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
82
' %(levelname)s: %(message)s'))
83
logger.addHandler(console)
85
class AvahiError(Exception):
86
def __init__(self, value, *args, **kwargs):
88
super(AvahiError, self).__init__(value, *args, **kwargs)
89
def __unicode__(self):
90
return unicode(repr(self.value))
92
class AvahiServiceError(AvahiError):
95
class AvahiGroupError(AvahiError):
99
class AvahiService(object):
100
"""An Avahi (Zeroconf) service.
102
interface: integer; avahi.IF_UNSPEC or an interface index.
103
Used to optionally bind to the specified interface.
104
name: string; Example: 'Mandos'
105
type: string; Example: '_mandos._tcp'.
106
See <http://www.dns-sd.org/ServiceTypes.html>
107
port: integer; what port to announce
108
TXT: list of strings; TXT record for the service
109
domain: string; Domain to publish on, default to .local if empty.
110
host: string; Host to publish records for, default is localhost
111
max_renames: integer; maximum number of renames
112
rename_count: integer; counter so we only rename after collisions
113
a sensible number of times
115
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
116
servicetype = None, port = None, TXT = None,
117
domain = "", host = "", max_renames = 32768):
118
self.interface = interface
120
self.type = servicetype
122
self.TXT = TXT if TXT is not None else []
125
self.rename_count = 0
126
self.max_renames = max_renames
128
"""Derived from the Avahi example code"""
129
if self.rename_count >= self.max_renames:
130
logger.critical(u"No suitable Zeroconf service name found"
131
u" after %i retries, exiting.",
133
raise AvahiServiceError(u"Too many renames")
134
self.name = server.GetAlternativeServiceName(self.name)
135
logger.info(u"Changing Zeroconf service name to %r ...",
137
syslogger.setFormatter(logging.Formatter
138
('Mandos (%s): %%(levelname)s:'
139
' %%(message)s' % self.name))
142
self.rename_count += 1
144
"""Derived from the Avahi example code"""
145
if group is not None:
148
"""Derived from the Avahi example code"""
151
group = dbus.Interface(bus.get_object
153
server.EntryGroupNew()),
154
avahi.DBUS_INTERFACE_ENTRY_GROUP)
155
group.connect_to_signal('StateChanged',
156
entry_group_state_changed)
157
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
158
service.name, service.type)
160
self.interface, # interface
161
avahi.PROTO_INET6, # protocol
162
dbus.UInt32(0), # flags
163
self.name, self.type,
164
self.domain, self.host,
165
dbus.UInt16(self.port),
166
avahi.string_array_to_txt_array(self.TXT))
169
# From the Avahi example code:
170
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
171
45
# End of Avahi example code
174
def _datetime_to_dbus(dt, variant_level=0):
175
"""Convert a UTC datetime.datetime() to a D-Bus type."""
176
return dbus.String(dt.isoformat(), variant_level=variant_level)
179
class Client(dbus.service.Object):
180
49
"""A representation of a client host served by this server.
182
name: string; from the config file, used in log messages and
51
name: string; from the config file, used in log messages
184
52
fingerprint: string (40 or 32 hexadecimal digits); used to
185
53
uniquely identify the client
186
secret: bytestring; sent verbatim (over TLS) to client
187
host: string; available for use by the checker command
188
created: datetime.datetime(); (UTC) object creation
189
last_enabled: datetime.datetime(); (UTC)
191
last_checked_ok: datetime.datetime(); (UTC) or None
192
timeout: datetime.timedelta(); How long from last_checked_ok
193
until this client is invalid
194
interval: datetime.timedelta(); How often to start a new checker
195
disable_hook: If set, called by disable() as disable_hook(self)
196
checker: subprocess.Popen(); a running checker process used
197
to see if the client lives.
198
'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.
199
65
checker_initiator_tag: a gobject event source tag, or None
200
disable_initiator_tag: - '' -
66
stop_initiator_tag: - '' -
201
67
checker_callback_tag: - '' -
202
68
checker_command: string; External command which is run to check if
203
client lives. %() expansions are done at
69
client lives. %()s expansions are done at
204
70
runtime with vars(self) as dict, so that for
205
71
instance %(name)s can be used in the command.
206
use_dbus: bool(); Whether to provide D-Bus interface and signals
207
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: - '' -
209
def timeout_milliseconds(self):
210
"Return the 'timeout' attribute in milliseconds"
211
return ((self.timeout.days * 24 * 60 * 60 * 1000)
212
+ (self.timeout.seconds * 1000)
213
+ (self.timeout.microseconds // 1000))
215
def interval_milliseconds(self):
216
"Return the 'interval' attribute in milliseconds"
217
return ((self.interval.days * 24 * 60 * 60 * 1000)
218
+ (self.interval.seconds * 1000)
219
+ (self.interval.microseconds // 1000))
221
def __init__(self, name = None, disable_hook=None, config=None,
223
"""Note: the 'checker' key in 'config' sets the
224
'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):
229
logger.debug(u"Creating client %r", self.name)
230
self.use_dbus = False # During __init__
231
# Uppercase and remove spaces from fingerprint for later
232
# comparison purposes with return value from the fingerprint()
234
self.fingerprint = (config["fingerprint"].upper()
236
logger.debug(u" Fingerprint: %s", self.fingerprint)
237
if "secret" in config:
238
self.secret = config["secret"].decode(u"base64")
239
elif "secfile" in config:
240
with closing(open(os.path.expanduser
242
(config["secfile"])))) as secfile:
243
self.secret = secfile.read()
245
raise TypeError(u"No secret or secfile for client %s"
247
self.host = config.get("host", "")
248
self.created = datetime.datetime.utcnow()
250
self.last_enabled = None
251
self.last_checked_ok = None
252
self.timeout = string_to_delta(config["timeout"])
253
self.interval = string_to_delta(config["interval"])
254
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
255
130
self.checker = None
256
131
self.checker_initiator_tag = None
257
self.disable_initiator_tag = None
132
self.stop_initiator_tag = None
258
133
self.checker_callback_tag = None
259
self.checker_command = config["checker"]
260
self.last_connect = None
261
# Only now, when this client is initialized, can it show up on
263
self.use_dbus = use_dbus
265
self.dbus_object_path = (dbus.ObjectPath
267
+ self.name.replace(".", "_")))
268
dbus.service.Object.__init__(self, bus,
269
self.dbus_object_path)
272
"""Start this client's checker and timeout hooks"""
273
self.last_enabled = datetime.datetime.utcnow()
134
self.check_command = checker
136
"""Start this clients checker and timeout hooks"""
274
137
# Schedule a new checker to be started an 'interval' from now,
275
138
# and every interval from then on.
276
self.checker_initiator_tag = (gobject.timeout_add
277
(self.interval_milliseconds(),
139
self.checker_initiator_tag = gobject.timeout_add\
140
(self._interval_milliseconds,
279
142
# Also start a new checker *right now*.
280
143
self.start_checker()
281
# Schedule a disable() when 'timeout' has passed
282
self.disable_initiator_tag = (gobject.timeout_add
283
(self.timeout_milliseconds(),
288
self.PropertyChanged(dbus.String(u"enabled"),
289
dbus.Boolean(True, variant_level=1))
290
self.PropertyChanged(dbus.String(u"last_enabled"),
291
(_datetime_to_dbus(self.last_enabled,
295
"""Disable this client."""
296
if not getattr(self, "enabled", False):
298
logger.info(u"Disabling client %s", self.name)
299
if getattr(self, "disable_initiator_tag", False):
300
gobject.source_remove(self.disable_initiator_tag)
301
self.disable_initiator_tag = None
302
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:
303
159
gobject.source_remove(self.checker_initiator_tag)
304
160
self.checker_initiator_tag = None
305
161
self.stop_checker()
306
if self.disable_hook:
307
self.disable_hook(self)
311
self.PropertyChanged(dbus.String(u"enabled"),
312
dbus.Boolean(False, variant_level=1))
313
164
# Do not run this again if called by a gobject.timeout_add
316
166
def __del__(self):
317
self.disable_hook = None
320
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):
321
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"
322
198
self.checker_callback_tag = None
326
self.PropertyChanged(dbus.String(u"checker_running"),
327
dbus.Boolean(False, variant_level=1))
328
if os.WIFEXITED(condition):
329
exitstatus = os.WEXITSTATUS(condition)
331
logger.info(u"Checker for %(name)s succeeded",
335
logger.info(u"Checker for %(name)s failed",
339
self.CheckerCompleted(dbus.Int16(exitstatus),
340
dbus.Int64(condition),
341
dbus.String(command))
343
logger.warning(u"Checker for %(name)s crashed?",
347
self.CheckerCompleted(dbus.Int16(-1),
348
dbus.Int64(condition),
349
dbus.String(command))
351
def checked_ok(self):
352
"""Bump up the timeout for this client.
353
This should only be called when the client has been seen,
356
self.last_checked_ok = datetime.datetime.utcnow()
357
gobject.source_remove(self.disable_initiator_tag)
358
self.disable_initiator_tag = (gobject.timeout_add
359
(self.timeout_milliseconds(),
363
self.PropertyChanged(
364
dbus.String(u"last_checked_ok"),
365
(_datetime_to_dbus(self.last_checked_ok,
368
199
def start_checker(self):
369
200
"""Start a new checker subprocess if one is not running.
370
201
If a checker already exists, leave it running and do
372
# The reason for not killing a running checker is that if we
373
# did that, then if a checker (for some reason) started
374
# running slowly and taking more than 'interval' time, the
375
# client would inevitably timeout, since no checker would get
376
# a chance to run to completion. If we instead leave running
377
# checkers alone, the checker would have to take more time
378
# than 'timeout' for the client to be declared invalid, which
379
# is as it should be.
380
203
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
382
# In case checker_command has exactly one % operator
383
command = self.checker_command % self.host
208
command = self.check_command % self.fqdn
384
209
except TypeError:
385
# Escape attributes for the shell
386
210
escaped_attrs = dict((key, re.escape(str(val)))
388
212
vars(self).iteritems())
390
command = self.checker_command % escaped_attrs
391
except TypeError, error:
392
logger.error(u'Could not format string "%s":'
393
u' %s', self.checker_command, error)
394
return True # Try again later
213
command = self.check_command % escaped_attrs
396
logger.info(u"Starting checker %r for %s",
398
# We don't need to redirect stdout and stderr, since
399
# in normal mode, that is already done by daemon(),
400
# and in debug mode we don't want to. (Stdin is
401
# always replaced by /dev/null.)
402
self.checker = subprocess.Popen(command,
407
self.CheckerStarted(command)
408
self.PropertyChanged(
409
dbus.String("checker_running"),
410
dbus.Boolean(True, variant_level=1))
411
self.checker_callback_tag = (gobject.child_watch_add
413
self.checker_callback,
415
except OSError, error:
416
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"
418
227
# Re-run this periodically if run by gobject.timeout_add
421
229
def stop_checker(self):
422
230
"""Force the checker process, if any, to stop."""
423
if self.checker_callback_tag:
424
gobject.source_remove(self.checker_callback_tag)
425
self.checker_callback_tag = None
426
if getattr(self, "checker", None) is None:
231
if not hasattr(self, "checker") or self.checker is None:
428
logger.debug(u"Stopping checker for %(name)s", vars(self))
430
os.kill(self.checker.pid, signal.SIGTERM)
432
#if self.checker.poll() is None:
433
# os.kill(self.checker.pid, signal.SIGKILL)
434
except OSError, error:
435
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)
437
238
self.checker = None
439
self.PropertyChanged(dbus.String(u"checker_running"),
440
dbus.Boolean(False, variant_level=1))
442
def still_valid(self):
239
def still_valid(self, now=None):
443
240
"""Has the timeout not yet passed for this client?"""
444
if not getattr(self, "enabled", False):
446
now = datetime.datetime.utcnow()
447
if self.last_checked_ok is None:
242
now = datetime.datetime.now()
243
if self.last_seen is None:
448
244
return now < (self.created + self.timeout)
450
return now < (self.last_checked_ok + self.timeout)
452
## D-Bus methods & signals
453
_interface = u"se.bsnet.fukt.Mandos.Client"
456
CheckedOK = dbus.service.method(_interface)(checked_ok)
457
CheckedOK.__name__ = "CheckedOK"
459
# CheckerCompleted - signal
460
@dbus.service.signal(_interface, signature="nxs")
461
def CheckerCompleted(self, exitcode, waitstatus, command):
465
# CheckerStarted - signal
466
@dbus.service.signal(_interface, signature="s")
467
def CheckerStarted(self, command):
471
# GetAllProperties - method
472
@dbus.service.method(_interface, out_signature="a{sv}")
473
def GetAllProperties(self):
475
return dbus.Dictionary({
477
dbus.String(self.name, variant_level=1),
478
dbus.String("fingerprint"):
479
dbus.String(self.fingerprint, variant_level=1),
481
dbus.String(self.host, variant_level=1),
482
dbus.String("created"):
483
_datetime_to_dbus(self.created, variant_level=1),
484
dbus.String("last_enabled"):
485
(_datetime_to_dbus(self.last_enabled,
487
if self.last_enabled is not None
488
else dbus.Boolean(False, variant_level=1)),
489
dbus.String("enabled"):
490
dbus.Boolean(self.enabled, variant_level=1),
491
dbus.String("last_checked_ok"):
492
(_datetime_to_dbus(self.last_checked_ok,
494
if self.last_checked_ok is not None
495
else dbus.Boolean (False, variant_level=1)),
496
dbus.String("timeout"):
497
dbus.UInt64(self.timeout_milliseconds(),
499
dbus.String("interval"):
500
dbus.UInt64(self.interval_milliseconds(),
502
dbus.String("checker"):
503
dbus.String(self.checker_command,
505
dbus.String("checker_running"):
506
dbus.Boolean(self.checker is not None,
508
dbus.String("object_path"):
509
dbus.ObjectPath(self.dbus_object_path,
513
# IsStillValid - method
514
IsStillValid = (dbus.service.method(_interface, out_signature="b")
516
IsStillValid.__name__ = "IsStillValid"
518
# PropertyChanged - signal
519
@dbus.service.signal(_interface, signature="sv")
520
def PropertyChanged(self, property, value):
524
# SetChecker - method
525
@dbus.service.method(_interface, in_signature="s")
526
def SetChecker(self, checker):
527
"D-Bus setter method"
528
self.checker_command = checker
530
self.PropertyChanged(dbus.String(u"checker"),
531
dbus.String(self.checker_command,
535
@dbus.service.method(_interface, in_signature="s")
536
def SetHost(self, host):
537
"D-Bus setter method"
540
self.PropertyChanged(dbus.String(u"host"),
541
dbus.String(self.host, variant_level=1))
543
# SetInterval - method
544
@dbus.service.method(_interface, in_signature="t")
545
def SetInterval(self, milliseconds):
546
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
548
self.PropertyChanged(dbus.String(u"interval"),
549
(dbus.UInt64(self.interval_milliseconds(),
553
@dbus.service.method(_interface, in_signature="ay",
555
def SetSecret(self, secret):
556
"D-Bus setter method"
557
self.secret = str(secret)
559
# SetTimeout - method
560
@dbus.service.method(_interface, in_signature="t")
561
def SetTimeout(self, milliseconds):
562
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
564
self.PropertyChanged(dbus.String(u"timeout"),
565
(dbus.UInt64(self.timeout_milliseconds(),
569
Enable = dbus.service.method(_interface)(enable)
570
Enable.__name__ = "Enable"
572
# StartChecker - method
573
@dbus.service.method(_interface)
574
def StartChecker(self):
579
@dbus.service.method(_interface)
584
# StopChecker - method
585
StopChecker = dbus.service.method(_interface)(stop_checker)
586
StopChecker.__name__ = "StopChecker"
246
return now < (self.last_seen + self.timeout)
591
249
def peer_certificate(session):
592
"Return the peer's OpenPGP certificate as a bytestring"
593
250
# If not an OpenPGP certificate...
594
if (gnutls.library.functions
595
.gnutls_certificate_type_get(session._c_object)
596
!= 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:
597
254
# ...do the normal thing
598
255
return session.peer_certificate
599
list_size = ctypes.c_uint(1)
600
cert_list = (gnutls.library.functions
601
.gnutls_certificate_get_peers
602
(session._c_object, ctypes.byref(list_size)))
603
if not bool(cert_list) and list_size.value != 0:
604
raise gnutls.errors.GNUTLSError("error getting peer"
256
list_size = ctypes.c_uint()
257
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
258
(session._c_object, ctypes.byref(list_size))
606
259
if list_size.value == 0:
608
261
cert = cert_list[0]
612
265
def fingerprint(openpgp):
613
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
614
# New GnuTLS "datum" with the OpenPGP public key
615
datum = (gnutls.library.types
616
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
619
ctypes.c_uint(len(openpgp))))
620
266
# New empty GnuTLS certificate
621
267
crt = gnutls.library.types.gnutls_openpgp_crt_t()
622
(gnutls.library.functions
623
.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)))
624
275
# Import the OpenPGP public key into the certificate
625
(gnutls.library.functions
626
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
627
gnutls.library.constants
628
.GNUTLS_OPENPGP_FMT_RAW))
629
# Verify the self signature in the key
630
crtverify = ctypes.c_uint()
631
(gnutls.library.functions
632
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
633
if crtverify.value != 0:
634
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
635
raise gnutls.errors.CertificateSecurityError("Verify failed")
276
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
279
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
636
280
# New buffer for the fingerprint
637
buf = ctypes.create_string_buffer(20)
638
buf_len = ctypes.c_size_t()
281
buffer = ctypes.create_string_buffer(20)
282
buffer_length = ctypes.c_size_t()
639
283
# Get the fingerprint from the certificate into the buffer
640
(gnutls.library.functions
641
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
642
ctypes.byref(buf_len)))
284
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
643
286
# Deinit the certificate
644
287
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
645
288
# Convert the buffer to a Python bytestring
646
fpr = ctypes.string_at(buf, buf_len.value)
289
fpr = ctypes.string_at(buffer, buffer_length.value)
647
290
# Convert the bytestring to hexadecimal notation
648
291
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
652
class TCP_handler(SocketServer.BaseRequestHandler, object):
295
class tcp_handler(SocketServer.BaseRequestHandler, object):
653
296
"""A TCP request handler class.
654
297
Instantiated by IPv6_TCPServer for each request to handle it.
655
298
Note: This will run in its own forked process."""
657
300
def handle(self):
658
logger.info(u"TCP connection from: %s",
659
unicode(self.client_address))
660
session = (gnutls.connection
661
.ClientSession(self.request,
665
line = self.request.makefile().readline()
666
logger.debug(u"Protocol version: %r", line)
668
if int(line.strip().split()[0]) > 1:
670
except (ValueError, IndexError, RuntimeError), error:
671
logger.error(u"Unknown protocol version: %s", error)
674
# Note: gnutls.connection.X509Credentials is really a generic
675
# GnuTLS certificate credentials object so long as no X.509
676
# keys are added to it. Therefore, we can use it here despite
677
# 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,
679
311
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
680
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
682
# Use a fallback default, since this MUST be set.
683
priority = self.server.settings.get("priority", "NORMAL")
684
(gnutls.library.functions
685
.gnutls_priority_set_direct(session._c_object,
312
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
314
priority = "SECURE256"
316
gnutls.library.functions.gnutls_priority_set_direct\
317
(session._c_object, priority, None);
689
320
session.handshake()
690
321
except gnutls.errors.GNUTLSError, error:
691
logger.warning(u"Handshake failed: %s", error)
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
692
324
# Do not run session.bye() here: the session is not
693
325
# established. Just abandon the request.
695
logger.debug(u"Handshake succeeded")
697
328
fpr = fingerprint(peer_certificate(session))
698
329
except (TypeError, gnutls.errors.GNUTLSError), error:
699
logger.warning(u"Bad certificate: %s", error)
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
702
logger.debug(u"Fingerprint: %s", fpr)
704
for c in self.server.clients:
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
705
338
if c.fingerprint == fpr:
709
logger.warning(u"Client not found for fingerprint: %s",
713
341
# Have to check if client.still_valid(), since it is possible
714
342
# that the client timed out while establishing the GnuTLS
716
if not client.still_valid():
717
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)
721
## This won't work here, since we're in a fork.
722
# client.checked_ok()
724
355
while sent_size < len(client.secret):
725
356
sent = session.send(client.secret[sent_size:])
726
logger.debug(u"Sent: %d, remaining: %d",
727
sent, len(client.secret)
728
- (sent_size + sent))
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
729
361
sent_size += sent
733
class IPv6_TCPServer(SocketServer.ForkingMixIn,
734
SocketServer.TCPServer, object):
365
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
735
366
"""IPv6 TCP server. Accepts 'None' as address and/or port.
737
settings: Server settings
368
options: Command line options
738
369
clients: Set() of Client objects
739
enabled: Boolean; whether this server is activated yet
741
371
address_family = socket.AF_INET6
742
372
def __init__(self, *args, **kwargs):
743
if "settings" in kwargs:
744
self.settings = kwargs["settings"]
745
del kwargs["settings"]
373
if "options" in kwargs:
374
self.options = kwargs["options"]
375
del kwargs["options"]
746
376
if "clients" in kwargs:
747
377
self.clients = kwargs["clients"]
748
378
del kwargs["clients"]
750
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
379
return super(type(self), self).__init__(*args, **kwargs)
751
380
def server_bind(self):
752
381
"""This overrides the normal server_bind() function
753
382
to bind to an interface if one was specified, and also NOT to
754
383
bind to an address or port if they were not specified."""
755
if self.settings["interface"]:
756
# 25 is from /usr/include/asm-i486/socket.h
757
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
759
389
self.socket.setsockopt(socket.SOL_SOCKET,
761
self.settings["interface"])
390
socket.SO_BINDTODEVICE,
391
self.options.interface)
762
392
except socket.error, error:
763
393
if error[0] == errno.EPERM:
764
logger.error(u"No permission to"
765
u" bind to interface %s",
766
self.settings["interface"])
394
sys.stderr.write(u"Warning: No permission to" \
395
u" bind to interface %s\n"
396
% self.options.interface)
769
399
# Only bind(2) the socket if we really need to.
770
400
if self.server_address[0] or self.server_address[1]:
771
401
if not self.server_address[0]:
772
402
in6addr_any = "::"
773
403
self.server_address = (in6addr_any,
774
404
self.server_address[1])
775
elif not self.server_address[1]:
405
elif self.server_address[1] is None:
776
406
self.server_address = (self.server_address[0],
778
# if self.settings["interface"]:
779
# self.server_address = (self.server_address[0],
785
return super(IPv6_TCPServer, self).server_bind()
786
def server_activate(self):
788
return super(IPv6_TCPServer, self).server_activate()
408
return super(type(self), self).server_bind()
793
411
def string_to_delta(interval):
794
412
"""Parse a string and return a datetime.timedelta
796
414
>>> string_to_delta('7d')
797
415
datetime.timedelta(7)
798
416
>>> string_to_delta('60s')
803
421
datetime.timedelta(1)
804
422
>>> string_to_delta(u'1w')
805
423
datetime.timedelta(7)
806
>>> string_to_delta('5m 30s')
807
datetime.timedelta(0, 330)
809
timevalue = datetime.timedelta(0)
810
for s in interval.split():
812
suffix = unicode(s[-1])
815
delta = datetime.timedelta(value)
817
delta = datetime.timedelta(0, value)
819
delta = datetime.timedelta(0, 0, 0, 0, value)
821
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
823
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
826
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:
832
479
def server_state_changed(state):
833
"""Derived from the Avahi example code"""
480
"""From the Avahi server example code"""
834
481
if state == avahi.SERVER_COLLISION:
835
logger.error(u"Zeroconf server name collision")
482
sys.stderr.write(u"WARNING: Server name collision\n")
837
484
elif state == avahi.SERVER_RUNNING:
841
488
def entry_group_state_changed(state, error):
842
"""Derived from the Avahi example code"""
843
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)
845
495
if state == avahi.ENTRY_GROUP_ESTABLISHED:
846
logger.debug(u"Zeroconf service established.")
497
sys.stderr.write(u"Service established.\n")
847
498
elif state == avahi.ENTRY_GROUP_COLLISION:
848
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"
850
513
elif state == avahi.ENTRY_GROUP_FAILURE:
851
logger.critical(u"Avahi: Error in group state changed %s",
853
raise AvahiGroupError(u"State changed: %s" % unicode(error))
514
sys.stderr.write(u"Error in group state changed %s\n"
855
520
def if_nametoindex(interface):
856
"""Call the C function if_nametoindex(), or equivalent"""
857
global if_nametoindex
521
"""Call the C function if_nametoindex()"""
859
if_nametoindex = (ctypes.cdll.LoadLibrary
860
(ctypes.util.find_library("c"))
523
libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
return libc.if_nametoindex(interface)
862
525
except (OSError, AttributeError):
863
526
if "struct" not in sys.modules:
865
528
if "fcntl" not in sys.modules:
867
def if_nametoindex(interface):
868
"Get an interface index the hard way, i.e. using fcntl()"
869
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
870
with closing(socket.socket()) as s:
871
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
872
struct.pack("16s16x", interface))
873
interface_index = struct.unpack("I", ifreq[16:20])[0]
874
return interface_index
875
return if_nametoindex(interface)
878
def daemon(nochdir = False, noclose = False):
879
"""See daemon(3). Standard BSD Unix function.
880
This should really exist as os.daemon, but it doesn't (yet)."""
889
# Close all standard open file descriptors
890
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
891
if not stat.S_ISCHR(os.fstat(null).st_mode):
892
raise OSError(errno.ENODEV,
893
"/dev/null not a character device")
894
os.dup2(null, sys.stdin.fileno())
895
os.dup2(null, sys.stdout.fileno())
896
os.dup2(null, sys.stderr.fileno())
902
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()
903
541
parser.add_option("-i", "--interface", type="string",
904
metavar="IF", help="Bind to interface IF")
905
parser.add_option("-a", "--address", type="string",
906
help="Address to listen for requests on")
907
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,
908
557
help="Port number to receive requests on")
909
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,
910
565
help="Run self-test")
911
parser.add_option("--debug", action="store_true",
912
help="Debug mode; run in foreground and log to"
914
parser.add_option("--priority", type="string", help="GnuTLS"
915
" priority string (see GnuTLS documentation)")
916
parser.add_option("--servicename", type="string", metavar="NAME",
917
help="Zeroconf service name")
918
parser.add_option("--configdir", type="string",
919
default="/etc/mandos", metavar="DIR",
920
help="Directory to search for configuration"
922
parser.add_option("--no-dbus", action="store_false",
924
help="Do not provide D-Bus system bus"
926
options = parser.parse_args()[0]
566
parser.add_option("--debug", action="store_true", default=False,
568
(options, args) = parser.parse_args()
928
570
if options.check:
930
572
doctest.testmod()
933
# Default values for config file for server-global settings
934
server_defaults = { "interface": "",
939
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
940
"servicename": "Mandos",
944
# Parse config file for server-global settings
945
server_config = ConfigParser.SafeConfigParser(server_defaults)
947
server_config.read(os.path.join(options.configdir, "mandos.conf"))
948
# Convert the SafeConfigParser object to a dict
949
server_settings = server_config.defaults()
950
# Use the appropriate methods on the non-string config options
951
server_settings["debug"] = server_config.getboolean("DEFAULT",
953
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
955
if server_settings["port"]:
956
server_settings["port"] = server_config.getint("DEFAULT",
960
# Override the settings from the config file with command line
962
for option in ("interface", "address", "port", "debug",
963
"priority", "servicename", "configdir",
965
value = getattr(options, option)
966
if value is not None:
967
server_settings[option] = value
969
# Now we have our good server settings in "server_settings"
972
debug = server_settings["debug"]
973
use_dbus = server_settings["use_dbus"]
976
syslogger.setLevel(logging.WARNING)
977
console.setLevel(logging.WARNING)
979
if server_settings["servicename"] != "Mandos":
980
syslogger.setFormatter(logging.Formatter
981
('Mandos (%s): %%(levelname)s:'
983
% server_settings["servicename"]))
985
# Parse config file with clients
986
client_defaults = { "timeout": "1h",
988
"checker": "fping -q -- %%(host)s",
991
client_config = ConfigParser.SafeConfigParser(client_defaults)
992
client_config.read(os.path.join(server_settings["configdir"],
996
tcp_server = IPv6_TCPServer((server_settings["address"],
997
server_settings["port"]),
999
settings=server_settings,
1001
pidfilename = "/var/run/mandos.pid"
1003
pidfile = open(pidfilename, "w")
1005
logger.error("Could not open file %r", pidfilename)
1008
uid = pwd.getpwnam("_mandos").pw_uid
1009
gid = pwd.getpwnam("_mandos").pw_gid
1012
uid = pwd.getpwnam("mandos").pw_uid
1013
gid = pwd.getpwnam("mandos").pw_gid
1016
uid = pwd.getpwnam("nobody").pw_uid
1017
gid = pwd.getpwnam("nogroup").pw_gid
1024
except OSError, error:
1025
if error[0] != errno.EPERM:
1028
# Enable all possible GnuTLS debugging
1030
# "Use a log level over 10 to enable all debugging options."
1032
gnutls.library.functions.gnutls_global_set_log_level(11)
1034
@gnutls.library.types.gnutls_log_func
1035
def debug_gnutls(level, string):
1036
logger.debug("GnuTLS: %s", string[:-1])
1038
(gnutls.library.functions
1039
.gnutls_global_set_log_function(debug_gnutls))
1042
service = AvahiService(name = server_settings["servicename"],
1043
servicetype = "_mandos._tcp", )
1044
if server_settings["interface"]:
1045
service.interface = (if_nametoindex
1046
(server_settings["interface"]))
1051
# 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
1052
592
DBusGMainLoop(set_as_default=True )
1053
593
main_loop = gobject.MainLoop()
1054
594
bus = dbus.SystemBus()
1055
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1056
avahi.DBUS_PATH_SERVER),
1057
avahi.DBUS_INTERFACE_SERVER)
595
server = dbus.Interface(
596
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
597
avahi.DBUS_INTERFACE_SERVER )
1058
598
# End of Avahi example code
1060
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1062
clients.update(Set(Client(name = section,
1064
= dict(client_config.items(section)),
1065
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\
1066
614
for section in client_config.sections()))
1068
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]
1071
# Redirect stdin so all checkers get /dev/null
1072
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1073
os.dup2(null, sys.stdin.fileno())
1077
# No console logging
1078
logger.removeHandler(console)
1079
# Close all input and output, do double fork, etc.
1084
pidfile.write(str(pid) + "\n")
1088
logger.error(u"Could not write to file %r with PID %d",
1091
# "pidfile" was never created
1096
"Cleanup function; run on exit"
1098
# From the Avahi example code
1099
if not group is None:
1102
# End of Avahi example code
1105
client = clients.pop()
1106
client.disable_hook = None
1109
atexit.register(cleanup)
1112
signal.signal(signal.SIGINT, signal.SIG_IGN)
1113
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1114
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1117
class MandosServer(dbus.service.Object):
1118
"""A D-Bus proxy object"""
1120
dbus.service.Object.__init__(self, bus, "/")
1121
_interface = u"se.bsnet.fukt.Mandos"
1123
@dbus.service.signal(_interface, signature="oa{sv}")
1124
def ClientAdded(self, objpath, properties):
1128
@dbus.service.signal(_interface, signature="os")
1129
def ClientRemoved(self, objpath, name):
1133
@dbus.service.method(_interface, out_signature="ao")
1134
def GetAllClients(self):
1136
return dbus.Array(c.dbus_object_path for c in clients)
1138
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1139
def GetAllClientsWithProperties(self):
1141
return dbus.Dictionary(
1142
((c.dbus_object_path, c.GetAllProperties())
1146
@dbus.service.method(_interface, in_signature="o")
1147
def RemoveClient(self, object_path):
1150
if c.dbus_object_path == object_path:
1152
# Don't signal anything except ClientRemoved
1156
self.ClientRemoved(object_path, c.name)
1162
mandos_server = MandosServer()
1164
for client in clients:
1167
mandos_server.ClientAdded(client.dbus_object_path,
1168
client.GetAllProperties())
1172
tcp_server.server_activate()
1174
# Find out what port we got
1175
service.port = tcp_server.socket.getsockname()[1]
1176
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1177
u" scope_id %d" % tcp_server.socket.getsockname())
1179
#service.interface = tcp_server.socket.getsockname()[3]
1182
# From the Avahi example code
1183
server.connect_to_signal("StateChanged", server_state_changed)
1185
server_state_changed(server.GetState())
1186
except dbus.exceptions.DBusException, error:
1187
logger.critical(u"DBusException: %s", error)
1189
# End of Avahi example code
1191
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1192
lambda *args, **kwargs:
1193
(tcp_server.handle_request
1194
(*args[2:], **kwargs) or True))
1196
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:],
1198
except AvahiError, error:
1199
logger.critical(u"AvahiError: %s", error)
1201
641
except KeyboardInterrupt:
1204
logger.debug("Server received KeyboardInterrupt")
1205
logger.debug("Server exiting")
1207
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