57
57
import logging.handlers
59
from contextlib import closing
65
62
from dbus.mainloop.glib import DBusGMainLoop
65
# Brief description of the operation of this program:
67
# This server announces itself as a Zeroconf service. Connecting
68
# clients use the TLS protocol, with the unusual quirk that this
69
# server program acts as a TLS "client" while the connecting clients
70
# acts as a TLS "server". The clients (acting as a TLS "server") must
71
# supply an OpenPGP certificate, and the fingerprint of this
72
# certificate is used by this server to look up (in a list read from a
73
# file at start time) which binary blob to give the client. No other
74
# authentication or authorization is done by this server.
71
77
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
syslogger = logging.handlers.SysLogHandler\
79
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
syslogger.setFormatter(logging.Formatter\
81
('%(levelname)s: %(message)s'))
78
82
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
protocol = avahi.PROTO_UNSPEC):
119
self.interface = interface
121
self.type = servicetype
123
self.TXT = TXT if TXT is not None else []
126
self.rename_count = 0
127
self.max_renames = max_renames
128
self.protocol = protocol
130
"""Derived from the Avahi example code"""
131
if self.rename_count >= self.max_renames:
132
logger.critical(u"No suitable Zeroconf service name found"
133
u" after %i retries, exiting.",
135
raise AvahiServiceError(u"Too many renames")
136
self.name = server.GetAlternativeServiceName(self.name)
137
logger.info(u"Changing Zeroconf service name to %r ...",
139
syslogger.setFormatter(logging.Formatter
140
('Mandos (%s): %%(levelname)s:'
141
' %%(message)s' % self.name))
144
self.rename_count += 1
146
"""Derived from the Avahi example code"""
147
if group is not None:
150
"""Derived from the Avahi example code"""
153
group = dbus.Interface(bus.get_object
155
server.EntryGroupNew()),
156
avahi.DBUS_INTERFACE_ENTRY_GROUP)
157
group.connect_to_signal('StateChanged',
158
entry_group_state_changed)
159
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
160
service.name, service.type)
162
self.interface, # interface
163
self.protocol, # protocol
164
dbus.UInt32(0), # flags
165
self.name, self.type,
166
self.domain, self.host,
167
dbus.UInt16(self.port),
168
avahi.string_array_to_txt_array(self.TXT))
85
# This variable is used to optionally bind to a specified interface.
86
# It is a global variable to fit in with the other variables from the
88
serviceInterface = avahi.IF_UNSPEC
171
89
# From the Avahi example code:
172
group = None # our entry group
90
serviceName = "Mandos"
91
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
servicePort = None # Not known at startup
93
serviceTXT = [] # TXT record for the service
94
domain = "" # Domain to publish on, default to .local
95
host = "" # Host to publish records for, default to localhost
96
group = None #our entry group
97
rename_count = 12 # Counter so we only rename after collisions a
98
# sensible number of times
173
99
# End of Avahi example code
176
def _datetime_to_dbus(dt, variant_level=0):
177
"""Convert a UTC datetime.datetime() to a D-Bus type."""
178
return dbus.String(dt.isoformat(), variant_level=variant_level)
181
class Client(dbus.service.Object):
102
class Client(object):
182
103
"""A representation of a client host served by this server.
184
name: string; from the config file, used in log messages and
105
name: string; from the config file, used in log messages
186
106
fingerprint: string (40 or 32 hexadecimal digits); used to
187
107
uniquely identify the client
188
secret: bytestring; sent verbatim (over TLS) to client
189
host: string; available for use by the checker command
190
created: datetime.datetime(); (UTC) object creation
191
last_enabled: datetime.datetime(); (UTC)
193
last_checked_ok: datetime.datetime(); (UTC) or None
194
timeout: datetime.timedelta(); How long from last_checked_ok
195
until this client is invalid
196
interval: datetime.timedelta(); How often to start a new checker
197
disable_hook: If set, called by disable() as disable_hook(self)
198
checker: subprocess.Popen(); a running checker process used
199
to see if the client lives.
200
'None' if no process is running.
108
secret: bytestring; sent verbatim (over TLS) to client
109
fqdn: string (FQDN); available for use by the checker command
110
created: datetime.datetime()
111
last_seen: datetime.datetime() or None if not yet seen
112
timeout: datetime.timedelta(); How long from last_seen until
113
this client is invalid
114
interval: datetime.timedelta(); How often to start a new checker
115
stop_hook: If set, called by stop() as stop_hook(self)
116
checker: subprocess.Popen(); a running checker process used
117
to see if the client lives.
118
Is None if no process is running.
201
119
checker_initiator_tag: a gobject event source tag, or None
202
disable_initiator_tag: - '' -
120
stop_initiator_tag: - '' -
203
121
checker_callback_tag: - '' -
204
122
checker_command: string; External command which is run to check if
205
client lives. %() expansions are done at
123
client lives. %()s expansions are done at
206
124
runtime with vars(self) as dict, so that for
207
125
instance %(name)s can be used in the command.
208
use_dbus: bool(); Whether to provide D-Bus interface and signals
209
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
127
_timeout: Real variable for 'timeout'
128
_interval: Real variable for 'interval'
129
_timeout_milliseconds: Used by gobject.timeout_add()
130
_interval_milliseconds: - '' -
211
def timeout_milliseconds(self):
212
"Return the 'timeout' attribute in milliseconds"
213
return ((self.timeout.days * 24 * 60 * 60 * 1000)
214
+ (self.timeout.seconds * 1000)
215
+ (self.timeout.microseconds // 1000))
217
def interval_milliseconds(self):
218
"Return the 'interval' attribute in milliseconds"
219
return ((self.interval.days * 24 * 60 * 60 * 1000)
220
+ (self.interval.seconds * 1000)
221
+ (self.interval.microseconds // 1000))
223
def __init__(self, name = None, disable_hook=None, config=None,
225
"""Note: the 'checker' key in 'config' sets the
226
'checker_command' attribute and *not* the 'checker'
132
def _set_timeout(self, timeout):
133
"Setter function for 'timeout' attribute"
134
self._timeout = timeout
135
self._timeout_milliseconds = ((self.timeout.days
136
* 24 * 60 * 60 * 1000)
137
+ (self.timeout.seconds * 1000)
138
+ (self.timeout.microseconds
140
timeout = property(lambda self: self._timeout,
143
def _set_interval(self, interval):
144
"Setter function for 'interval' attribute"
145
self._interval = interval
146
self._interval_milliseconds = ((self.interval.days
147
* 24 * 60 * 60 * 1000)
148
+ (self.interval.seconds
150
+ (self.interval.microseconds
152
interval = property(lambda self: self._interval,
155
def __init__(self, name=None, options=None, stop_hook=None,
156
fingerprint=None, secret=None, secfile=None,
157
fqdn=None, timeout=None, interval=-1, checker=None):
158
"""Note: the 'checker' argument sets the 'checker_command'
159
attribute and not the 'checker' attribute.."""
231
logger.debug(u"Creating client %r", self.name)
232
self.use_dbus = False # During __init__
233
# Uppercase and remove spaces from fingerprint for later
234
# comparison purposes with return value from the fingerprint()
236
self.fingerprint = (config["fingerprint"].upper()
238
logger.debug(u" Fingerprint: %s", self.fingerprint)
239
if "secret" in config:
240
self.secret = config["secret"].decode(u"base64")
241
elif "secfile" in config:
242
with closing(open(os.path.expanduser
244
(config["secfile"])))) as secfile:
245
self.secret = secfile.read()
247
raise TypeError(u"No secret or secfile for client %s"
249
self.host = config.get("host", "")
250
self.created = datetime.datetime.utcnow()
252
self.last_enabled = None
253
self.last_checked_ok = None
254
self.timeout = string_to_delta(config["timeout"])
255
self.interval = string_to_delta(config["interval"])
256
self.disable_hook = disable_hook
161
# Uppercase and remove spaces from fingerprint
162
# for later comparison purposes with return value of
163
# the fingerprint() function
164
self.fingerprint = fingerprint.upper().replace(u" ", u"")
166
self.secret = secret.decode(u"base64")
169
self.secret = sf.read()
172
raise RuntimeError(u"No secret or secfile for client %s"
174
self.fqdn = fqdn # string
175
self.created = datetime.datetime.now()
176
self.last_seen = None
178
self.timeout = options.timeout
180
self.timeout = string_to_delta(timeout)
182
self.interval = options.interval
184
self.interval = string_to_delta(interval)
185
self.stop_hook = stop_hook
257
186
self.checker = None
258
187
self.checker_initiator_tag = None
259
self.disable_initiator_tag = None
188
self.stop_initiator_tag = None
260
189
self.checker_callback_tag = None
261
self.checker_command = config["checker"]
262
self.last_connect = None
263
# Only now, when this client is initialized, can it show up on
265
self.use_dbus = use_dbus
267
self.dbus_object_path = (dbus.ObjectPath
269
+ self.name.replace(".", "_")))
270
dbus.service.Object.__init__(self, bus,
271
self.dbus_object_path)
190
self.check_command = checker
274
192
"""Start this client's checker and timeout hooks"""
275
self.last_enabled = datetime.datetime.utcnow()
276
193
# Schedule a new checker to be started an 'interval' from now,
277
194
# and every interval from then on.
278
self.checker_initiator_tag = (gobject.timeout_add
279
(self.interval_milliseconds(),
195
self.checker_initiator_tag = gobject.timeout_add\
196
(self._interval_milliseconds,
281
198
# Also start a new checker *right now*.
282
199
self.start_checker()
283
# Schedule a disable() when 'timeout' has passed
284
self.disable_initiator_tag = (gobject.timeout_add
285
(self.timeout_milliseconds(),
290
self.PropertyChanged(dbus.String(u"enabled"),
291
dbus.Boolean(True, variant_level=1))
292
self.PropertyChanged(dbus.String(u"last_enabled"),
293
(_datetime_to_dbus(self.last_enabled,
297
"""Disable this client."""
298
if not getattr(self, "enabled", False):
200
# Schedule a stop() when 'timeout' has passed
201
self.stop_initiator_tag = gobject.timeout_add\
202
(self._timeout_milliseconds,
206
The possibility that this client might be restarted is left
207
open, but not currently used."""
208
# If this client doesn't have a secret, it is already stopped.
210
logger.debug(u"Stopping client %s", self.name)
300
logger.info(u"Disabling client %s", self.name)
301
if getattr(self, "disable_initiator_tag", False):
302
gobject.source_remove(self.disable_initiator_tag)
303
self.disable_initiator_tag = None
304
if getattr(self, "checker_initiator_tag", False):
214
if hasattr(self, "stop_initiator_tag") \
215
and self.stop_initiator_tag:
216
gobject.source_remove(self.stop_initiator_tag)
217
self.stop_initiator_tag = None
218
if hasattr(self, "checker_initiator_tag") \
219
and self.checker_initiator_tag:
305
220
gobject.source_remove(self.checker_initiator_tag)
306
221
self.checker_initiator_tag = None
307
222
self.stop_checker()
308
if self.disable_hook:
309
self.disable_hook(self)
313
self.PropertyChanged(dbus.String(u"enabled"),
314
dbus.Boolean(False, variant_level=1))
315
225
# Do not run this again if called by a gobject.timeout_add
318
227
def __del__(self):
319
self.disable_hook = None
322
def checker_callback(self, pid, condition, command):
228
self.stop_hook = None
230
def checker_callback(self, pid, condition):
323
231
"""The checker has completed, so take appropriate actions."""
232
now = datetime.datetime.now()
324
233
self.checker_callback_tag = None
325
234
self.checker = None
328
self.PropertyChanged(dbus.String(u"checker_running"),
329
dbus.Boolean(False, variant_level=1))
330
if os.WIFEXITED(condition):
331
exitstatus = os.WEXITSTATUS(condition)
333
logger.info(u"Checker for %(name)s succeeded",
337
logger.info(u"Checker for %(name)s failed",
341
self.CheckerCompleted(dbus.Int16(exitstatus),
342
dbus.Int64(condition),
343
dbus.String(command))
235
if os.WIFEXITED(condition) \
236
and (os.WEXITSTATUS(condition) == 0):
237
logger.debug(u"Checker for %(name)s succeeded",
240
gobject.source_remove(self.stop_initiator_tag)
241
self.stop_initiator_tag = gobject.timeout_add\
242
(self._timeout_milliseconds,
244
elif not os.WIFEXITED(condition):
345
245
logger.warning(u"Checker for %(name)s crashed?",
349
self.CheckerCompleted(dbus.Int16(-1),
350
dbus.Int64(condition),
351
dbus.String(command))
353
def checked_ok(self):
354
"""Bump up the timeout for this client.
355
This should only be called when the client has been seen,
358
self.last_checked_ok = datetime.datetime.utcnow()
359
gobject.source_remove(self.disable_initiator_tag)
360
self.disable_initiator_tag = (gobject.timeout_add
361
(self.timeout_milliseconds(),
365
self.PropertyChanged(
366
dbus.String(u"last_checked_ok"),
367
(_datetime_to_dbus(self.last_checked_ok,
248
logger.debug(u"Checker for %(name)s failed",
370
250
def start_checker(self):
371
251
"""Start a new checker subprocess if one is not running.
372
252
If a checker already exists, leave it running and do
381
261
# is as it should be.
382
262
if self.checker is None:
384
# In case checker_command has exactly one % operator
385
command = self.checker_command % self.host
264
command = self.check_command % self.fqdn
386
265
except TypeError:
387
# Escape attributes for the shell
388
266
escaped_attrs = dict((key, re.escape(str(val)))
390
268
vars(self).iteritems())
392
command = self.checker_command % escaped_attrs
270
command = self.check_command % escaped_attrs
393
271
except TypeError, error:
394
logger.error(u'Could not format string "%s":'
395
u' %s', self.checker_command, error)
272
logger.critical(u'Could not format string "%s":'
273
u' %s', self.check_command, error)
396
274
return True # Try again later
398
logger.info(u"Starting checker %r for %s",
400
# We don't need to redirect stdout and stderr, since
401
# in normal mode, that is already done by daemon(),
402
# and in debug mode we don't want to. (Stdin is
403
# always replaced by /dev/null.)
404
self.checker = subprocess.Popen(command,
409
self.CheckerStarted(command)
410
self.PropertyChanged(
411
dbus.String("checker_running"),
412
dbus.Boolean(True, variant_level=1))
413
self.checker_callback_tag = (gobject.child_watch_add
415
self.checker_callback,
417
# The checker may have completed before the gobject
418
# watch was added. Check for this.
419
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
421
gobject.source_remove(self.checker_callback_tag)
422
self.checker_callback(pid, status, command)
423
except OSError, error:
276
logger.debug(u"Starting checker %r for %s",
278
self.checker = subprocess.\
280
close_fds=True, shell=True,
282
self.checker_callback_tag = gobject.child_watch_add\
284
self.checker_callback)
285
except subprocess.OSError, error:
424
286
logger.error(u"Failed to start subprocess: %s",
426
288
# Re-run this periodically if run by gobject.timeout_add
429
290
def stop_checker(self):
430
291
"""Force the checker process, if any, to stop."""
431
292
if self.checker_callback_tag:
432
293
gobject.source_remove(self.checker_callback_tag)
433
294
self.checker_callback_tag = None
434
if getattr(self, "checker", None) is None:
295
if not hasattr(self, "checker") or self.checker is None:
436
logger.debug(u"Stopping checker for %(name)s", vars(self))
297
logger.debug("Stopping checker for %(name)s", vars(self))
438
299
os.kill(self.checker.pid, signal.SIGTERM)
440
301
#if self.checker.poll() is None:
441
302
# os.kill(self.checker.pid, signal.SIGKILL)
442
303
except OSError, error:
443
if error.errno != errno.ESRCH: # No such process
304
if error.errno != errno.ESRCH:
445
306
self.checker = None
447
self.PropertyChanged(dbus.String(u"checker_running"),
448
dbus.Boolean(False, variant_level=1))
450
def still_valid(self):
307
def still_valid(self, now=None):
451
308
"""Has the timeout not yet passed for this client?"""
452
if not getattr(self, "enabled", False):
454
now = datetime.datetime.utcnow()
455
if self.last_checked_ok is None:
310
now = datetime.datetime.now()
311
if self.last_seen is None:
456
312
return now < (self.created + self.timeout)
458
return now < (self.last_checked_ok + self.timeout)
460
## D-Bus methods & signals
461
_interface = u"se.bsnet.fukt.Mandos.Client"
464
CheckedOK = dbus.service.method(_interface)(checked_ok)
465
CheckedOK.__name__ = "CheckedOK"
467
# CheckerCompleted - signal
468
@dbus.service.signal(_interface, signature="nxs")
469
def CheckerCompleted(self, exitcode, waitstatus, command):
473
# CheckerStarted - signal
474
@dbus.service.signal(_interface, signature="s")
475
def CheckerStarted(self, command):
479
# GetAllProperties - method
480
@dbus.service.method(_interface, out_signature="a{sv}")
481
def GetAllProperties(self):
483
return dbus.Dictionary({
485
dbus.String(self.name, variant_level=1),
486
dbus.String("fingerprint"):
487
dbus.String(self.fingerprint, variant_level=1),
489
dbus.String(self.host, variant_level=1),
490
dbus.String("created"):
491
_datetime_to_dbus(self.created, variant_level=1),
492
dbus.String("last_enabled"):
493
(_datetime_to_dbus(self.last_enabled,
495
if self.last_enabled is not None
496
else dbus.Boolean(False, variant_level=1)),
497
dbus.String("enabled"):
498
dbus.Boolean(self.enabled, variant_level=1),
499
dbus.String("last_checked_ok"):
500
(_datetime_to_dbus(self.last_checked_ok,
502
if self.last_checked_ok is not None
503
else dbus.Boolean (False, variant_level=1)),
504
dbus.String("timeout"):
505
dbus.UInt64(self.timeout_milliseconds(),
507
dbus.String("interval"):
508
dbus.UInt64(self.interval_milliseconds(),
510
dbus.String("checker"):
511
dbus.String(self.checker_command,
513
dbus.String("checker_running"):
514
dbus.Boolean(self.checker is not None,
516
dbus.String("object_path"):
517
dbus.ObjectPath(self.dbus_object_path,
521
# IsStillValid - method
522
IsStillValid = (dbus.service.method(_interface, out_signature="b")
524
IsStillValid.__name__ = "IsStillValid"
526
# PropertyChanged - signal
527
@dbus.service.signal(_interface, signature="sv")
528
def PropertyChanged(self, property, value):
532
# SetChecker - method
533
@dbus.service.method(_interface, in_signature="s")
534
def SetChecker(self, checker):
535
"D-Bus setter method"
536
self.checker_command = checker
538
self.PropertyChanged(dbus.String(u"checker"),
539
dbus.String(self.checker_command,
543
@dbus.service.method(_interface, in_signature="s")
544
def SetHost(self, host):
545
"D-Bus setter method"
548
self.PropertyChanged(dbus.String(u"host"),
549
dbus.String(self.host, variant_level=1))
551
# SetInterval - method
552
@dbus.service.method(_interface, in_signature="t")
553
def SetInterval(self, milliseconds):
554
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
556
self.PropertyChanged(dbus.String(u"interval"),
557
(dbus.UInt64(self.interval_milliseconds(),
561
@dbus.service.method(_interface, in_signature="ay",
563
def SetSecret(self, secret):
564
"D-Bus setter method"
565
self.secret = str(secret)
567
# SetTimeout - method
568
@dbus.service.method(_interface, in_signature="t")
569
def SetTimeout(self, milliseconds):
570
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
572
self.PropertyChanged(dbus.String(u"timeout"),
573
(dbus.UInt64(self.timeout_milliseconds(),
577
Enable = dbus.service.method(_interface)(enable)
578
Enable.__name__ = "Enable"
580
# StartChecker - method
581
@dbus.service.method(_interface)
582
def StartChecker(self):
587
@dbus.service.method(_interface)
592
# StopChecker - method
593
StopChecker = dbus.service.method(_interface)(stop_checker)
594
StopChecker.__name__ = "StopChecker"
314
return now < (self.last_seen + self.timeout)
599
317
def peer_certificate(session):
600
318
"Return the peer's OpenPGP certificate as a bytestring"
601
319
# If not an OpenPGP certificate...
602
if (gnutls.library.functions
603
.gnutls_certificate_type_get(session._c_object)
604
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
320
if gnutls.library.functions.gnutls_certificate_type_get\
321
(session._c_object) \
322
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
605
323
# ...do the normal thing
606
324
return session.peer_certificate
607
list_size = ctypes.c_uint(1)
608
cert_list = (gnutls.library.functions
609
.gnutls_certificate_get_peers
610
(session._c_object, ctypes.byref(list_size)))
611
if not bool(cert_list) and list_size.value != 0:
612
raise gnutls.errors.GNUTLSError("error getting peer"
325
list_size = ctypes.c_uint()
326
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
327
(session._c_object, ctypes.byref(list_size))
614
328
if list_size.value == 0:
616
330
cert = cert_list[0]
620
334
def fingerprint(openpgp):
621
335
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
622
# New GnuTLS "datum" with the OpenPGP public key
623
datum = (gnutls.library.types
624
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
627
ctypes.c_uint(len(openpgp))))
628
336
# New empty GnuTLS certificate
629
337
crt = gnutls.library.types.gnutls_openpgp_crt_t()
630
(gnutls.library.functions
631
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
338
gnutls.library.functions.gnutls_openpgp_crt_init\
340
# New GnuTLS "datum" with the OpenPGP public key
341
datum = gnutls.library.types.gnutls_datum_t\
342
(ctypes.cast(ctypes.c_char_p(openpgp),
343
ctypes.POINTER(ctypes.c_ubyte)),
344
ctypes.c_uint(len(openpgp)))
632
345
# Import the OpenPGP public key into the certificate
633
(gnutls.library.functions
634
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
635
gnutls.library.constants
636
.GNUTLS_OPENPGP_FMT_RAW))
637
# Verify the self signature in the key
638
crtverify = ctypes.c_uint()
639
(gnutls.library.functions
640
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
641
if crtverify.value != 0:
642
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
643
raise gnutls.errors.CertificateSecurityError("Verify failed")
346
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
349
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
644
350
# New buffer for the fingerprint
645
buf = ctypes.create_string_buffer(20)
646
buf_len = ctypes.c_size_t()
351
buffer = ctypes.create_string_buffer(20)
352
buffer_length = ctypes.c_size_t()
647
353
# Get the fingerprint from the certificate into the buffer
648
(gnutls.library.functions
649
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
650
ctypes.byref(buf_len)))
354
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
355
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
651
356
# Deinit the certificate
652
357
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
653
358
# Convert the buffer to a Python bytestring
654
fpr = ctypes.string_at(buf, buf_len.value)
359
fpr = ctypes.string_at(buffer, buffer_length.value)
655
360
# Convert the bytestring to hexadecimal notation
656
361
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
660
class TCP_handler(SocketServer.BaseRequestHandler, object):
365
class tcp_handler(SocketServer.BaseRequestHandler, object):
661
366
"""A TCP request handler class.
662
367
Instantiated by IPv6_TCPServer for each request to handle it.
663
368
Note: This will run in its own forked process."""
665
370
def handle(self):
666
logger.info(u"TCP connection from: %s",
667
unicode(self.client_address))
668
session = (gnutls.connection
669
.ClientSession(self.request,
673
line = self.request.makefile().readline()
674
logger.debug(u"Protocol version: %r", line)
676
if int(line.strip().split()[0]) > 1:
678
except (ValueError, IndexError, RuntimeError), error:
679
logger.error(u"Unknown protocol version: %s", error)
682
# Note: gnutls.connection.X509Credentials is really a generic
683
# GnuTLS certificate credentials object so long as no X.509
684
# keys are added to it. Therefore, we can use it here despite
685
# using OpenPGP certificates.
371
logger.debug(u"TCP connection from: %s",
372
unicode(self.client_address))
373
session = gnutls.connection.ClientSession(self.request,
687
377
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
688
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
690
# Use a fallback default, since this MUST be set.
691
priority = self.server.settings.get("priority", "NORMAL")
692
(gnutls.library.functions
693
.gnutls_priority_set_direct(session._c_object,
378
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
380
priority = "SECURE256"
382
gnutls.library.functions.gnutls_priority_set_direct\
383
(session._c_object, priority, None);
697
386
session.handshake()
698
387
except gnutls.errors.GNUTLSError, error:
699
logger.warning(u"Handshake failed: %s", error)
388
logger.debug(u"Handshake failed: %s", error)
700
389
# Do not run session.bye() here: the session is not
701
390
# established. Just abandon the request.
703
logger.debug(u"Handshake succeeded")
705
393
fpr = fingerprint(peer_certificate(session))
706
394
except (TypeError, gnutls.errors.GNUTLSError), error:
707
logger.warning(u"Bad certificate: %s", error)
395
logger.debug(u"Bad certificate: %s", error)
710
398
logger.debug(u"Fingerprint: %s", fpr)
712
400
for c in self.server.clients:
713
401
if c.fingerprint == fpr:
717
logger.warning(u"Client not found for fingerprint: %s",
721
404
# Have to check if client.still_valid(), since it is possible
722
405
# that the client timed out while establishing the GnuTLS
724
if not client.still_valid():
725
logger.warning(u"Client %(name)s is invalid",
407
if (not client) or (not client.still_valid()):
409
logger.debug(u"Client %(name)s is invalid",
412
logger.debug(u"Client not found for fingerprint: %s",
729
## This won't work here, since we're in a fork.
730
# client.checked_ok()
732
417
while sent_size < len(client.secret):
733
418
sent = session.send(client.secret[sent_size:])
741
class IPv6_TCPServer(SocketServer.ForkingMixIn,
742
SocketServer.TCPServer, object):
743
"""IPv6-capable TCP server. Accepts 'None' as address and/or port.
426
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
427
"""IPv6 TCP server. Accepts 'None' as address and/or port.
745
settings: Server settings
429
options: Command line options
746
430
clients: Set() of Client objects
747
enabled: Boolean; whether this server is activated yet
749
432
address_family = socket.AF_INET6
750
433
def __init__(self, *args, **kwargs):
751
if "settings" in kwargs:
752
self.settings = kwargs["settings"]
753
del kwargs["settings"]
434
if "options" in kwargs:
435
self.options = kwargs["options"]
436
del kwargs["options"]
754
437
if "clients" in kwargs:
755
438
self.clients = kwargs["clients"]
756
439
del kwargs["clients"]
757
if "use_ipv6" in kwargs:
758
if not kwargs["use_ipv6"]:
759
self.address_family = socket.AF_INET
760
del kwargs["use_ipv6"]
762
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
440
return super(type(self), self).__init__(*args, **kwargs)
763
441
def server_bind(self):
764
442
"""This overrides the normal server_bind() function
765
443
to bind to an interface if one was specified, and also NOT to
766
444
bind to an address or port if they were not specified."""
767
if self.settings["interface"]:
768
# 25 is from /usr/include/asm-i486/socket.h
769
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
445
if self.options.interface:
446
if not hasattr(socket, "SO_BINDTODEVICE"):
447
# From /usr/include/asm-i486/socket.h
448
socket.SO_BINDTODEVICE = 25
771
450
self.socket.setsockopt(socket.SOL_SOCKET,
773
self.settings["interface"])
451
socket.SO_BINDTODEVICE,
452
self.options.interface)
774
453
except socket.error, error:
775
454
if error[0] == errno.EPERM:
776
logger.error(u"No permission to"
777
u" bind to interface %s",
778
self.settings["interface"])
455
logger.warning(u"No permission to"
456
u" bind to interface %s",
457
self.options.interface)
781
460
# Only bind(2) the socket if we really need to.
782
461
if self.server_address[0] or self.server_address[1]:
783
462
if not self.server_address[0]:
784
if self.address_family == socket.AF_INET6:
785
any_address = "::" # in6addr_any
787
any_address = socket.INADDR_ANY
788
self.server_address = (any_address,
464
self.server_address = (in6addr_any,
789
465
self.server_address[1])
790
elif not self.server_address[1]:
466
elif self.server_address[1] is None:
791
467
self.server_address = (self.server_address[0],
793
# if self.settings["interface"]:
794
# self.server_address = (self.server_address[0],
800
return super(IPv6_TCPServer, self).server_bind()
801
def server_activate(self):
803
return super(IPv6_TCPServer, self).server_activate()
469
return super(type(self), self).server_bind()
808
472
def string_to_delta(interval):
809
473
"""Parse a string and return a datetime.timedelta
811
475
>>> string_to_delta('7d')
812
476
datetime.timedelta(7)
813
477
>>> string_to_delta('60s')
818
482
datetime.timedelta(1)
819
483
>>> string_to_delta(u'1w')
820
484
datetime.timedelta(7)
821
>>> string_to_delta('5m 30s')
822
datetime.timedelta(0, 330)
824
timevalue = datetime.timedelta(0)
825
for s in interval.split():
827
suffix = unicode(s[-1])
830
delta = datetime.timedelta(value)
832
delta = datetime.timedelta(0, value)
834
delta = datetime.timedelta(0, 0, 0, 0, value)
836
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
838
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
841
except (ValueError, IndexError):
487
suffix=unicode(interval[-1])
488
value=int(interval[:-1])
490
delta = datetime.timedelta(value)
492
delta = datetime.timedelta(0, value)
494
delta = datetime.timedelta(0, 0, 0, 0, value)
496
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
498
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
501
except (ValueError, IndexError):
507
"""Derived from the Avahi example code"""
508
global group, serviceName, serviceType, servicePort, serviceTXT, \
511
group = dbus.Interface(
512
bus.get_object( avahi.DBUS_NAME,
513
server.EntryGroupNew()),
514
avahi.DBUS_INTERFACE_ENTRY_GROUP)
515
group.connect_to_signal('StateChanged',
516
entry_group_state_changed)
517
logger.debug(u"Adding service '%s' of type '%s' ...",
518
serviceName, serviceType)
521
serviceInterface, # interface
522
avahi.PROTO_INET6, # protocol
523
dbus.UInt32(0), # flags
524
serviceName, serviceType,
526
dbus.UInt16(servicePort),
527
avahi.string_array_to_txt_array(serviceTXT))
531
def remove_service():
532
"""From the Avahi example code"""
535
if not group is None:
847
539
def server_state_changed(state):
848
540
"""Derived from the Avahi example code"""
849
541
if state == avahi.SERVER_COLLISION:
850
logger.error(u"Zeroconf server name collision")
542
logger.warning(u"Server name collision")
852
544
elif state == avahi.SERVER_RUNNING:
856
548
def entry_group_state_changed(state, error):
857
549
"""Derived from the Avahi example code"""
858
logger.debug(u"Avahi state change: %i", state)
550
global serviceName, server, rename_count
552
logger.debug(u"state change: %i", state)
860
554
if state == avahi.ENTRY_GROUP_ESTABLISHED:
861
logger.debug(u"Zeroconf service established.")
555
logger.debug(u"Service established.")
862
556
elif state == avahi.ENTRY_GROUP_COLLISION:
863
logger.warning(u"Zeroconf service name collision.")
558
rename_count = rename_count - 1
560
name = server.GetAlternativeServiceName(name)
561
logger.warning(u"Service name collision, "
562
u"changing name to '%s' ...", name)
567
logger.error(u"No suitable service name found after %i"
568
u" retries, exiting.", n_rename)
865
570
elif state == avahi.ENTRY_GROUP_FAILURE:
866
logger.critical(u"Avahi: Error in group state changed %s",
868
raise AvahiGroupError(u"State changed: %s" % unicode(error))
571
logger.error(u"Error in group state changed %s",
870
576
def if_nametoindex(interface):
871
"""Call the C function if_nametoindex(), or equivalent"""
872
global if_nametoindex
577
"""Call the C function if_nametoindex()"""
874
if_nametoindex = (ctypes.cdll.LoadLibrary
875
(ctypes.util.find_library("c"))
579
libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
return libc.if_nametoindex(interface)
877
581
except (OSError, AttributeError):
878
582
if "struct" not in sys.modules:
880
584
if "fcntl" not in sys.modules:
882
def if_nametoindex(interface):
883
"Get an interface index the hard way, i.e. using fcntl()"
884
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
885
with closing(socket.socket()) as s:
886
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
887
struct.pack("16s16x", interface))
888
interface_index = struct.unpack("I", ifreq[16:20])[0]
889
return interface_index
890
return if_nametoindex(interface)
893
def daemon(nochdir = False, noclose = False):
586
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
588
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
589
struct.pack("16s16x", interface))
591
interface_index = struct.unpack("I", ifreq[16:20])[0]
592
return interface_index
595
def daemon(nochdir, noclose):
894
596
"""See daemon(3). Standard BSD Unix function.
895
597
This should really exist as os.daemon, but it doesn't (yet)."""
616
def killme(status = 0):
617
logger.debug("Stopping server with exit status %d", status)
619
if main_loop_started:
917
parser = optparse.OptionParser(version = "%%prog %s" % version)
628
global main_loop_started
629
main_loop_started = False
631
parser = OptionParser()
918
632
parser.add_option("-i", "--interface", type="string",
919
metavar="IF", help="Bind to interface IF")
920
parser.add_option("-a", "--address", type="string",
633
default=None, metavar="IF",
634
help="Bind to interface IF")
635
parser.add_option("-a", "--address", type="string", default=None,
921
636
help="Address to listen for requests on")
922
parser.add_option("-p", "--port", type="int",
637
parser.add_option("-p", "--port", type="int", default=None,
923
638
help="Port number to receive requests on")
924
parser.add_option("--check", action="store_true",
639
parser.add_option("--timeout", type="string", # Parsed later
641
help="Amount of downtime allowed for clients")
642
parser.add_option("--interval", type="string", # Parsed later
644
help="How often to check that a client is up")
645
parser.add_option("--check", action="store_true", default=False,
925
646
help="Run self-test")
926
parser.add_option("--debug", action="store_true",
927
help="Debug mode; run in foreground and log to"
929
parser.add_option("--priority", type="string", help="GnuTLS"
930
" priority string (see GnuTLS documentation)")
931
parser.add_option("--servicename", type="string", metavar="NAME",
932
help="Zeroconf service name")
933
parser.add_option("--configdir", type="string",
934
default="/etc/mandos", metavar="DIR",
935
help="Directory to search for configuration"
937
parser.add_option("--no-dbus", action="store_false",
939
help="Do not provide D-Bus system bus"
941
parser.add_option("--no-ipv6", action="store_false",
942
dest="use_ipv6", help="Do not use IPv6")
943
options = parser.parse_args()[0]
647
parser.add_option("--debug", action="store_true", default=False,
649
(options, args) = parser.parse_args()
945
651
if options.check:
947
653
doctest.testmod()
950
# Default values for config file for server-global settings
951
server_defaults = { "interface": "",
956
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
957
"servicename": "Mandos",
962
# Parse config file for server-global settings
963
server_config = ConfigParser.SafeConfigParser(server_defaults)
965
server_config.read(os.path.join(options.configdir, "mandos.conf"))
966
# Convert the SafeConfigParser object to a dict
967
server_settings = server_config.defaults()
968
# Use the appropriate methods on the non-string config options
969
server_settings["debug"] = server_config.getboolean("DEFAULT",
971
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
973
server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
975
if server_settings["port"]:
976
server_settings["port"] = server_config.getint("DEFAULT",
980
# Override the settings from the config file with command line
982
for option in ("interface", "address", "port", "debug",
983
"priority", "servicename", "configdir",
984
"use_dbus", "use_ipv6"):
985
value = getattr(options, option)
986
if value is not None:
987
server_settings[option] = value
989
# Now we have our good server settings in "server_settings"
992
debug = server_settings["debug"]
993
use_dbus = server_settings["use_dbus"]
994
use_ipv6 = server_settings["use_ipv6"]
997
syslogger.setLevel(logging.WARNING)
998
console.setLevel(logging.WARNING)
1000
if server_settings["servicename"] != "Mandos":
1001
syslogger.setFormatter(logging.Formatter
1002
('Mandos (%s): %%(levelname)s:'
1004
% server_settings["servicename"]))
1006
# Parse config file with clients
1007
client_defaults = { "timeout": "1h",
1009
"checker": "fping -q -- %%(host)s",
1012
client_config = ConfigParser.SafeConfigParser(client_defaults)
1013
client_config.read(os.path.join(server_settings["configdir"],
1017
tcp_server = IPv6_TCPServer((server_settings["address"],
1018
server_settings["port"]),
1020
settings=server_settings,
1021
clients=clients, use_ipv6=use_ipv6)
1022
pidfilename = "/var/run/mandos.pid"
1024
pidfile = open(pidfilename, "w")
1026
logger.error("Could not open file %r", pidfilename)
1029
uid = pwd.getpwnam("_mandos").pw_uid
1030
gid = pwd.getpwnam("_mandos").pw_gid
1033
uid = pwd.getpwnam("mandos").pw_uid
1034
gid = pwd.getpwnam("mandos").pw_gid
1037
uid = pwd.getpwnam("nobody").pw_uid
1038
gid = pwd.getpwnam("nogroup").pw_gid
1045
except OSError, error:
1046
if error[0] != errno.EPERM:
1049
# Enable all possible GnuTLS debugging
1051
# "Use a log level over 10 to enable all debugging options."
1053
gnutls.library.functions.gnutls_global_set_log_level(11)
1055
@gnutls.library.types.gnutls_log_func
1056
def debug_gnutls(level, string):
1057
logger.debug("GnuTLS: %s", string[:-1])
1059
(gnutls.library.functions
1060
.gnutls_global_set_log_function(debug_gnutls))
1063
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1064
service = AvahiService(name = server_settings["servicename"],
1065
servicetype = "_mandos._tcp",
1066
protocol = protocol)
1067
if server_settings["interface"]:
1068
service.interface = (if_nametoindex
1069
(server_settings["interface"]))
656
# Parse the time arguments
658
options.timeout = string_to_delta(options.timeout)
660
parser.error("option --timeout: Unparseable time")
662
options.interval = string_to_delta(options.interval)
664
parser.error("option --interval: Unparseable time")
667
defaults = { "checker": "fping -q -- %%(fqdn)s" }
668
client_config = ConfigParser.SafeConfigParser(defaults)
669
#client_config.readfp(open("global.conf"), "global.conf")
670
client_config.read("mandos-clients.conf")
1071
672
global main_loop
1128
720
client = clients.pop()
1129
client.disable_hook = None
721
client.stop_hook = None
1132
724
atexit.register(cleanup)
1135
727
signal.signal(signal.SIGINT, signal.SIG_IGN)
1136
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1137
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1140
class MandosServer(dbus.service.Object):
1141
"""A D-Bus proxy object"""
1143
dbus.service.Object.__init__(self, bus, "/")
1144
_interface = u"se.bsnet.fukt.Mandos"
1146
@dbus.service.signal(_interface, signature="oa{sv}")
1147
def ClientAdded(self, objpath, properties):
1151
@dbus.service.signal(_interface, signature="os")
1152
def ClientRemoved(self, objpath, name):
1156
@dbus.service.method(_interface, out_signature="ao")
1157
def GetAllClients(self):
1159
return dbus.Array(c.dbus_object_path for c in clients)
1161
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1162
def GetAllClientsWithProperties(self):
1164
return dbus.Dictionary(
1165
((c.dbus_object_path, c.GetAllProperties())
1169
@dbus.service.method(_interface, in_signature="o")
1170
def RemoveClient(self, object_path):
1173
if c.dbus_object_path == object_path:
1175
# Don't signal anything except ClientRemoved
1179
self.ClientRemoved(object_path, c.name)
1185
mandos_server = MandosServer()
728
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
1187
731
for client in clients:
1190
mandos_server.ClientAdded(client.dbus_object_path,
1191
client.GetAllProperties())
1195
tcp_server.server_activate()
1197
# Find out what port we got
1198
service.port = tcp_server.socket.getsockname()[1]
1200
logger.info(u"Now listening on address %r, port %d,"
1201
" flowinfo %d, scope_id %d"
1202
% tcp_server.socket.getsockname())
1204
logger.info(u"Now listening on address %r, port %d"
1205
% tcp_server.socket.getsockname())
1207
#service.interface = tcp_server.socket.getsockname()[3]
1210
# From the Avahi example code
1211
server.connect_to_signal("StateChanged", server_state_changed)
1213
server_state_changed(server.GetState())
1214
except dbus.exceptions.DBusException, error:
1215
logger.critical(u"DBusException: %s", error)
1217
# End of Avahi example code
1219
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1220
lambda *args, **kwargs:
1221
(tcp_server.handle_request
1222
(*args[2:], **kwargs) or True))
1224
logger.debug(u"Starting main loop")
734
tcp_server = IPv6_TCPServer((options.address, options.port),
738
# Find out what random port we got
740
servicePort = tcp_server.socket.getsockname()[1]
741
logger.debug(u"Now listening on port %d", servicePort)
743
if options.interface is not None:
744
global serviceInterface
745
serviceInterface = if_nametoindex(options.interface)
747
# From the Avahi example code
748
server.connect_to_signal("StateChanged", server_state_changed)
750
server_state_changed(server.GetState())
751
except dbus.exceptions.DBusException, error:
752
logger.critical(u"DBusException: %s", error)
754
# End of Avahi example code
756
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
757
lambda *args, **kwargs:
758
tcp_server.handle_request(*args[2:],
761
logger.debug("Starting main loop")
762
main_loop_started = True
1226
except AvahiError, error:
1227
logger.critical(u"AvahiError: %s", error)
1229
764
except KeyboardInterrupt:
1232
logger.debug("Server received KeyboardInterrupt")
1233
logger.debug("Server exiting")
1235
770
if __name__ == '__main__':