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 = use_dbus
231
self.dbus_object_path = (dbus.ObjectPath
233
+ self.name.replace(".", "_")))
234
dbus.service.Object.__init__(self, bus,
235
self.dbus_object_path)
236
# Uppercase and remove spaces from fingerprint for later
237
# comparison purposes with return value from the fingerprint()
239
self.fingerprint = (config["fingerprint"].upper()
241
logger.debug(u" Fingerprint: %s", self.fingerprint)
242
if "secret" in config:
243
self.secret = config["secret"].decode(u"base64")
244
elif "secfile" in config:
245
with closing(open(os.path.expanduser
247
(config["secfile"])))) as secfile:
248
self.secret = secfile.read()
250
raise TypeError(u"No secret or secfile for client %s"
252
self.host = config.get("host", "")
253
self.created = datetime.datetime.utcnow()
255
self.last_enabled = None
256
self.last_checked_ok = None
257
self.timeout = string_to_delta(config["timeout"])
258
self.interval = string_to_delta(config["interval"])
259
self.disable_hook = disable_hook
261
self.checker_initiator_tag = None
262
self.disable_initiator_tag = None
263
self.checker_callback_tag = None
264
self.checker_command = config["checker"]
267
"""Start this client's checker and timeout hooks"""
268
self.last_enabled = datetime.datetime.utcnow()
269
# Schedule a new checker to be started an 'interval' from now,
270
# and every interval from then on.
271
self.checker_initiator_tag = (gobject.timeout_add
272
(self.interval_milliseconds(),
274
# Also start a new checker *right now*.
276
# Schedule a disable() when 'timeout' has passed
277
self.disable_initiator_tag = (gobject.timeout_add
278
(self.timeout_milliseconds(),
283
self.PropertyChanged(dbus.String(u"enabled"),
284
dbus.Boolean(True, variant_level=1))
285
self.PropertyChanged(dbus.String(u"last_enabled"),
286
(_datetime_to_dbus(self.last_enabled,
290
"""Disable this client."""
291
if not getattr(self, "enabled", False):
293
logger.info(u"Disabling client %s", self.name)
294
if getattr(self, "disable_initiator_tag", False):
295
gobject.source_remove(self.disable_initiator_tag)
296
self.disable_initiator_tag = None
297
if getattr(self, "checker_initiator_tag", False):
298
gobject.source_remove(self.checker_initiator_tag)
299
self.checker_initiator_tag = None
301
if self.disable_hook:
302
self.disable_hook(self)
306
self.PropertyChanged(dbus.String(u"enabled"),
307
dbus.Boolean(False, variant_level=1))
308
# Do not run this again if called by a gobject.timeout_add
312
self.disable_hook = None
315
def checker_callback(self, pid, condition, command):
316
"""The checker has completed, so take appropriate actions."""
317
self.checker_callback_tag = None
321
self.PropertyChanged(dbus.String(u"checker_running"),
322
dbus.Boolean(False, variant_level=1))
323
if (os.WIFEXITED(condition)
324
and (os.WEXITSTATUS(condition) == 0)):
325
logger.info(u"Checker for %(name)s succeeded",
329
self.CheckerCompleted(dbus.Boolean(True),
330
dbus.UInt16(condition),
331
dbus.String(command))
333
elif not os.WIFEXITED(condition):
334
logger.warning(u"Checker for %(name)s crashed?",
338
self.CheckerCompleted(dbus.Boolean(False),
339
dbus.UInt16(condition),
340
dbus.String(command))
342
logger.info(u"Checker for %(name)s failed",
346
self.CheckerCompleted(dbus.Boolean(False),
347
dbus.UInt16(condition),
348
dbus.String(command))
350
def bump_timeout(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())
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"org.mandos_system.Mandos.Client"
454
# BumpTimeout - method
455
BumpTimeout = dbus.service.method(_interface)(bump_timeout)
456
BumpTimeout.__name__ = "BumpTimeout"
458
# CheckerCompleted - signal
459
@dbus.service.signal(_interface, signature="bqs")
460
def CheckerCompleted(self, success, condition, 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.bump_timeout()
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))
15
def __init__(self, name=None, dn=None, password=None,
16
passfile=None, fqdn=None, timeout=None,
21
self.password = password
23
self.password = open(passfile).readall()
25
print "No Password or Passfile in client config file"
26
# raise RuntimeError XXX
27
self.password = "gazonk"
32
timeout = self.server.options.timeout
33
self.timeout = timeout
35
interval = self.server.options.interval
36
self.interval = interval
38
def server_bind(self):
39
if self.options.interface:
40
if not hasattr(socket, "SO_BINDTODEVICE"):
41
# From /usr/include/asm-i486/socket.h
42
socket.SO_BINDTODEVICE = 25
44
self.socket.setsockopt(socket.SOL_SOCKET,
45
socket.SO_BINDTODEVICE,
46
self.options.interface)
47
except socket.error, error:
48
if error[0] == errno.EPERM:
49
print "Warning: Denied permission to bind to interface", \
50
self.options.interface
53
return super(type(self), self).server_bind()
56
def init_with_options(self, *args, **kwargs):
57
if "options" in kwargs:
58
self.options = kwargs["options"]
60
if "clients" in kwargs:
61
self.clients = kwargs["clients"]
63
if "credentials" in kwargs:
64
self.credentials = kwargs["credentials"]
65
del kwargs["credentials"]
66
return super(type(self), self).__init__(*args, **kwargs)
69
class udp_handler(SocketServer.DatagramRequestHandler, object):
71
self.wfile.write("Polo")
72
print "UDP request answered"
75
class IPv6_UDPServer(SocketServer.UDPServer, object):
76
__init__ = init_with_options
77
address_family = socket.AF_INET6
78
allow_reuse_address = True
79
server_bind = server_bind
80
def verify_request(self, request, client_address):
81
print "UDP request came"
82
return request[0] == "Marco"
85
class tcp_handler(SocketServer.BaseRequestHandler, object):
87
print "TCP request came"
88
print "Request:", self.request
89
print "Client Address:", self.client_address
90
print "Server:", self.server
91
session = gnutls.connection.ServerSession(self.request,
92
self.server.credentials)
94
if session.peer_certificate:
95
print "DN:", session.peer_certificate.subject
98
except gnutls.errors.CertificateError, error:
99
print "Verify failed", error
103
session.send(dict((client.dn, client.password)
104
for client in self.server.clients)
105
[session.peer_certificate.subject])
107
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
111
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
112
__init__ = init_with_options
735
113
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()
787
def string_to_delta(interval):
788
"""Parse a string and return a datetime.timedelta
790
>>> string_to_delta('7d')
791
datetime.timedelta(7)
792
>>> string_to_delta('60s')
793
datetime.timedelta(0, 60)
794
>>> string_to_delta('60m')
795
datetime.timedelta(0, 3600)
796
>>> string_to_delta('24h')
797
datetime.timedelta(1)
798
>>> string_to_delta(u'1w')
799
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):
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())
114
allow_reuse_address = True
115
request_queue_size = 1024
116
server_bind = server_bind
896
parser = optparse.OptionParser(version = "%%prog %s" % version)
124
parser = OptionParser()
897
125
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",
126
default="eth0", metavar="IF",
127
help="Interface to bind to")
128
parser.add_option("--cert", type="string", default="cert.pem",
130
help="Public key certificate to use")
131
parser.add_option("--key", type="string", default="key.pem",
133
help="Private key to use")
134
parser.add_option("--ca", type="string", default="ca.pem",
136
help="Certificate Authority certificate to use")
137
parser.add_option("--crl", type="string", default="crl.pem",
139
help="Certificate Revokation List to use")
140
parser.add_option("-p", "--port", type="int", default=49001,
902
141
help="Port number to receive requests on")
903
parser.add_option("--check", action="store_true",
904
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]
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"org.mandos-system.Mandos",
1041
clients.update(Set(Client(name = section,
1043
= dict(client_config.items(section)),
1044
use_dbus = use_dbus)
1045
for section in client_config.sections()))
1047
logger.warning(u"No clients defined")
1050
# Redirect stdin so all checkers get /dev/null
1051
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1052
os.dup2(null, sys.stdin.fileno())
1056
# No console logging
1057
logger.removeHandler(console)
1058
# Close all input and output, do double fork, etc.
1063
pidfile.write(str(pid) + "\n")
1067
logger.error(u"Could not write to file %r with PID %d",
1070
# "pidfile" was never created
1075
"Cleanup function; run on exit"
1077
# From the Avahi example code
1078
if not group is None:
1081
# End of Avahi example code
1084
client = clients.pop()
1085
client.disable_hook = None
1088
atexit.register(cleanup)
1091
signal.signal(signal.SIGINT, signal.SIG_IGN)
1092
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1093
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1096
class MandosServer(dbus.service.Object):
1097
"""A D-Bus proxy object"""
1099
dbus.service.Object.__init__(self, bus,
1101
_interface = u"org.mandos_system.Mandos"
1103
@dbus.service.signal(_interface, signature="oa{sv}")
1104
def ClientAdded(self, objpath, properties):
1108
@dbus.service.signal(_interface, signature="os")
1109
def ClientRemoved(self, objpath, name):
1113
@dbus.service.method(_interface, out_signature="ao")
1114
def GetAllClients(self):
1115
return dbus.Array(c.dbus_object_path for c in clients)
1117
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1118
def GetAllClientsWithProperties(self):
1119
return dbus.Dictionary(
1120
((c.dbus_object_path, c.GetAllProperties())
1124
@dbus.service.method(_interface, in_signature="o")
1125
def RemoveClient(self, object_path):
1127
if c.dbus_object_path == object_path:
1129
# Don't signal anything except ClientRemoved
1133
self.ClientRemoved(object_path, c.name)
1137
@dbus.service.method(_interface, in_signature="s")
1138
def RemoveClientByName(self, name):
1142
# Don't signal anything except ClientRemoved
1146
self.ClientRemoved(c.dbus_object_path, name)
1150
@dbus.service.method(_interface)
1156
mandos_server = MandosServer()
1158
for client in clients:
1161
mandos_server.ClientAdded(client.dbus_object_path,
1162
client.GetAllProperties())
1166
tcp_server.server_activate()
1168
# Find out what port we got
1169
service.port = tcp_server.socket.getsockname()[1]
1170
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1171
u" scope_id %d" % tcp_server.socket.getsockname())
1173
#service.interface = tcp_server.socket.getsockname()[3]
1176
# From the Avahi example code
1177
server.connect_to_signal("StateChanged", server_state_changed)
1179
server_state_changed(server.GetState())
1180
except dbus.exceptions.DBusException, error:
1181
logger.critical(u"DBusException: %s", error)
1183
# End of Avahi example code
1185
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1186
lambda *args, **kwargs:
1187
(tcp_server.handle_request
1188
(*args[2:], **kwargs) or True))
1190
logger.debug(u"Starting main loop")
1192
except AvahiError, error:
1193
logger.critical(u"AvahiError: %s", error)
1195
except KeyboardInterrupt:
1199
if __name__ == '__main__':
142
parser.add_option("--dh", type="int", metavar="BITS",
143
help="DH group to use")
144
parser.add_option("-t", "--timeout", type="string", # Parsed later
146
help="Amount of downtime allowed for clients")
147
(options, args) = parser.parse_args()
149
# Parse the time argument
151
suffix=options.timeout[-1]
152
value=int(options.timeout[:-1])
154
options.timeout = datetime.timedelta(value)
156
options.timeout = datetime.timedelta(0, value)
158
options.timeout = datetime.timedelta(0, 0, 0, 0, value)
160
options.timeout = datetime.timedelta(0, 0, 0, 0, 0, value)
162
options.timeout = datetime.timedelta(0, 0, 0, 0, 0, 0,
166
except (ValueError, IndexError):
167
parser.error("option --timeout: Unparseable time")
169
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
170
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
171
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
172
crl = gnutls.crypto.X509CRL(open(options.crl).read())
173
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
177
client_config_object = ConfigParser.SafeConfigParser(defaults)
178
client_config_object.read("mandos-clients.conf")
179
clients = [Client(name=section,
180
**(dict(client_config_object.items(section))))
181
for section in client_config_object.sections()]
183
udp_server = IPv6_UDPServer((in6addr_any, options.port),
187
tcp_server = IPv6_TCPServer((in6addr_any, options.port),
194
in_, out, err = select.select((udp_server,
197
server.handle_request()
200
if __name__ == "__main__":