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()
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()
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",
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
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())
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(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
session = (gnutls.connection
660
.ClientSession(self.request,
664
line = self.request.makefile().readline()
665
logger.debug(u"Protocol version: %r", line)
667
if int(line.strip().split()[0]) > 1:
669
except (ValueError, IndexError, RuntimeError), error:
670
logger.error(u"Unknown protocol version: %s", error)
673
# Note: gnutls.connection.X509Credentials is really a generic
674
# GnuTLS certificate credentials object so long as no X.509
675
# keys are added to it. Therefore, we can use it here despite
676
# using OpenPGP certificates.
678
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
679
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
681
# Use a fallback default, since this MUST be set.
682
priority = self.server.settings.get("priority", "NORMAL")
683
(gnutls.library.functions
684
.gnutls_priority_set_direct(session._c_object,
689
except gnutls.errors.GNUTLSError, error:
690
logger.warning(u"Handshake failed: %s", error)
691
# Do not run session.bye() here: the session is not
692
# established. Just abandon the request.
694
logger.debug(u"Handshake succeeded")
696
fpr = fingerprint(peer_certificate(session))
697
except (TypeError, gnutls.errors.GNUTLSError), error:
698
logger.warning(u"Bad certificate: %s", error)
701
logger.debug(u"Fingerprint: %s", fpr)
702
for c in self.server.clients:
703
if c.fingerprint == fpr:
707
logger.warning(u"Client not found for fingerprint: %s",
711
# Have to check if client.still_valid(), since it is possible
712
# that the client timed out while establishing the GnuTLS
714
if not client.still_valid():
715
logger.warning(u"Client %(name)s is invalid",
719
## This won't work here, since we're in a fork.
720
# client.checked_ok()
722
while sent_size < len(client.secret):
723
sent = session.send(client.secret[sent_size:])
724
logger.debug(u"Sent: %d, remaining: %d",
725
sent, len(client.secret)
726
- (sent_size + sent))
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")
731
class IPv6_TCPServer(SocketServer.ForkingMixIn,
732
SocketServer.TCPServer, object):
733
"""IPv6 TCP server. Accepts 'None' as address and/or port.
735
settings: Server settings
736
clients: Set() of Client objects
737
enabled: Boolean; whether this server is activated yet
111
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
112
__init__ = init_with_options
739
113
address_family = socket.AF_INET6
740
def __init__(self, *args, **kwargs):
741
if "settings" in kwargs:
742
self.settings = kwargs["settings"]
743
del kwargs["settings"]
744
if "clients" in kwargs:
745
self.clients = kwargs["clients"]
746
del kwargs["clients"]
748
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
749
def server_bind(self):
750
"""This overrides the normal server_bind() function
751
to bind to an interface if one was specified, and also NOT to
752
bind to an address or port if they were not specified."""
753
if self.settings["interface"]:
754
# 25 is from /usr/include/asm-i486/socket.h
755
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
757
self.socket.setsockopt(socket.SOL_SOCKET,
759
self.settings["interface"])
760
except socket.error, error:
761
if error[0] == errno.EPERM:
762
logger.error(u"No permission to"
763
u" bind to interface %s",
764
self.settings["interface"])
767
# Only bind(2) the socket if we really need to.
768
if self.server_address[0] or self.server_address[1]:
769
if not self.server_address[0]:
771
self.server_address = (in6addr_any,
772
self.server_address[1])
773
elif not self.server_address[1]:
774
self.server_address = (self.server_address[0],
776
# if self.settings["interface"]:
777
# self.server_address = (self.server_address[0],
783
return super(IPv6_TCPServer, self).server_bind()
784
def server_activate(self):
786
return super(IPv6_TCPServer, self).server_activate()
791
def string_to_delta(interval):
792
"""Parse a string and return a datetime.timedelta
794
>>> string_to_delta('7d')
795
datetime.timedelta(7)
796
>>> string_to_delta('60s')
797
datetime.timedelta(0, 60)
798
>>> string_to_delta('60m')
799
datetime.timedelta(0, 3600)
800
>>> string_to_delta('24h')
801
datetime.timedelta(1)
802
>>> string_to_delta(u'1w')
803
datetime.timedelta(7)
804
>>> string_to_delta('5m 30s')
805
datetime.timedelta(0, 330)
807
timevalue = datetime.timedelta(0)
808
for s in interval.split():
810
suffix = unicode(s[-1])
813
delta = datetime.timedelta(value)
815
delta = datetime.timedelta(0, value)
817
delta = datetime.timedelta(0, 0, 0, 0, value)
819
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
821
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
824
except (ValueError, IndexError):
830
def server_state_changed(state):
831
"""Derived from the Avahi example code"""
832
if state == avahi.SERVER_COLLISION:
833
logger.error(u"Zeroconf server name collision")
835
elif state == avahi.SERVER_RUNNING:
839
def entry_group_state_changed(state, error):
840
"""Derived from the Avahi example code"""
841
logger.debug(u"Avahi state change: %i", state)
843
if state == avahi.ENTRY_GROUP_ESTABLISHED:
844
logger.debug(u"Zeroconf service established.")
845
elif state == avahi.ENTRY_GROUP_COLLISION:
846
logger.warning(u"Zeroconf service name collision.")
848
elif state == avahi.ENTRY_GROUP_FAILURE:
849
logger.critical(u"Avahi: Error in group state changed %s",
851
raise AvahiGroupError(u"State changed: %s" % unicode(error))
853
def if_nametoindex(interface):
854
"""Call the C function if_nametoindex(), or equivalent"""
855
global if_nametoindex
857
if_nametoindex = (ctypes.cdll.LoadLibrary
858
(ctypes.util.find_library("c"))
860
except (OSError, AttributeError):
861
if "struct" not in sys.modules:
863
if "fcntl" not in sys.modules:
865
def if_nametoindex(interface):
866
"Get an interface index the hard way, i.e. using fcntl()"
867
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
868
with closing(socket.socket()) as s:
869
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
870
struct.pack("16s16x", interface))
871
interface_index = struct.unpack("I", ifreq[16:20])[0]
872
return interface_index
873
return if_nametoindex(interface)
876
def daemon(nochdir = False, noclose = False):
877
"""See daemon(3). Standard BSD Unix function.
878
This should really exist as os.daemon, but it doesn't (yet)."""
887
# Close all standard open file descriptors
888
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
889
if not stat.S_ISCHR(os.fstat(null).st_mode):
890
raise OSError(errno.ENODEV,
891
"/dev/null not a character device")
892
os.dup2(null, sys.stdin.fileno())
893
os.dup2(null, sys.stdout.fileno())
894
os.dup2(null, sys.stderr.fileno())
114
allow_reuse_address = True
115
request_queue_size = 1024
116
server_bind = server_bind
900
parser = optparse.OptionParser(version = "%%prog %s" % version)
124
parser = OptionParser()
901
125
parser.add_option("-i", "--interface", type="string",
902
metavar="IF", help="Bind to interface IF")
903
parser.add_option("-a", "--address", type="string",
904
help="Address to listen for requests on")
905
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,
906
141
help="Port number to receive requests on")
907
parser.add_option("--check", action="store_true",
908
help="Run self-test")
909
parser.add_option("--debug", action="store_true",
910
help="Debug mode; run in foreground and log to"
912
parser.add_option("--priority", type="string", help="GnuTLS"
913
" priority string (see GnuTLS documentation)")
914
parser.add_option("--servicename", type="string", metavar="NAME",
915
help="Zeroconf service name")
916
parser.add_option("--configdir", type="string",
917
default="/etc/mandos", metavar="DIR",
918
help="Directory to search for configuration"
920
parser.add_option("--no-dbus", action="store_false",
922
help="Do not provide D-Bus system bus"
924
options = parser.parse_args()[0]
931
# Default values for config file for server-global settings
932
server_defaults = { "interface": "",
937
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
938
"servicename": "Mandos",
942
# Parse config file for server-global settings
943
server_config = ConfigParser.SafeConfigParser(server_defaults)
945
server_config.read(os.path.join(options.configdir, "mandos.conf"))
946
# Convert the SafeConfigParser object to a dict
947
server_settings = server_config.defaults()
948
# Use the appropriate methods on the non-string config options
949
server_settings["debug"] = server_config.getboolean("DEFAULT",
951
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
953
if server_settings["port"]:
954
server_settings["port"] = server_config.getint("DEFAULT",
958
# Override the settings from the config file with command line
960
for option in ("interface", "address", "port", "debug",
961
"priority", "servicename", "configdir",
963
value = getattr(options, option)
964
if value is not None:
965
server_settings[option] = value
967
# Now we have our good server settings in "server_settings"
970
debug = server_settings["debug"]
971
use_dbus = server_settings["use_dbus"]
974
syslogger.setLevel(logging.WARNING)
975
console.setLevel(logging.WARNING)
977
if server_settings["servicename"] != "Mandos":
978
syslogger.setFormatter(logging.Formatter
979
('Mandos (%s): %%(levelname)s:'
981
% server_settings["servicename"]))
983
# Parse config file with clients
984
client_defaults = { "timeout": "1h",
986
"checker": "fping -q -- %%(host)s",
989
client_config = ConfigParser.SafeConfigParser(client_defaults)
990
client_config.read(os.path.join(server_settings["configdir"],
994
tcp_server = IPv6_TCPServer((server_settings["address"],
995
server_settings["port"]),
997
settings=server_settings,
999
pidfilename = "/var/run/mandos.pid"
1001
pidfile = open(pidfilename, "w")
1002
except IOError, error:
1003
logger.error("Could not open file %r", pidfilename)
1006
uid = pwd.getpwnam("_mandos").pw_uid
1007
gid = pwd.getpwnam("_mandos").pw_gid
1010
uid = pwd.getpwnam("mandos").pw_uid
1011
gid = pwd.getpwnam("mandos").pw_gid
1014
uid = pwd.getpwnam("nobody").pw_uid
1015
gid = pwd.getpwnam("nogroup").pw_gid
1022
except OSError, error:
1023
if error[0] != errno.EPERM:
1027
service = AvahiService(name = server_settings["servicename"],
1028
servicetype = "_mandos._tcp", )
1029
if server_settings["interface"]:
1030
service.interface = (if_nametoindex
1031
(server_settings["interface"]))
1036
# From the Avahi example code
1037
DBusGMainLoop(set_as_default=True )
1038
main_loop = gobject.MainLoop()
1039
bus = dbus.SystemBus()
1040
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1041
avahi.DBUS_PATH_SERVER),
1042
avahi.DBUS_INTERFACE_SERVER)
1043
# End of Avahi example code
1045
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1047
clients.update(Set(Client(name = section,
1049
= dict(client_config.items(section)),
1050
use_dbus = use_dbus)
1051
for section in client_config.sections()))
1053
logger.warning(u"No clients defined")
1056
# Redirect stdin so all checkers get /dev/null
1057
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1058
os.dup2(null, sys.stdin.fileno())
1062
# No console logging
1063
logger.removeHandler(console)
1064
# Close all input and output, do double fork, etc.
1069
pidfile.write(str(pid) + "\n")
1073
logger.error(u"Could not write to file %r with PID %d",
1076
# "pidfile" was never created
1081
"Cleanup function; run on exit"
1083
# From the Avahi example code
1084
if not group is None:
1087
# End of Avahi example code
1090
client = clients.pop()
1091
client.disable_hook = None
1094
atexit.register(cleanup)
1097
signal.signal(signal.SIGINT, signal.SIG_IGN)
1098
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1099
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1102
class MandosServer(dbus.service.Object):
1103
"""A D-Bus proxy object"""
1105
dbus.service.Object.__init__(self, bus, "/")
1106
_interface = u"se.bsnet.fukt.Mandos"
1108
@dbus.service.signal(_interface, signature="oa{sv}")
1109
def ClientAdded(self, objpath, properties):
1113
@dbus.service.signal(_interface, signature="os")
1114
def ClientRemoved(self, objpath, name):
1118
@dbus.service.method(_interface, out_signature="ao")
1119
def GetAllClients(self):
1121
return dbus.Array(c.dbus_object_path for c in clients)
1123
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1124
def GetAllClientsWithProperties(self):
1126
return dbus.Dictionary(
1127
((c.dbus_object_path, c.GetAllProperties())
1131
@dbus.service.method(_interface, in_signature="o")
1132
def RemoveClient(self, object_path):
1135
if c.dbus_object_path == object_path:
1137
# Don't signal anything except ClientRemoved
1141
self.ClientRemoved(object_path, c.name)
1147
mandos_server = MandosServer()
1149
for client in clients:
1152
mandos_server.ClientAdded(client.dbus_object_path,
1153
client.GetAllProperties())
1157
tcp_server.server_activate()
1159
# Find out what port we got
1160
service.port = tcp_server.socket.getsockname()[1]
1161
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1162
u" scope_id %d" % tcp_server.socket.getsockname())
1164
#service.interface = tcp_server.socket.getsockname()[3]
1167
# From the Avahi example code
1168
server.connect_to_signal("StateChanged", server_state_changed)
1170
server_state_changed(server.GetState())
1171
except dbus.exceptions.DBusException, error:
1172
logger.critical(u"DBusException: %s", error)
1174
# End of Avahi example code
1176
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1177
lambda *args, **kwargs:
1178
(tcp_server.handle_request
1179
(*args[2:], **kwargs) or True))
1181
logger.debug(u"Starting main loop")
1183
except AvahiError, error:
1184
logger.critical(u"AvahiError: %s", error)
1186
except KeyboardInterrupt:
1190
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__":