2
# -*- mode: python; coding: utf-8 -*-
4
# Mandos server - give out binary blobs to connecting clients.
6
# This program is partly derived from an example program for an Avahi
7
# service publisher, downloaded from
8
# <http://avahi.org/wiki/PythonPublishExample>. This includes the
9
# methods "add" and "remove" in the "AvahiService" class, the
10
# "server_state_changed" and "entry_group_state_changed" functions,
11
# and some lines in "main".
14
# Copyright © 2008,2009 Teddy Hogeborn
15
# Copyright © 2008,2009 Björn Påhlsson
17
# This program is free software: you can redistribute it and/or modify
18
# it under the terms of the GNU General Public License as published by
19
# the Free Software Foundation, either version 3 of the License, or
20
# (at your option) any later version.
22
# This program is distributed in the hope that it will be useful,
23
# but WITHOUT ANY WARRANTY; without even the implied warranty of
24
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
# GNU General Public License for more details.
27
# You should have received a copy of the GNU General Public License
28
# along with this program. If not, see
29
# <http://www.gnu.org/licenses/>.
31
# Contact the authors at <mandos@fukt.bsnet.se>.
34
from __future__ import division, with_statement, absolute_import
6
from optparse import OptionParser
41
9
import gnutls.crypto
42
10
import gnutls.connection
43
11
import gnutls.errors
44
import gnutls.library.functions
45
import gnutls.library.constants
46
import gnutls.library.types
47
12
import ConfigParser
57
import logging.handlers
59
from contextlib import closing
65
from dbus.mainloop.glib import DBusGMainLoop
71
logger = logging.Logger('mandos')
72
syslogger = (logging.handlers.SysLogHandler
73
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
address = "/dev/log"))
75
syslogger.setFormatter(logging.Formatter
76
('Mandos [%(process)d]: %(levelname)s:'
78
logger.addHandler(syslogger)
80
console = logging.StreamHandler()
81
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
82
' %(levelname)s: %(message)s'))
83
logger.addHandler(console)
85
class AvahiError(Exception):
86
def __init__(self, value, *args, **kwargs):
88
super(AvahiError, self).__init__(value, *args, **kwargs)
89
def __unicode__(self):
90
return unicode(repr(self.value))
92
class AvahiServiceError(AvahiError):
95
class AvahiGroupError(AvahiError):
99
class AvahiService(object):
100
"""An Avahi (Zeroconf) service.
102
interface: integer; avahi.IF_UNSPEC or an interface index.
103
Used to optionally bind to the specified interface.
104
name: string; Example: 'Mandos'
105
type: string; Example: '_mandos._tcp'.
106
See <http://www.dns-sd.org/ServiceTypes.html>
107
port: integer; what port to announce
108
TXT: list of strings; TXT record for the service
109
domain: string; Domain to publish on, default to .local if empty.
110
host: string; Host to publish records for, default is localhost
111
max_renames: integer; maximum number of renames
112
rename_count: integer; counter so we only rename after collisions
113
a sensible number of times
115
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
116
servicetype = None, port = None, TXT = None,
117
domain = "", host = "", max_renames = 32768,
118
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) [%%(process)d]:'
141
' %%(levelname)s: %%(message)s'
145
self.rename_count += 1
147
"""Derived from the Avahi example code"""
148
if group is not None:
151
"""Derived from the Avahi example code"""
154
group = dbus.Interface(bus.get_object
156
server.EntryGroupNew()),
157
avahi.DBUS_INTERFACE_ENTRY_GROUP)
158
group.connect_to_signal('StateChanged',
159
entry_group_state_changed)
160
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
161
service.name, service.type)
163
self.interface, # interface
164
self.protocol, # protocol
165
dbus.UInt32(0), # flags
166
self.name, self.type,
167
self.domain, self.host,
168
dbus.UInt16(self.port),
169
avahi.string_array_to_txt_array(self.TXT))
172
# From the Avahi example code:
173
group = None # our entry group
174
# End of Avahi example code
177
def _datetime_to_dbus(dt, variant_level=0):
178
"""Convert a UTC datetime.datetime() to a D-Bus type."""
179
return dbus.String(dt.isoformat(), variant_level=variant_level)
182
class Client(dbus.service.Object):
183
"""A representation of a client host served by this server.
185
name: string; from the config file, used in log messages and
187
fingerprint: string (40 or 32 hexadecimal digits); used to
188
uniquely identify the client
189
secret: bytestring; sent verbatim (over TLS) to client
190
host: string; available for use by the checker command
191
created: datetime.datetime(); (UTC) object creation
192
last_enabled: datetime.datetime(); (UTC)
194
last_checked_ok: datetime.datetime(); (UTC) or None
195
timeout: datetime.timedelta(); How long from last_checked_ok
196
until this client is invalid
197
interval: datetime.timedelta(); How often to start a new checker
198
disable_hook: If set, called by disable() as disable_hook(self)
199
checker: subprocess.Popen(); a running checker process used
200
to see if the client lives.
201
'None' if no process is running.
202
checker_initiator_tag: a gobject event source tag, or None
203
disable_initiator_tag: - '' -
204
checker_callback_tag: - '' -
205
checker_command: string; External command which is run to check if
206
client lives. %() expansions are done at
207
runtime with vars(self) as dict, so that for
208
instance %(name)s can be used in the command.
209
current_checker_command: string; current running checker_command
210
use_dbus: bool(); Whether to provide D-Bus interface and signals
211
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
213
def timeout_milliseconds(self):
214
"Return the 'timeout' attribute in milliseconds"
215
return ((self.timeout.days * 24 * 60 * 60 * 1000)
216
+ (self.timeout.seconds * 1000)
217
+ (self.timeout.microseconds // 1000))
219
def interval_milliseconds(self):
220
"Return the 'interval' attribute in milliseconds"
221
return ((self.interval.days * 24 * 60 * 60 * 1000)
222
+ (self.interval.seconds * 1000)
223
+ (self.interval.microseconds // 1000))
225
def __init__(self, name = None, disable_hook=None, config=None,
227
"""Note: the 'checker' key in 'config' sets the
228
'checker_command' attribute and *not* the 'checker'
233
logger.debug(u"Creating client %r", self.name)
234
self.use_dbus = False # During __init__
235
# Uppercase and remove spaces from fingerprint for later
236
# comparison purposes with return value from the fingerprint()
238
self.fingerprint = (config["fingerprint"].upper()
240
logger.debug(u" Fingerprint: %s", self.fingerprint)
241
if "secret" in config:
242
self.secret = config["secret"].decode(u"base64")
243
elif "secfile" in config:
244
with closing(open(os.path.expanduser
246
(config["secfile"])))) as secfile:
247
self.secret = secfile.read()
249
raise TypeError(u"No secret or secfile for client %s"
251
self.host = config.get("host", "")
252
self.created = datetime.datetime.utcnow()
254
self.last_enabled = None
255
self.last_checked_ok = None
256
self.timeout = string_to_delta(config["timeout"])
257
self.interval = string_to_delta(config["interval"])
258
self.disable_hook = disable_hook
260
self.checker_initiator_tag = None
261
self.disable_initiator_tag = None
262
self.checker_callback_tag = None
263
self.checker_command = config["checker"]
264
self.current_checker_command = None
265
self.last_connect = None
266
# Only now, when this client is initialized, can it show up on
268
self.use_dbus = use_dbus
270
self.dbus_object_path = (dbus.ObjectPath
272
+ self.name.replace(".", "_")))
273
dbus.service.Object.__init__(self, bus,
274
self.dbus_object_path)
277
"""Start this client's checker and timeout hooks"""
278
self.last_enabled = datetime.datetime.utcnow()
279
# Schedule a new checker to be started an 'interval' from now,
280
# and every interval from then on.
281
self.checker_initiator_tag = (gobject.timeout_add
282
(self.interval_milliseconds(),
284
# Also start a new checker *right now*.
286
# Schedule a disable() when 'timeout' has passed
287
self.disable_initiator_tag = (gobject.timeout_add
288
(self.timeout_milliseconds(),
293
self.PropertyChanged(dbus.String(u"enabled"),
294
dbus.Boolean(True, variant_level=1))
295
self.PropertyChanged(dbus.String(u"last_enabled"),
296
(_datetime_to_dbus(self.last_enabled,
300
"""Disable this client."""
301
if not getattr(self, "enabled", False):
303
logger.info(u"Disabling client %s", self.name)
304
if getattr(self, "disable_initiator_tag", False):
305
gobject.source_remove(self.disable_initiator_tag)
306
self.disable_initiator_tag = None
307
if getattr(self, "checker_initiator_tag", False):
308
gobject.source_remove(self.checker_initiator_tag)
309
self.checker_initiator_tag = None
311
if self.disable_hook:
312
self.disable_hook(self)
316
self.PropertyChanged(dbus.String(u"enabled"),
317
dbus.Boolean(False, variant_level=1))
318
# Do not run this again if called by a gobject.timeout_add
322
self.disable_hook = None
325
def checker_callback(self, pid, condition, command):
326
"""The checker has completed, so take appropriate actions."""
327
self.checker_callback_tag = None
331
self.PropertyChanged(dbus.String(u"checker_running"),
332
dbus.Boolean(False, variant_level=1))
333
if os.WIFEXITED(condition):
334
exitstatus = os.WEXITSTATUS(condition)
336
logger.info(u"Checker for %(name)s succeeded",
340
logger.info(u"Checker for %(name)s failed",
344
self.CheckerCompleted(dbus.Int16(exitstatus),
345
dbus.Int64(condition),
346
dbus.String(command))
348
logger.warning(u"Checker for %(name)s crashed?",
352
self.CheckerCompleted(dbus.Int16(-1),
353
dbus.Int64(condition),
354
dbus.String(command))
356
def checked_ok(self):
357
"""Bump up the timeout for this client.
358
This should only be called when the client has been seen,
361
self.last_checked_ok = datetime.datetime.utcnow()
362
gobject.source_remove(self.disable_initiator_tag)
363
self.disable_initiator_tag = (gobject.timeout_add
364
(self.timeout_milliseconds(),
368
self.PropertyChanged(
369
dbus.String(u"last_checked_ok"),
370
(_datetime_to_dbus(self.last_checked_ok,
373
def start_checker(self):
374
"""Start a new checker subprocess if one is not running.
375
If a checker already exists, leave it running and do
377
# The reason for not killing a running checker is that if we
378
# did that, then if a checker (for some reason) started
379
# running slowly and taking more than 'interval' time, the
380
# client would inevitably timeout, since no checker would get
381
# a chance to run to completion. If we instead leave running
382
# checkers alone, the checker would have to take more time
383
# than 'timeout' for the client to be declared invalid, which
384
# is as it should be.
386
# If a checker exists, make sure it is not a zombie
387
if self.checker is not None:
388
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
390
logger.warning("Checker was a zombie")
391
gobject.source_remove(self.checker_callback_tag)
392
self.checker_callback(pid, status,
393
self.current_checker_command)
394
# Start a new checker if needed
395
if self.checker is None:
397
# In case checker_command has exactly one % operator
398
command = self.checker_command % self.host
400
# Escape attributes for the shell
401
escaped_attrs = dict((key, re.escape(str(val)))
403
vars(self).iteritems())
17
def __init__(self, name=None, options=None, dn=None,
18
password=None, passfile=None, fqdn=None,
19
timeout=None, interval=-1):
23
self.password = password
25
self.password = open(passfile).readall()
27
print "No Password or Passfile in client config file"
28
# raise RuntimeError XXX
29
self.password = "gazonk"
31
self.created = datetime.datetime.now()
34
timeout = options.timeout
35
self.timeout = timeout
37
interval = options.interval
38
self.interval = interval
39
self.next_check = datetime.datetime.now()
42
class server_metaclass(type):
43
"Common behavior for the UDP and TCP server classes"
44
def __new__(cls, name, bases, attrs):
45
attrs["address_family"] = socket.AF_INET6
46
attrs["allow_reuse_address"] = True
47
def server_bind(self):
48
if self.options.interface:
49
if not hasattr(socket, "SO_BINDTODEVICE"):
50
# From /usr/include/asm-i486/socket.h
51
socket.SO_BINDTODEVICE = 25
405
command = self.checker_command % escaped_attrs
406
except TypeError, error:
407
logger.error(u'Could not format string "%s":'
408
u' %s', self.checker_command, error)
409
return True # Try again later
410
self.current_checker_command = command
412
logger.info(u"Starting checker %r for %s",
414
# We don't need to redirect stdout and stderr, since
415
# in normal mode, that is already done by daemon(),
416
# and in debug mode we don't want to. (Stdin is
417
# always replaced by /dev/null.)
418
self.checker = subprocess.Popen(command,
423
self.CheckerStarted(command)
424
self.PropertyChanged(
425
dbus.String("checker_running"),
426
dbus.Boolean(True, variant_level=1))
427
self.checker_callback_tag = (gobject.child_watch_add
429
self.checker_callback,
431
# The checker may have completed before the gobject
432
# watch was added. Check for this.
433
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
435
gobject.source_remove(self.checker_callback_tag)
436
self.checker_callback(pid, status, command)
437
except OSError, error:
438
logger.error(u"Failed to start subprocess: %s",
440
# Re-run this periodically if run by gobject.timeout_add
443
def stop_checker(self):
444
"""Force the checker process, if any, to stop."""
445
if self.checker_callback_tag:
446
gobject.source_remove(self.checker_callback_tag)
447
self.checker_callback_tag = None
448
if getattr(self, "checker", None) is None:
53
self.socket.setsockopt(socket.SOL_SOCKET,
54
socket.SO_BINDTODEVICE,
55
self.options.interface)
56
except socket.error, error:
57
if error[0] == errno.EPERM:
58
print "Warning: No permission to bind to interface", \
59
self.options.interface
62
return super(type(self), self).server_bind()
63
attrs["server_bind"] = server_bind
64
def init(self, *args, **kwargs):
65
if "options" in kwargs:
66
self.options = kwargs["options"]
68
if "clients" in kwargs:
69
self.clients = kwargs["clients"]
71
if "credentials" in kwargs:
72
self.credentials = kwargs["credentials"]
73
del kwargs["credentials"]
74
return super(type(self), self).__init__(*args, **kwargs)
75
attrs["__init__"] = init
76
return type.__new__(cls, name, bases, attrs)
79
class udp_handler(SocketServer.DatagramRequestHandler, object):
81
self.wfile.write("Polo")
82
print "UDP request answered"
85
class IPv6_UDPServer(SocketServer.UDPServer, object):
86
__metaclass__ = server_metaclass
87
def verify_request(self, request, client_address):
88
print "UDP request came"
89
return request[0] == "Marco"
92
class tcp_handler(SocketServer.BaseRequestHandler, object):
94
print "TCP request came"
95
print "Request:", self.request
96
print "Client Address:", self.client_address
97
print "Server:", self.server
98
session = gnutls.connection.ServerSession(self.request,
99
self.server.credentials)
101
if session.peer_certificate:
102
print "DN:", session.peer_certificate.subject
104
session.verify_peer()
105
except gnutls.errors.CertificateError, error:
106
print "Verify failed", error
450
logger.debug(u"Stopping checker for %(name)s", vars(self))
452
os.kill(self.checker.pid, signal.SIGTERM)
454
#if self.checker.poll() is None:
455
# os.kill(self.checker.pid, signal.SIGKILL)
456
except OSError, error:
457
if error.errno != errno.ESRCH: # No such process
461
self.PropertyChanged(dbus.String(u"checker_running"),
462
dbus.Boolean(False, variant_level=1))
464
def still_valid(self):
465
"""Has the timeout not yet passed for this client?"""
466
if not getattr(self, "enabled", False):
468
now = datetime.datetime.utcnow()
469
if self.last_checked_ok is None:
470
return now < (self.created + self.timeout)
472
return now < (self.last_checked_ok + self.timeout)
474
## D-Bus methods & signals
475
_interface = u"se.bsnet.fukt.Mandos.Client"
478
CheckedOK = dbus.service.method(_interface)(checked_ok)
479
CheckedOK.__name__ = "CheckedOK"
481
# CheckerCompleted - signal
482
@dbus.service.signal(_interface, signature="nxs")
483
def CheckerCompleted(self, exitcode, waitstatus, command):
487
# CheckerStarted - signal
488
@dbus.service.signal(_interface, signature="s")
489
def CheckerStarted(self, command):
493
# GetAllProperties - method
494
@dbus.service.method(_interface, out_signature="a{sv}")
495
def GetAllProperties(self):
497
return dbus.Dictionary({
499
dbus.String(self.name, variant_level=1),
500
dbus.String("fingerprint"):
501
dbus.String(self.fingerprint, variant_level=1),
503
dbus.String(self.host, variant_level=1),
504
dbus.String("created"):
505
_datetime_to_dbus(self.created, variant_level=1),
506
dbus.String("last_enabled"):
507
(_datetime_to_dbus(self.last_enabled,
509
if self.last_enabled is not None
510
else dbus.Boolean(False, variant_level=1)),
511
dbus.String("enabled"):
512
dbus.Boolean(self.enabled, variant_level=1),
513
dbus.String("last_checked_ok"):
514
(_datetime_to_dbus(self.last_checked_ok,
516
if self.last_checked_ok is not None
517
else dbus.Boolean (False, variant_level=1)),
518
dbus.String("timeout"):
519
dbus.UInt64(self.timeout_milliseconds(),
521
dbus.String("interval"):
522
dbus.UInt64(self.interval_milliseconds(),
524
dbus.String("checker"):
525
dbus.String(self.checker_command,
527
dbus.String("checker_running"):
528
dbus.Boolean(self.checker is not None,
530
dbus.String("object_path"):
531
dbus.ObjectPath(self.dbus_object_path,
535
# IsStillValid - method
536
IsStillValid = (dbus.service.method(_interface, out_signature="b")
538
IsStillValid.__name__ = "IsStillValid"
540
# PropertyChanged - signal
541
@dbus.service.signal(_interface, signature="sv")
542
def PropertyChanged(self, property, value):
546
# ReceivedSecret - signal
547
@dbus.service.signal(_interface)
548
def ReceivedSecret(self):
553
@dbus.service.signal(_interface)
558
# SetChecker - method
559
@dbus.service.method(_interface, in_signature="s")
560
def SetChecker(self, checker):
561
"D-Bus setter method"
562
self.checker_command = checker
564
self.PropertyChanged(dbus.String(u"checker"),
565
dbus.String(self.checker_command,
569
@dbus.service.method(_interface, in_signature="s")
570
def SetHost(self, host):
571
"D-Bus setter method"
574
self.PropertyChanged(dbus.String(u"host"),
575
dbus.String(self.host, variant_level=1))
577
# SetInterval - method
578
@dbus.service.method(_interface, in_signature="t")
579
def SetInterval(self, milliseconds):
580
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
582
self.PropertyChanged(dbus.String(u"interval"),
583
(dbus.UInt64(self.interval_milliseconds(),
587
@dbus.service.method(_interface, in_signature="ay",
589
def SetSecret(self, secret):
590
"D-Bus setter method"
591
self.secret = str(secret)
593
# SetTimeout - method
594
@dbus.service.method(_interface, in_signature="t")
595
def SetTimeout(self, milliseconds):
596
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
598
self.PropertyChanged(dbus.String(u"timeout"),
599
(dbus.UInt64(self.timeout_milliseconds(),
603
Enable = dbus.service.method(_interface)(enable)
604
Enable.__name__ = "Enable"
606
# StartChecker - method
607
@dbus.service.method(_interface)
608
def StartChecker(self):
613
@dbus.service.method(_interface)
618
# StopChecker - method
619
StopChecker = dbus.service.method(_interface)(stop_checker)
620
StopChecker.__name__ = "StopChecker"
625
def peer_certificate(session):
626
"Return the peer's OpenPGP certificate as a bytestring"
627
# If not an OpenPGP certificate...
628
if (gnutls.library.functions
629
.gnutls_certificate_type_get(session._c_object)
630
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
631
# ...do the normal thing
632
return session.peer_certificate
633
list_size = ctypes.c_uint(1)
634
cert_list = (gnutls.library.functions
635
.gnutls_certificate_get_peers
636
(session._c_object, ctypes.byref(list_size)))
637
if not bool(cert_list) and list_size.value != 0:
638
raise gnutls.errors.GNUTLSError("error getting peer"
640
if list_size.value == 0:
643
return ctypes.string_at(cert.data, cert.size)
646
def fingerprint(openpgp):
647
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
648
# New GnuTLS "datum" with the OpenPGP public key
649
datum = (gnutls.library.types
650
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
653
ctypes.c_uint(len(openpgp))))
654
# New empty GnuTLS certificate
655
crt = gnutls.library.types.gnutls_openpgp_crt_t()
656
(gnutls.library.functions
657
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
658
# Import the OpenPGP public key into the certificate
659
(gnutls.library.functions
660
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
661
gnutls.library.constants
662
.GNUTLS_OPENPGP_FMT_RAW))
663
# Verify the self signature in the key
664
crtverify = ctypes.c_uint()
665
(gnutls.library.functions
666
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
667
if crtverify.value != 0:
668
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
669
raise gnutls.errors.CertificateSecurityError("Verify failed")
670
# New buffer for the fingerprint
671
buf = ctypes.create_string_buffer(20)
672
buf_len = ctypes.c_size_t()
673
# Get the fingerprint from the certificate into the buffer
674
(gnutls.library.functions
675
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
676
ctypes.byref(buf_len)))
677
# Deinit the certificate
678
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
679
# Convert the buffer to a Python bytestring
680
fpr = ctypes.string_at(buf, buf_len.value)
681
# Convert the bytestring to hexadecimal notation
682
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
686
class TCP_handler(SocketServer.BaseRequestHandler, object):
687
"""A TCP request handler class.
688
Instantiated by IPv6_TCPServer for each request to handle it.
689
Note: This will run in its own forked process."""
692
logger.info(u"TCP connection from: %s",
693
unicode(self.client_address))
694
logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
695
# Open IPC pipe to parent process
696
with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
697
session = (gnutls.connection
698
.ClientSession(self.request,
702
line = self.request.makefile().readline()
703
logger.debug(u"Protocol version: %r", line)
705
if int(line.strip().split()[0]) > 1:
707
except (ValueError, IndexError, RuntimeError), error:
708
logger.error(u"Unknown protocol version: %s", error)
711
# Note: gnutls.connection.X509Credentials is really a
712
# generic GnuTLS certificate credentials object so long as
713
# no X.509 keys are added to it. Therefore, we can use it
714
# here despite using OpenPGP certificates.
716
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
717
# "+AES-256-CBC", "+SHA1",
718
# "+COMP-NULL", "+CTYPE-OPENPGP",
720
# Use a fallback default, since this MUST be set.
721
priority = self.server.settings.get("priority", "NORMAL")
722
(gnutls.library.functions
723
.gnutls_priority_set_direct(session._c_object,
728
except gnutls.errors.GNUTLSError, error:
729
logger.warning(u"Handshake failed: %s", error)
730
# Do not run session.bye() here: the session is not
731
# established. Just abandon the request.
733
logger.debug(u"Handshake succeeded")
735
fpr = fingerprint(peer_certificate(session))
736
except (TypeError, gnutls.errors.GNUTLSError), error:
737
logger.warning(u"Bad certificate: %s", error)
740
logger.debug(u"Fingerprint: %s", fpr)
742
for c in self.server.clients:
743
if c.fingerprint == fpr:
747
logger.warning(u"Client not found for fingerprint: %s",
749
ipc.write("NOTFOUND %s\n" % fpr)
752
# Have to check if client.still_valid(), since it is
753
# possible that the client timed out while establishing
754
# the GnuTLS session.
755
if not client.still_valid():
756
logger.warning(u"Client %(name)s is invalid",
758
ipc.write("INVALID %s\n" % client.name)
761
ipc.write("SENDING %s\n" % client.name)
763
while sent_size < len(client.secret):
764
sent = session.send(client.secret[sent_size:])
765
logger.debug(u"Sent: %d, remaining: %d",
766
sent, len(client.secret)
767
- (sent_size + sent))
772
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
773
"""Like SocketServer.ForkingMixIn, but also pass a pipe.
774
Assumes a gobject.MainLoop event loop.
776
def process_request(self, request, client_address):
777
"""This overrides and wraps the original process_request().
778
This function creates a new pipe in self.pipe
780
self.pipe = os.pipe()
781
super(ForkingMixInWithPipe,
782
self).process_request(request, client_address)
783
os.close(self.pipe[1]) # close write end
784
# Call "handle_ipc" for both data and EOF events
785
gobject.io_add_watch(self.pipe[0],
786
gobject.IO_IN | gobject.IO_HUP,
788
def handle_ipc(source, condition):
789
"""Dummy function; override as necessary"""
794
class IPv6_TCPServer(ForkingMixInWithPipe,
795
SocketServer.TCPServer, object):
796
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
798
settings: Server settings
799
clients: Set() of Client objects
800
enabled: Boolean; whether this server is activated yet
802
address_family = socket.AF_INET6
803
def __init__(self, *args, **kwargs):
804
if "settings" in kwargs:
805
self.settings = kwargs["settings"]
806
del kwargs["settings"]
807
if "clients" in kwargs:
808
self.clients = kwargs["clients"]
809
del kwargs["clients"]
810
if "use_ipv6" in kwargs:
811
if not kwargs["use_ipv6"]:
812
self.address_family = socket.AF_INET
813
del kwargs["use_ipv6"]
815
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
816
def server_bind(self):
817
"""This overrides the normal server_bind() function
818
to bind to an interface if one was specified, and also NOT to
819
bind to an address or port if they were not specified."""
820
if self.settings["interface"]:
821
# 25 is from /usr/include/asm-i486/socket.h
822
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
824
self.socket.setsockopt(socket.SOL_SOCKET,
826
self.settings["interface"])
827
except socket.error, error:
828
if error[0] == errno.EPERM:
829
logger.error(u"No permission to"
830
u" bind to interface %s",
831
self.settings["interface"])
834
# Only bind(2) the socket if we really need to.
835
if self.server_address[0] or self.server_address[1]:
836
if not self.server_address[0]:
837
if self.address_family == socket.AF_INET6:
838
any_address = "::" # in6addr_any
840
any_address = socket.INADDR_ANY
841
self.server_address = (any_address,
842
self.server_address[1])
843
elif not self.server_address[1]:
844
self.server_address = (self.server_address[0],
846
# if self.settings["interface"]:
847
# self.server_address = (self.server_address[0],
853
return super(IPv6_TCPServer, self).server_bind()
854
def server_activate(self):
856
return super(IPv6_TCPServer, self).server_activate()
859
def handle_ipc(self, source, condition, file_objects={}):
861
gobject.IO_IN: "IN", # There is data to read.
862
gobject.IO_OUT: "OUT", # Data can be written (without
864
gobject.IO_PRI: "PRI", # There is urgent data to read.
865
gobject.IO_ERR: "ERR", # Error condition.
866
gobject.IO_HUP: "HUP" # Hung up (the connection has been
867
# broken, usually for pipes and
870
conditions_string = ' | '.join(name
872
condition_names.iteritems()
874
logger.debug("Handling IPC: FD = %d, condition = %s", source,
877
# Turn the pipe file descriptor into a Python file object
878
if source not in file_objects:
879
file_objects[source] = os.fdopen(source, "r", 1)
881
# Read a line from the file object
882
cmdline = file_objects[source].readline()
883
if not cmdline: # Empty line means end of file
885
file_objects[source].close()
886
del file_objects[source]
888
# Stop calling this function
891
logger.debug("IPC command: %r\n" % cmdline)
893
# Parse and act on command
894
cmd, args = cmdline.split(None, 1)
895
if cmd == "NOTFOUND":
896
if self.settings["use_dbus"]:
898
mandos_dbus_service.ClientNotFound(args)
899
elif cmd == "INVALID":
900
if self.settings["use_dbus"]:
901
for client in self.clients:
902
if client.name == args:
906
elif cmd == "SENDING":
907
for client in self.clients:
908
if client.name == args:
910
if self.settings["use_dbus"]:
912
client.ReceivedSecret()
915
logger.error("Unknown IPC command: %r", cmdline)
917
# Keep calling this function
110
session.send(dict((client.dn, client.password)
111
for client in self.server.clients)
112
[session.peer_certificate.subject])
114
session.send("gazonk")
119
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
120
__metaclass__ = server_metaclass
121
request_queue_size = 1024
921
128
def string_to_delta(interval):
922
129
"""Parse a string and return a datetime.timedelta
924
131
>>> string_to_delta('7d')
925
132
datetime.timedelta(7)
926
133
>>> string_to_delta('60s')
931
138
datetime.timedelta(1)
932
139
>>> string_to_delta(u'1w')
933
140
datetime.timedelta(7)
934
>>> string_to_delta('5m 30s')
935
datetime.timedelta(0, 330)
937
timevalue = datetime.timedelta(0)
938
for s in interval.split():
940
suffix = unicode(s[-1])
943
delta = datetime.timedelta(value)
945
delta = datetime.timedelta(0, value)
947
delta = datetime.timedelta(0, 0, 0, 0, value)
949
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
951
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
954
except (ValueError, IndexError):
143
suffix=unicode(interval[-1])
144
value=int(interval[:-1])
146
delta = datetime.timedelta(value)
148
delta = datetime.timedelta(0, value)
150
delta = datetime.timedelta(0, 0, 0, 0, value)
152
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
154
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
960
def server_state_changed(state):
961
"""Derived from the Avahi example code"""
962
if state == avahi.SERVER_COLLISION:
963
logger.error(u"Zeroconf server name collision")
965
elif state == avahi.SERVER_RUNNING:
969
def entry_group_state_changed(state, error):
970
"""Derived from the Avahi example code"""
971
logger.debug(u"Avahi state change: %i", state)
973
if state == avahi.ENTRY_GROUP_ESTABLISHED:
974
logger.debug(u"Zeroconf service established.")
975
elif state == avahi.ENTRY_GROUP_COLLISION:
976
logger.warning(u"Zeroconf service name collision.")
978
elif state == avahi.ENTRY_GROUP_FAILURE:
979
logger.critical(u"Avahi: Error in group state changed %s",
981
raise AvahiGroupError(u"State changed: %s" % unicode(error))
983
def if_nametoindex(interface):
984
"""Call the C function if_nametoindex(), or equivalent"""
985
global if_nametoindex
987
if_nametoindex = (ctypes.cdll.LoadLibrary
988
(ctypes.util.find_library("c"))
990
except (OSError, AttributeError):
991
if "struct" not in sys.modules:
993
if "fcntl" not in sys.modules:
995
def if_nametoindex(interface):
996
"Get an interface index the hard way, i.e. using fcntl()"
997
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
998
with closing(socket.socket()) as s:
999
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1000
struct.pack("16s16x", interface))
1001
interface_index = struct.unpack("I", ifreq[16:20])[0]
1002
return interface_index
1003
return if_nametoindex(interface)
1006
def daemon(nochdir = False, noclose = False):
1007
"""See daemon(3). Standard BSD Unix function.
1008
This should really exist as os.daemon, but it doesn't (yet)."""
1017
# Close all standard open file descriptors
1018
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1019
if not stat.S_ISCHR(os.fstat(null).st_mode):
1020
raise OSError(errno.ENODEV,
1021
"/dev/null not a character device")
1022
os.dup2(null, sys.stdin.fileno())
1023
os.dup2(null, sys.stdout.fileno())
1024
os.dup2(null, sys.stderr.fileno())
157
except (ValueError, IndexError):
1031
######################################################################
1032
# Parsing of options, both command line and config file
1034
parser = optparse.OptionParser(version = "%%prog %s" % version)
162
parser = OptionParser()
1035
163
parser.add_option("-i", "--interface", type="string",
1036
metavar="IF", help="Bind to interface IF")
1037
parser.add_option("-a", "--address", type="string",
1038
help="Address to listen for requests on")
1039
parser.add_option("-p", "--port", type="int",
164
default="eth0", metavar="IF",
165
help="Interface to bind to")
166
parser.add_option("--cert", type="string", default="cert.pem",
168
help="Public key certificate to use")
169
parser.add_option("--key", type="string", default="key.pem",
171
help="Private key to use")
172
parser.add_option("--ca", type="string", default="ca.pem",
174
help="Certificate Authority certificate to use")
175
parser.add_option("--crl", type="string", default="crl.pem",
177
help="Certificate Revokation List to use")
178
parser.add_option("-p", "--port", type="int", default=49001,
1040
179
help="Port number to receive requests on")
1041
parser.add_option("--check", action="store_true",
180
parser.add_option("--dh", type="int", metavar="BITS",
181
help="DH group to use")
182
parser.add_option("-t", "--timeout", type="string", # Parsed later
184
help="Amount of downtime allowed for clients")
185
parser.add_option("--interval", type="string", # Parsed later
187
help="How often to check that a client is up")
188
parser.add_option("--check", action="store_true", default=False,
1042
189
help="Run self-test")
1043
parser.add_option("--debug", action="store_true",
1044
help="Debug mode; run in foreground and log to"
1046
parser.add_option("--priority", type="string", help="GnuTLS"
1047
" priority string (see GnuTLS documentation)")
1048
parser.add_option("--servicename", type="string", metavar="NAME",
1049
help="Zeroconf service name")
1050
parser.add_option("--configdir", type="string",
1051
default="/etc/mandos", metavar="DIR",
1052
help="Directory to search for configuration"
1054
parser.add_option("--no-dbus", action="store_false",
1056
help="Do not provide D-Bus system bus"
1058
parser.add_option("--no-ipv6", action="store_false",
1059
dest="use_ipv6", help="Do not use IPv6")
1060
options = parser.parse_args()[0]
190
(options, args) = parser.parse_args()
1062
192
if options.check:
1064
194
doctest.testmod()
1067
# Default values for config file for server-global settings
1068
server_defaults = { "interface": "",
1073
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1074
"servicename": "Mandos",
1079
# Parse config file for server-global settings
1080
server_config = ConfigParser.SafeConfigParser(server_defaults)
1082
server_config.read(os.path.join(options.configdir, "mandos.conf"))
1083
# Convert the SafeConfigParser object to a dict
1084
server_settings = server_config.defaults()
1085
# Use the appropriate methods on the non-string config options
1086
server_settings["debug"] = server_config.getboolean("DEFAULT",
1088
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
1090
server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
1092
if server_settings["port"]:
1093
server_settings["port"] = server_config.getint("DEFAULT",
1097
# Override the settings from the config file with command line
1099
for option in ("interface", "address", "port", "debug",
1100
"priority", "servicename", "configdir",
1101
"use_dbus", "use_ipv6"):
1102
value = getattr(options, option)
1103
if value is not None:
1104
server_settings[option] = value
1106
# Now we have our good server settings in "server_settings"
1108
##################################################################
1111
debug = server_settings["debug"]
1112
use_dbus = server_settings["use_dbus"]
1113
use_ipv6 = server_settings["use_ipv6"]
1116
syslogger.setLevel(logging.WARNING)
1117
console.setLevel(logging.WARNING)
1119
if server_settings["servicename"] != "Mandos":
1120
syslogger.setFormatter(logging.Formatter
1121
('Mandos (%s) [%%(process)d]:'
1122
' %%(levelname)s: %%(message)s'
1123
% server_settings["servicename"]))
1125
# Parse config file with clients
1126
client_defaults = { "timeout": "1h",
1128
"checker": "fping -q -- %%(host)s",
1131
client_config = ConfigParser.SafeConfigParser(client_defaults)
1132
client_config.read(os.path.join(server_settings["configdir"],
1135
global mandos_dbus_service
1136
mandos_dbus_service = None
1139
tcp_server = IPv6_TCPServer((server_settings["address"],
1140
server_settings["port"]),
1142
settings=server_settings,
1143
clients=clients, use_ipv6=use_ipv6)
1144
pidfilename = "/var/run/mandos.pid"
1146
pidfile = open(pidfilename, "w")
1148
logger.error("Could not open file %r", pidfilename)
1151
uid = pwd.getpwnam("_mandos").pw_uid
1152
gid = pwd.getpwnam("_mandos").pw_gid
1155
uid = pwd.getpwnam("mandos").pw_uid
1156
gid = pwd.getpwnam("mandos").pw_gid
1159
uid = pwd.getpwnam("nobody").pw_uid
1160
gid = pwd.getpwnam("nogroup").pw_gid
1167
except OSError, error:
1168
if error[0] != errno.EPERM:
1171
# Enable all possible GnuTLS debugging
1173
# "Use a log level over 10 to enable all debugging options."
1175
gnutls.library.functions.gnutls_global_set_log_level(11)
1177
@gnutls.library.types.gnutls_log_func
1178
def debug_gnutls(level, string):
1179
logger.debug("GnuTLS: %s", string[:-1])
1181
(gnutls.library.functions
1182
.gnutls_global_set_log_function(debug_gnutls))
1185
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1186
service = AvahiService(name = server_settings["servicename"],
1187
servicetype = "_mandos._tcp",
1188
protocol = protocol)
1189
if server_settings["interface"]:
1190
service.interface = (if_nametoindex
1191
(server_settings["interface"]))
1196
# From the Avahi example code
1197
DBusGMainLoop(set_as_default=True )
1198
main_loop = gobject.MainLoop()
1199
bus = dbus.SystemBus()
1200
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1201
avahi.DBUS_PATH_SERVER),
1202
avahi.DBUS_INTERFACE_SERVER)
1203
# End of Avahi example code
1205
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1207
clients.update(Set(Client(name = section,
1209
= dict(client_config.items(section)),
1210
use_dbus = use_dbus)
1211
for section in client_config.sections()))
1213
logger.warning(u"No clients defined")
1216
# Redirect stdin so all checkers get /dev/null
1217
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1218
os.dup2(null, sys.stdin.fileno())
1222
# No console logging
1223
logger.removeHandler(console)
1224
# Close all input and output, do double fork, etc.
1228
with closing(pidfile):
1230
pidfile.write(str(pid) + "\n")
1233
logger.error(u"Could not write to file %r with PID %d",
1236
# "pidfile" was never created
1241
"Cleanup function; run on exit"
1243
# From the Avahi example code
1244
if not group is None:
1247
# End of Avahi example code
1250
client = clients.pop()
1251
client.disable_hook = None
1254
atexit.register(cleanup)
1257
signal.signal(signal.SIGINT, signal.SIG_IGN)
1258
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1259
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1262
class MandosDBusService(dbus.service.Object):
1263
"""A D-Bus proxy object"""
1265
dbus.service.Object.__init__(self, bus, "/")
1266
_interface = u"se.bsnet.fukt.Mandos"
1268
@dbus.service.signal(_interface, signature="oa{sv}")
1269
def ClientAdded(self, objpath, properties):
1273
@dbus.service.signal(_interface, signature="s")
1274
def ClientNotFound(self, fingerprint):
1278
@dbus.service.signal(_interface, signature="os")
1279
def ClientRemoved(self, objpath, name):
1283
@dbus.service.method(_interface, out_signature="ao")
1284
def GetAllClients(self):
1286
return dbus.Array(c.dbus_object_path for c in clients)
1288
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1289
def GetAllClientsWithProperties(self):
1291
return dbus.Dictionary(
1292
((c.dbus_object_path, c.GetAllProperties())
1296
@dbus.service.method(_interface, in_signature="o")
1297
def RemoveClient(self, object_path):
1300
if c.dbus_object_path == object_path:
1302
# Don't signal anything except ClientRemoved
1306
self.ClientRemoved(object_path, c.name)
1312
mandos_dbus_service = MandosDBusService()
1314
for client in clients:
1317
mandos_dbus_service.ClientAdded(client.dbus_object_path,
1318
client.GetAllProperties())
1322
tcp_server.server_activate()
1324
# Find out what port we got
1325
service.port = tcp_server.socket.getsockname()[1]
1327
logger.info(u"Now listening on address %r, port %d,"
1328
" flowinfo %d, scope_id %d"
1329
% tcp_server.socket.getsockname())
1331
logger.info(u"Now listening on address %r, port %d"
1332
% tcp_server.socket.getsockname())
1334
#service.interface = tcp_server.socket.getsockname()[3]
1337
# From the Avahi example code
1338
server.connect_to_signal("StateChanged", server_state_changed)
1340
server_state_changed(server.GetState())
1341
except dbus.exceptions.DBusException, error:
1342
logger.critical(u"DBusException: %s", error)
1344
# End of Avahi example code
1346
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1347
lambda *args, **kwargs:
1348
(tcp_server.handle_request
1349
(*args[2:], **kwargs) or True))
1351
logger.debug(u"Starting main loop")
1353
except AvahiError, error:
1354
logger.critical(u"AvahiError: %s", error)
1356
except KeyboardInterrupt:
1359
logger.debug("Server received KeyboardInterrupt")
1360
logger.debug("Server exiting")
1362
if __name__ == '__main__':
197
# Parse the time arguments
199
options.timeout = string_to_delta(options.timeout)
201
parser.error("option --timeout: Unparseable time")
204
options.interval = string_to_delta(options.interval)
206
parser.error("option --interval: Unparseable time")
208
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
209
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
210
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
211
crl = gnutls.crypto.X509CRL(open(options.crl).read())
212
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
216
client_config_object = ConfigParser.SafeConfigParser(defaults)
217
client_config_object.read("mandos-clients.conf")
218
clients = [Client(name=section, options=options,
219
**(dict(client_config_object.items(section))))
220
for section in client_config_object.sections()]
222
udp_server = IPv6_UDPServer((in6addr_any, options.port),
226
tcp_server = IPv6_TCPServer((in6addr_any, options.port),
233
in_, out, err = select.select((udp_server,
236
server.handle_request()
239
if __name__ == "__main__":