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 Teddy Hogeborn & Björn Påhlsson
16
# This program is free software: you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
18
# the Free Software Foundation, either version 3 of the License, or
19
# (at your option) any later version.
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
# GNU General Public License for more details.
26
# You should have received a copy of the GNU General Public License
27
# along with this program. If not, see
28
# <http://www.gnu.org/licenses/>.
30
# Contact the authors at <mandos@fukt.bsnet.se>.
33
from __future__ import division, with_statement, absolute_import
37
6
from optparse import OptionParser
40
9
import gnutls.crypto
41
10
import gnutls.connection
42
11
import gnutls.errors
43
import gnutls.library.functions
44
import gnutls.library.constants
45
import gnutls.library.types
46
12
import ConfigParser
56
import logging.handlers
58
from contextlib import closing
64
from dbus.mainloop.glib import DBusGMainLoop
70
logger = logging.Logger('mandos')
71
syslogger = logging.handlers.SysLogHandler\
72
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
syslogger.setFormatter(logging.Formatter\
75
('Mandos: %(levelname)s: %(message)s'))
76
logger.addHandler(syslogger)
78
console = logging.StreamHandler()
79
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
81
logger.addHandler(console)
83
class AvahiError(Exception):
84
def __init__(self, value):
86
super(AvahiError, self).__init__()
88
return repr(self.value)
90
class AvahiServiceError(AvahiError):
93
class AvahiGroupError(AvahiError):
97
class AvahiService(object):
98
"""An Avahi (Zeroconf) service.
100
interface: integer; avahi.IF_UNSPEC or an interface index.
101
Used to optionally bind to the specified interface.
102
name: string; Example: 'Mandos'
103
type: string; Example: '_mandos._tcp'.
104
See <http://www.dns-sd.org/ServiceTypes.html>
105
port: integer; what port to announce
106
TXT: list of strings; TXT record for the service
107
domain: string; Domain to publish on, default to .local if empty.
108
host: string; Host to publish records for, default is localhost
109
max_renames: integer; maximum number of renames
110
rename_count: integer; counter so we only rename after collisions
111
a sensible number of times
113
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
114
servicetype = None, port = None, TXT = None, domain = "",
115
host = "", max_renames = 32768):
116
self.interface = interface
118
self.type = servicetype
126
self.rename_count = 0
127
self.max_renames = max_renames
129
"""Derived from the Avahi example code"""
130
if self.rename_count >= self.max_renames:
131
logger.critical(u"No suitable Zeroconf service name found"
132
u" after %i retries, exiting.",
134
raise AvahiServiceError("Too many renames")
135
self.name = server.GetAlternativeServiceName(self.name)
136
logger.info(u"Changing Zeroconf service name to %r ...",
138
syslogger.setFormatter(logging.Formatter\
139
('Mandos (%s): %%(levelname)s:'
140
' %%(message)s' % self.name))
143
self.rename_count += 1
145
"""Derived from the Avahi example code"""
146
if group is not None:
149
"""Derived from the Avahi example code"""
152
group = dbus.Interface\
153
(bus.get_object(avahi.DBUS_NAME,
154
server.EntryGroupNew()),
155
avahi.DBUS_INTERFACE_ENTRY_GROUP)
156
group.connect_to_signal('StateChanged',
157
entry_group_state_changed)
158
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
159
service.name, service.type)
161
self.interface, # interface
162
avahi.PROTO_INET6, # protocol
163
dbus.UInt32(0), # flags
164
self.name, self.type,
165
self.domain, self.host,
166
dbus.UInt16(self.port),
167
avahi.string_array_to_txt_array(self.TXT))
170
# From the Avahi example code:
171
group = None # our entry group
172
# End of Avahi example code
175
class Client(dbus.service.Object):
176
"""A representation of a client host served by this server.
178
name: string; from the config file, used in log messages
179
fingerprint: string (40 or 32 hexadecimal digits); used to
180
uniquely identify the client
181
secret: bytestring; sent verbatim (over TLS) to client
182
host: string; available for use by the checker command
183
created: datetime.datetime(); object creation, not client host
185
last_checked_ok: datetime.datetime() or None if not yet checked OK
186
timeout: datetime.timedelta(); How long from last_checked_ok
187
until this client is invalid
188
interval: datetime.timedelta(); How often to start a new checker
189
stop_hook: If set, called by stop() as stop_hook(self)
190
checker: subprocess.Popen(); a running checker process used
191
to see if the client lives.
192
'None' if no process is running.
193
checker_initiator_tag: a gobject event source tag, or None
194
stop_initiator_tag: - '' -
195
checker_callback_tag: - '' -
196
checker_command: string; External command which is run to check if
197
client lives. %() expansions are done at
198
runtime with vars(self) as dict, so that for
199
instance %(name)s can be used in the command.
201
_timeout: Real variable for 'timeout'
202
_interval: Real variable for 'interval'
203
_timeout_milliseconds: Used when calling gobject.timeout_add()
204
_interval_milliseconds: - '' -
206
interface = u"org.mandos_system.Mandos.Clients"
208
@dbus.service.method(interface, out_signature="s")
210
"D-Bus getter method"
213
@dbus.service.method(interface, out_signature="s")
214
def getFingerprint(self):
215
"D-Bus getter method"
216
return self.fingerprint
218
@dbus.service.method(interface, in_signature="ay",
220
def setSecret(self, secret):
221
"D-Bus setter method"
224
def _set_timeout(self, timeout):
225
"Setter function for the 'timeout' attribute"
226
self._timeout = timeout
227
self._timeout_milliseconds = ((self.timeout.days
228
* 24 * 60 * 60 * 1000)
229
+ (self.timeout.seconds * 1000)
230
+ (self.timeout.microseconds
233
self.TimeoutChanged(self._timeout_milliseconds)
234
timeout = property(lambda self: self._timeout, _set_timeout)
237
@dbus.service.method(interface, out_signature="t")
238
def getTimeout(self):
239
"D-Bus getter method"
240
return self._timeout_milliseconds
242
@dbus.service.signal(interface, signature="t")
243
def TimeoutChanged(self, t):
247
def _set_interval(self, interval):
248
"Setter function for the 'interval' attribute"
249
self._interval = interval
250
self._interval_milliseconds = ((self.interval.days
251
* 24 * 60 * 60 * 1000)
252
+ (self.interval.seconds
254
+ (self.interval.microseconds
257
self.IntervalChanged(self._interval_milliseconds)
258
interval = property(lambda self: self._interval, _set_interval)
261
@dbus.service.method(interface, out_signature="t")
262
def getInterval(self):
263
"D-Bus getter method"
264
return self._interval_milliseconds
266
@dbus.service.signal(interface, signature="t")
267
def IntervalChanged(self, t):
271
def __init__(self, name = None, stop_hook=None, config=None):
272
"""Note: the 'checker' key in 'config' sets the
273
'checker_command' attribute and *not* the 'checker'
275
dbus.service.Object.__init__(self, bus,
277
% name.replace(".", "_"))
281
logger.debug(u"Creating client %r", self.name)
282
# Uppercase and remove spaces from fingerprint for later
283
# comparison purposes with return value from the fingerprint()
285
self.fingerprint = config["fingerprint"].upper()\
287
logger.debug(u" Fingerprint: %s", self.fingerprint)
288
if "secret" in config:
289
self.secret = config["secret"].decode(u"base64")
290
elif "secfile" in config:
291
with closing(open(os.path.expanduser
293
(config["secfile"])))) \
295
self.secret = secfile.read()
297
raise TypeError(u"No secret or secfile for client %s"
299
self.host = config.get("host", "")
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"
300
31
self.created = datetime.datetime.now()
302
self.last_checked_ok = None
303
self.timeout = string_to_delta(config["timeout"])
304
self.interval = string_to_delta(config["interval"])
305
self.stop_hook = stop_hook
307
self.checker_initiator_tag = None
308
self.stop_initiator_tag = None
309
self.checker_callback_tag = None
310
self.check_command = config["checker"]
313
"""Start this client's checker and timeout hooks"""
315
# Schedule a new checker to be started an 'interval' from now,
316
# and every interval from then on.
317
self.checker_initiator_tag = gobject.timeout_add\
318
(self._interval_milliseconds,
320
# Also start a new checker *right now*.
322
# Schedule a stop() when 'timeout' has passed
323
self.stop_initiator_tag = gobject.timeout_add\
324
(self._timeout_milliseconds,
327
self.StateChanged(True)
329
@dbus.service.signal(interface, signature="b")
330
def StateChanged(self, started):
335
"""Stop this client."""
336
if getattr(self, "started", False):
337
logger.info(u"Stopping client %s", self.name)
340
if getattr(self, "stop_initiator_tag", False):
341
gobject.source_remove(self.stop_initiator_tag)
342
self.stop_initiator_tag = None
343
if getattr(self, "checker_initiator_tag", False):
344
gobject.source_remove(self.checker_initiator_tag)
345
self.checker_initiator_tag = None
350
self.StateChanged(False)
351
# Do not run this again if called by a gobject.timeout_add
354
Stop = dbus.service.method(interface)(stop)
357
self.stop_hook = None
360
def checker_callback(self, pid, condition):
361
"""The checker has completed, so take appropriate actions."""
362
self.checker_callback_tag = None
364
if os.WIFEXITED(condition) \
365
and (os.WEXITSTATUS(condition) == 0):
366
logger.info(u"Checker for %(name)s succeeded",
369
self.CheckerCompleted(True)
371
elif not os.WIFEXITED(condition):
372
logger.warning(u"Checker for %(name)s crashed?",
375
self.CheckerCompleted(False)
377
logger.info(u"Checker for %(name)s failed",
380
self.CheckerCompleted(False)
382
@dbus.service.signal(interface, signature="b")
383
def CheckerCompleted(self, success):
387
def bump_timeout(self):
388
"""Bump up the timeout for this client.
389
This should only be called when the client has been seen,
392
self.last_checked_ok = datetime.datetime.now()
393
gobject.source_remove(self.stop_initiator_tag)
394
self.stop_initiator_tag = gobject.timeout_add\
395
(self._timeout_milliseconds, self.stop)
397
bumpTimeout = dbus.service.method(interface)(bump_timeout)
399
def start_checker(self):
400
"""Start a new checker subprocess if one is not running.
401
If a checker already exists, leave it running and do
403
# The reason for not killing a running checker is that if we
404
# did that, then if a checker (for some reason) started
405
# running slowly and taking more than 'interval' time, the
406
# client would inevitably timeout, since no checker would get
407
# a chance to run to completion. If we instead leave running
408
# checkers alone, the checker would have to take more time
409
# than 'timeout' for the client to be declared invalid, which
410
# is as it should be.
411
if self.checker is None:
413
# In case check_command has exactly one % operator
414
command = self.check_command % self.host
416
# Escape attributes for the shell
417
escaped_attrs = dict((key, re.escape(str(val)))
419
vars(self).iteritems())
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
421
command = self.check_command % escaped_attrs
422
except TypeError, error:
423
logger.error(u'Could not format string "%s":'
424
u' %s', self.check_command, error)
425
return True # Try again later
427
logger.info(u"Starting checker %r for %s",
429
# We don't need to redirect stdout and stderr, since
430
# in normal mode, that is already done by daemon(),
431
# and in debug mode we don't want to. (Stdin is
432
# always replaced by /dev/null.)
433
self.checker = subprocess.Popen(command,
436
self.checker_callback_tag = gobject.child_watch_add\
438
self.checker_callback)
440
self.CheckerStarted(command)
441
except OSError, error:
442
logger.error(u"Failed to start subprocess: %s",
444
# Re-run this periodically if run by gobject.timeout_add
447
@dbus.service.signal(interface, signature="s")
448
def CheckerStarted(self, command):
451
@dbus.service.method(interface, out_signature="b")
452
def checkerIsRunning(self):
453
"D-Bus getter method"
454
return self.checker is not None
456
def stop_checker(self):
457
"""Force the checker process, if any, to stop."""
458
if self.checker_callback_tag:
459
gobject.source_remove(self.checker_callback_tag)
460
self.checker_callback_tag = None
461
if getattr(self, "checker", None) is None:
463
logger.debug(u"Stopping checker for %(name)s", vars(self))
465
os.kill(self.checker.pid, signal.SIGTERM)
467
#if self.checker.poll() is None:
468
# os.kill(self.checker.pid, signal.SIGKILL)
469
except OSError, error:
470
if error.errno != errno.ESRCH: # No such process
474
StopChecker = dbus.service.method(interface)(stop_checker)
476
def still_valid(self):
477
"""Has the timeout not yet passed for this client?"""
480
now = datetime.datetime.now()
481
if self.last_checked_ok is None:
482
return now < (self.created + self.timeout)
484
return now < (self.last_checked_ok + self.timeout)
486
stillValid = dbus.service.method(interface, out_signature="b")\
492
def peer_certificate(session):
493
"Return the peer's OpenPGP certificate as a bytestring"
494
# If not an OpenPGP certificate...
495
if gnutls.library.functions.gnutls_certificate_type_get\
496
(session._c_object) \
497
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
498
# ...do the normal thing
499
return session.peer_certificate
500
list_size = ctypes.c_uint()
501
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
502
(session._c_object, ctypes.byref(list_size))
503
if list_size.value == 0:
506
return ctypes.string_at(cert.data, cert.size)
509
def fingerprint(openpgp):
510
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
511
# New GnuTLS "datum" with the OpenPGP public key
512
datum = gnutls.library.types.gnutls_datum_t\
513
(ctypes.cast(ctypes.c_char_p(openpgp),
514
ctypes.POINTER(ctypes.c_ubyte)),
515
ctypes.c_uint(len(openpgp)))
516
# New empty GnuTLS certificate
517
crt = gnutls.library.types.gnutls_openpgp_crt_t()
518
gnutls.library.functions.gnutls_openpgp_crt_init\
520
# Import the OpenPGP public key into the certificate
521
gnutls.library.functions.gnutls_openpgp_crt_import\
522
(crt, ctypes.byref(datum),
523
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
524
# Verify the self signature in the key
525
crtverify = ctypes.c_uint()
526
gnutls.library.functions.gnutls_openpgp_crt_verify_self\
527
(crt, 0, ctypes.byref(crtverify))
528
if crtverify.value != 0:
529
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
530
raise gnutls.errors.CertificateSecurityError("Verify failed")
531
# New buffer for the fingerprint
532
buf = ctypes.create_string_buffer(20)
533
buf_len = ctypes.c_size_t()
534
# Get the fingerprint from the certificate into the buffer
535
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
536
(crt, ctypes.byref(buf), ctypes.byref(buf_len))
537
# Deinit the certificate
538
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
539
# Convert the buffer to a Python bytestring
540
fpr = ctypes.string_at(buf, buf_len.value)
541
# Convert the bytestring to hexadecimal notation
542
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
546
class TCP_handler(SocketServer.BaseRequestHandler, object):
547
"""A TCP request handler class.
548
Instantiated by IPv6_TCPServer for each request to handle it.
549
Note: This will run in its own forked process."""
552
logger.info(u"TCP connection from: %s",
553
unicode(self.client_address))
554
session = gnutls.connection.ClientSession\
555
(self.request, gnutls.connection.X509Credentials())
557
line = self.request.makefile().readline()
558
logger.debug(u"Protocol version: %r", line)
560
if int(line.strip().split()[0]) > 1:
562
except (ValueError, IndexError, RuntimeError), error:
563
logger.error(u"Unknown protocol version: %s", error)
566
# Note: gnutls.connection.X509Credentials is really a generic
567
# GnuTLS certificate credentials object so long as no X.509
568
# keys are added to it. Therefore, we can use it here despite
569
# using OpenPGP certificates.
571
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
572
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
574
# Use a fallback default, since this MUST be set.
575
priority = self.server.settings.get("priority", "NORMAL")
576
gnutls.library.functions.gnutls_priority_set_direct\
577
(session._c_object, priority, None)
581
except gnutls.errors.GNUTLSError, error:
582
logger.warning(u"Handshake failed: %s", error)
583
# Do not run session.bye() here: the session is not
584
# established. Just abandon the request.
587
fpr = fingerprint(peer_certificate(session))
588
except (TypeError, gnutls.errors.GNUTLSError), error:
589
logger.warning(u"Bad certificate: %s", error)
592
logger.debug(u"Fingerprint: %s", fpr)
594
for c in self.server.clients:
595
if c.fingerprint == fpr:
599
logger.warning(u"Client not found for fingerprint: %s",
603
# Have to check if client.still_valid(), since it is possible
604
# that the client timed out while establishing the GnuTLS
606
if not client.still_valid():
607
logger.warning(u"Client %(name)s is invalid",
611
## This won't work here, since we're in a fork.
612
# client.bump_timeout()
614
while sent_size < len(client.secret):
615
sent = session.send(client.secret[sent_size:])
616
logger.debug(u"Sent: %d, remaining: %d",
617
sent, len(client.secret)
618
- (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")
623
class IPv6_TCPServer(SocketServer.ForkingMixIn,
624
SocketServer.TCPServer, object):
625
"""IPv6 TCP server. Accepts 'None' as address and/or port.
627
settings: Server settings
628
clients: Set() of Client objects
629
enabled: Boolean; whether this server is activated yet
631
address_family = socket.AF_INET6
632
def __init__(self, *args, **kwargs):
633
if "settings" in kwargs:
634
self.settings = kwargs["settings"]
635
del kwargs["settings"]
636
if "clients" in kwargs:
637
self.clients = kwargs["clients"]
638
del kwargs["clients"]
640
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
641
def server_bind(self):
642
"""This overrides the normal server_bind() function
643
to bind to an interface if one was specified, and also NOT to
644
bind to an address or port if they were not specified."""
645
if self.settings["interface"]:
646
# 25 is from /usr/include/asm-i486/socket.h
647
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
649
self.socket.setsockopt(socket.SOL_SOCKET,
651
self.settings["interface"])
652
except socket.error, error:
653
if error[0] == errno.EPERM:
654
logger.error(u"No permission to"
655
u" bind to interface %s",
656
self.settings["interface"])
659
# Only bind(2) the socket if we really need to.
660
if self.server_address[0] or self.server_address[1]:
661
if not self.server_address[0]:
663
self.server_address = (in6addr_any,
664
self.server_address[1])
665
elif not self.server_address[1]:
666
self.server_address = (self.server_address[0],
668
# if self.settings["interface"]:
669
# self.server_address = (self.server_address[0],
675
return super(IPv6_TCPServer, self).server_bind()
676
def server_activate(self):
678
return super(IPv6_TCPServer, self).server_activate()
119
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
120
__metaclass__ = server_metaclass
121
request_queue_size = 1024
683
128
def string_to_delta(interval):
684
129
"""Parse a string and return a datetime.timedelta
693
138
datetime.timedelta(1)
694
139
>>> string_to_delta(u'1w')
695
140
datetime.timedelta(7)
696
>>> string_to_delta('5m 30s')
697
datetime.timedelta(0, 330)
699
timevalue = datetime.timedelta(0)
700
for s in interval.split():
702
suffix = unicode(s[-1])
705
delta = datetime.timedelta(value)
707
delta = datetime.timedelta(0, value)
709
delta = datetime.timedelta(0, 0, 0, 0, value)
711
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
713
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
716
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)
722
def server_state_changed(state):
723
"""Derived from the Avahi example code"""
724
if state == avahi.SERVER_COLLISION:
725
logger.error(u"Zeroconf server name collision")
727
elif state == avahi.SERVER_RUNNING:
731
def entry_group_state_changed(state, error):
732
"""Derived from the Avahi example code"""
733
logger.debug(u"Avahi state change: %i", state)
735
if state == avahi.ENTRY_GROUP_ESTABLISHED:
736
logger.debug(u"Zeroconf service established.")
737
elif state == avahi.ENTRY_GROUP_COLLISION:
738
logger.warning(u"Zeroconf service name collision.")
740
elif state == avahi.ENTRY_GROUP_FAILURE:
741
logger.critical(u"Avahi: Error in group state changed %s",
743
raise AvahiGroupError("State changed: %s", str(error))
745
def if_nametoindex(interface):
746
"""Call the C function if_nametoindex(), or equivalent"""
747
global if_nametoindex
749
if_nametoindex = ctypes.cdll.LoadLibrary\
750
(ctypes.util.find_library("c")).if_nametoindex
751
except (OSError, AttributeError):
752
if "struct" not in sys.modules:
754
if "fcntl" not in sys.modules:
756
def if_nametoindex(interface):
757
"Get an interface index the hard way, i.e. using fcntl()"
758
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
759
with closing(socket.socket()) as s:
760
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
761
struct.pack("16s16x", interface))
762
interface_index = struct.unpack("I", ifreq[16:20])[0]
763
return interface_index
764
return if_nametoindex(interface)
767
def daemon(nochdir = False, noclose = False):
768
"""See daemon(3). Standard BSD Unix function.
769
This should really exist as os.daemon, but it doesn't (yet)."""
778
# Close all standard open file descriptors
779
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
780
if not stat.S_ISCHR(os.fstat(null).st_mode):
781
raise OSError(errno.ENODEV,
782
"/dev/null not a character device")
783
os.dup2(null, sys.stdin.fileno())
784
os.dup2(null, sys.stdout.fileno())
785
os.dup2(null, sys.stderr.fileno())
157
except (ValueError, IndexError):
791
parser = OptionParser(version = "%%prog %s" % version)
162
parser = OptionParser()
792
163
parser.add_option("-i", "--interface", type="string",
793
metavar="IF", help="Bind to interface IF")
794
parser.add_option("-a", "--address", type="string",
795
help="Address to listen for requests on")
796
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,
797
179
help="Port number to receive requests on")
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")
798
188
parser.add_option("--check", action="store_true", default=False,
799
189
help="Run self-test")
800
parser.add_option("--debug", action="store_true",
801
help="Debug mode; run in foreground and log to"
803
parser.add_option("--priority", type="string", help="GnuTLS"
804
" priority string (see GnuTLS documentation)")
805
parser.add_option("--servicename", type="string", metavar="NAME",
806
help="Zeroconf service name")
807
parser.add_option("--configdir", type="string",
808
default="/etc/mandos", metavar="DIR",
809
help="Directory to search for configuration"
811
options = parser.parse_args()[0]
190
(options, args) = parser.parse_args()
813
192
if options.check:
815
194
doctest.testmod()
818
# Default values for config file for server-global settings
819
server_defaults = { "interface": "",
824
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
825
"servicename": "Mandos",
828
# Parse config file for server-global settings
829
server_config = ConfigParser.SafeConfigParser(server_defaults)
831
server_config.read(os.path.join(options.configdir, "mandos.conf"))
832
# Convert the SafeConfigParser object to a dict
833
server_settings = server_config.defaults()
834
# Use getboolean on the boolean config option
835
server_settings["debug"] = server_config.getboolean\
839
# Override the settings from the config file with command line
841
for option in ("interface", "address", "port", "debug",
842
"priority", "servicename", "configdir"):
843
value = getattr(options, option)
844
if value is not None:
845
server_settings[option] = value
847
# Now we have our good server settings in "server_settings"
849
debug = server_settings["debug"]
852
syslogger.setLevel(logging.WARNING)
853
console.setLevel(logging.WARNING)
855
if server_settings["servicename"] != "Mandos":
856
syslogger.setFormatter(logging.Formatter\
857
('Mandos (%s): %%(levelname)s:'
859
% server_settings["servicename"]))
861
# Parse config file with clients
862
client_defaults = { "timeout": "1h",
864
"checker": "fping -q -- %(host)s",
867
client_config = ConfigParser.SafeConfigParser(client_defaults)
868
client_config.read(os.path.join(server_settings["configdir"],
872
tcp_server = IPv6_TCPServer((server_settings["address"],
873
server_settings["port"]),
875
settings=server_settings,
877
pidfilename = "/var/run/mandos.pid"
879
pidfile = open(pidfilename, "w")
880
except IOError, error:
881
logger.error("Could not open file %r", pidfilename)
886
uid = pwd.getpwnam("mandos").pw_uid
889
uid = pwd.getpwnam("nobody").pw_uid
893
gid = pwd.getpwnam("mandos").pw_gid
896
gid = pwd.getpwnam("nogroup").pw_gid
902
except OSError, error:
903
if error[0] != errno.EPERM:
907
service = AvahiService(name = server_settings["servicename"],
908
servicetype = "_mandos._tcp", )
909
if server_settings["interface"]:
910
service.interface = if_nametoindex\
911
(server_settings["interface"])
916
# From the Avahi example code
917
DBusGMainLoop(set_as_default=True )
918
main_loop = gobject.MainLoop()
919
bus = dbus.SystemBus()
920
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
921
avahi.DBUS_PATH_SERVER),
922
avahi.DBUS_INTERFACE_SERVER)
923
# End of Avahi example code
925
def remove_from_clients(client):
926
clients.remove(client)
928
logger.critical(u"No clients left, exiting")
931
clients.update(Set(Client(name = section,
932
stop_hook = remove_from_clients,
934
= dict(client_config.items(section)))
935
for section in client_config.sections()))
937
logger.critical(u"No clients defined")
941
# Redirect stdin so all checkers get /dev/null
942
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
943
os.dup2(null, sys.stdin.fileno())
948
logger.removeHandler(console)
949
# Close all input and output, do double fork, etc.
954
pidfile.write(str(pid) + "\n")
958
logger.error(u"Could not write to file %r with PID %d",
961
# "pidfile" was never created
966
"Cleanup function; run on exit"
968
# From the Avahi example code
969
if not group is None:
972
# End of Avahi example code
975
client = clients.pop()
976
client.stop_hook = None
979
atexit.register(cleanup)
982
signal.signal(signal.SIGINT, signal.SIG_IGN)
983
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
984
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
986
for client in clients:
990
tcp_server.server_activate()
992
# Find out what port we got
993
service.port = tcp_server.socket.getsockname()[1]
994
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
995
u" scope_id %d" % tcp_server.socket.getsockname())
997
#service.interface = tcp_server.socket.getsockname()[3]
1000
# From the Avahi example code
1001
server.connect_to_signal("StateChanged", server_state_changed)
1003
server_state_changed(server.GetState())
1004
except dbus.exceptions.DBusException, error:
1005
logger.critical(u"DBusException: %s", error)
1007
# End of Avahi example code
1009
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1010
lambda *args, **kwargs:
1011
tcp_server.handle_request\
1012
(*args[2:], **kwargs) or True)
1014
logger.debug(u"Starting main loop")
1016
except AvahiError, error:
1017
logger.critical(u"AvahiError: %s" + unicode(error))
1019
except KeyboardInterrupt:
1023
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__":