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
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, stop_hook=None, fingerprint=None,
156
secret=None, secfile=None, fqdn=None, timeout=None,
157
interval=-1, checker=None):
158
"""Note: the 'checker' argument sets the 'checker_command'
159
attribute and not the 'checker' attribute.."""
231
161
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()
162
# Uppercase and remove spaces from fingerprint
163
# for later comparison purposes with return value of
164
# the fingerprint() function
165
self.fingerprint = fingerprint.upper().replace(u" ", u"")
238
166
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()
168
self.secret = secret.decode(u"base64")
171
self.secret = sf.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
174
raise RuntimeError(u"No secret or secfile for client %s"
176
self.fqdn = fqdn # string
177
self.created = datetime.datetime.now()
178
self.last_seen = None
179
self.timeout = string_to_delta(timeout)
180
self.interval = string_to_delta(interval)
181
self.stop_hook = stop_hook
257
182
self.checker = None
258
183
self.checker_initiator_tag = None
259
self.disable_initiator_tag = None
184
self.stop_initiator_tag = None
260
185
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)
186
self.check_command = checker
274
188
"""Start this client's checker and timeout hooks"""
275
self.last_enabled = datetime.datetime.utcnow()
276
189
# Schedule a new checker to be started an 'interval' from now,
277
190
# and every interval from then on.
278
self.checker_initiator_tag = (gobject.timeout_add
279
(self.interval_milliseconds(),
191
self.checker_initiator_tag = gobject.timeout_add\
192
(self._interval_milliseconds,
281
194
# Also start a new checker *right now*.
282
195
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):
196
# Schedule a stop() when 'timeout' has passed
197
self.stop_initiator_tag = gobject.timeout_add\
198
(self._timeout_milliseconds,
202
The possibility that this client might be restarted is left
203
open, but not currently used."""
204
# If this client doesn't have a secret, it is already stopped.
206
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):
210
if hasattr(self, "stop_initiator_tag") \
211
and self.stop_initiator_tag:
212
gobject.source_remove(self.stop_initiator_tag)
213
self.stop_initiator_tag = None
214
if hasattr(self, "checker_initiator_tag") \
215
and self.checker_initiator_tag:
305
216
gobject.source_remove(self.checker_initiator_tag)
306
217
self.checker_initiator_tag = None
307
218
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
221
# Do not run this again if called by a gobject.timeout_add
318
223
def __del__(self):
319
self.disable_hook = None
322
def checker_callback(self, pid, condition, command):
224
self.stop_hook = None
226
def checker_callback(self, pid, condition):
323
227
"""The checker has completed, so take appropriate actions."""
228
now = datetime.datetime.now()
324
229
self.checker_callback_tag = None
325
230
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))
231
if os.WIFEXITED(condition) \
232
and (os.WEXITSTATUS(condition) == 0):
233
logger.debug(u"Checker for %(name)s succeeded",
236
gobject.source_remove(self.stop_initiator_tag)
237
self.stop_initiator_tag = gobject.timeout_add\
238
(self._timeout_milliseconds,
240
elif not os.WIFEXITED(condition):
345
241
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,
244
logger.debug(u"Checker for %(name)s failed",
370
246
def start_checker(self):
371
247
"""Start a new checker subprocess if one is not running.
372
248
If a checker already exists, leave it running and do
381
257
# is as it should be.
382
258
if self.checker is None:
384
# In case checker_command has exactly one % operator
385
command = self.checker_command % self.host
260
command = self.check_command % self.fqdn
386
261
except TypeError:
387
# Escape attributes for the shell
388
262
escaped_attrs = dict((key, re.escape(str(val)))
390
264
vars(self).iteritems())
392
command = self.checker_command % escaped_attrs
266
command = self.check_command % escaped_attrs
393
267
except TypeError, error:
394
logger.error(u'Could not format string "%s":'
395
u' %s', self.checker_command, error)
268
logger.critical(u'Could not format string "%s":'
269
u' %s', self.check_command, error)
396
270
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:
272
logger.debug(u"Starting checker %r for %s",
274
self.checker = subprocess.\
276
close_fds=True, shell=True,
278
self.checker_callback_tag = gobject.child_watch_add\
280
self.checker_callback)
281
except subprocess.OSError, error:
424
282
logger.error(u"Failed to start subprocess: %s",
426
284
# Re-run this periodically if run by gobject.timeout_add
429
286
def stop_checker(self):
430
287
"""Force the checker process, if any, to stop."""
431
288
if self.checker_callback_tag:
432
289
gobject.source_remove(self.checker_callback_tag)
433
290
self.checker_callback_tag = None
434
if getattr(self, "checker", None) is None:
291
if not hasattr(self, "checker") or self.checker is None:
436
logger.debug(u"Stopping checker for %(name)s", vars(self))
293
logger.debug("Stopping checker for %(name)s", vars(self))
438
295
os.kill(self.checker.pid, signal.SIGTERM)
440
297
#if self.checker.poll() is None:
441
298
# os.kill(self.checker.pid, signal.SIGKILL)
442
299
except OSError, error:
443
if error.errno != errno.ESRCH: # No such process
300
if error.errno != errno.ESRCH:
445
302
self.checker = None
447
self.PropertyChanged(dbus.String(u"checker_running"),
448
dbus.Boolean(False, variant_level=1))
450
def still_valid(self):
303
def still_valid(self, now=None):
451
304
"""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:
306
now = datetime.datetime.now()
307
if self.last_seen is None:
456
308
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"
310
return now < (self.last_seen + self.timeout)
599
313
def peer_certificate(session):
600
314
"Return the peer's OpenPGP certificate as a bytestring"
601
315
# 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):
316
if gnutls.library.functions.gnutls_certificate_type_get\
317
(session._c_object) \
318
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
605
319
# ...do the normal thing
606
320
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"
321
list_size = ctypes.c_uint()
322
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
323
(session._c_object, ctypes.byref(list_size))
614
324
if list_size.value == 0:
616
326
cert = cert_list[0]
620
330
def fingerprint(openpgp):
621
331
"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
332
# New empty GnuTLS certificate
629
333
crt = gnutls.library.types.gnutls_openpgp_crt_t()
630
(gnutls.library.functions
631
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
334
gnutls.library.functions.gnutls_openpgp_crt_init\
336
# New GnuTLS "datum" with the OpenPGP public key
337
datum = gnutls.library.types.gnutls_datum_t\
338
(ctypes.cast(ctypes.c_char_p(openpgp),
339
ctypes.POINTER(ctypes.c_ubyte)),
340
ctypes.c_uint(len(openpgp)))
632
341
# 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")
342
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
345
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
644
346
# New buffer for the fingerprint
645
buf = ctypes.create_string_buffer(20)
646
buf_len = ctypes.c_size_t()
347
buffer = ctypes.create_string_buffer(20)
348
buffer_length = ctypes.c_size_t()
647
349
# 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)))
350
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
351
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
651
352
# Deinit the certificate
652
353
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
653
354
# Convert the buffer to a Python bytestring
654
fpr = ctypes.string_at(buf, buf_len.value)
355
fpr = ctypes.string_at(buffer, buffer_length.value)
655
356
# Convert the bytestring to hexadecimal notation
656
357
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
660
class TCP_handler(SocketServer.BaseRequestHandler, object):
361
class tcp_handler(SocketServer.BaseRequestHandler, object):
661
362
"""A TCP request handler class.
662
363
Instantiated by IPv6_TCPServer for each request to handle it.
663
364
Note: This will run in its own forked process."""
665
366
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.
367
logger.debug(u"TCP connection from: %s",
368
unicode(self.client_address))
369
session = gnutls.connection.ClientSession(self.request,
687
373
#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,
374
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
377
if self.server.options.priority:
378
priority = self.server.options.priority
379
gnutls.library.functions.gnutls_priority_set_direct\
380
(session._c_object, priority, None);
697
383
session.handshake()
698
384
except gnutls.errors.GNUTLSError, error:
699
logger.warning(u"Handshake failed: %s", error)
385
logger.debug(u"Handshake failed: %s", error)
700
386
# Do not run session.bye() here: the session is not
701
387
# established. Just abandon the request.
703
logger.debug(u"Handshake succeeded")
705
390
fpr = fingerprint(peer_certificate(session))
706
391
except (TypeError, gnutls.errors.GNUTLSError), error:
707
logger.warning(u"Bad certificate: %s", error)
392
logger.debug(u"Bad certificate: %s", error)
710
395
logger.debug(u"Fingerprint: %s", fpr)
712
397
for c in self.server.clients:
713
398
if c.fingerprint == fpr:
717
logger.warning(u"Client not found for fingerprint: %s",
721
401
# Have to check if client.still_valid(), since it is possible
722
402
# that the client timed out while establishing the GnuTLS
724
if not client.still_valid():
725
logger.warning(u"Client %(name)s is invalid",
404
if (not client) or (not client.still_valid()):
406
logger.debug(u"Client %(name)s is invalid",
409
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
414
while sent_size < len(client.secret):
733
415
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.
423
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
424
"""IPv6 TCP server. Accepts 'None' as address and/or port.
745
settings: Server settings
426
options: Command line options
746
427
clients: Set() of Client objects
747
enabled: Boolean; whether this server is activated yet
749
429
address_family = socket.AF_INET6
750
430
def __init__(self, *args, **kwargs):
751
if "settings" in kwargs:
752
self.settings = kwargs["settings"]
753
del kwargs["settings"]
431
if "options" in kwargs:
432
self.options = kwargs["options"]
433
del kwargs["options"]
754
434
if "clients" in kwargs:
755
435
self.clients = kwargs["clients"]
756
436
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)
437
return super(type(self), self).__init__(*args, **kwargs)
763
438
def server_bind(self):
764
439
"""This overrides the normal server_bind() function
765
440
to bind to an interface if one was specified, and also NOT to
766
441
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)
442
if self.options.interface:
443
if not hasattr(socket, "SO_BINDTODEVICE"):
444
# From /usr/include/asm-i486/socket.h
445
socket.SO_BINDTODEVICE = 25
771
447
self.socket.setsockopt(socket.SOL_SOCKET,
773
self.settings["interface"])
448
socket.SO_BINDTODEVICE,
449
self.options.interface)
774
450
except socket.error, error:
775
451
if error[0] == errno.EPERM:
776
logger.error(u"No permission to"
777
u" bind to interface %s",
778
self.settings["interface"])
452
logger.warning(u"No permission to"
453
u" bind to interface %s",
454
self.options.interface)
781
457
# Only bind(2) the socket if we really need to.
782
458
if self.server_address[0] or self.server_address[1]:
783
459
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,
461
self.server_address = (in6addr_any,
789
462
self.server_address[1])
790
elif not self.server_address[1]:
463
elif self.server_address[1] is None:
791
464
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()
466
return super(type(self), self).server_bind()
808
469
def string_to_delta(interval):
809
470
"""Parse a string and return a datetime.timedelta
811
472
>>> string_to_delta('7d')
812
473
datetime.timedelta(7)
813
474
>>> string_to_delta('60s')
818
479
datetime.timedelta(1)
819
480
>>> string_to_delta(u'1w')
820
481
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):
484
suffix=unicode(interval[-1])
485
value=int(interval[:-1])
487
delta = datetime.timedelta(value)
489
delta = datetime.timedelta(0, value)
491
delta = datetime.timedelta(0, 0, 0, 0, value)
493
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
495
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
498
except (ValueError, IndexError):
504
"""Derived from the Avahi example code"""
505
global group, serviceName, serviceType, servicePort, serviceTXT, \
508
group = dbus.Interface(
509
bus.get_object( avahi.DBUS_NAME,
510
server.EntryGroupNew()),
511
avahi.DBUS_INTERFACE_ENTRY_GROUP)
512
group.connect_to_signal('StateChanged',
513
entry_group_state_changed)
514
logger.debug(u"Adding service '%s' of type '%s' ...",
515
serviceName, serviceType)
518
serviceInterface, # interface
519
avahi.PROTO_INET6, # protocol
520
dbus.UInt32(0), # flags
521
serviceName, serviceType,
523
dbus.UInt16(servicePort),
524
avahi.string_array_to_txt_array(serviceTXT))
528
def remove_service():
529
"""From the Avahi example code"""
532
if not group is None:
847
536
def server_state_changed(state):
848
537
"""Derived from the Avahi example code"""
849
538
if state == avahi.SERVER_COLLISION:
850
logger.error(u"Zeroconf server name collision")
539
logger.warning(u"Server name collision")
852
541
elif state == avahi.SERVER_RUNNING:
856
545
def entry_group_state_changed(state, error):
857
546
"""Derived from the Avahi example code"""
858
logger.debug(u"Avahi state change: %i", state)
547
global serviceName, server, rename_count
549
logger.debug(u"state change: %i", state)
860
551
if state == avahi.ENTRY_GROUP_ESTABLISHED:
861
logger.debug(u"Zeroconf service established.")
552
logger.debug(u"Service established.")
862
553
elif state == avahi.ENTRY_GROUP_COLLISION:
863
logger.warning(u"Zeroconf service name collision.")
555
rename_count = rename_count - 1
557
name = server.GetAlternativeServiceName(name)
558
logger.warning(u"Service name collision, "
559
u"changing name to '%s' ...", name)
564
logger.error(u"No suitable service name found after %i"
565
u" retries, exiting.", n_rename)
865
567
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))
568
logger.error(u"Error in group state changed %s",
870
573
def if_nametoindex(interface):
871
"""Call the C function if_nametoindex(), or equivalent"""
872
global if_nametoindex
574
"""Call the C function if_nametoindex()"""
874
if_nametoindex = (ctypes.cdll.LoadLibrary
875
(ctypes.util.find_library("c"))
576
libc = ctypes.cdll.LoadLibrary("libc.so.6")
577
return libc.if_nametoindex(interface)
877
578
except (OSError, AttributeError):
878
579
if "struct" not in sys.modules:
880
581
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):
583
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
585
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
586
struct.pack("16s16x", interface))
588
interface_index = struct.unpack("I", ifreq[16:20])[0]
589
return interface_index
592
def daemon(nochdir, noclose):
894
593
"""See daemon(3). Standard BSD Unix function.
895
594
This should really exist as os.daemon, but it doesn't (yet)."""
613
def killme(status = 0):
614
logger.debug("Stopping server with exit status %d", status)
616
if main_loop_started:
917
parser = optparse.OptionParser(version = "%%prog %s" % version)
625
global main_loop_started
626
main_loop_started = False
628
parser = OptionParser()
918
629
parser.add_option("-i", "--interface", type="string",
919
metavar="IF", help="Bind to interface IF")
920
parser.add_option("-a", "--address", type="string",
630
default=None, metavar="IF",
631
help="Bind to interface IF")
632
parser.add_option("-a", "--address", type="string", default=None,
921
633
help="Address to listen for requests on")
922
parser.add_option("-p", "--port", type="int",
634
parser.add_option("-p", "--port", type="int", default=None,
923
635
help="Port number to receive requests on")
924
parser.add_option("--check", action="store_true",
636
parser.add_option("--check", action="store_true", default=False,
925
637
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]
638
parser.add_option("--debug", action="store_true", default=False,
640
parser.add_option("--priority", type="string",
642
help="GnuTLS priority string"
643
" (see GnuTLS documentation)")
644
parser.add_option("--servicename", type="string",
645
default="Mandos", help="Zeroconf service name")
646
(options, args) = parser.parse_args()
945
648
if options.check:
947
650
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"]))
654
defaults = { "timeout": "1h",
656
"checker": "fping -q -- %%(fqdn)s",
658
client_config = ConfigParser.SafeConfigParser(defaults)
659
#client_config.readfp(open("global.conf"), "global.conf")
660
client_config.read("mandos-clients.conf")
663
serviceName = options.servicename;
1071
665
global main_loop
1128
713
client = clients.pop()
1129
client.disable_hook = None
714
client.stop_hook = None
1132
717
atexit.register(cleanup)
1135
720
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()
721
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
722
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
1187
724
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")
727
tcp_server = IPv6_TCPServer((options.address, options.port),
731
# Find out what random port we got
733
servicePort = tcp_server.socket.getsockname()[1]
734
logger.debug(u"Now listening on port %d", servicePort)
736
if options.interface is not None:
737
global serviceInterface
738
serviceInterface = if_nametoindex(options.interface)
740
# From the Avahi example code
741
server.connect_to_signal("StateChanged", server_state_changed)
743
server_state_changed(server.GetState())
744
except dbus.exceptions.DBusException, error:
745
logger.critical(u"DBusException: %s", error)
747
# End of Avahi example code
749
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
750
lambda *args, **kwargs:
751
tcp_server.handle_request(*args[2:],
754
logger.debug("Starting main loop")
755
main_loop_started = True
1226
except AvahiError, error:
1227
logger.critical(u"AvahiError: %s", error)
1229
757
except KeyboardInterrupt:
1232
logger.debug("Server received KeyboardInterrupt")
1233
logger.debug("Server exiting")
1235
763
if __name__ == '__main__':