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
 
14
173
class Client(object):
 
15
 
    def __init__(self, name=None, dn=None, password=None,
 
16
 
                 passfile=None, fqdn=None, timeout=None,
 
 
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'
 
21
 
            self.password = password
 
23
 
            self.password = open(passfile).readall()
 
25
 
            print "No Password or Passfile in client config file"
 
26
 
            # raise RuntimeError XXX
 
27
 
            self.password = "gazonk"
 
32
 
            timeout = self.server.options.timeout
 
33
 
        self.timeout = timeout
 
35
 
            interval = self.server.options.interval
 
36
 
        self.interval = interval
 
38
 
def server_bind(self):
 
39
 
    if self.options.interface:
 
40
 
        if not hasattr(socket, "SO_BINDTODEVICE"):
 
41
 
            # From /usr/include/asm-i486/socket.h
 
42
 
            socket.SO_BINDTODEVICE = 25
 
44
 
            self.socket.setsockopt(socket.SOL_SOCKET,
 
45
 
                                   socket.SO_BINDTODEVICE,
 
46
 
                                   self.options.interface)
 
47
 
        except socket.error, error:
 
48
 
            if error[0] == errno.EPERM:
 
49
 
                print "Warning: Denied permission to bind to interface", \
 
50
 
                      self.options.interface
 
53
 
    return super(type(self), self).server_bind()
 
56
 
def init_with_options(self, *args, **kwargs):
 
57
 
    if "options" in kwargs:
 
58
 
        self.options = kwargs["options"]
 
60
 
    if "clients" in kwargs:
 
61
 
        self.clients = kwargs["clients"]
 
63
 
    if "credentials" in kwargs:
 
64
 
        self.credentials = kwargs["credentials"]
 
65
 
        del kwargs["credentials"]
 
66
 
    return super(type(self), self).__init__(*args, **kwargs)
 
69
 
class udp_handler(SocketServer.DatagramRequestHandler, object):
 
71
 
        self.wfile.write("Polo")
 
72
 
        print "UDP request answered"
 
75
 
class IPv6_UDPServer(SocketServer.UDPServer, object):
 
76
 
    __init__ = init_with_options
 
77
 
    address_family = socket.AF_INET6
 
78
 
    allow_reuse_address = True
 
79
 
    server_bind = server_bind
 
80
 
    def verify_request(self, request, client_address):
 
81
 
        print "UDP request came"
 
82
 
        return request[0] == "Marco"
 
85
 
class tcp_handler(SocketServer.BaseRequestHandler, object):
 
87
 
        print "TCP request came"
 
88
 
        print "Request:", self.request
 
89
 
        print "Client Address:", self.client_address
 
90
 
        print "Server:", self.server
 
91
 
        session = gnutls.connection.ServerSession(self.request,
 
92
 
                                                  self.server.credentials)
 
94
 
        if session.peer_certificate:
 
95
 
            print "DN:", session.peer_certificate.subject
 
98
 
        except gnutls.errors.CertificateError, error:
 
99
 
            print "Verify failed", error
 
103
 
            session.send(dict((client.dn, client.password)
 
104
 
                              for client in self.server.clients)
 
105
 
                         [session.peer_certificate.subject])
 
107
 
            session.send("gazonk")
 
 
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(os.path.expanduser(os.path.expandvars
 
 
244
                                              (config["secfile"])))
 
 
245
            self.secret = secfile.read()
 
 
248
            raise TypeError(u"No secret or secfile for client %s"
 
 
250
        self.host = config.get("host", "")
 
 
251
        self.created = datetime.datetime.now()
 
 
252
        self.last_checked_ok = None
 
 
253
        self.timeout = string_to_delta(config["timeout"])
 
 
254
        self.interval = string_to_delta(config["interval"])
 
 
255
        self.stop_hook = stop_hook
 
 
257
        self.checker_initiator_tag = None
 
 
258
        self.stop_initiator_tag = None
 
 
259
        self.checker_callback_tag = None
 
 
260
        self.check_command = config["checker"]
 
 
262
        """Start this client's checker and timeout hooks"""
 
 
263
        # Schedule a new checker to be started an 'interval' from now,
 
 
264
        # and every interval from then on.
 
 
265
        self.checker_initiator_tag = gobject.timeout_add\
 
 
266
                                     (self._interval_milliseconds,
 
 
268
        # Also start a new checker *right now*.
 
 
270
        # Schedule a stop() when 'timeout' has passed
 
 
271
        self.stop_initiator_tag = gobject.timeout_add\
 
 
272
                                  (self._timeout_milliseconds,
 
 
276
        The possibility that a client might be restarted is left open,
 
 
277
        but not currently used."""
 
 
278
        # If this client doesn't have a secret, it is already stopped.
 
 
279
        if hasattr(self, "secret") and self.secret:
 
 
280
            logger.info(u"Stopping client %s", self.name)
 
 
284
        if getattr(self, "stop_initiator_tag", False):
 
 
285
            gobject.source_remove(self.stop_initiator_tag)
 
 
286
            self.stop_initiator_tag = None
 
 
287
        if getattr(self, "checker_initiator_tag", False):
 
 
288
            gobject.source_remove(self.checker_initiator_tag)
 
 
289
            self.checker_initiator_tag = None
 
 
293
        # Do not run this again if called by a gobject.timeout_add
 
 
296
        self.stop_hook = None
 
 
298
    def checker_callback(self, pid, condition):
 
 
299
        """The checker has completed, so take appropriate actions."""
 
 
300
        now = datetime.datetime.now()
 
 
301
        self.checker_callback_tag = None
 
 
303
        if os.WIFEXITED(condition) \
 
 
304
               and (os.WEXITSTATUS(condition) == 0):
 
 
305
            logger.info(u"Checker for %(name)s succeeded",
 
 
307
            self.last_checked_ok = now
 
 
308
            gobject.source_remove(self.stop_initiator_tag)
 
 
309
            self.stop_initiator_tag = gobject.timeout_add\
 
 
310
                                      (self._timeout_milliseconds,
 
 
312
        elif not os.WIFEXITED(condition):
 
 
313
            logger.warning(u"Checker for %(name)s crashed?",
 
 
316
            logger.info(u"Checker for %(name)s failed",
 
 
318
    def start_checker(self):
 
 
319
        """Start a new checker subprocess if one is not running.
 
 
320
        If a checker already exists, leave it running and do
 
 
322
        # The reason for not killing a running checker is that if we
 
 
323
        # did that, then if a checker (for some reason) started
 
 
324
        # running slowly and taking more than 'interval' time, the
 
 
325
        # client would inevitably timeout, since no checker would get
 
 
326
        # a chance to run to completion.  If we instead leave running
 
 
327
        # checkers alone, the checker would have to take more time
 
 
328
        # than 'timeout' for the client to be declared invalid, which
 
 
329
        # is as it should be.
 
 
330
        if self.checker is None:
 
 
332
                # In case check_command has exactly one % operator
 
 
333
                command = self.check_command % self.host
 
 
335
                # Escape attributes for the shell
 
 
336
                escaped_attrs = dict((key, re.escape(str(val)))
 
 
338
                                     vars(self).iteritems())
 
 
340
                    command = self.check_command % escaped_attrs
 
 
341
                except TypeError, error:
 
 
342
                    logger.error(u'Could not format string "%s":'
 
 
343
                                 u' %s', self.check_command, error)
 
 
344
                    return True # Try again later
 
 
346
                logger.info(u"Starting checker %r for %s",
 
 
348
                # We don't need to redirect stdout and stderr, since
 
 
349
                # in normal mode, that is already done by daemon(),
 
 
350
                # and in debug mode we don't want to.  (Stdin is
 
 
351
                # always replaced by /dev/null.)
 
 
352
                self.checker = subprocess.Popen(command,
 
 
355
                self.checker_callback_tag = gobject.child_watch_add\
 
 
357
                                             self.checker_callback)
 
 
358
            except OSError, error:
 
 
359
                logger.error(u"Failed to start subprocess: %s",
 
 
361
        # Re-run this periodically if run by gobject.timeout_add
 
 
363
    def stop_checker(self):
 
 
364
        """Force the checker process, if any, to stop."""
 
 
365
        if self.checker_callback_tag:
 
 
366
            gobject.source_remove(self.checker_callback_tag)
 
 
367
            self.checker_callback_tag = None
 
 
368
        if getattr(self, "checker", None) is None:
 
 
370
        logger.debug(u"Stopping checker for %(name)s", vars(self))
 
 
372
            os.kill(self.checker.pid, signal.SIGTERM)
 
 
374
            #if self.checker.poll() is None:
 
 
375
            #    os.kill(self.checker.pid, signal.SIGKILL)
 
 
376
        except OSError, error:
 
 
377
            if error.errno != errno.ESRCH: # No such process
 
 
380
    def still_valid(self):
 
 
381
        """Has the timeout not yet passed for this client?"""
 
 
382
        now = datetime.datetime.now()
 
 
383
        if self.last_checked_ok is None:
 
 
384
            return now < (self.created + self.timeout)
 
 
386
            return now < (self.last_checked_ok + self.timeout)
 
 
389
def peer_certificate(session):
 
 
390
    "Return the peer's OpenPGP certificate as a bytestring"
 
 
391
    # If not an OpenPGP certificate...
 
 
392
    if gnutls.library.functions.gnutls_certificate_type_get\
 
 
393
            (session._c_object) \
 
 
394
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
 
395
        # ...do the normal thing
 
 
396
        return session.peer_certificate
 
 
397
    list_size = ctypes.c_uint()
 
 
398
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
 
 
399
        (session._c_object, ctypes.byref(list_size))
 
 
400
    if list_size.value == 0:
 
 
403
    return ctypes.string_at(cert.data, cert.size)
 
 
406
def fingerprint(openpgp):
 
 
407
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
 
408
    # New GnuTLS "datum" with the OpenPGP public key
 
 
409
    datum = gnutls.library.types.gnutls_datum_t\
 
 
410
        (ctypes.cast(ctypes.c_char_p(openpgp),
 
 
411
                     ctypes.POINTER(ctypes.c_ubyte)),
 
 
412
         ctypes.c_uint(len(openpgp)))
 
 
413
    # New empty GnuTLS certificate
 
 
414
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
 
415
    gnutls.library.functions.gnutls_openpgp_crt_init\
 
 
417
    # Import the OpenPGP public key into the certificate
 
 
418
    gnutls.library.functions.gnutls_openpgp_crt_import\
 
 
419
                    (crt, ctypes.byref(datum),
 
 
420
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
 
421
    # Verify the self signature in the key
 
 
422
    crtverify = ctypes.c_uint()
 
 
423
    gnutls.library.functions.gnutls_openpgp_crt_verify_self\
 
 
424
        (crt, 0, ctypes.byref(crtverify))
 
 
425
    if crtverify.value != 0:
 
 
426
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
 
427
        raise gnutls.errors.CertificateSecurityError("Verify failed")
 
 
428
    # New buffer for the fingerprint
 
 
429
    buf = ctypes.create_string_buffer(20)
 
 
430
    buf_len = ctypes.c_size_t()
 
 
431
    # Get the fingerprint from the certificate into the buffer
 
 
432
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
 
 
433
        (crt, ctypes.byref(buf), ctypes.byref(buf_len))
 
 
434
    # Deinit the certificate
 
 
435
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
 
436
    # Convert the buffer to a Python bytestring
 
 
437
    fpr = ctypes.string_at(buf, buf_len.value)
 
 
438
    # Convert the bytestring to hexadecimal notation
 
 
439
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
 
443
class TCP_handler(SocketServer.BaseRequestHandler, object):
 
 
444
    """A TCP request handler class.
 
 
445
    Instantiated by IPv6_TCPServer for each request to handle it.
 
 
446
    Note: This will run in its own forked process."""
 
 
449
        logger.info(u"TCP connection from: %s",
 
 
450
                     unicode(self.client_address))
 
 
451
        session = gnutls.connection.ClientSession\
 
 
452
                  (self.request, gnutls.connection.X509Credentials())
 
 
454
        line = self.request.makefile().readline()
 
 
455
        logger.debug(u"Protocol version: %r", line)
 
 
457
            if int(line.strip().split()[0]) > 1:
 
 
459
        except (ValueError, IndexError, RuntimeError), error:
 
 
460
            logger.error(u"Unknown protocol version: %s", error)
 
 
463
        # Note: gnutls.connection.X509Credentials is really a generic
 
 
464
        # GnuTLS certificate credentials object so long as no X.509
 
 
465
        # keys are added to it.  Therefore, we can use it here despite
 
 
466
        # using OpenPGP certificates.
 
 
468
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
 
469
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
 
471
        priority = "NORMAL"             # Fallback default, since this
 
 
473
        if self.server.settings["priority"]:
 
 
474
            priority = self.server.settings["priority"]
 
 
475
        gnutls.library.functions.gnutls_priority_set_direct\
 
 
476
            (session._c_object, priority, None)
 
 
480
        except gnutls.errors.GNUTLSError, error:
 
 
481
            logger.warning(u"Handshake failed: %s", error)
 
 
482
            # Do not run session.bye() here: the session is not
 
 
483
            # established.  Just abandon the request.
 
 
486
            fpr = fingerprint(peer_certificate(session))
 
 
487
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
 
488
            logger.warning(u"Bad certificate: %s", error)
 
 
491
        logger.debug(u"Fingerprint: %s", fpr)
 
 
493
        for c in self.server.clients:
 
 
494
            if c.fingerprint == fpr:
 
 
498
            logger.warning(u"Client not found for fingerprint: %s",
 
 
502
        # Have to check if client.still_valid(), since it is possible
 
 
503
        # that the client timed out while establishing the GnuTLS
 
 
505
        if not client.still_valid():
 
 
506
            logger.warning(u"Client %(name)s is invalid",
 
 
511
        while sent_size < len(client.secret):
 
 
512
            sent = session.send(client.secret[sent_size:])
 
 
513
            logger.debug(u"Sent: %d, remaining: %d",
 
 
514
                         sent, len(client.secret)
 
 
515
                         - (sent_size + sent))
 
111
520
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
 
112
 
    __init__ = init_with_options
 
 
521
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
 
 
523
        settings:       Server settings
 
 
524
        clients:        Set() of Client objects
 
 
525
        enabled:        Boolean; whether this server is activated yet
 
113
527
    address_family = socket.AF_INET6
 
114
 
    allow_reuse_address = True
 
115
 
    request_queue_size = 1024
 
116
 
    server_bind = server_bind
 
 
528
    def __init__(self, *args, **kwargs):
 
 
529
        if "settings" in kwargs:
 
 
530
            self.settings = kwargs["settings"]
 
 
531
            del kwargs["settings"]
 
 
532
        if "clients" in kwargs:
 
 
533
            self.clients = kwargs["clients"]
 
 
534
            del kwargs["clients"]
 
 
536
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
 
 
537
    def server_bind(self):
 
 
538
        """This overrides the normal server_bind() function
 
 
539
        to bind to an interface if one was specified, and also NOT to
 
 
540
        bind to an address or port if they were not specified."""
 
 
541
        if self.settings["interface"]:
 
 
542
            # 25 is from /usr/include/asm-i486/socket.h
 
 
543
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
 
 
545
                self.socket.setsockopt(socket.SOL_SOCKET,
 
 
547
                                       self.settings["interface"])
 
 
548
            except socket.error, error:
 
 
549
                if error[0] == errno.EPERM:
 
 
550
                    logger.error(u"No permission to"
 
 
551
                                 u" bind to interface %s",
 
 
552
                                 self.settings["interface"])
 
 
555
        # Only bind(2) the socket if we really need to.
 
 
556
        if self.server_address[0] or self.server_address[1]:
 
 
557
            if not self.server_address[0]:
 
 
559
                self.server_address = (in6addr_any,
 
 
560
                                       self.server_address[1])
 
 
561
            elif not self.server_address[1]:
 
 
562
                self.server_address = (self.server_address[0],
 
 
564
#                 if self.settings["interface"]:
 
 
565
#                     self.server_address = (self.server_address[0],
 
 
571
            return super(IPv6_TCPServer, self).server_bind()
 
 
572
    def server_activate(self):
 
 
574
            return super(IPv6_TCPServer, self).server_activate()
 
 
579
def string_to_delta(interval):
 
 
580
    """Parse a string and return a datetime.timedelta
 
 
582
    >>> string_to_delta('7d')
 
 
583
    datetime.timedelta(7)
 
 
584
    >>> string_to_delta('60s')
 
 
585
    datetime.timedelta(0, 60)
 
 
586
    >>> string_to_delta('60m')
 
 
587
    datetime.timedelta(0, 3600)
 
 
588
    >>> string_to_delta('24h')
 
 
589
    datetime.timedelta(1)
 
 
590
    >>> string_to_delta(u'1w')
 
 
591
    datetime.timedelta(7)
 
 
592
    >>> string_to_delta('5m 30s')
 
 
593
    datetime.timedelta(0, 330)
 
 
595
    timevalue = datetime.timedelta(0)
 
 
596
    for s in interval.split():
 
 
598
            suffix = unicode(s[-1])
 
 
601
                delta = datetime.timedelta(value)
 
 
603
                delta = datetime.timedelta(0, value)
 
 
605
                delta = datetime.timedelta(0, 0, 0, 0, value)
 
 
607
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
 
 
609
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
 
 
612
        except (ValueError, IndexError):
 
 
618
def server_state_changed(state):
 
 
619
    """Derived from the Avahi example code"""
 
 
620
    if state == avahi.SERVER_COLLISION:
 
 
621
        logger.error(u"Zeroconf server name collision")
 
 
623
    elif state == avahi.SERVER_RUNNING:
 
 
627
def entry_group_state_changed(state, error):
 
 
628
    """Derived from the Avahi example code"""
 
 
629
    logger.debug(u"Avahi state change: %i", state)
 
 
631
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
 
 
632
        logger.debug(u"Zeroconf service established.")
 
 
633
    elif state == avahi.ENTRY_GROUP_COLLISION:
 
 
634
        logger.warning(u"Zeroconf service name collision.")
 
 
636
    elif state == avahi.ENTRY_GROUP_FAILURE:
 
 
637
        logger.critical(u"Avahi: Error in group state changed %s",
 
 
639
        raise AvahiGroupError("State changed: %s", str(error))
 
 
641
def if_nametoindex(interface):
 
 
642
    """Call the C function if_nametoindex(), or equivalent"""
 
 
643
    global if_nametoindex
 
 
645
        if_nametoindex = ctypes.cdll.LoadLibrary\
 
 
646
            (ctypes.util.find_library("c")).if_nametoindex
 
 
647
    except (OSError, AttributeError):
 
 
648
        if "struct" not in sys.modules:
 
 
650
        if "fcntl" not in sys.modules:
 
 
652
        def if_nametoindex(interface):
 
 
653
            "Get an interface index the hard way, i.e. using fcntl()"
 
 
654
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
 
 
656
            ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
 
 
657
                                struct.pack("16s16x", interface))
 
 
659
            interface_index = struct.unpack("I", ifreq[16:20])[0]
 
 
660
            return interface_index
 
 
661
    return if_nametoindex(interface)
 
 
664
def daemon(nochdir = False, noclose = False):
 
 
665
    """See daemon(3).  Standard BSD Unix function.
 
 
666
    This should really exist as os.daemon, but it doesn't (yet)."""
 
 
675
        # Close all standard open file descriptors
 
 
676
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
 
677
        if not stat.S_ISCHR(os.fstat(null).st_mode):
 
 
678
            raise OSError(errno.ENODEV,
 
 
679
                          "/dev/null not a character device")
 
 
680
        os.dup2(null, sys.stdin.fileno())
 
 
681
        os.dup2(null, sys.stdout.fileno())
 
 
682
        os.dup2(null, sys.stderr.fileno())
 
124
 
    parser = OptionParser()
 
 
688
    parser = OptionParser(version = "%%prog %s" % version)
 
125
689
    parser.add_option("-i", "--interface", type="string",
 
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,
 
 
690
                      metavar="IF", help="Bind to interface IF")
 
 
691
    parser.add_option("-a", "--address", type="string",
 
 
692
                      help="Address to listen for requests on")
 
 
693
    parser.add_option("-p", "--port", type="int",
 
141
694
                      help="Port number to receive requests on")
 
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__":
 
 
695
    parser.add_option("--check", action="store_true", default=False,
 
 
696
                      help="Run self-test")
 
 
697
    parser.add_option("--debug", action="store_true",
 
 
698
                      help="Debug mode; run in foreground and log to"
 
 
700
    parser.add_option("--priority", type="string", help="GnuTLS"
 
 
701
                      " priority string (see GnuTLS documentation)")
 
 
702
    parser.add_option("--servicename", type="string", metavar="NAME",
 
 
703
                      help="Zeroconf service name")
 
 
704
    parser.add_option("--configdir", type="string",
 
 
705
                      default="/etc/mandos", metavar="DIR",
 
 
706
                      help="Directory to search for configuration"
 
 
708
    options = parser.parse_args()[0]
 
 
715
    # Default values for config file for server-global settings
 
 
716
    server_defaults = { "interface": "",
 
 
721
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
 
722
                        "servicename": "Mandos",
 
 
725
    # Parse config file for server-global settings
 
 
726
    server_config = ConfigParser.SafeConfigParser(server_defaults)
 
 
728
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
 
729
    # Convert the SafeConfigParser object to a dict
 
 
730
    server_settings = server_config.defaults()
 
 
731
    # Use getboolean on the boolean config option
 
 
732
    server_settings["debug"] = server_config.getboolean\
 
 
736
    # Override the settings from the config file with command line
 
 
738
    for option in ("interface", "address", "port", "debug",
 
 
739
                   "priority", "servicename", "configdir"):
 
 
740
        value = getattr(options, option)
 
 
741
        if value is not None:
 
 
742
            server_settings[option] = value
 
 
744
    # Now we have our good server settings in "server_settings"
 
 
746
    debug = server_settings["debug"]
 
 
749
        syslogger.setLevel(logging.WARNING)
 
 
750
        console.setLevel(logging.WARNING)
 
 
752
    if server_settings["servicename"] != "Mandos":
 
 
753
        syslogger.setFormatter(logging.Formatter\
 
 
754
                               ('Mandos (%s): %%(levelname)s:'
 
 
756
                                % server_settings["servicename"]))
 
 
758
    # Parse config file with clients
 
 
759
    client_defaults = { "timeout": "1h",
 
 
761
                        "checker": "fping -q -- %(host)s",
 
 
764
    client_config = ConfigParser.SafeConfigParser(client_defaults)
 
 
765
    client_config.read(os.path.join(server_settings["configdir"],
 
 
769
    tcp_server = IPv6_TCPServer((server_settings["address"],
 
 
770
                                 server_settings["port"]),
 
 
772
                                settings=server_settings,
 
 
774
    pidfilename = "/var/run/mandos.pid"
 
 
776
        pidfile = open(pidfilename, "w")
 
 
777
    except IOError, error:
 
 
778
        logger.error("Could not open file %r", pidfilename)
 
 
783
        uid = pwd.getpwnam("mandos").pw_uid
 
 
786
            uid = pwd.getpwnam("nobody").pw_uid
 
 
790
        gid = pwd.getpwnam("mandos").pw_gid
 
 
793
            gid = pwd.getpwnam("nogroup").pw_gid
 
 
799
    except OSError, error:
 
 
800
        if error[0] != errno.EPERM:
 
 
804
    service = AvahiService(name = server_settings["servicename"],
 
 
805
                           servicetype = "_mandos._tcp", )
 
 
806
    if server_settings["interface"]:
 
 
807
        service.interface = if_nametoindex\
 
 
808
                            (server_settings["interface"])
 
 
813
    # From the Avahi example code
 
 
814
    DBusGMainLoop(set_as_default=True )
 
 
815
    main_loop = gobject.MainLoop()
 
 
816
    bus = dbus.SystemBus()
 
 
817
    server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
 
 
818
                                           avahi.DBUS_PATH_SERVER),
 
 
819
                            avahi.DBUS_INTERFACE_SERVER)
 
 
820
    # End of Avahi example code
 
 
822
    def remove_from_clients(client):
 
 
823
        clients.remove(client)
 
 
825
            logger.critical(u"No clients left, exiting")
 
 
828
    clients.update(Set(Client(name = section,
 
 
829
                              stop_hook = remove_from_clients,
 
 
831
                              = dict(client_config.items(section)))
 
 
832
                       for section in client_config.sections()))
 
 
834
        logger.critical(u"No clients defined")
 
 
838
        # Redirect stdin so all checkers get /dev/null
 
 
839
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
 
 
840
        os.dup2(null, sys.stdin.fileno())
 
 
845
        logger.removeHandler(console)
 
 
846
        # Close all input and output, do double fork, etc.
 
 
851
        pidfile.write(str(pid) + "\n")
 
 
855
        logger.error(u"Could not write to file %r with PID %d",
 
 
858
        # "pidfile" was never created
 
 
863
        "Cleanup function; run on exit"
 
 
865
        # From the Avahi example code
 
 
866
        if not group is None:
 
 
869
        # End of Avahi example code
 
 
872
            client = clients.pop()
 
 
873
            client.stop_hook = None
 
 
876
    atexit.register(cleanup)
 
 
879
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
 
880
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
 
 
881
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
 
 
883
    for client in clients:
 
 
887
    tcp_server.server_activate()
 
 
889
    # Find out what port we got
 
 
890
    service.port = tcp_server.socket.getsockname()[1]
 
 
891
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
 
892
                u" scope_id %d" % tcp_server.socket.getsockname())
 
 
894
    #service.interface = tcp_server.socket.getsockname()[3]
 
 
897
        # From the Avahi example code
 
 
898
        server.connect_to_signal("StateChanged", server_state_changed)
 
 
900
            server_state_changed(server.GetState())
 
 
901
        except dbus.exceptions.DBusException, error:
 
 
902
            logger.critical(u"DBusException: %s", error)
 
 
904
        # End of Avahi example code
 
 
906
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
 
 
907
                             lambda *args, **kwargs:
 
 
908
                             tcp_server.handle_request\
 
 
909
                             (*args[2:], **kwargs) or True)
 
 
911
        logger.debug(u"Starting main loop")
 
 
913
    except AvahiError, error:
 
 
914
        logger.critical(u"AvahiError: %s" + unicode(error))
 
 
916
    except KeyboardInterrupt:
 
 
920
if __name__ == '__main__':