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: %(levelname)s: %(message)s'))
77
logger.addHandler(syslogger)
79
console = logging.StreamHandler()
80
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
82
logger.addHandler(console)
84
class AvahiError(Exception):
85
def __init__(self, value, *args, **kwargs):
87
super(AvahiError, self).__init__(value, *args, **kwargs)
88
def __unicode__(self):
89
return unicode(repr(self.value))
91
class AvahiServiceError(AvahiError):
94
class AvahiGroupError(AvahiError):
98
class AvahiService(object):
99
"""An Avahi (Zeroconf) service.
101
interface: integer; avahi.IF_UNSPEC or an interface index.
102
Used to optionally bind to the specified interface.
103
name: string; Example: 'Mandos'
104
type: string; Example: '_mandos._tcp'.
105
See <http://www.dns-sd.org/ServiceTypes.html>
106
port: integer; what port to announce
107
TXT: list of strings; TXT record for the service
108
domain: string; Domain to publish on, default to .local if empty.
109
host: string; Host to publish records for, default is localhost
110
max_renames: integer; maximum number of renames
111
rename_count: integer; counter so we only rename after collisions
112
a sensible number of times
114
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
servicetype = None, port = None, TXT = None,
116
domain = "", host = "", max_renames = 32768):
117
self.interface = interface
119
self.type = servicetype
121
self.TXT = TXT if TXT is not None else []
124
self.rename_count = 0
125
self.max_renames = max_renames
127
"""Derived from the Avahi example code"""
128
if self.rename_count >= self.max_renames:
129
logger.critical(u"No suitable Zeroconf service name found"
130
u" after %i retries, exiting.",
132
raise AvahiServiceError(u"Too many renames")
133
self.name = server.GetAlternativeServiceName(self.name)
134
logger.info(u"Changing Zeroconf service name to %r ...",
136
syslogger.setFormatter(logging.Formatter
137
('Mandos (%s): %%(levelname)s:'
138
' %%(message)s' % self.name))
141
self.rename_count += 1
143
"""Derived from the Avahi example code"""
144
if group is not None:
147
"""Derived from the Avahi example code"""
150
group = dbus.Interface(bus.get_object
152
server.EntryGroupNew()),
153
avahi.DBUS_INTERFACE_ENTRY_GROUP)
154
group.connect_to_signal('StateChanged',
155
entry_group_state_changed)
156
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
157
service.name, service.type)
159
self.interface, # interface
160
avahi.PROTO_INET6, # protocol
161
dbus.UInt32(0), # flags
162
self.name, self.type,
163
self.domain, self.host,
164
dbus.UInt16(self.port),
165
avahi.string_array_to_txt_array(self.TXT))
168
# From the Avahi example code:
169
group = None # our entry group
170
# End of Avahi example code
173
def _datetime_to_dbus(dt, variant_level=0):
174
"""Convert a UTC datetime.datetime() to a D-Bus type."""
175
return dbus.String(dt.isoformat(), variant_level=variant_level)
178
class Client(dbus.service.Object):
179
"""A representation of a client host served by this server.
181
name: string; from the config file, used in log messages and
183
fingerprint: string (40 or 32 hexadecimal digits); used to
184
uniquely identify the client
185
secret: bytestring; sent verbatim (over TLS) to client
186
host: string; available for use by the checker command
187
created: datetime.datetime(); (UTC) object creation
188
last_enabled: datetime.datetime(); (UTC)
190
last_checked_ok: datetime.datetime(); (UTC) or None
191
timeout: datetime.timedelta(); How long from last_checked_ok
192
until this client is invalid
193
interval: datetime.timedelta(); How often to start a new checker
194
disable_hook: If set, called by disable() as disable_hook(self)
195
checker: subprocess.Popen(); a running checker process used
196
to see if the client lives.
197
'None' if no process is running.
198
checker_initiator_tag: a gobject event source tag, or None
199
disable_initiator_tag: - '' -
200
checker_callback_tag: - '' -
201
checker_command: string; External command which is run to check if
202
client lives. %() expansions are done at
203
runtime with vars(self) as dict, so that for
204
instance %(name)s can be used in the command.
205
use_dbus: bool(); Whether to provide D-Bus interface and signals
206
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
208
def timeout_milliseconds(self):
209
"Return the 'timeout' attribute in milliseconds"
210
return ((self.timeout.days * 24 * 60 * 60 * 1000)
211
+ (self.timeout.seconds * 1000)
212
+ (self.timeout.microseconds // 1000))
214
def interval_milliseconds(self):
215
"Return the 'interval' attribute in milliseconds"
216
return ((self.interval.days * 24 * 60 * 60 * 1000)
217
+ (self.interval.seconds * 1000)
218
+ (self.interval.microseconds // 1000))
220
def __init__(self, name = None, disable_hook=None, config=None,
222
"""Note: the 'checker' key in 'config' sets the
223
'checker_command' attribute and *not* the 'checker'
228
logger.debug(u"Creating client %r", self.name)
229
self.use_dbus = False # During __init__
230
# Uppercase and remove spaces from fingerprint for later
231
# comparison purposes with return value from the fingerprint()
233
self.fingerprint = (config["fingerprint"].upper()
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
236
if "secret" in config:
237
self.secret = config["secret"].decode(u"base64")
238
elif "secfile" in config:
239
with closing(open(os.path.expanduser
241
(config["secfile"])))) as secfile:
242
self.secret = secfile.read()
244
raise TypeError(u"No secret or secfile for client %s"
246
self.host = config.get("host", "")
247
self.created = datetime.datetime.utcnow()
249
self.last_enabled = None
250
self.last_checked_ok = None
251
self.timeout = string_to_delta(config["timeout"])
252
self.interval = string_to_delta(config["interval"])
253
self.disable_hook = disable_hook
255
self.checker_initiator_tag = None
256
self.disable_initiator_tag = None
257
self.checker_callback_tag = None
258
self.checker_command = config["checker"]
259
self.last_connect = None
260
# Only now, when this client is initialized, can it show up on
262
self.use_dbus = use_dbus
264
self.dbus_object_path = (dbus.ObjectPath
266
+ self.name.replace(".", "_")))
267
dbus.service.Object.__init__(self, bus,
268
self.dbus_object_path)
271
"""Start this client's checker and timeout hooks"""
272
self.last_enabled = datetime.datetime.utcnow()
273
# Schedule a new checker to be started an 'interval' from now,
274
# and every interval from then on.
275
self.checker_initiator_tag = (gobject.timeout_add
276
(self.interval_milliseconds(),
278
# Also start a new checker *right now*.
280
# Schedule a disable() when 'timeout' has passed
281
self.disable_initiator_tag = (gobject.timeout_add
282
(self.timeout_milliseconds(),
287
self.PropertyChanged(dbus.String(u"enabled"),
288
dbus.Boolean(True, variant_level=1))
289
self.PropertyChanged(dbus.String(u"last_enabled"),
290
(_datetime_to_dbus(self.last_enabled,
294
"""Disable this client."""
295
if not getattr(self, "enabled", False):
297
logger.info(u"Disabling client %s", self.name)
298
if getattr(self, "disable_initiator_tag", False):
299
gobject.source_remove(self.disable_initiator_tag)
300
self.disable_initiator_tag = None
301
if getattr(self, "checker_initiator_tag", False):
302
gobject.source_remove(self.checker_initiator_tag)
303
self.checker_initiator_tag = None
305
if self.disable_hook:
306
self.disable_hook(self)
310
self.PropertyChanged(dbus.String(u"enabled"),
311
dbus.Boolean(False, variant_level=1))
312
# Do not run this again if called by a gobject.timeout_add
316
self.disable_hook = None
319
def checker_callback(self, pid, condition, command):
320
"""The checker has completed, so take appropriate actions."""
321
self.checker_callback_tag = None
325
self.PropertyChanged(dbus.String(u"checker_running"),
326
dbus.Boolean(False, variant_level=1))
327
if os.WIFEXITED(condition):
328
exitstatus = os.WEXITSTATUS(condition)
330
logger.info(u"Checker for %(name)s succeeded",
334
logger.info(u"Checker for %(name)s failed",
338
self.CheckerCompleted(dbus.Int16(exitstatus),
339
dbus.Int64(condition),
340
dbus.String(command))
342
logger.warning(u"Checker for %(name)s crashed?",
346
self.CheckerCompleted(dbus.Int16(-1),
347
dbus.Int64(condition),
348
dbus.String(command))
350
def checked_ok(self):
351
"""Bump up the timeout for this client.
352
This should only be called when the client has been seen,
355
self.last_checked_ok = datetime.datetime.utcnow()
356
gobject.source_remove(self.disable_initiator_tag)
357
self.disable_initiator_tag = (gobject.timeout_add
358
(self.timeout_milliseconds(),
362
self.PropertyChanged(
363
dbus.String(u"last_checked_ok"),
364
(_datetime_to_dbus(self.last_checked_ok,
367
def start_checker(self):
368
"""Start a new checker subprocess if one is not running.
369
If a checker already exists, leave it running and do
371
# The reason for not killing a running checker is that if we
372
# did that, then if a checker (for some reason) started
373
# running slowly and taking more than 'interval' time, the
374
# client would inevitably timeout, since no checker would get
375
# a chance to run to completion. If we instead leave running
376
# checkers alone, the checker would have to take more time
377
# than 'timeout' for the client to be declared invalid, which
378
# is as it should be.
379
if self.checker is None:
381
# In case checker_command has exactly one % operator
382
command = self.checker_command % self.host
384
# Escape attributes for the shell
385
escaped_attrs = dict((key, re.escape(str(val)))
387
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
389
command = self.checker_command % escaped_attrs
390
except TypeError, error:
391
logger.error(u'Could not format string "%s":'
392
u' %s', self.checker_command, error)
393
return True # Try again later
395
logger.info(u"Starting checker %r for %s",
397
# We don't need to redirect stdout and stderr, since
398
# in normal mode, that is already done by daemon(),
399
# and in debug mode we don't want to. (Stdin is
400
# always replaced by /dev/null.)
401
self.checker = subprocess.Popen(command,
406
self.CheckerStarted(command)
407
self.PropertyChanged(
408
dbus.String("checker_running"),
409
dbus.Boolean(True, variant_level=1))
410
self.checker_callback_tag = (gobject.child_watch_add
412
self.checker_callback,
414
except OSError, error:
415
logger.error(u"Failed to start subprocess: %s",
417
# Re-run this periodically if run by gobject.timeout_add
420
def stop_checker(self):
421
"""Force the checker process, if any, to stop."""
422
if self.checker_callback_tag:
423
gobject.source_remove(self.checker_callback_tag)
424
self.checker_callback_tag = None
425
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
427
logger.debug(u"Stopping checker for %(name)s", vars(self))
429
os.kill(self.checker.pid, signal.SIGTERM)
431
#if self.checker.poll() is None:
432
# os.kill(self.checker.pid, signal.SIGKILL)
433
except OSError, error:
434
if error.errno != errno.ESRCH: # No such process
438
self.PropertyChanged(dbus.String(u"checker_running"),
439
dbus.Boolean(False, variant_level=1))
441
def still_valid(self):
442
"""Has the timeout not yet passed for this client?"""
443
if not getattr(self, "enabled", False):
445
now = datetime.datetime.utcnow()
446
if self.last_checked_ok is None:
447
return now < (self.created + self.timeout)
449
return now < (self.last_checked_ok + self.timeout)
451
## D-Bus methods & signals
452
_interface = u"se.bsnet.fukt.Mandos.Client"
455
CheckedOK = dbus.service.method(_interface)(checked_ok)
456
CheckedOK.__name__ = "CheckedOK"
458
# CheckerCompleted - signal
459
@dbus.service.signal(_interface, signature="nxs")
460
def CheckerCompleted(self, exitcode, waitstatus, command):
464
# CheckerStarted - signal
465
@dbus.service.signal(_interface, signature="s")
466
def CheckerStarted(self, command):
470
# GetAllProperties - method
471
@dbus.service.method(_interface, out_signature="a{sv}")
472
def GetAllProperties(self):
474
return dbus.Dictionary({
476
dbus.String(self.name, variant_level=1),
477
dbus.String("fingerprint"):
478
dbus.String(self.fingerprint, variant_level=1),
480
dbus.String(self.host, variant_level=1),
481
dbus.String("created"):
482
_datetime_to_dbus(self.created, variant_level=1),
483
dbus.String("last_enabled"):
484
(_datetime_to_dbus(self.last_enabled,
486
if self.last_enabled is not None
487
else dbus.Boolean(False, variant_level=1)),
488
dbus.String("enabled"):
489
dbus.Boolean(self.enabled, variant_level=1),
490
dbus.String("last_checked_ok"):
491
(_datetime_to_dbus(self.last_checked_ok,
493
if self.last_checked_ok is not None
494
else dbus.Boolean (False, variant_level=1)),
495
dbus.String("timeout"):
496
dbus.UInt64(self.timeout_milliseconds(),
498
dbus.String("interval"):
499
dbus.UInt64(self.interval_milliseconds(),
501
dbus.String("checker"):
502
dbus.String(self.checker_command,
504
dbus.String("checker_running"):
505
dbus.Boolean(self.checker is not None,
507
dbus.String("object_path"):
508
dbus.ObjectPath(self.dbus_object_path,
512
# IsStillValid - method
513
IsStillValid = (dbus.service.method(_interface, out_signature="b")
515
IsStillValid.__name__ = "IsStillValid"
517
# PropertyChanged - signal
518
@dbus.service.signal(_interface, signature="sv")
519
def PropertyChanged(self, property, value):
523
# SetChecker - method
524
@dbus.service.method(_interface, in_signature="s")
525
def SetChecker(self, checker):
526
"D-Bus setter method"
527
self.checker_command = checker
529
self.PropertyChanged(dbus.String(u"checker"),
530
dbus.String(self.checker_command,
534
@dbus.service.method(_interface, in_signature="s")
535
def SetHost(self, host):
536
"D-Bus setter method"
539
self.PropertyChanged(dbus.String(u"host"),
540
dbus.String(self.host, variant_level=1))
542
# SetInterval - method
543
@dbus.service.method(_interface, in_signature="t")
544
def SetInterval(self, milliseconds):
545
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
547
self.PropertyChanged(dbus.String(u"interval"),
548
(dbus.UInt64(self.interval_milliseconds(),
552
@dbus.service.method(_interface, in_signature="ay",
554
def SetSecret(self, secret):
555
"D-Bus setter method"
556
self.secret = str(secret)
558
# SetTimeout - method
559
@dbus.service.method(_interface, in_signature="t")
560
def SetTimeout(self, milliseconds):
561
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
563
self.PropertyChanged(dbus.String(u"timeout"),
564
(dbus.UInt64(self.timeout_milliseconds(),
568
Enable = dbus.service.method(_interface)(enable)
569
Enable.__name__ = "Enable"
571
# StartChecker - method
572
@dbus.service.method(_interface)
573
def StartChecker(self):
578
@dbus.service.method(_interface)
583
# StopChecker - method
584
StopChecker = dbus.service.method(_interface)(stop_checker)
585
StopChecker.__name__ = "StopChecker"
590
def peer_certificate(session):
591
"Return the peer's OpenPGP certificate as a bytestring"
592
# If not an OpenPGP certificate...
593
if (gnutls.library.functions
594
.gnutls_certificate_type_get(session._c_object)
595
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
596
# ...do the normal thing
597
return session.peer_certificate
598
list_size = ctypes.c_uint(1)
599
cert_list = (gnutls.library.functions
600
.gnutls_certificate_get_peers
601
(session._c_object, ctypes.byref(list_size)))
602
if not bool(cert_list) and list_size.value != 0:
603
raise gnutls.errors.GNUTLSError("error getting peer"
605
if list_size.value == 0:
608
return ctypes.string_at(cert.data, cert.size)
611
def fingerprint(openpgp):
612
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
613
# New GnuTLS "datum" with the OpenPGP public key
614
datum = (gnutls.library.types
615
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
618
ctypes.c_uint(len(openpgp))))
619
# New empty GnuTLS certificate
620
crt = gnutls.library.types.gnutls_openpgp_crt_t()
621
(gnutls.library.functions
622
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
623
# Import the OpenPGP public key into the certificate
624
(gnutls.library.functions
625
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
626
gnutls.library.constants
627
.GNUTLS_OPENPGP_FMT_RAW))
628
# Verify the self signature in the key
629
crtverify = ctypes.c_uint()
630
(gnutls.library.functions
631
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
632
if crtverify.value != 0:
633
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
634
raise gnutls.errors.CertificateSecurityError("Verify failed")
635
# New buffer for the fingerprint
636
buf = ctypes.create_string_buffer(20)
637
buf_len = ctypes.c_size_t()
638
# Get the fingerprint from the certificate into the buffer
639
(gnutls.library.functions
640
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
641
ctypes.byref(buf_len)))
642
# Deinit the certificate
643
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
644
# Convert the buffer to a Python bytestring
645
fpr = ctypes.string_at(buf, buf_len.value)
646
# Convert the bytestring to hexadecimal notation
647
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
651
class TCP_handler(SocketServer.BaseRequestHandler, object):
652
"""A TCP request handler class.
653
Instantiated by IPv6_TCPServer for each request to handle it.
654
Note: This will run in its own forked process."""
657
logger.info(u"TCP connection from: %s",
658
unicode(self.client_address))
659
logger.debug(u"Pipe: %d", self.server.pipe[1])
660
# Open IPC pipe to parent process
661
with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
662
session = (gnutls.connection
663
.ClientSession(self.request,
667
line = self.request.makefile().readline()
668
logger.debug(u"Protocol version: %r", line)
670
if int(line.strip().split()[0]) > 1:
672
except (ValueError, IndexError, RuntimeError), error:
673
logger.error(u"Unknown protocol version: %s", error)
676
# Note: gnutls.connection.X509Credentials is really a
677
# generic GnuTLS certificate credentials object so long as
678
# no X.509 keys are added to it. Therefore, we can use it
679
# here despite using OpenPGP certificates.
681
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
682
# "+AES-256-CBC", "+SHA1",
683
# "+COMP-NULL", "+CTYPE-OPENPGP",
685
# Use a fallback default, since this MUST be set.
686
priority = self.server.settings.get("priority", "NORMAL")
687
(gnutls.library.functions
688
.gnutls_priority_set_direct(session._c_object,
693
except gnutls.errors.GNUTLSError, error:
694
logger.warning(u"Handshake failed: %s", error)
695
# Do not run session.bye() here: the session is not
696
# established. Just abandon the request.
698
logger.debug(u"Handshake succeeded")
700
fpr = fingerprint(peer_certificate(session))
701
except (TypeError, gnutls.errors.GNUTLSError), error:
702
logger.warning(u"Bad certificate: %s", error)
705
logger.debug(u"Fingerprint: %s", fpr)
706
for c in self.server.clients:
707
if c.fingerprint == fpr:
711
logger.warning(u"Client not found for fingerprint: %s",
713
ipc.write("NOTFOUND %s\n" % fpr)
716
# Have to check if client.still_valid(), since it is
717
# possible that the client timed out while establishing
718
# the GnuTLS session.
719
if not client.still_valid():
720
logger.warning(u"Client %(name)s is invalid",
722
ipc.write("INVALID %s\n" % client.name)
725
ipc.write("SENDING %s\n" % client.name)
726
## This won't work here, since we're in a fork.
727
# client.checked_ok()
729
while sent_size < len(client.secret):
730
sent = session.send(client.secret[sent_size:])
731
logger.debug(u"Sent: %d, remaining: %d",
732
sent, len(client.secret)
733
- (sent_size + sent))
738
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
739
"""Like SocketServer.ForkingMixIn, but also pass a pipe.
740
Assumes a gobject.MainLoop event loop.
742
def process_request(self, request, client_address):
743
"""This overrides and wraps the original process_request().
744
This function creates a new pipe in self.pipe
746
self.pipe = os.pipe()
747
super(ForkingMixInWithPipe,
748
self).process_request(request, client_address)
749
os.close(self.pipe[1]) # close write end
750
# Call "handle_ipc" for both data and EOF events
751
gobject.io_add_watch(self.pipe[0],
752
gobject.IO_IN | gobject.IO_HUP,
754
def handle_ipc(source, condition):
755
"""Dummy function; override as necessary"""
760
class IPv6_TCPServer(ForkingMixInWithPipe,
761
SocketServer.TCPServer, object):
762
"""IPv6 TCP server. Accepts 'None' as address and/or port.
764
settings: Server settings
765
clients: Set() of Client objects
766
enabled: Boolean; whether this server is activated yet
768
address_family = socket.AF_INET6
769
def __init__(self, *args, **kwargs):
770
if "settings" in kwargs:
771
self.settings = kwargs["settings"]
772
del kwargs["settings"]
773
if "clients" in kwargs:
774
self.clients = kwargs["clients"]
775
del kwargs["clients"]
777
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
778
def server_bind(self):
779
"""This overrides the normal server_bind() function
780
to bind to an interface if one was specified, and also NOT to
781
bind to an address or port if they were not specified."""
782
if self.settings["interface"]:
783
# 25 is from /usr/include/asm-i486/socket.h
784
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
786
self.socket.setsockopt(socket.SOL_SOCKET,
788
self.settings["interface"])
789
except socket.error, error:
790
if error[0] == errno.EPERM:
791
logger.error(u"No permission to"
792
u" bind to interface %s",
793
self.settings["interface"])
796
# Only bind(2) the socket if we really need to.
797
if self.server_address[0] or self.server_address[1]:
798
if not self.server_address[0]:
800
self.server_address = (in6addr_any,
801
self.server_address[1])
802
elif not self.server_address[1]:
803
self.server_address = (self.server_address[0],
805
# if self.settings["interface"]:
806
# self.server_address = (self.server_address[0],
812
return super(IPv6_TCPServer, self).server_bind()
813
def server_activate(self):
815
return super(IPv6_TCPServer, self).server_activate()
818
def handle_ipc(self, source, condition, file_objects={}):
819
logger.debug("Handling IPC: %r : %r", source, condition)
821
# Turn a file descriptor into a Python file object
822
if source not in file_objects:
823
file_objects[source] = os.fdopen(source, "r", 1)
825
# Read a line from the file object
826
cmdline = file_objects[source].readline()
827
if not cmdline: # Empty line means end of file
829
logger.debug("Closing: %r", source)
830
file_objects[source].close()
831
del file_objects[source]
833
# Stop calling this function
836
logger.debug("IPC command: %r\n" % cmdline)
838
# Parse and act on command
839
cmd, args = cmdline.split(None, 1)
840
if cmd == "NOTFOUND":
842
elif cmd == "INVALID":
844
elif cmd == "SENDING":
847
logger.error("Unknown IPC command: %r", cmdline)
849
# 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
853
126
def string_to_delta(interval):
854
127
"""Parse a string and return a datetime.timedelta
863
136
datetime.timedelta(1)
864
137
>>> string_to_delta(u'1w')
865
138
datetime.timedelta(7)
866
>>> string_to_delta('5m 30s')
867
datetime.timedelta(0, 330)
869
timevalue = datetime.timedelta(0)
870
for s in interval.split():
872
suffix = unicode(s[-1])
875
delta = datetime.timedelta(value)
877
delta = datetime.timedelta(0, value)
879
delta = datetime.timedelta(0, 0, 0, 0, value)
881
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
883
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
886
except (ValueError, IndexError):
141
suffix=unicode(interval[-1])
142
value=int(interval[:-1])
144
delta = datetime.timedelta(value)
146
delta = datetime.timedelta(0, value)
148
delta = datetime.timedelta(0, 0, 0, 0, value)
150
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
152
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
892
def server_state_changed(state):
893
"""Derived from the Avahi example code"""
894
if state == avahi.SERVER_COLLISION:
895
logger.error(u"Zeroconf server name collision")
897
elif state == avahi.SERVER_RUNNING:
901
def entry_group_state_changed(state, error):
902
"""Derived from the Avahi example code"""
903
logger.debug(u"Avahi state change: %i", state)
905
if state == avahi.ENTRY_GROUP_ESTABLISHED:
906
logger.debug(u"Zeroconf service established.")
907
elif state == avahi.ENTRY_GROUP_COLLISION:
908
logger.warning(u"Zeroconf service name collision.")
910
elif state == avahi.ENTRY_GROUP_FAILURE:
911
logger.critical(u"Avahi: Error in group state changed %s",
913
raise AvahiGroupError(u"State changed: %s" % unicode(error))
915
def if_nametoindex(interface):
916
"""Call the C function if_nametoindex(), or equivalent"""
917
global if_nametoindex
919
if_nametoindex = (ctypes.cdll.LoadLibrary
920
(ctypes.util.find_library("c"))
922
except (OSError, AttributeError):
923
if "struct" not in sys.modules:
925
if "fcntl" not in sys.modules:
927
def if_nametoindex(interface):
928
"Get an interface index the hard way, i.e. using fcntl()"
929
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
930
with closing(socket.socket()) as s:
931
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
932
struct.pack("16s16x", interface))
933
interface_index = struct.unpack("I", ifreq[16:20])[0]
934
return interface_index
935
return if_nametoindex(interface)
938
def daemon(nochdir = False, noclose = False):
939
"""See daemon(3). Standard BSD Unix function.
940
This should really exist as os.daemon, but it doesn't (yet)."""
949
# Close all standard open file descriptors
950
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
951
if not stat.S_ISCHR(os.fstat(null).st_mode):
952
raise OSError(errno.ENODEV,
953
"/dev/null not a character device")
954
os.dup2(null, sys.stdin.fileno())
955
os.dup2(null, sys.stdout.fileno())
956
os.dup2(null, sys.stderr.fileno())
155
except (ValueError, IndexError):
962
parser = optparse.OptionParser(version = "%%prog %s" % version)
161
parser = OptionParser()
963
162
parser.add_option("-i", "--interface", type="string",
964
metavar="IF", help="Bind to interface IF")
965
parser.add_option("-a", "--address", type="string",
966
help="Address to listen for requests on")
967
parser.add_option("-p", "--port", type="int",
163
default="eth0", metavar="IF",
164
help="Interface to bind to")
165
parser.add_option("--cert", type="string", default="cert.pem",
167
help="Public key certificate to use")
168
parser.add_option("--key", type="string", default="key.pem",
170
help="Private key to use")
171
parser.add_option("--ca", type="string", default="ca.pem",
173
help="Certificate Authority certificate to use")
174
parser.add_option("--crl", type="string", default="crl.pem",
176
help="Certificate Revokation List to use")
177
parser.add_option("-p", "--port", type="int", default=49001,
968
178
help="Port number to receive requests on")
969
parser.add_option("--check", action="store_true",
179
parser.add_option("--dh", type="int", metavar="BITS",
180
help="DH group to use")
181
parser.add_option("-t", "--timeout", type="string", # Parsed later
183
help="Amount of downtime allowed for clients")
184
parser.add_option("--interval", type="string", # Parsed later
186
help="How often to check that a client is up")
187
parser.add_option("--check", action="store_true", default=False,
970
188
help="Run self-test")
971
parser.add_option("--debug", action="store_true",
972
help="Debug mode; run in foreground and log to"
974
parser.add_option("--priority", type="string", help="GnuTLS"
975
" priority string (see GnuTLS documentation)")
976
parser.add_option("--servicename", type="string", metavar="NAME",
977
help="Zeroconf service name")
978
parser.add_option("--configdir", type="string",
979
default="/etc/mandos", metavar="DIR",
980
help="Directory to search for configuration"
982
parser.add_option("--no-dbus", action="store_false",
984
help="Do not provide D-Bus system bus"
986
options = parser.parse_args()[0]
189
(options, args) = parser.parse_args()
988
191
if options.check:
990
193
doctest.testmod()
993
# Default values for config file for server-global settings
994
server_defaults = { "interface": "",
999
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1000
"servicename": "Mandos",
1004
# Parse config file for server-global settings
1005
server_config = ConfigParser.SafeConfigParser(server_defaults)
1007
server_config.read(os.path.join(options.configdir, "mandos.conf"))
1008
# Convert the SafeConfigParser object to a dict
1009
server_settings = server_config.defaults()
1010
# Use the appropriate methods on the non-string config options
1011
server_settings["debug"] = server_config.getboolean("DEFAULT",
1013
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
1015
if server_settings["port"]:
1016
server_settings["port"] = server_config.getint("DEFAULT",
1020
# Override the settings from the config file with command line
1022
for option in ("interface", "address", "port", "debug",
1023
"priority", "servicename", "configdir",
1025
value = getattr(options, option)
1026
if value is not None:
1027
server_settings[option] = value
1029
# Now we have our good server settings in "server_settings"
1032
debug = server_settings["debug"]
1033
use_dbus = server_settings["use_dbus"]
1036
syslogger.setLevel(logging.WARNING)
1037
console.setLevel(logging.WARNING)
1039
if server_settings["servicename"] != "Mandos":
1040
syslogger.setFormatter(logging.Formatter
1041
('Mandos (%s): %%(levelname)s:'
1043
% server_settings["servicename"]))
1045
# Parse config file with clients
1046
client_defaults = { "timeout": "1h",
1048
"checker": "fping -q -- %%(host)s",
1051
client_config = ConfigParser.SafeConfigParser(client_defaults)
1052
client_config.read(os.path.join(server_settings["configdir"],
1056
tcp_server = IPv6_TCPServer((server_settings["address"],
1057
server_settings["port"]),
1059
settings=server_settings,
1061
pidfilename = "/var/run/mandos.pid"
1063
pidfile = open(pidfilename, "w")
1064
except IOError, error:
1065
logger.error("Could not open file %r", pidfilename)
1068
uid = pwd.getpwnam("_mandos").pw_uid
1069
gid = pwd.getpwnam("_mandos").pw_gid
1072
uid = pwd.getpwnam("mandos").pw_uid
1073
gid = pwd.getpwnam("mandos").pw_gid
1076
uid = pwd.getpwnam("nobody").pw_uid
1077
gid = pwd.getpwnam("nogroup").pw_gid
1084
except OSError, error:
1085
if error[0] != errno.EPERM:
1089
service = AvahiService(name = server_settings["servicename"],
1090
servicetype = "_mandos._tcp", )
1091
if server_settings["interface"]:
1092
service.interface = (if_nametoindex
1093
(server_settings["interface"]))
1098
# From the Avahi example code
1099
DBusGMainLoop(set_as_default=True )
1100
main_loop = gobject.MainLoop()
1101
bus = dbus.SystemBus()
1102
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1103
avahi.DBUS_PATH_SERVER),
1104
avahi.DBUS_INTERFACE_SERVER)
1105
# End of Avahi example code
1107
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1109
clients.update(Set(Client(name = section,
1111
= dict(client_config.items(section)),
1112
use_dbus = use_dbus)
1113
for section in client_config.sections()))
1115
logger.warning(u"No clients defined")
1118
# Redirect stdin so all checkers get /dev/null
1119
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1120
os.dup2(null, sys.stdin.fileno())
1124
# No console logging
1125
logger.removeHandler(console)
1126
# Close all input and output, do double fork, etc.
1131
pidfile.write(str(pid) + "\n")
1135
logger.error(u"Could not write to file %r with PID %d",
1138
# "pidfile" was never created
1143
"Cleanup function; run on exit"
1145
# From the Avahi example code
1146
if not group is None:
1149
# End of Avahi example code
1152
client = clients.pop()
1153
client.disable_hook = None
1156
atexit.register(cleanup)
1159
signal.signal(signal.SIGINT, signal.SIG_IGN)
1160
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1161
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1164
class MandosServer(dbus.service.Object):
1165
"""A D-Bus proxy object"""
1167
dbus.service.Object.__init__(self, bus, "/")
1168
_interface = u"se.bsnet.fukt.Mandos"
1170
@dbus.service.signal(_interface, signature="oa{sv}")
1171
def ClientAdded(self, objpath, properties):
1175
@dbus.service.signal(_interface, signature="os")
1176
def ClientRemoved(self, objpath, name):
1180
@dbus.service.method(_interface, out_signature="ao")
1181
def GetAllClients(self):
1183
return dbus.Array(c.dbus_object_path for c in clients)
1185
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1186
def GetAllClientsWithProperties(self):
1188
return dbus.Dictionary(
1189
((c.dbus_object_path, c.GetAllProperties())
1193
@dbus.service.method(_interface, in_signature="o")
1194
def RemoveClient(self, object_path):
1197
if c.dbus_object_path == object_path:
1199
# Don't signal anything except ClientRemoved
1203
self.ClientRemoved(object_path, c.name)
1209
mandos_server = MandosServer()
1211
for client in clients:
1214
mandos_server.ClientAdded(client.dbus_object_path,
1215
client.GetAllProperties())
1219
tcp_server.server_activate()
1221
# Find out what port we got
1222
service.port = tcp_server.socket.getsockname()[1]
1223
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1224
u" scope_id %d" % tcp_server.socket.getsockname())
1226
#service.interface = tcp_server.socket.getsockname()[3]
1229
# From the Avahi example code
1230
server.connect_to_signal("StateChanged", server_state_changed)
1232
server_state_changed(server.GetState())
1233
except dbus.exceptions.DBusException, error:
1234
logger.critical(u"DBusException: %s", error)
1236
# End of Avahi example code
1238
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1239
lambda *args, **kwargs:
1240
(tcp_server.handle_request
1241
(*args[2:], **kwargs) or True))
1243
logger.debug(u"Starting main loop")
1245
except AvahiError, error:
1246
logger.critical(u"AvahiError: %s", error)
1248
except KeyboardInterrupt:
1252
if __name__ == '__main__':
196
# Parse the time arguments
198
options.timeout = string_to_delta(options.timeout)
200
parser.error("option --timeout: Unparseable time")
203
options.interval = string_to_delta(options.interval)
205
parser.error("option --interval: Unparseable time")
207
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
208
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
209
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
210
crl = gnutls.crypto.X509CRL(open(options.crl).read())
211
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
215
client_config_object = ConfigParser.SafeConfigParser(defaults)
216
client_config_object.read("mandos-clients.conf")
217
clients = [Client(name=section, options=options,
218
**(dict(client_config_object.items(section))))
219
for section in client_config_object.sections()]
221
udp_server = IPv6_UDPServer((in6addr_any, options.port),
225
tcp_server = IPv6_TCPServer((in6addr_any, options.port),
232
in_, out, err = select.select((udp_server,
235
server.handle_request()
238
if __name__ == "__main__":