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
 
6
37
from optparse import OptionParser
 
9
40
import gnutls.crypto
 
10
41
import gnutls.connection
 
11
42
import gnutls.errors
 
 
43
import gnutls.library.functions
 
 
44
import gnutls.library.constants
 
 
45
import gnutls.library.types
 
12
46
import ConfigParser
 
 
56
import logging.handlers
 
 
62
from dbus.mainloop.glib import DBusGMainLoop
 
 
68
logger = logging.Logger('mandos')
 
 
69
syslogger = logging.handlers.SysLogHandler\
 
 
70
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
 
72
syslogger.setFormatter(logging.Formatter\
 
 
73
                        ('Mandos: %(levelname)s: %(message)s'))
 
 
74
logger.addHandler(syslogger)
 
 
76
console = logging.StreamHandler()
 
 
77
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
 
 
79
logger.addHandler(console)
 
 
81
class AvahiError(Exception):
 
 
82
    def __init__(self, value):
 
 
84
        super(AvahiError, self).__init__()
 
 
86
        return repr(self.value)
 
 
88
class AvahiServiceError(AvahiError):
 
 
91
class AvahiGroupError(AvahiError):
 
 
95
class AvahiService(object):
 
 
96
    """An Avahi (Zeroconf) service.
 
 
98
    interface: integer; avahi.IF_UNSPEC or an interface index.
 
 
99
               Used to optionally bind to the specified interface.
 
 
100
    name: string; Example: 'Mandos'
 
 
101
    type: string; Example: '_mandos._tcp'.
 
 
102
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
 
103
    port: integer; what port to announce
 
 
104
    TXT: list of strings; TXT record for the service
 
 
105
    domain: string; Domain to publish on, default to .local if empty.
 
 
106
    host: string; Host to publish records for, default is localhost
 
 
107
    max_renames: integer; maximum number of renames
 
 
108
    rename_count: integer; counter so we only rename after collisions
 
 
109
                  a sensible number of times
 
 
111
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
 
 
112
                 servicetype = None, port = None, TXT = None, domain = "",
 
 
113
                 host = "", max_renames = 32768):
 
 
114
        self.interface = interface
 
 
116
        self.type = servicetype
 
 
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("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\
 
 
151
                    (bus.get_object(avahi.DBUS_NAME,
 
 
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
 
16
173
class Client(object):
 
17
 
    def __init__(self, name=None, options=None, dn=None,
 
18
 
                 password=None, passfile=None, fqdn=None,
 
19
 
                 timeout=None, interval=-1):
 
 
174
    """A representation of a client host served by this server.
 
 
176
    name:      string; from the config file, used in log messages
 
 
177
    fingerprint: string (40 or 32 hexadecimal digits); used to
 
 
178
                 uniquely identify the client
 
 
179
    secret:    bytestring; sent verbatim (over TLS) to client
 
 
180
    host:      string; available for use by the checker command
 
 
181
    created:   datetime.datetime(); object creation, not client host
 
 
182
    last_checked_ok: datetime.datetime() or None if not yet checked OK
 
 
183
    timeout:   datetime.timedelta(); How long from last_checked_ok
 
 
184
                                     until this client is invalid
 
 
185
    interval:  datetime.timedelta(); How often to start a new checker
 
 
186
    stop_hook: If set, called by stop() as stop_hook(self)
 
 
187
    checker:   subprocess.Popen(); a running checker process used
 
 
188
                                   to see if the client lives.
 
 
189
                                   'None' if no process is running.
 
 
190
    checker_initiator_tag: a gobject event source tag, or None
 
 
191
    stop_initiator_tag:    - '' -
 
 
192
    checker_callback_tag:  - '' -
 
 
193
    checker_command: string; External command which is run to check if
 
 
194
                     client lives.  %() expansions are done at
 
 
195
                     runtime with vars(self) as dict, so that for
 
 
196
                     instance %(name)s can be used in the command.
 
 
198
    _timeout: Real variable for 'timeout'
 
 
199
    _interval: Real variable for 'interval'
 
 
200
    _timeout_milliseconds: Used when calling gobject.timeout_add()
 
 
201
    _interval_milliseconds: - '' -
 
 
203
    def _set_timeout(self, timeout):
 
 
204
        "Setter function for 'timeout' attribute"
 
 
205
        self._timeout = timeout
 
 
206
        self._timeout_milliseconds = ((self.timeout.days
 
 
207
                                       * 24 * 60 * 60 * 1000)
 
 
208
                                      + (self.timeout.seconds * 1000)
 
 
209
                                      + (self.timeout.microseconds
 
 
211
    timeout = property(lambda self: self._timeout,
 
 
214
    def _set_interval(self, interval):
 
 
215
        "Setter function for 'interval' attribute"
 
 
216
        self._interval = interval
 
 
217
        self._interval_milliseconds = ((self.interval.days
 
 
218
                                        * 24 * 60 * 60 * 1000)
 
 
219
                                       + (self.interval.seconds
 
 
221
                                       + (self.interval.microseconds
 
 
223
    interval = property(lambda self: self._interval,
 
 
226
    def __init__(self, name = None, stop_hook=None, config=None):
 
 
227
        """Note: the 'checker' key in 'config' sets the
 
 
228
        'checker_command' attribute and *not* the 'checker'
 
23
 
            self.password = password
 
25
 
            self.password = open(passfile).readall()
 
 
233
        logger.debug(u"Creating client %r", self.name)
 
 
234
        # Uppercase and remove spaces from fingerprint for later
 
 
235
        # comparison purposes with return value from the fingerprint()
 
 
237
        self.fingerprint = config["fingerprint"].upper()\
 
 
239
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
 
 
240
        if "secret" in config:
 
 
241
            self.secret = config["secret"].decode(u"base64")
 
 
242
        elif "secfile" in config:
 
 
243
            secfile = open(config["secfile"])
 
 
244
            self.secret = secfile.read()
 
27
 
            print "No Password or Passfile in client config file"
 
28
 
            # raise RuntimeError XXX
 
29
 
            self.password = "gazonk"
 
 
247
            raise TypeError(u"No secret or secfile for client %s"
 
 
249
        self.host = config.get("host", "")
 
31
250
        self.created = datetime.datetime.now()
 
34
 
            timeout = options.timeout
 
35
 
        self.timeout = timeout
 
37
 
            interval = options.interval
 
38
 
        self.interval = interval
 
39
 
        self.next_check = datetime.datetime.now()
 
42
 
class server_metaclass(type):
 
43
 
    "Common behavior for the UDP and TCP server classes"
 
44
 
    def __new__(cls, name, bases, attrs):
 
45
 
        attrs["address_family"] = socket.AF_INET6
 
46
 
        attrs["allow_reuse_address"] = True
 
47
 
        def server_bind(self):
 
48
 
            if self.options.interface:
 
49
 
                if not hasattr(socket, "SO_BINDTODEVICE"):
 
50
 
                    # From /usr/include/asm-i486/socket.h
 
51
 
                    socket.SO_BINDTODEVICE = 25
 
 
251
        self.last_checked_ok = None
 
 
252
        self.timeout = string_to_delta(config["timeout"])
 
 
253
        self.interval = string_to_delta(config["interval"])
 
 
254
        self.stop_hook = stop_hook
 
 
256
        self.checker_initiator_tag = None
 
 
257
        self.stop_initiator_tag = None
 
 
258
        self.checker_callback_tag = None
 
 
259
        self.check_command = config["checker"]
 
 
261
        """Start this client's checker and timeout hooks"""
 
 
262
        # Schedule a new checker to be started an 'interval' from now,
 
 
263
        # and every interval from then on.
 
 
264
        self.checker_initiator_tag = gobject.timeout_add\
 
 
265
                                     (self._interval_milliseconds,
 
 
267
        # Also start a new checker *right now*.
 
 
269
        # Schedule a stop() when 'timeout' has passed
 
 
270
        self.stop_initiator_tag = gobject.timeout_add\
 
 
271
                                  (self._timeout_milliseconds,
 
 
275
        The possibility that a client might be restarted is left open,
 
 
276
        but not currently used."""
 
 
277
        # If this client doesn't have a secret, it is already stopped.
 
 
278
        if hasattr(self, "secret") and self.secret:
 
 
279
            logger.info(u"Stopping client %s", self.name)
 
 
283
        if getattr(self, "stop_initiator_tag", False):
 
 
284
            gobject.source_remove(self.stop_initiator_tag)
 
 
285
            self.stop_initiator_tag = None
 
 
286
        if getattr(self, "checker_initiator_tag", False):
 
 
287
            gobject.source_remove(self.checker_initiator_tag)
 
 
288
            self.checker_initiator_tag = None
 
 
292
        # Do not run this again if called by a gobject.timeout_add
 
 
295
        self.stop_hook = None
 
 
297
    def checker_callback(self, pid, condition):
 
 
298
        """The checker has completed, so take appropriate actions."""
 
 
299
        now = datetime.datetime.now()
 
 
300
        self.checker_callback_tag = None
 
 
302
        if os.WIFEXITED(condition) \
 
 
303
               and (os.WEXITSTATUS(condition) == 0):
 
 
304
            logger.info(u"Checker for %(name)s succeeded",
 
 
306
            self.last_checked_ok = now
 
 
307
            gobject.source_remove(self.stop_initiator_tag)
 
 
308
            self.stop_initiator_tag = gobject.timeout_add\
 
 
309
                                      (self._timeout_milliseconds,
 
 
311
        elif not os.WIFEXITED(condition):
 
 
312
            logger.warning(u"Checker for %(name)s crashed?",
 
 
315
            logger.info(u"Checker for %(name)s failed",
 
 
317
    def start_checker(self):
 
 
318
        """Start a new checker subprocess if one is not running.
 
 
319
        If a checker already exists, leave it running and do
 
 
321
        # The reason for not killing a running checker is that if we
 
 
322
        # did that, then if a checker (for some reason) started
 
 
323
        # running slowly and taking more than 'interval' time, the
 
 
324
        # client would inevitably timeout, since no checker would get
 
 
325
        # a chance to run to completion.  If we instead leave running
 
 
326
        # checkers alone, the checker would have to take more time
 
 
327
        # than 'timeout' for the client to be declared invalid, which
 
 
328
        # is as it should be.
 
 
329
        if self.checker is None:
 
 
331
                # In case check_command has exactly one % operator
 
 
332
                command = self.check_command % self.host
 
 
334
                # Escape attributes for the shell
 
 
335
                escaped_attrs = dict((key, re.escape(str(val)))
 
 
337
                                     vars(self).iteritems())
 
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")
 
 
339
                    command = self.check_command % escaped_attrs
 
 
340
                except TypeError, error:
 
 
341
                    logger.error(u'Could not format string "%s":'
 
 
342
                                 u' %s', self.check_command, error)
 
 
343
                    return True # Try again later
 
 
345
                logger.info(u"Starting checker %r for %s",
 
 
347
                # We don't need to redirect stdout and stderr, since
 
 
348
                # in normal mode, that is already done by daemon(),
 
 
349
                # and in debug mode we don't want to.  (Stdin is
 
 
350
                # always replaced by /dev/null.)
 
 
351
                self.checker = subprocess.Popen(command,
 
 
354
                self.checker_callback_tag = gobject.child_watch_add\
 
 
356
                                             self.checker_callback)
 
 
357
            except OSError, error:
 
 
358
                logger.error(u"Failed to start subprocess: %s",
 
 
360
        # Re-run this periodically if run by gobject.timeout_add
 
 
362
    def stop_checker(self):
 
 
363
        """Force the checker process, if any, to stop."""
 
 
364
        if self.checker_callback_tag:
 
 
365
            gobject.source_remove(self.checker_callback_tag)
 
 
366
            self.checker_callback_tag = None
 
 
367
        if getattr(self, "checker", None) is None:
 
 
369
        logger.debug(u"Stopping checker for %(name)s", vars(self))
 
 
371
            os.kill(self.checker.pid, signal.SIGTERM)
 
 
373
            #if self.checker.poll() is None:
 
 
374
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
 
375
        except OSError, error:
 
 
376
            if error.errno != errno.ESRCH: # No such process
 
 
379
    def still_valid(self):
 
 
380
        """Has the timeout not yet passed for this client?"""
 
 
381
        now = datetime.datetime.now()
 
 
382
        if self.last_checked_ok is None:
 
 
383
            return now < (self.created + self.timeout)
 
 
385
            return now < (self.last_checked_ok + self.timeout)
 
 
388
def peer_certificate(session):
 
 
389
    "Return the peer's OpenPGP certificate as a bytestring"
 
 
390
    # If not an OpenPGP certificate...
 
 
391
    if gnutls.library.functions.gnutls_certificate_type_get\
 
 
392
            (session._c_object) \
 
 
393
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
 
394
        # ...do the normal thing
 
 
395
        return session.peer_certificate
 
 
396
    list_size = ctypes.c_uint()
 
 
397
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
 
 
398
        (session._c_object, ctypes.byref(list_size))
 
 
399
    if list_size.value == 0:
 
 
402
    return ctypes.string_at(cert.data, cert.size)
 
 
405
def fingerprint(openpgp):
 
 
406
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
 
407
    # New GnuTLS "datum" with the OpenPGP public key
 
 
408
    datum = gnutls.library.types.gnutls_datum_t\
 
 
409
        (ctypes.cast(ctypes.c_char_p(openpgp),
 
 
410
                     ctypes.POINTER(ctypes.c_ubyte)),
 
 
411
         ctypes.c_uint(len(openpgp)))
 
 
412
    # New empty GnuTLS certificate
 
 
413
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
 
414
    gnutls.library.functions.gnutls_openpgp_crt_init\
 
 
416
    # Import the OpenPGP public key into the certificate
 
 
417
    gnutls.library.functions.gnutls_openpgp_crt_import\
 
 
418
                    (crt, ctypes.byref(datum),
 
 
419
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
 
420
    # Verify the self signature in the key
 
 
421
    crtverify = ctypes.c_uint()
 
 
422
    gnutls.library.functions.gnutls_openpgp_crt_verify_self\
 
 
423
        (crt, 0, ctypes.byref(crtverify))
 
 
424
    if crtverify.value != 0:
 
 
425
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
 
426
        raise gnutls.errors.CertificateSecurityError("Verify failed")
 
 
427
    # New buffer for the fingerprint
 
 
428
    buf = ctypes.create_string_buffer(20)
 
 
429
    buf_len = ctypes.c_size_t()
 
 
430
    # Get the fingerprint from the certificate into the buffer
 
 
431
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
 
 
432
        (crt, ctypes.byref(buf), ctypes.byref(buf_len))
 
 
433
    # Deinit the certificate
 
 
434
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
 
435
    # Convert the buffer to a Python bytestring
 
 
436
    fpr = ctypes.string_at(buf, buf_len.value)
 
 
437
    # Convert the bytestring to hexadecimal notation
 
 
438
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
 
442
class TCP_handler(SocketServer.BaseRequestHandler, object):
 
 
443
    """A TCP request handler class.
 
 
444
    Instantiated by IPv6_TCPServer for each request to handle it.
 
 
445
    Note: This will run in its own forked process."""
 
 
448
        logger.info(u"TCP connection from: %s",
 
 
449
                     unicode(self.client_address))
 
 
450
        session = gnutls.connection.ClientSession\
 
 
451
                  (self.request, gnutls.connection.X509Credentials())
 
 
453
        line = self.request.makefile().readline()
 
 
454
        logger.debug(u"Protocol version: %r", line)
 
 
456
            if int(line.strip().split()[0]) > 1:
 
 
458
        except (ValueError, IndexError, RuntimeError), error:
 
 
459
            logger.error(u"Unknown protocol version: %s", error)
 
 
462
        # Note: gnutls.connection.X509Credentials is really a generic
 
 
463
        # GnuTLS certificate credentials object so long as no X.509
 
 
464
        # keys are added to it.  Therefore, we can use it here despite
 
 
465
        # using OpenPGP certificates.
 
 
467
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
 
468
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
 
470
        priority = "NORMAL"             # Fallback default, since this
 
 
472
        if self.server.settings["priority"]:
 
 
473
            priority = self.server.settings["priority"]
 
 
474
        gnutls.library.functions.gnutls_priority_set_direct\
 
 
475
            (session._c_object, priority, None)
 
 
479
        except gnutls.errors.GNUTLSError, error:
 
 
480
            logger.warning(u"Handshake failed: %s", error)
 
 
481
            # Do not run session.bye() here: the session is not
 
 
482
            # established.  Just abandon the request.
 
 
485
            fpr = fingerprint(peer_certificate(session))
 
 
486
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
 
487
            logger.warning(u"Bad certificate: %s", error)
 
 
490
        logger.debug(u"Fingerprint: %s", fpr)
 
 
492
        for c in self.server.clients:
 
 
493
            if c.fingerprint == fpr:
 
 
497
            logger.warning(u"Client not found for fingerprint: %s",
 
 
501
        # Have to check if client.still_valid(), since it is possible
 
 
502
        # that the client timed out while establishing the GnuTLS
 
 
504
        if not client.still_valid():
 
 
505
            logger.warning(u"Client %(name)s is invalid",
 
 
510
        while sent_size < len(client.secret):
 
 
511
            sent = session.send(client.secret[sent_size:])
 
 
512
            logger.debug(u"Sent: %d, remaining: %d",
 
 
513
                         sent, len(client.secret)
 
 
514
                         - (sent_size + sent))
 
119
519
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
 
120
 
    __metaclass__ = server_metaclass
 
121
 
    request_queue_size = 1024
 
 
520
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
 
522
        settings:       Server settings
 
 
523
        clients:        Set() of Client objects
 
 
524
        enabled:        Boolean; whether this server is activated yet
 
 
526
    address_family = socket.AF_INET6
 
 
527
    def __init__(self, *args, **kwargs):
 
 
528
        if "settings" in kwargs:
 
 
529
            self.settings = kwargs["settings"]
 
 
530
            del kwargs["settings"]
 
 
531
        if "clients" in kwargs:
 
 
532
            self.clients = kwargs["clients"]
 
 
533
            del kwargs["clients"]
 
 
535
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
 
 
536
    def server_bind(self):
 
 
537
        """This overrides the normal server_bind() function
 
 
538
        to bind to an interface if one was specified, and also NOT to
 
 
539
        bind to an address or port if they were not specified."""
 
 
540
        if self.settings["interface"]:
 
 
541
            # 25 is from /usr/include/asm-i486/socket.h
 
 
542
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
 
 
544
                self.socket.setsockopt(socket.SOL_SOCKET,
 
 
546
                                       self.settings["interface"])
 
 
547
            except socket.error, error:
 
 
548
                if error[0] == errno.EPERM:
 
 
549
                    logger.error(u"No permission to"
 
 
550
                                 u" bind to interface %s",
 
 
551
                                 self.settings["interface"])
 
 
554
        # Only bind(2) the socket if we really need to.
 
 
555
        if self.server_address[0] or self.server_address[1]:
 
 
556
            if not self.server_address[0]:
 
 
558
                self.server_address = (in6addr_any,
 
 
559
                                       self.server_address[1])
 
 
560
            elif not self.server_address[1]:
 
 
561
                self.server_address = (self.server_address[0],
 
 
563
#                 if self.settings["interface"]:
 
 
564
#                     self.server_address = (self.server_address[0],
 
 
570
            return super(IPv6_TCPServer, self).server_bind()
 
 
571
    def server_activate(self):
 
 
573
            return super(IPv6_TCPServer, self).server_activate()
 
128
578
def string_to_delta(interval):
 
129
579
    """Parse a string and return a datetime.timedelta
 
 
138
588
    datetime.timedelta(1)
 
139
589
    >>> string_to_delta(u'1w')
 
140
590
    datetime.timedelta(7)
 
 
591
    >>> string_to_delta('5m 30s')
 
 
592
    datetime.timedelta(0, 330)
 
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)
 
 
594
    timevalue = datetime.timedelta(0)
 
 
595
    for s in interval.split():
 
 
597
            suffix = unicode(s[-1])
 
 
600
                delta = datetime.timedelta(value)
 
 
602
                delta = datetime.timedelta(0, value)
 
 
604
                delta = datetime.timedelta(0, 0, 0, 0, value)
 
 
606
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
 
608
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
 
611
        except (ValueError, IndexError):
 
157
 
    except (ValueError, IndexError):
 
 
617
def server_state_changed(state):
 
 
618
    """Derived from the Avahi example code"""
 
 
619
    if state == avahi.SERVER_COLLISION:
 
 
620
        logger.error(u"Zeroconf server name collision")
 
 
622
    elif state == avahi.SERVER_RUNNING:
 
 
626
def entry_group_state_changed(state, error):
 
 
627
    """Derived from the Avahi example code"""
 
 
628
    logger.debug(u"Avahi state change: %i", state)
 
 
630
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
 
631
        logger.debug(u"Zeroconf service established.")
 
 
632
    elif state == avahi.ENTRY_GROUP_COLLISION:
 
 
633
        logger.warning(u"Zeroconf service name collision.")
 
 
635
    elif state == avahi.ENTRY_GROUP_FAILURE:
 
 
636
        logger.critical(u"Avahi: Error in group state changed %s",
 
 
638
        raise AvahiGroupError("State changed: %s", str(error))
 
 
640
def if_nametoindex(interface):
 
 
641
    """Call the C function if_nametoindex(), or equivalent"""
 
 
642
    global if_nametoindex
 
 
644
        if_nametoindex = ctypes.cdll.LoadLibrary\
 
 
645
            (ctypes.util.find_library("c")).if_nametoindex
 
 
646
    except (OSError, AttributeError):
 
 
647
        if "struct" not in sys.modules:
 
 
649
        if "fcntl" not in sys.modules:
 
 
651
        def if_nametoindex(interface):
 
 
652
            "Get an interface index the hard way, i.e. using fcntl()"
 
 
653
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
 
655
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
 
656
                                struct.pack("16s16x", interface))
 
 
658
            interface_index = struct.unpack("I", ifreq[16:20])[0]
 
 
659
            return interface_index
 
 
660
    return if_nametoindex(interface)
 
 
663
def daemon(nochdir = False, noclose = False):
 
 
664
    """See daemon(3).  Standard BSD Unix function.
 
 
665
    This should really exist as os.daemon, but it doesn't (yet)."""
 
 
674
        # Close all standard open file descriptors
 
 
675
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
 
676
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
 
677
            raise OSError(errno.ENODEV,
 
 
678
                          "/dev/null not a character device")
 
 
679
        os.dup2(null, sys.stdin.fileno())
 
 
680
        os.dup2(null, sys.stdout.fileno())
 
 
681
        os.dup2(null, sys.stderr.fileno())
 
162
 
    parser = OptionParser()
 
 
687
    parser = OptionParser(version = "%%prog %s" % version)
 
163
688
    parser.add_option("-i", "--interface", type="string",
 
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,
 
 
689
                      metavar="IF", help="Bind to interface IF")
 
 
690
    parser.add_option("-a", "--address", type="string",
 
 
691
                      help="Address to listen for requests on")
 
 
692
    parser.add_option("-p", "--port", type="int",
 
179
693
                      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")
 
188
694
    parser.add_option("--check", action="store_true", default=False,
 
189
695
                      help="Run self-test")
 
190
 
    (options, args) = parser.parse_args()
 
 
696
    parser.add_option("--debug", action="store_true",
 
 
697
                      help="Debug mode; run in foreground and log to"
 
 
699
    parser.add_option("--priority", type="string", help="GnuTLS"
 
 
700
                      " priority string (see GnuTLS documentation)")
 
 
701
    parser.add_option("--servicename", type="string", metavar="NAME",
 
 
702
                      help="Zeroconf service name")
 
 
703
    parser.add_option("--configdir", type="string",
 
 
704
                      default="/etc/mandos", metavar="DIR",
 
 
705
                      help="Directory to search for configuration"
 
 
707
    options = parser.parse_args()[0]
 
192
709
    if options.check:
 
194
711
        doctest.testmod()
 
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__":
 
 
714
    # Default values for config file for server-global settings
 
 
715
    server_defaults = { "interface": "",
 
 
720
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
 
721
                        "servicename": "Mandos",
 
 
724
    # Parse config file for server-global settings
 
 
725
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
 
727
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
 
728
    # Convert the SafeConfigParser object to a dict
 
 
729
    server_settings = server_config.defaults()
 
 
730
    # Use getboolean on the boolean config option
 
 
731
    server_settings["debug"] = server_config.getboolean\
 
 
735
    # Override the settings from the config file with command line
 
 
737
    for option in ("interface", "address", "port", "debug",
 
 
738
                   "priority", "servicename", "configdir"):
 
 
739
        value = getattr(options, option)
 
 
740
        if value is not None:
 
 
741
            server_settings[option] = value
 
 
743
    # Now we have our good server settings in "server_settings"
 
 
745
    debug = server_settings["debug"]
 
 
748
        syslogger.setLevel(logging.WARNING)
 
 
749
        console.setLevel(logging.WARNING)
 
 
751
    if server_settings["servicename"] != "Mandos":
 
 
752
        syslogger.setFormatter(logging.Formatter\
 
 
753
                               ('Mandos (%s): %%(levelname)s:'
 
 
755
                                % server_settings["servicename"]))
 
 
757
    # Parse config file with clients
 
 
758
    client_defaults = { "timeout": "1h",
 
 
760
                        "checker": "fping -q -- %(host)s",
 
 
763
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
 
764
    client_config.read(os.path.join(server_settings["configdir"],
 
 
768
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
 
769
                                 server_settings["port"]),
 
 
771
                                settings=server_settings,
 
 
773
    pidfilename = "/var/run/mandos.pid"
 
 
775
        pidfile = open(pidfilename, "w")
 
 
776
    except IOError, error:
 
 
777
        logger.error("Could not open file %r", pidfilename)
 
 
782
        uid = pwd.getpwnam("mandos").pw_uid
 
 
785
            uid = pwd.getpwnam("nobody").pw_uid
 
 
789
        gid = pwd.getpwnam("mandos").pw_gid
 
 
792
            gid = pwd.getpwnam("nogroup").pw_gid
 
 
798
    except OSError, error:
 
 
799
        if error[0] != errno.EPERM:
 
 
803
    service = AvahiService(name = server_settings["servicename"],
 
 
804
                           servicetype = "_mandos._tcp", )
 
 
805
    if server_settings["interface"]:
 
 
806
        service.interface = if_nametoindex\
 
 
807
                            (server_settings["interface"])
 
 
812
    # From the Avahi example code
 
 
813
    DBusGMainLoop(set_as_default=True )
 
 
814
    main_loop = gobject.MainLoop()
 
 
815
    bus = dbus.SystemBus()
 
 
816
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
 
 
817
                                           avahi.DBUS_PATH_SERVER),
 
 
818
                            avahi.DBUS_INTERFACE_SERVER)
 
 
819
    # End of Avahi example code
 
 
821
    def remove_from_clients(client):
 
 
822
        clients.remove(client)
 
 
824
            logger.critical(u"No clients left, exiting")
 
 
827
    clients.update(Set(Client(name = section,
 
 
828
                              stop_hook = remove_from_clients,
 
 
830
                              = dict(client_config.items(section)))
 
 
831
                       for section in client_config.sections()))
 
 
833
        logger.critical(u"No clients defined")
 
 
837
        # Redirect stdin so all checkers get /dev/null
 
 
838
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
 
839
        os.dup2(null, sys.stdin.fileno())
 
 
844
        logger.removeHandler(console)
 
 
845
        # Close all input and output, do double fork, etc.
 
 
850
        pidfile.write(str(pid) + "\n")
 
 
854
        logger.error(u"Could not write to file %r with PID %d",
 
 
857
        # "pidfile" was never created
 
 
862
        "Cleanup function; run on exit"
 
 
864
        # From the Avahi example code
 
 
865
        if not group is None:
 
 
868
        # End of Avahi example code
 
 
871
            client = clients.pop()
 
 
872
            client.stop_hook = None
 
 
875
    atexit.register(cleanup)
 
 
878
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
 
879
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
 
880
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
 
882
    for client in clients:
 
 
886
    tcp_server.server_activate()
 
 
888
    # Find out what port we got
 
 
889
    service.port = tcp_server.socket.getsockname()[1]
 
 
890
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
 
891
                u" scope_id %d" % tcp_server.socket.getsockname())
 
 
893
    #service.interface = tcp_server.socket.getsockname()[3]
 
 
896
        # From the Avahi example code
 
 
897
        server.connect_to_signal("StateChanged", server_state_changed)
 
 
899
            server_state_changed(server.GetState())
 
 
900
        except dbus.exceptions.DBusException, error:
 
 
901
            logger.critical(u"DBusException: %s", error)
 
 
903
        # End of Avahi example code
 
 
905
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
 
906
                             lambda *args, **kwargs:
 
 
907
                             tcp_server.handle_request\
 
 
908
                             (*args[2:], **kwargs) or True)
 
 
910
        logger.debug(u"Starting main loop")
 
 
912
    except AvahiError, error:
 
 
913
        logger.critical(u"AvahiError: %s" + unicode(error))
 
 
915
    except KeyboardInterrupt:
 
 
919
if __name__ == '__main__':