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:
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()
599
cert_list = (gnutls.library.functions
600
.gnutls_certificate_get_peers
601
(session._c_object, ctypes.byref(list_size)))
602
if list_size.value == 0:
605
return ctypes.string_at(cert.data, cert.size)
608
def fingerprint(openpgp):
609
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
610
# New GnuTLS "datum" with the OpenPGP public key
611
datum = (gnutls.library.types
612
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
615
ctypes.c_uint(len(openpgp))))
616
# New empty GnuTLS certificate
617
crt = gnutls.library.types.gnutls_openpgp_crt_t()
618
(gnutls.library.functions
619
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
620
# Import the OpenPGP public key into the certificate
621
(gnutls.library.functions
622
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
623
gnutls.library.constants
624
.GNUTLS_OPENPGP_FMT_RAW))
625
# Verify the self signature in the key
626
crtverify = ctypes.c_uint()
627
(gnutls.library.functions
628
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
629
if crtverify.value != 0:
630
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
631
raise gnutls.errors.CertificateSecurityError("Verify failed")
632
# New buffer for the fingerprint
633
buf = ctypes.create_string_buffer(20)
634
buf_len = ctypes.c_size_t()
635
# Get the fingerprint from the certificate into the buffer
636
(gnutls.library.functions
637
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
638
ctypes.byref(buf_len)))
639
# Deinit the certificate
640
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
641
# Convert the buffer to a Python bytestring
642
fpr = ctypes.string_at(buf, buf_len.value)
643
# Convert the bytestring to hexadecimal notation
644
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
648
class TCP_handler(SocketServer.BaseRequestHandler, object):
649
"""A TCP request handler class.
650
Instantiated by IPv6_TCPServer for each request to handle it.
651
Note: This will run in its own forked process."""
654
logger.info(u"TCP connection from: %s",
655
unicode(self.client_address))
656
session = (gnutls.connection
657
.ClientSession(self.request,
661
line = self.request.makefile().readline()
662
logger.debug(u"Protocol version: %r", line)
664
if int(line.strip().split()[0]) > 1:
666
except (ValueError, IndexError, RuntimeError), error:
667
logger.error(u"Unknown protocol version: %s", error)
670
# Note: gnutls.connection.X509Credentials is really a generic
671
# GnuTLS certificate credentials object so long as no X.509
672
# keys are added to it. Therefore, we can use it here despite
673
# using OpenPGP certificates.
675
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
676
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
678
# Use a fallback default, since this MUST be set.
679
priority = self.server.settings.get("priority", "NORMAL")
680
(gnutls.library.functions
681
.gnutls_priority_set_direct(session._c_object,
686
except gnutls.errors.GNUTLSError, error:
687
logger.warning(u"Handshake failed: %s", error)
688
# Do not run session.bye() here: the session is not
689
# established. Just abandon the request.
692
fpr = fingerprint(peer_certificate(session))
693
except (TypeError, gnutls.errors.GNUTLSError), error:
694
logger.warning(u"Bad certificate: %s", error)
697
logger.debug(u"Fingerprint: %s", fpr)
698
for c in self.server.clients:
699
if c.fingerprint == fpr:
703
logger.warning(u"Client not found for fingerprint: %s",
707
# Have to check if client.still_valid(), since it is possible
708
# that the client timed out while establishing the GnuTLS
710
if not client.still_valid():
711
logger.warning(u"Client %(name)s is invalid",
715
## This won't work here, since we're in a fork.
716
# client.checked_ok()
718
while sent_size < len(client.secret):
719
sent = session.send(client.secret[sent_size:])
720
logger.debug(u"Sent: %d, remaining: %d",
721
sent, len(client.secret)
722
- (sent_size + sent))
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
110
session.send(dict((client.dn, client.password)
111
for client in self.server.clients)
112
[session.peer_certificate.subject])
114
session.send("gazonk")
727
class IPv6_TCPServer(SocketServer.ForkingMixIn,
728
SocketServer.TCPServer, object):
729
"""IPv6 TCP server. Accepts 'None' as address and/or port.
731
settings: Server settings
732
clients: Set() of Client objects
733
enabled: Boolean; whether this server is activated yet
735
address_family = socket.AF_INET6
736
def __init__(self, *args, **kwargs):
737
if "settings" in kwargs:
738
self.settings = kwargs["settings"]
739
del kwargs["settings"]
740
if "clients" in kwargs:
741
self.clients = kwargs["clients"]
742
del kwargs["clients"]
744
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
745
def server_bind(self):
746
"""This overrides the normal server_bind() function
747
to bind to an interface if one was specified, and also NOT to
748
bind to an address or port if they were not specified."""
749
if self.settings["interface"]:
750
# 25 is from /usr/include/asm-i486/socket.h
751
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
753
self.socket.setsockopt(socket.SOL_SOCKET,
755
self.settings["interface"])
756
except socket.error, error:
757
if error[0] == errno.EPERM:
758
logger.error(u"No permission to"
759
u" bind to interface %s",
760
self.settings["interface"])
763
# Only bind(2) the socket if we really need to.
764
if self.server_address[0] or self.server_address[1]:
765
if not self.server_address[0]:
767
self.server_address = (in6addr_any,
768
self.server_address[1])
769
elif not self.server_address[1]:
770
self.server_address = (self.server_address[0],
772
# if self.settings["interface"]:
773
# self.server_address = (self.server_address[0],
779
return super(IPv6_TCPServer, self).server_bind()
780
def server_activate(self):
782
return super(IPv6_TCPServer, self).server_activate()
119
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
120
__metaclass__ = server_metaclass
121
request_queue_size = 1024
787
128
def string_to_delta(interval):
788
129
"""Parse a string and return a datetime.timedelta
797
138
datetime.timedelta(1)
798
139
>>> string_to_delta(u'1w')
799
140
datetime.timedelta(7)
800
>>> string_to_delta('5m 30s')
801
datetime.timedelta(0, 330)
803
timevalue = datetime.timedelta(0)
804
for s in interval.split():
806
suffix = unicode(s[-1])
809
delta = datetime.timedelta(value)
811
delta = datetime.timedelta(0, value)
813
delta = datetime.timedelta(0, 0, 0, 0, value)
815
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
817
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
820
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)
826
def server_state_changed(state):
827
"""Derived from the Avahi example code"""
828
if state == avahi.SERVER_COLLISION:
829
logger.error(u"Zeroconf server name collision")
831
elif state == avahi.SERVER_RUNNING:
835
def entry_group_state_changed(state, error):
836
"""Derived from the Avahi example code"""
837
logger.debug(u"Avahi state change: %i", state)
839
if state == avahi.ENTRY_GROUP_ESTABLISHED:
840
logger.debug(u"Zeroconf service established.")
841
elif state == avahi.ENTRY_GROUP_COLLISION:
842
logger.warning(u"Zeroconf service name collision.")
844
elif state == avahi.ENTRY_GROUP_FAILURE:
845
logger.critical(u"Avahi: Error in group state changed %s",
847
raise AvahiGroupError(u"State changed: %s" % unicode(error))
849
def if_nametoindex(interface):
850
"""Call the C function if_nametoindex(), or equivalent"""
851
global if_nametoindex
853
if_nametoindex = (ctypes.cdll.LoadLibrary
854
(ctypes.util.find_library("c"))
856
except (OSError, AttributeError):
857
if "struct" not in sys.modules:
859
if "fcntl" not in sys.modules:
861
def if_nametoindex(interface):
862
"Get an interface index the hard way, i.e. using fcntl()"
863
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
864
with closing(socket.socket()) as s:
865
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
866
struct.pack("16s16x", interface))
867
interface_index = struct.unpack("I", ifreq[16:20])[0]
868
return interface_index
869
return if_nametoindex(interface)
872
def daemon(nochdir = False, noclose = False):
873
"""See daemon(3). Standard BSD Unix function.
874
This should really exist as os.daemon, but it doesn't (yet)."""
883
# Close all standard open file descriptors
884
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
885
if not stat.S_ISCHR(os.fstat(null).st_mode):
886
raise OSError(errno.ENODEV,
887
"/dev/null not a character device")
888
os.dup2(null, sys.stdin.fileno())
889
os.dup2(null, sys.stdout.fileno())
890
os.dup2(null, sys.stderr.fileno())
157
except (ValueError, IndexError):
896
parser = optparse.OptionParser(version = "%%prog %s" % version)
162
parser = OptionParser()
897
163
parser.add_option("-i", "--interface", type="string",
898
metavar="IF", help="Bind to interface IF")
899
parser.add_option("-a", "--address", type="string",
900
help="Address to listen for requests on")
901
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,
902
179
help="Port number to receive requests on")
903
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,
904
189
help="Run self-test")
905
parser.add_option("--debug", action="store_true",
906
help="Debug mode; run in foreground and log to"
908
parser.add_option("--priority", type="string", help="GnuTLS"
909
" priority string (see GnuTLS documentation)")
910
parser.add_option("--servicename", type="string", metavar="NAME",
911
help="Zeroconf service name")
912
parser.add_option("--configdir", type="string",
913
default="/etc/mandos", metavar="DIR",
914
help="Directory to search for configuration"
916
parser.add_option("--no-dbus", action="store_false",
918
help="Do not provide D-Bus system bus"
920
options = parser.parse_args()[0]
190
(options, args) = parser.parse_args()
922
192
if options.check:
924
194
doctest.testmod()
927
# Default values for config file for server-global settings
928
server_defaults = { "interface": "",
933
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
934
"servicename": "Mandos",
938
# Parse config file for server-global settings
939
server_config = ConfigParser.SafeConfigParser(server_defaults)
941
server_config.read(os.path.join(options.configdir, "mandos.conf"))
942
# Convert the SafeConfigParser object to a dict
943
server_settings = server_config.defaults()
944
# Use getboolean on the boolean config options
945
server_settings["debug"] = (server_config.getboolean
946
("DEFAULT", "debug"))
947
server_settings["use_dbus"] = (server_config.getboolean
948
("DEFAULT", "use_dbus"))
951
# Override the settings from the config file with command line
953
for option in ("interface", "address", "port", "debug",
954
"priority", "servicename", "configdir",
956
value = getattr(options, option)
957
if value is not None:
958
server_settings[option] = value
960
# Now we have our good server settings in "server_settings"
963
debug = server_settings["debug"]
964
use_dbus = server_settings["use_dbus"]
967
syslogger.setLevel(logging.WARNING)
968
console.setLevel(logging.WARNING)
970
if server_settings["servicename"] != "Mandos":
971
syslogger.setFormatter(logging.Formatter
972
('Mandos (%s): %%(levelname)s:'
974
% server_settings["servicename"]))
976
# Parse config file with clients
977
client_defaults = { "timeout": "1h",
979
"checker": "fping -q -- %%(host)s",
982
client_config = ConfigParser.SafeConfigParser(client_defaults)
983
client_config.read(os.path.join(server_settings["configdir"],
987
tcp_server = IPv6_TCPServer((server_settings["address"],
988
server_settings["port"]),
990
settings=server_settings,
992
pidfilename = "/var/run/mandos.pid"
994
pidfile = open(pidfilename, "w")
995
except IOError, error:
996
logger.error("Could not open file %r", pidfilename)
999
uid = pwd.getpwnam("_mandos").pw_uid
1000
gid = pwd.getpwnam("_mandos").pw_gid
1003
uid = pwd.getpwnam("mandos").pw_uid
1004
gid = pwd.getpwnam("mandos").pw_gid
1007
uid = pwd.getpwnam("nobody").pw_uid
1008
gid = pwd.getpwnam("nogroup").pw_gid
1015
except OSError, error:
1016
if error[0] != errno.EPERM:
1020
service = AvahiService(name = server_settings["servicename"],
1021
servicetype = "_mandos._tcp", )
1022
if server_settings["interface"]:
1023
service.interface = (if_nametoindex
1024
(server_settings["interface"]))
1029
# From the Avahi example code
1030
DBusGMainLoop(set_as_default=True )
1031
main_loop = gobject.MainLoop()
1032
bus = dbus.SystemBus()
1033
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1034
avahi.DBUS_PATH_SERVER),
1035
avahi.DBUS_INTERFACE_SERVER)
1036
# End of Avahi example code
1038
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1040
clients.update(Set(Client(name = section,
1042
= dict(client_config.items(section)),
1043
use_dbus = use_dbus)
1044
for section in client_config.sections()))
1046
logger.warning(u"No clients defined")
1049
# Redirect stdin so all checkers get /dev/null
1050
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1051
os.dup2(null, sys.stdin.fileno())
1055
# No console logging
1056
logger.removeHandler(console)
1057
# Close all input and output, do double fork, etc.
1062
pidfile.write(str(pid) + "\n")
1066
logger.error(u"Could not write to file %r with PID %d",
1069
# "pidfile" was never created
1074
"Cleanup function; run on exit"
1076
# From the Avahi example code
1077
if not group is None:
1080
# End of Avahi example code
1083
client = clients.pop()
1084
client.disable_hook = None
1087
atexit.register(cleanup)
1090
signal.signal(signal.SIGINT, signal.SIG_IGN)
1091
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1092
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1095
class MandosServer(dbus.service.Object):
1096
"""A D-Bus proxy object"""
1098
dbus.service.Object.__init__(self, bus, "/")
1099
_interface = u"se.bsnet.fukt.Mandos"
1101
@dbus.service.signal(_interface, signature="oa{sv}")
1102
def ClientAdded(self, objpath, properties):
1106
@dbus.service.signal(_interface, signature="os")
1107
def ClientRemoved(self, objpath, name):
1111
@dbus.service.method(_interface, out_signature="ao")
1112
def GetAllClients(self):
1113
return dbus.Array(c.dbus_object_path for c in clients)
1115
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1116
def GetAllClientsWithProperties(self):
1117
return dbus.Dictionary(
1118
((c.dbus_object_path, c.GetAllProperties())
1122
@dbus.service.method(_interface, in_signature="o")
1123
def RemoveClient(self, object_path):
1125
if c.dbus_object_path == object_path:
1127
# Don't signal anything except ClientRemoved
1131
self.ClientRemoved(object_path, c.name)
1137
mandos_server = MandosServer()
1139
for client in clients:
1142
mandos_server.ClientAdded(client.dbus_object_path,
1143
client.GetAllProperties())
1147
tcp_server.server_activate()
1149
# Find out what port we got
1150
service.port = tcp_server.socket.getsockname()[1]
1151
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1152
u" scope_id %d" % tcp_server.socket.getsockname())
1154
#service.interface = tcp_server.socket.getsockname()[3]
1157
# From the Avahi example code
1158
server.connect_to_signal("StateChanged", server_state_changed)
1160
server_state_changed(server.GetState())
1161
except dbus.exceptions.DBusException, error:
1162
logger.critical(u"DBusException: %s", error)
1164
# End of Avahi example code
1166
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1167
lambda *args, **kwargs:
1168
(tcp_server.handle_request
1169
(*args[2:], **kwargs) or True))
1171
logger.debug(u"Starting main loop")
1173
except AvahiError, error:
1174
logger.critical(u"AvahiError: %s", error)
1176
except KeyboardInterrupt:
1180
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__":