57
57
import logging.handlers
59
from contextlib import closing
62
65
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.
77
71
logger = logging.Logger('mandos')
78
syslogger = logging.handlers.SysLogHandler\
79
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
syslogger.setFormatter(logging.Formatter\
81
('%(levelname)s: %(message)s'))
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:'
82
78
logger.addHandler(syslogger)
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
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))
89
171
# From the Avahi example code:
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
172
group = None # our entry group
99
173
# End of Avahi example code
102
class Client(object):
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):
103
182
"""A representation of a client host served by this server.
105
name: string; from the config file, used in log messages
184
name: string; from the config file, used in log messages and
106
186
fingerprint: string (40 or 32 hexadecimal digits); used to
107
187
uniquely identify the client
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.
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.
119
201
checker_initiator_tag: a gobject event source tag, or None
120
stop_initiator_tag: - '' -
202
disable_initiator_tag: - '' -
121
203
checker_callback_tag: - '' -
122
204
checker_command: string; External command which is run to check if
123
client lives. %()s expansions are done at
205
client lives. %() expansions are done at
124
206
runtime with vars(self) as dict, so that for
125
207
instance %(name)s can be used in the command.
127
_timeout: Real variable for 'timeout'
128
_interval: Real variable for 'interval'
129
_timeout_milliseconds: Used by gobject.timeout_add()
130
_interval_milliseconds: - '' -
208
current_checker_command: string; current running checker_command
209
use_dbus: bool(); Whether to provide D-Bus interface and signals
210
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
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.."""
212
def timeout_milliseconds(self):
213
"Return the 'timeout' attribute in milliseconds"
214
return ((self.timeout.days * 24 * 60 * 60 * 1000)
215
+ (self.timeout.seconds * 1000)
216
+ (self.timeout.microseconds // 1000))
218
def interval_milliseconds(self):
219
"Return the 'interval' attribute in milliseconds"
220
return ((self.interval.days * 24 * 60 * 60 * 1000)
221
+ (self.interval.seconds * 1000)
222
+ (self.interval.microseconds // 1000))
224
def __init__(self, name = None, disable_hook=None, config=None,
226
"""Note: the 'checker' key in 'config' sets the
227
'checker_command' attribute and *not* the 'checker'
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
232
logger.debug(u"Creating client %r", self.name)
233
self.use_dbus = False # During __init__
234
# Uppercase and remove spaces from fingerprint for later
235
# comparison purposes with return value from the fingerprint()
237
self.fingerprint = (config["fingerprint"].upper()
239
logger.debug(u" Fingerprint: %s", self.fingerprint)
240
if "secret" in config:
241
self.secret = config["secret"].decode(u"base64")
242
elif "secfile" in config:
243
with closing(open(os.path.expanduser
245
(config["secfile"])))) as secfile:
246
self.secret = secfile.read()
248
raise TypeError(u"No secret or secfile for client %s"
250
self.host = config.get("host", "")
251
self.created = datetime.datetime.utcnow()
253
self.last_enabled = None
254
self.last_checked_ok = None
255
self.timeout = string_to_delta(config["timeout"])
256
self.interval = string_to_delta(config["interval"])
257
self.disable_hook = disable_hook
186
258
self.checker = None
187
259
self.checker_initiator_tag = None
188
self.stop_initiator_tag = None
260
self.disable_initiator_tag = None
189
261
self.checker_callback_tag = None
190
self.check_command = checker
262
self.checker_command = config["checker"]
263
self.current_checker_command = None
264
self.last_connect = None
265
# Only now, when this client is initialized, can it show up on
267
self.use_dbus = use_dbus
269
self.dbus_object_path = (dbus.ObjectPath
271
+ self.name.replace(".", "_")))
272
dbus.service.Object.__init__(self, bus,
273
self.dbus_object_path)
192
276
"""Start this client's checker and timeout hooks"""
277
self.last_enabled = datetime.datetime.utcnow()
193
278
# Schedule a new checker to be started an 'interval' from now,
194
279
# and every interval from then on.
195
self.checker_initiator_tag = gobject.timeout_add\
196
(self._interval_milliseconds,
280
self.checker_initiator_tag = (gobject.timeout_add
281
(self.interval_milliseconds(),
198
283
# Also start a new checker *right now*.
199
284
self.start_checker()
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)
285
# Schedule a disable() when 'timeout' has passed
286
self.disable_initiator_tag = (gobject.timeout_add
287
(self.timeout_milliseconds(),
292
self.PropertyChanged(dbus.String(u"enabled"),
293
dbus.Boolean(True, variant_level=1))
294
self.PropertyChanged(dbus.String(u"last_enabled"),
295
(_datetime_to_dbus(self.last_enabled,
299
"""Disable this client."""
300
if not getattr(self, "enabled", 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:
302
logger.info(u"Disabling client %s", self.name)
303
if getattr(self, "disable_initiator_tag", False):
304
gobject.source_remove(self.disable_initiator_tag)
305
self.disable_initiator_tag = None
306
if getattr(self, "checker_initiator_tag", False):
220
307
gobject.source_remove(self.checker_initiator_tag)
221
308
self.checker_initiator_tag = None
222
309
self.stop_checker()
310
if self.disable_hook:
311
self.disable_hook(self)
315
self.PropertyChanged(dbus.String(u"enabled"),
316
dbus.Boolean(False, variant_level=1))
225
317
# Do not run this again if called by a gobject.timeout_add
227
320
def __del__(self):
228
self.stop_hook = None
230
def checker_callback(self, pid, condition):
321
self.disable_hook = None
324
def checker_callback(self, pid, condition, command):
231
325
"""The checker has completed, so take appropriate actions."""
232
now = datetime.datetime.now()
233
326
self.checker_callback_tag = None
234
327
self.checker = None
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):
330
self.PropertyChanged(dbus.String(u"checker_running"),
331
dbus.Boolean(False, variant_level=1))
332
if os.WIFEXITED(condition):
333
exitstatus = os.WEXITSTATUS(condition)
335
logger.info(u"Checker for %(name)s succeeded",
339
logger.info(u"Checker for %(name)s failed",
343
self.CheckerCompleted(dbus.Int16(exitstatus),
344
dbus.Int64(condition),
345
dbus.String(command))
245
347
logger.warning(u"Checker for %(name)s crashed?",
248
logger.debug(u"Checker for %(name)s failed",
351
self.CheckerCompleted(dbus.Int16(-1),
352
dbus.Int64(condition),
353
dbus.String(command))
355
def checked_ok(self):
356
"""Bump up the timeout for this client.
357
This should only be called when the client has been seen,
360
self.last_checked_ok = datetime.datetime.utcnow()
361
gobject.source_remove(self.disable_initiator_tag)
362
self.disable_initiator_tag = (gobject.timeout_add
363
(self.timeout_milliseconds(),
367
self.PropertyChanged(
368
dbus.String(u"last_checked_ok"),
369
(_datetime_to_dbus(self.last_checked_ok,
250
372
def start_checker(self):
251
373
"""Start a new checker subprocess if one is not running.
252
374
If a checker already exists, leave it running and do
259
381
# checkers alone, the checker would have to take more time
260
382
# than 'timeout' for the client to be declared invalid, which
261
383
# is as it should be.
385
# If a checker exists, make sure it is not a zombie
386
if self.checker is not None:
387
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
389
logger.warning("Checker was a zombie")
390
gobject.source_remove(self.checker_callback_tag)
391
self.checker_callback(pid, status,
392
self.current_checker_command)
393
# Start a new checker if needed
262
394
if self.checker is None:
264
command = self.check_command % self.fqdn
396
# In case checker_command has exactly one % operator
397
command = self.checker_command % self.host
265
398
except TypeError:
399
# Escape attributes for the shell
266
400
escaped_attrs = dict((key, re.escape(str(val)))
268
402
vars(self).iteritems())
270
command = self.check_command % escaped_attrs
404
command = self.checker_command % escaped_attrs
271
405
except TypeError, error:
272
logger.critical(u'Could not format string "%s":'
273
u' %s', self.check_command, error)
406
logger.error(u'Could not format string "%s":'
407
u' %s', self.checker_command, error)
274
408
return True # Try again later
409
self.current_checker_command = command
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:
411
logger.info(u"Starting checker %r for %s",
413
# We don't need to redirect stdout and stderr, since
414
# in normal mode, that is already done by daemon(),
415
# and in debug mode we don't want to. (Stdin is
416
# always replaced by /dev/null.)
417
self.checker = subprocess.Popen(command,
422
self.CheckerStarted(command)
423
self.PropertyChanged(
424
dbus.String("checker_running"),
425
dbus.Boolean(True, variant_level=1))
426
self.checker_callback_tag = (gobject.child_watch_add
428
self.checker_callback,
430
# The checker may have completed before the gobject
431
# watch was added. Check for this.
432
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
434
gobject.source_remove(self.checker_callback_tag)
435
self.checker_callback(pid, status, command)
436
except OSError, error:
286
437
logger.error(u"Failed to start subprocess: %s",
288
439
# Re-run this periodically if run by gobject.timeout_add
290
442
def stop_checker(self):
291
443
"""Force the checker process, if any, to stop."""
292
444
if self.checker_callback_tag:
293
445
gobject.source_remove(self.checker_callback_tag)
294
446
self.checker_callback_tag = None
295
if not hasattr(self, "checker") or self.checker is None:
447
if getattr(self, "checker", None) is None:
297
logger.debug("Stopping checker for %(name)s", vars(self))
449
logger.debug(u"Stopping checker for %(name)s", vars(self))
299
451
os.kill(self.checker.pid, signal.SIGTERM)
301
453
#if self.checker.poll() is None:
302
454
# os.kill(self.checker.pid, signal.SIGKILL)
303
455
except OSError, error:
304
if error.errno != errno.ESRCH:
456
if error.errno != errno.ESRCH: # No such process
306
458
self.checker = None
307
def still_valid(self, now=None):
460
self.PropertyChanged(dbus.String(u"checker_running"),
461
dbus.Boolean(False, variant_level=1))
463
def still_valid(self):
308
464
"""Has the timeout not yet passed for this client?"""
310
now = datetime.datetime.now()
311
if self.last_seen is None:
465
if not getattr(self, "enabled", False):
467
now = datetime.datetime.utcnow()
468
if self.last_checked_ok is None:
312
469
return now < (self.created + self.timeout)
314
return now < (self.last_seen + self.timeout)
471
return now < (self.last_checked_ok + self.timeout)
473
## D-Bus methods & signals
474
_interface = u"se.bsnet.fukt.Mandos.Client"
477
CheckedOK = dbus.service.method(_interface)(checked_ok)
478
CheckedOK.__name__ = "CheckedOK"
480
# CheckerCompleted - signal
481
@dbus.service.signal(_interface, signature="nxs")
482
def CheckerCompleted(self, exitcode, waitstatus, command):
486
# CheckerStarted - signal
487
@dbus.service.signal(_interface, signature="s")
488
def CheckerStarted(self, command):
492
# GetAllProperties - method
493
@dbus.service.method(_interface, out_signature="a{sv}")
494
def GetAllProperties(self):
496
return dbus.Dictionary({
498
dbus.String(self.name, variant_level=1),
499
dbus.String("fingerprint"):
500
dbus.String(self.fingerprint, variant_level=1),
502
dbus.String(self.host, variant_level=1),
503
dbus.String("created"):
504
_datetime_to_dbus(self.created, variant_level=1),
505
dbus.String("last_enabled"):
506
(_datetime_to_dbus(self.last_enabled,
508
if self.last_enabled is not None
509
else dbus.Boolean(False, variant_level=1)),
510
dbus.String("enabled"):
511
dbus.Boolean(self.enabled, variant_level=1),
512
dbus.String("last_checked_ok"):
513
(_datetime_to_dbus(self.last_checked_ok,
515
if self.last_checked_ok is not None
516
else dbus.Boolean (False, variant_level=1)),
517
dbus.String("timeout"):
518
dbus.UInt64(self.timeout_milliseconds(),
520
dbus.String("interval"):
521
dbus.UInt64(self.interval_milliseconds(),
523
dbus.String("checker"):
524
dbus.String(self.checker_command,
526
dbus.String("checker_running"):
527
dbus.Boolean(self.checker is not None,
529
dbus.String("object_path"):
530
dbus.ObjectPath(self.dbus_object_path,
534
# IsStillValid - method
535
IsStillValid = (dbus.service.method(_interface, out_signature="b")
537
IsStillValid.__name__ = "IsStillValid"
539
# PropertyChanged - signal
540
@dbus.service.signal(_interface, signature="sv")
541
def PropertyChanged(self, property, value):
545
# SetChecker - method
546
@dbus.service.method(_interface, in_signature="s")
547
def SetChecker(self, checker):
548
"D-Bus setter method"
549
self.checker_command = checker
551
self.PropertyChanged(dbus.String(u"checker"),
552
dbus.String(self.checker_command,
556
@dbus.service.method(_interface, in_signature="s")
557
def SetHost(self, host):
558
"D-Bus setter method"
561
self.PropertyChanged(dbus.String(u"host"),
562
dbus.String(self.host, variant_level=1))
564
# SetInterval - method
565
@dbus.service.method(_interface, in_signature="t")
566
def SetInterval(self, milliseconds):
567
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
569
self.PropertyChanged(dbus.String(u"interval"),
570
(dbus.UInt64(self.interval_milliseconds(),
574
@dbus.service.method(_interface, in_signature="ay",
576
def SetSecret(self, secret):
577
"D-Bus setter method"
578
self.secret = str(secret)
580
# SetTimeout - method
581
@dbus.service.method(_interface, in_signature="t")
582
def SetTimeout(self, milliseconds):
583
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
585
self.PropertyChanged(dbus.String(u"timeout"),
586
(dbus.UInt64(self.timeout_milliseconds(),
590
Enable = dbus.service.method(_interface)(enable)
591
Enable.__name__ = "Enable"
593
# StartChecker - method
594
@dbus.service.method(_interface)
595
def StartChecker(self):
600
@dbus.service.method(_interface)
605
# StopChecker - method
606
StopChecker = dbus.service.method(_interface)(stop_checker)
607
StopChecker.__name__ = "StopChecker"
317
612
def peer_certificate(session):
318
613
"Return the peer's OpenPGP certificate as a bytestring"
319
614
# If not an OpenPGP certificate...
320
if gnutls.library.functions.gnutls_certificate_type_get\
321
(session._c_object) \
322
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
615
if (gnutls.library.functions
616
.gnutls_certificate_type_get(session._c_object)
617
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
323
618
# ...do the normal thing
324
619
return session.peer_certificate
325
list_size = ctypes.c_uint()
326
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
327
(session._c_object, ctypes.byref(list_size))
620
list_size = ctypes.c_uint(1)
621
cert_list = (gnutls.library.functions
622
.gnutls_certificate_get_peers
623
(session._c_object, ctypes.byref(list_size)))
624
if not bool(cert_list) and list_size.value != 0:
625
raise gnutls.errors.GNUTLSError("error getting peer"
328
627
if list_size.value == 0:
330
629
cert = cert_list[0]
334
633
def fingerprint(openpgp):
335
634
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
635
# New GnuTLS "datum" with the OpenPGP public key
636
datum = (gnutls.library.types
637
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
640
ctypes.c_uint(len(openpgp))))
336
641
# New empty GnuTLS certificate
337
642
crt = gnutls.library.types.gnutls_openpgp_crt_t()
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)))
643
(gnutls.library.functions
644
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
345
645
# Import the OpenPGP public key into the certificate
346
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
349
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
646
(gnutls.library.functions
647
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
648
gnutls.library.constants
649
.GNUTLS_OPENPGP_FMT_RAW))
650
# Verify the self signature in the key
651
crtverify = ctypes.c_uint()
652
(gnutls.library.functions
653
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
654
if crtverify.value != 0:
655
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
656
raise gnutls.errors.CertificateSecurityError("Verify failed")
350
657
# New buffer for the fingerprint
351
buffer = ctypes.create_string_buffer(20)
352
buffer_length = ctypes.c_size_t()
658
buf = ctypes.create_string_buffer(20)
659
buf_len = ctypes.c_size_t()
353
660
# Get the fingerprint from the certificate into the buffer
354
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
355
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
661
(gnutls.library.functions
662
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
663
ctypes.byref(buf_len)))
356
664
# Deinit the certificate
357
665
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
358
666
# Convert the buffer to a Python bytestring
359
fpr = ctypes.string_at(buffer, buffer_length.value)
667
fpr = ctypes.string_at(buf, buf_len.value)
360
668
# Convert the bytestring to hexadecimal notation
361
669
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
365
class tcp_handler(SocketServer.BaseRequestHandler, object):
673
class TCP_handler(SocketServer.BaseRequestHandler, object):
366
674
"""A TCP request handler class.
367
675
Instantiated by IPv6_TCPServer for each request to handle it.
368
676
Note: This will run in its own forked process."""
370
678
def handle(self):
371
logger.debug(u"TCP connection from: %s",
372
unicode(self.client_address))
373
session = gnutls.connection.ClientSession(self.request,
679
logger.info(u"TCP connection from: %s",
680
unicode(self.client_address))
681
session = (gnutls.connection
682
.ClientSession(self.request,
686
line = self.request.makefile().readline()
687
logger.debug(u"Protocol version: %r", line)
689
if int(line.strip().split()[0]) > 1:
691
except (ValueError, IndexError, RuntimeError), error:
692
logger.error(u"Unknown protocol version: %s", error)
695
# Note: gnutls.connection.X509Credentials is really a generic
696
# GnuTLS certificate credentials object so long as no X.509
697
# keys are added to it. Therefore, we can use it here despite
698
# using OpenPGP certificates.
377
700
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
378
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
380
priority = "SECURE256"
382
gnutls.library.functions.gnutls_priority_set_direct\
383
(session._c_object, priority, None);
701
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
703
# Use a fallback default, since this MUST be set.
704
priority = self.server.settings.get("priority", "NORMAL")
705
(gnutls.library.functions
706
.gnutls_priority_set_direct(session._c_object,
386
710
session.handshake()
387
711
except gnutls.errors.GNUTLSError, error:
388
logger.debug(u"Handshake failed: %s", error)
712
logger.warning(u"Handshake failed: %s", error)
389
713
# Do not run session.bye() here: the session is not
390
714
# established. Just abandon the request.
716
logger.debug(u"Handshake succeeded")
393
718
fpr = fingerprint(peer_certificate(session))
394
719
except (TypeError, gnutls.errors.GNUTLSError), error:
395
logger.debug(u"Bad certificate: %s", error)
720
logger.warning(u"Bad certificate: %s", error)
398
723
logger.debug(u"Fingerprint: %s", fpr)
400
725
for c in self.server.clients:
401
726
if c.fingerprint == fpr:
730
logger.warning(u"Client not found for fingerprint: %s",
404
734
# Have to check if client.still_valid(), since it is possible
405
735
# that the client timed out while establishing the GnuTLS
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",
737
if not client.still_valid():
738
logger.warning(u"Client %(name)s is invalid",
742
## This won't work here, since we're in a fork.
743
# client.checked_ok()
417
745
while sent_size < len(client.secret):
418
746
sent = session.send(client.secret[sent_size:])
426
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
427
"""IPv6 TCP server. Accepts 'None' as address and/or port.
754
class IPv6_TCPServer(SocketServer.ForkingMixIn,
755
SocketServer.TCPServer, object):
756
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
429
options: Command line options
758
settings: Server settings
430
759
clients: Set() of Client objects
760
enabled: Boolean; whether this server is activated yet
432
762
address_family = socket.AF_INET6
433
763
def __init__(self, *args, **kwargs):
434
if "options" in kwargs:
435
self.options = kwargs["options"]
436
del kwargs["options"]
764
if "settings" in kwargs:
765
self.settings = kwargs["settings"]
766
del kwargs["settings"]
437
767
if "clients" in kwargs:
438
768
self.clients = kwargs["clients"]
439
769
del kwargs["clients"]
440
return super(type(self), self).__init__(*args, **kwargs)
770
if "use_ipv6" in kwargs:
771
if not kwargs["use_ipv6"]:
772
self.address_family = socket.AF_INET
773
del kwargs["use_ipv6"]
775
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
441
776
def server_bind(self):
442
777
"""This overrides the normal server_bind() function
443
778
to bind to an interface if one was specified, and also NOT to
444
779
bind to an address or port if they were not specified."""
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
780
if self.settings["interface"]:
781
# 25 is from /usr/include/asm-i486/socket.h
782
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
450
784
self.socket.setsockopt(socket.SOL_SOCKET,
451
socket.SO_BINDTODEVICE,
452
self.options.interface)
786
self.settings["interface"])
453
787
except socket.error, error:
454
788
if error[0] == errno.EPERM:
455
logger.warning(u"No permission to"
456
u" bind to interface %s",
457
self.options.interface)
789
logger.error(u"No permission to"
790
u" bind to interface %s",
791
self.settings["interface"])
460
794
# Only bind(2) the socket if we really need to.
461
795
if self.server_address[0] or self.server_address[1]:
462
796
if not self.server_address[0]:
464
self.server_address = (in6addr_any,
797
if self.address_family == socket.AF_INET6:
798
any_address = "::" # in6addr_any
800
any_address = socket.INADDR_ANY
801
self.server_address = (any_address,
465
802
self.server_address[1])
466
elif self.server_address[1] is None:
803
elif not self.server_address[1]:
467
804
self.server_address = (self.server_address[0],
469
return super(type(self), self).server_bind()
806
# if self.settings["interface"]:
807
# self.server_address = (self.server_address[0],
813
return super(IPv6_TCPServer, self).server_bind()
814
def server_activate(self):
816
return super(IPv6_TCPServer, self).server_activate()
472
821
def string_to_delta(interval):
473
822
"""Parse a string and return a datetime.timedelta
475
824
>>> string_to_delta('7d')
476
825
datetime.timedelta(7)
477
826
>>> string_to_delta('60s')
482
831
datetime.timedelta(1)
483
832
>>> string_to_delta(u'1w')
484
833
datetime.timedelta(7)
834
>>> string_to_delta('5m 30s')
835
datetime.timedelta(0, 330)
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)
837
timevalue = datetime.timedelta(0)
838
for s in interval.split():
840
suffix = unicode(s[-1])
843
delta = datetime.timedelta(value)
845
delta = datetime.timedelta(0, value)
847
delta = datetime.timedelta(0, 0, 0, 0, value)
849
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
851
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
854
except (ValueError, IndexError):
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:
539
860
def server_state_changed(state):
540
861
"""Derived from the Avahi example code"""
541
862
if state == avahi.SERVER_COLLISION:
542
logger.warning(u"Server name collision")
863
logger.error(u"Zeroconf server name collision")
544
865
elif state == avahi.SERVER_RUNNING:
548
869
def entry_group_state_changed(state, error):
549
870
"""Derived from the Avahi example code"""
550
global serviceName, server, rename_count
552
logger.debug(u"state change: %i", state)
871
logger.debug(u"Avahi state change: %i", state)
554
873
if state == avahi.ENTRY_GROUP_ESTABLISHED:
555
logger.debug(u"Service established.")
874
logger.debug(u"Zeroconf service established.")
556
875
elif state == avahi.ENTRY_GROUP_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)
876
logger.warning(u"Zeroconf service name collision.")
570
878
elif state == avahi.ENTRY_GROUP_FAILURE:
571
logger.error(u"Error in group state changed %s",
879
logger.critical(u"Avahi: Error in group state changed %s",
881
raise AvahiGroupError(u"State changed: %s" % unicode(error))
576
883
def if_nametoindex(interface):
577
"""Call the C function if_nametoindex()"""
884
"""Call the C function if_nametoindex(), or equivalent"""
885
global if_nametoindex
579
libc = ctypes.cdll.LoadLibrary("libc.so.6")
580
return libc.if_nametoindex(interface)
887
if_nametoindex = (ctypes.cdll.LoadLibrary
888
(ctypes.util.find_library("c"))
581
890
except (OSError, AttributeError):
582
891
if "struct" not in sys.modules:
584
893
if "fcntl" not in sys.modules:
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):
895
def if_nametoindex(interface):
896
"Get an interface index the hard way, i.e. using fcntl()"
897
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
898
with closing(socket.socket()) as s:
899
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
900
struct.pack("16s16x", interface))
901
interface_index = struct.unpack("I", ifreq[16:20])[0]
902
return interface_index
903
return if_nametoindex(interface)
906
def daemon(nochdir = False, noclose = False):
596
907
"""See daemon(3). Standard BSD Unix function.
597
908
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:
628
global main_loop_started
629
main_loop_started = False
631
parser = OptionParser()
930
parser = optparse.OptionParser(version = "%%prog %s" % version)
632
931
parser.add_option("-i", "--interface", type="string",
633
default=None, metavar="IF",
634
help="Bind to interface IF")
635
parser.add_option("-a", "--address", type="string", default=None,
932
metavar="IF", help="Bind to interface IF")
933
parser.add_option("-a", "--address", type="string",
636
934
help="Address to listen for requests on")
637
parser.add_option("-p", "--port", type="int", default=None,
935
parser.add_option("-p", "--port", type="int",
638
936
help="Port number to receive requests on")
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,
937
parser.add_option("--check", action="store_true",
646
938
help="Run self-test")
647
parser.add_option("--debug", action="store_true", default=False,
649
(options, args) = parser.parse_args()
939
parser.add_option("--debug", action="store_true",
940
help="Debug mode; run in foreground and log to"
942
parser.add_option("--priority", type="string", help="GnuTLS"
943
" priority string (see GnuTLS documentation)")
944
parser.add_option("--servicename", type="string", metavar="NAME",
945
help="Zeroconf service name")
946
parser.add_option("--configdir", type="string",
947
default="/etc/mandos", metavar="DIR",
948
help="Directory to search for configuration"
950
parser.add_option("--no-dbus", action="store_false",
952
help="Do not provide D-Bus system bus"
954
parser.add_option("--no-ipv6", action="store_false",
955
dest="use_ipv6", help="Do not use IPv6")
956
options = parser.parse_args()[0]
651
958
if options.check:
653
960
doctest.testmod()
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")
963
# Default values for config file for server-global settings
964
server_defaults = { "interface": "",
969
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
970
"servicename": "Mandos",
975
# Parse config file for server-global settings
976
server_config = ConfigParser.SafeConfigParser(server_defaults)
978
server_config.read(os.path.join(options.configdir, "mandos.conf"))
979
# Convert the SafeConfigParser object to a dict
980
server_settings = server_config.defaults()
981
# Use the appropriate methods on the non-string config options
982
server_settings["debug"] = server_config.getboolean("DEFAULT",
984
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
986
server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
988
if server_settings["port"]:
989
server_settings["port"] = server_config.getint("DEFAULT",
993
# Override the settings from the config file with command line
995
for option in ("interface", "address", "port", "debug",
996
"priority", "servicename", "configdir",
997
"use_dbus", "use_ipv6"):
998
value = getattr(options, option)
999
if value is not None:
1000
server_settings[option] = value
1002
# Now we have our good server settings in "server_settings"
1005
debug = server_settings["debug"]
1006
use_dbus = server_settings["use_dbus"]
1007
use_ipv6 = server_settings["use_ipv6"]
1010
syslogger.setLevel(logging.WARNING)
1011
console.setLevel(logging.WARNING)
1013
if server_settings["servicename"] != "Mandos":
1014
syslogger.setFormatter(logging.Formatter
1015
('Mandos (%s): %%(levelname)s:'
1017
% server_settings["servicename"]))
1019
# Parse config file with clients
1020
client_defaults = { "timeout": "1h",
1022
"checker": "fping -q -- %%(host)s",
1025
client_config = ConfigParser.SafeConfigParser(client_defaults)
1026
client_config.read(os.path.join(server_settings["configdir"],
1030
tcp_server = IPv6_TCPServer((server_settings["address"],
1031
server_settings["port"]),
1033
settings=server_settings,
1034
clients=clients, use_ipv6=use_ipv6)
1035
pidfilename = "/var/run/mandos.pid"
1037
pidfile = open(pidfilename, "w")
1039
logger.error("Could not open file %r", pidfilename)
1042
uid = pwd.getpwnam("_mandos").pw_uid
1043
gid = pwd.getpwnam("_mandos").pw_gid
1046
uid = pwd.getpwnam("mandos").pw_uid
1047
gid = pwd.getpwnam("mandos").pw_gid
1050
uid = pwd.getpwnam("nobody").pw_uid
1051
gid = pwd.getpwnam("nogroup").pw_gid
1058
except OSError, error:
1059
if error[0] != errno.EPERM:
1062
# Enable all possible GnuTLS debugging
1064
# "Use a log level over 10 to enable all debugging options."
1066
gnutls.library.functions.gnutls_global_set_log_level(11)
1068
@gnutls.library.types.gnutls_log_func
1069
def debug_gnutls(level, string):
1070
logger.debug("GnuTLS: %s", string[:-1])
1072
(gnutls.library.functions
1073
.gnutls_global_set_log_function(debug_gnutls))
1076
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1077
service = AvahiService(name = server_settings["servicename"],
1078
servicetype = "_mandos._tcp",
1079
protocol = protocol)
1080
if server_settings["interface"]:
1081
service.interface = (if_nametoindex
1082
(server_settings["interface"]))
672
1084
global main_loop
720
1141
client = clients.pop()
721
client.stop_hook = None
1142
client.disable_hook = None
724
1145
atexit.register(cleanup)
727
1148
signal.signal(signal.SIGINT, signal.SIG_IGN)
728
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
1149
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1150
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1153
class MandosServer(dbus.service.Object):
1154
"""A D-Bus proxy object"""
1156
dbus.service.Object.__init__(self, bus, "/")
1157
_interface = u"se.bsnet.fukt.Mandos"
1159
@dbus.service.signal(_interface, signature="oa{sv}")
1160
def ClientAdded(self, objpath, properties):
1164
@dbus.service.signal(_interface, signature="os")
1165
def ClientRemoved(self, objpath, name):
1169
@dbus.service.method(_interface, out_signature="ao")
1170
def GetAllClients(self):
1172
return dbus.Array(c.dbus_object_path for c in clients)
1174
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1175
def GetAllClientsWithProperties(self):
1177
return dbus.Dictionary(
1178
((c.dbus_object_path, c.GetAllProperties())
1182
@dbus.service.method(_interface, in_signature="o")
1183
def RemoveClient(self, object_path):
1186
if c.dbus_object_path == object_path:
1188
# Don't signal anything except ClientRemoved
1192
self.ClientRemoved(object_path, c.name)
1198
mandos_server = MandosServer()
731
1200
for client in clients:
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
1203
mandos_server.ClientAdded(client.dbus_object_path,
1204
client.GetAllProperties())
1208
tcp_server.server_activate()
1210
# Find out what port we got
1211
service.port = tcp_server.socket.getsockname()[1]
1213
logger.info(u"Now listening on address %r, port %d,"
1214
" flowinfo %d, scope_id %d"
1215
% tcp_server.socket.getsockname())
1217
logger.info(u"Now listening on address %r, port %d"
1218
% tcp_server.socket.getsockname())
1220
#service.interface = tcp_server.socket.getsockname()[3]
1223
# From the Avahi example code
1224
server.connect_to_signal("StateChanged", server_state_changed)
1226
server_state_changed(server.GetState())
1227
except dbus.exceptions.DBusException, error:
1228
logger.critical(u"DBusException: %s", error)
1230
# End of Avahi example code
1232
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1233
lambda *args, **kwargs:
1234
(tcp_server.handle_request
1235
(*args[2:], **kwargs) or True))
1237
logger.debug(u"Starting main loop")
1239
except AvahiError, error:
1240
logger.critical(u"AvahiError: %s", error)
764
1242
except KeyboardInterrupt:
1245
logger.debug("Server received KeyboardInterrupt")
1246
logger.debug("Server exiting")
770
1248
if __name__ == '__main__':