/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2009-01-31 10:33:17 UTC
  • mfrom: (24.1.129 mandos)
  • Revision ID: teddy@fukt.bsnet.se-20090131103317-wzqvyr532sjcjt7u
Merge from Björn:

* mandos-ctl: New option "--remove-client".  Only default to listing
              clients if no clients were given on the command line.
* plugins.d/mandos-client.c: Lower kernel log level while bringing up
                             network interface.  New option "--delay"
                             to control the maximum delay to wait for
                             running interface.
* plugins.d/mandos-client.xml (SYNOPSIS, OPTIONS): New option
                                                   "--delay".

Show diffs side-by-side

added added

removed removed

Lines of Context:
66
66
import ctypes
67
67
import ctypes.util
68
68
 
69
 
try:
70
 
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
71
 
except AttributeError:
72
 
    try:
73
 
        from IN import SO_BINDTODEVICE
74
 
    except ImportError:
75
 
        # From /usr/include/asm/socket.h
76
 
        SO_BINDTODEVICE = 25
77
 
 
78
 
 
79
 
version = "1.0.8"
 
69
version = "1.0.5"
80
70
 
81
71
logger = logging.Logger('mandos')
82
72
syslogger = (logging.handlers.SysLogHandler
83
73
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
84
74
              address = "/dev/log"))
85
75
syslogger.setFormatter(logging.Formatter
86
 
                       ('Mandos [%(process)d]: %(levelname)s:'
87
 
                        ' %(message)s'))
 
76
                       ('Mandos: %(levelname)s: %(message)s'))
88
77
logger.addHandler(syslogger)
89
78
 
90
79
console = logging.StreamHandler()
91
 
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
92
 
                                       ' %(levelname)s: %(message)s'))
 
80
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
 
81
                                       ' %(message)s'))
93
82
logger.addHandler(console)
94
83
 
95
84
class AvahiError(Exception):
108
97
 
109
98
class AvahiService(object):
110
99
    """An Avahi (Zeroconf) service.
111
 
    
112
100
    Attributes:
113
101
    interface: integer; avahi.IF_UNSPEC or an interface index.
114
102
               Used to optionally bind to the specified interface.
125
113
    """
126
114
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
127
115
                 servicetype = None, port = None, TXT = None,
128
 
                 domain = "", host = "", max_renames = 32768,
129
 
                 protocol = avahi.PROTO_UNSPEC):
 
116
                 domain = "", host = "", max_renames = 32768):
130
117
        self.interface = interface
131
118
        self.name = name
132
119
        self.type = servicetype
136
123
        self.host = host
137
124
        self.rename_count = 0
138
125
        self.max_renames = max_renames
139
 
        self.protocol = protocol
140
126
    def rename(self):
141
127
        """Derived from the Avahi example code"""
142
128
        if self.rename_count >= self.max_renames:
148
134
        logger.info(u"Changing Zeroconf service name to %r ...",
149
135
                    str(self.name))
150
136
        syslogger.setFormatter(logging.Formatter
151
 
                               ('Mandos (%s) [%%(process)d]:'
152
 
                                ' %%(levelname)s: %%(message)s'
153
 
                                % self.name))
 
137
                               ('Mandos (%s): %%(levelname)s:'
 
138
                                ' %%(message)s' % self.name))
154
139
        self.remove()
155
140
        self.add()
156
141
        self.rename_count += 1
172
157
                     service.name, service.type)
173
158
        group.AddService(
174
159
                self.interface,         # interface
175
 
                self.protocol,          # protocol
 
160
                avahi.PROTO_INET6,      # protocol
176
161
                dbus.UInt32(0),         # flags
177
162
                self.name, self.type,
178
163
                self.domain, self.host,
190
175
    return dbus.String(dt.isoformat(), variant_level=variant_level)
191
176
 
192
177
 
193
 
class Client(object):
 
178
class Client(dbus.service.Object):
194
179
    """A representation of a client host served by this server.
195
 
    
196
180
    Attributes:
197
181
    name:       string; from the config file, used in log messages and
198
182
                        D-Bus identifiers
218
202
                     client lives.  %() expansions are done at
219
203
                     runtime with vars(self) as dict, so that for
220
204
                     instance %(name)s can be used in the command.
221
 
    current_checker_command: string; current running checker_command
 
205
    use_dbus: bool(); Whether to provide D-Bus interface and signals
 
206
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
222
207
    """
223
208
    def timeout_milliseconds(self):
224
209
        "Return the 'timeout' attribute in milliseconds"
232
217
                + (self.interval.seconds * 1000)
233
218
                + (self.interval.microseconds // 1000))
234
219
    
235
 
    def __init__(self, name = None, disable_hook=None, config=None):
 
220
    def __init__(self, name = None, disable_hook=None, config=None,
 
221
                 use_dbus=True):
236
222
        """Note: the 'checker' key in 'config' sets the
237
223
        'checker_command' attribute and *not* the 'checker'
238
224
        attribute."""
240
226
        if config is None:
241
227
            config = {}
242
228
        logger.debug(u"Creating client %r", self.name)
 
229
        self.use_dbus = False   # During __init__
243
230
        # Uppercase and remove spaces from fingerprint for later
244
231
        # comparison purposes with return value from the fingerprint()
245
232
        # function
269
256
        self.disable_initiator_tag = None
270
257
        self.checker_callback_tag = None
271
258
        self.checker_command = config["checker"]
272
 
        self.current_checker_command = None
273
259
        self.last_connect = None
 
260
        # Only now, when this client is initialized, can it show up on
 
261
        # the D-Bus
 
262
        self.use_dbus = use_dbus
 
263
        if self.use_dbus:
 
264
            self.dbus_object_path = (dbus.ObjectPath
 
265
                                     ("/clients/"
 
266
                                      + self.name.replace(".", "_")))
 
267
            dbus.service.Object.__init__(self, bus,
 
268
                                         self.dbus_object_path)
274
269
    
275
270
    def enable(self):
276
271
        """Start this client's checker and timeout hooks"""
287
282
                                   (self.timeout_milliseconds(),
288
283
                                    self.disable))
289
284
        self.enabled = True
 
285
        if self.use_dbus:
 
286
            # Emit D-Bus signals
 
287
            self.PropertyChanged(dbus.String(u"enabled"),
 
288
                                 dbus.Boolean(True, variant_level=1))
 
289
            self.PropertyChanged(dbus.String(u"last_enabled"),
 
290
                                 (_datetime_to_dbus(self.last_enabled,
 
291
                                                    variant_level=1)))
290
292
    
291
293
    def disable(self):
292
294
        """Disable this client."""
303
305
        if self.disable_hook:
304
306
            self.disable_hook(self)
305
307
        self.enabled = False
 
308
        if self.use_dbus:
 
309
            # Emit D-Bus signal
 
310
            self.PropertyChanged(dbus.String(u"enabled"),
 
311
                                 dbus.Boolean(False, variant_level=1))
306
312
        # Do not run this again if called by a gobject.timeout_add
307
313
        return False
308
314
    
314
320
        """The checker has completed, so take appropriate actions."""
315
321
        self.checker_callback_tag = None
316
322
        self.checker = None
 
323
        if self.use_dbus:
 
324
            # Emit D-Bus signal
 
325
            self.PropertyChanged(dbus.String(u"checker_running"),
 
326
                                 dbus.Boolean(False, variant_level=1))
317
327
        if os.WIFEXITED(condition):
318
328
            exitstatus = os.WEXITSTATUS(condition)
319
329
            if exitstatus == 0:
323
333
            else:
324
334
                logger.info(u"Checker for %(name)s failed",
325
335
                            vars(self))
 
336
            if self.use_dbus:
 
337
                # Emit D-Bus signal
 
338
                self.CheckerCompleted(dbus.Int16(exitstatus),
 
339
                                      dbus.Int64(condition),
 
340
                                      dbus.String(command))
326
341
        else:
327
342
            logger.warning(u"Checker for %(name)s crashed?",
328
343
                           vars(self))
 
344
            if self.use_dbus:
 
345
                # Emit D-Bus signal
 
346
                self.CheckerCompleted(dbus.Int16(-1),
 
347
                                      dbus.Int64(condition),
 
348
                                      dbus.String(command))
329
349
    
330
350
    def checked_ok(self):
331
351
        """Bump up the timeout for this client.
332
 
        
333
352
        This should only be called when the client has been seen,
334
353
        alive and well.
335
354
        """
338
357
        self.disable_initiator_tag = (gobject.timeout_add
339
358
                                      (self.timeout_milliseconds(),
340
359
                                       self.disable))
 
360
        if self.use_dbus:
 
361
            # Emit D-Bus signal
 
362
            self.PropertyChanged(
 
363
                dbus.String(u"last_checked_ok"),
 
364
                (_datetime_to_dbus(self.last_checked_ok,
 
365
                                   variant_level=1)))
341
366
    
342
367
    def start_checker(self):
343
368
        """Start a new checker subprocess if one is not running.
344
 
        
345
369
        If a checker already exists, leave it running and do
346
370
        nothing."""
347
371
        # The reason for not killing a running checker is that if we
352
376
        # checkers alone, the checker would have to take more time
353
377
        # than 'timeout' for the client to be declared invalid, which
354
378
        # is as it should be.
355
 
        
356
 
        # If a checker exists, make sure it is not a zombie
357
 
        if self.checker is not None:
358
 
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
359
 
            if pid:
360
 
                logger.warning("Checker was a zombie")
361
 
                gobject.source_remove(self.checker_callback_tag)
362
 
                self.checker_callback(pid, status,
363
 
                                      self.current_checker_command)
364
 
        # Start a new checker if needed
365
379
        if self.checker is None:
366
380
            try:
367
381
                # In case checker_command has exactly one % operator
377
391
                    logger.error(u'Could not format string "%s":'
378
392
                                 u' %s', self.checker_command, error)
379
393
                    return True # Try again later
380
 
            self.current_checker_command = command
381
394
            try:
382
395
                logger.info(u"Starting checker %r for %s",
383
396
                            command, self.name)
388
401
                self.checker = subprocess.Popen(command,
389
402
                                                close_fds=True,
390
403
                                                shell=True, cwd="/")
 
404
                if self.use_dbus:
 
405
                    # Emit D-Bus signal
 
406
                    self.CheckerStarted(command)
 
407
                    self.PropertyChanged(
 
408
                        dbus.String("checker_running"),
 
409
                        dbus.Boolean(True, variant_level=1))
391
410
                self.checker_callback_tag = (gobject.child_watch_add
392
411
                                             (self.checker.pid,
393
412
                                              self.checker_callback,
394
413
                                              data=command))
395
 
                # The checker may have completed before the gobject
396
 
                # watch was added.  Check for this.
397
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
398
 
                if pid:
399
 
                    gobject.source_remove(self.checker_callback_tag)
400
 
                    self.checker_callback(pid, status, command)
401
414
            except OSError, error:
402
415
                logger.error(u"Failed to start subprocess: %s",
403
416
                             error)
421
434
            if error.errno != errno.ESRCH: # No such process
422
435
                raise
423
436
        self.checker = None
 
437
        if self.use_dbus:
 
438
            self.PropertyChanged(dbus.String(u"checker_running"),
 
439
                                 dbus.Boolean(False, variant_level=1))
424
440
    
425
441
    def still_valid(self):
426
442
        """Has the timeout not yet passed for this client?"""
431
447
            return now < (self.created + self.timeout)
432
448
        else:
433
449
            return now < (self.last_checked_ok + self.timeout)
434
 
 
435
 
 
436
 
class ClientDBus(Client, dbus.service.Object):
437
 
    """A Client class using D-Bus
438
 
    
439
 
    Attributes:
440
 
    dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
441
 
    """
442
 
    # dbus.service.Object doesn't use super(), so we can't either.
443
 
    
444
 
    def __init__(self, *args, **kwargs):
445
 
        Client.__init__(self, *args, **kwargs)
446
 
        # Only now, when this client is initialized, can it show up on
447
 
        # the D-Bus
448
 
        self.dbus_object_path = (dbus.ObjectPath
449
 
                                 ("/clients/"
450
 
                                  + self.name.replace(".", "_")))
451
 
        dbus.service.Object.__init__(self, bus,
452
 
                                     self.dbus_object_path)
453
 
    def enable(self):
454
 
        oldstate = getattr(self, "enabled", False)
455
 
        r = Client.enable(self)
456
 
        if oldstate != self.enabled:
457
 
            # Emit D-Bus signals
458
 
            self.PropertyChanged(dbus.String(u"enabled"),
459
 
                                 dbus.Boolean(True, variant_level=1))
460
 
            self.PropertyChanged(dbus.String(u"last_enabled"),
461
 
                                 (_datetime_to_dbus(self.last_enabled,
462
 
                                                    variant_level=1)))
463
 
        return r
464
 
    
465
 
    def disable(self, signal = True):
466
 
        oldstate = getattr(self, "enabled", False)
467
 
        r = Client.disable(self)
468
 
        if signal and oldstate != self.enabled:
469
 
            # Emit D-Bus signal
470
 
            self.PropertyChanged(dbus.String(u"enabled"),
471
 
                                 dbus.Boolean(False, variant_level=1))
472
 
        return r
473
 
    
474
 
    def __del__(self, *args, **kwargs):
475
 
        try:
476
 
            self.remove_from_connection()
477
 
        except LookupError:
478
 
            pass
479
 
        if hasattr(dbus.service.Object, "__del__"):
480
 
            dbus.service.Object.__del__(self, *args, **kwargs)
481
 
        Client.__del__(self, *args, **kwargs)
482
 
    
483
 
    def checker_callback(self, pid, condition, command,
484
 
                         *args, **kwargs):
485
 
        self.checker_callback_tag = None
486
 
        self.checker = None
487
 
        # Emit D-Bus signal
488
 
        self.PropertyChanged(dbus.String(u"checker_running"),
489
 
                             dbus.Boolean(False, variant_level=1))
490
 
        if os.WIFEXITED(condition):
491
 
            exitstatus = os.WEXITSTATUS(condition)
492
 
            # Emit D-Bus signal
493
 
            self.CheckerCompleted(dbus.Int16(exitstatus),
494
 
                                  dbus.Int64(condition),
495
 
                                  dbus.String(command))
496
 
        else:
497
 
            # Emit D-Bus signal
498
 
            self.CheckerCompleted(dbus.Int16(-1),
499
 
                                  dbus.Int64(condition),
500
 
                                  dbus.String(command))
501
 
        
502
 
        return Client.checker_callback(self, pid, condition, command,
503
 
                                       *args, **kwargs)
504
 
    
505
 
    def checked_ok(self, *args, **kwargs):
506
 
        r = Client.checked_ok(self, *args, **kwargs)
507
 
        # Emit D-Bus signal
508
 
        self.PropertyChanged(
509
 
            dbus.String(u"last_checked_ok"),
510
 
            (_datetime_to_dbus(self.last_checked_ok,
511
 
                               variant_level=1)))
512
 
        return r
513
 
    
514
 
    def start_checker(self, *args, **kwargs):
515
 
        old_checker = self.checker
516
 
        if self.checker is not None:
517
 
            old_checker_pid = self.checker.pid
518
 
        else:
519
 
            old_checker_pid = None
520
 
        r = Client.start_checker(self, *args, **kwargs)
521
 
        # Only if new checker process was started
522
 
        if (self.checker is not None
523
 
            and old_checker_pid != self.checker.pid):
524
 
            # Emit D-Bus signal
525
 
            self.CheckerStarted(self.current_checker_command)
526
 
            self.PropertyChanged(
527
 
                dbus.String("checker_running"),
528
 
                dbus.Boolean(True, variant_level=1))
529
 
        return r
530
 
    
531
 
    def stop_checker(self, *args, **kwargs):
532
 
        old_checker = getattr(self, "checker", None)
533
 
        r = Client.stop_checker(self, *args, **kwargs)
534
 
        if (old_checker is not None
535
 
            and getattr(self, "checker", None) is None):
536
 
            self.PropertyChanged(dbus.String(u"checker_running"),
537
 
                                 dbus.Boolean(False, variant_level=1))
538
 
        return r
539
450
    
540
451
    ## D-Bus methods & signals
541
452
    _interface = u"se.bsnet.fukt.Mandos.Client"
599
510
                }, signature="sv")
600
511
    
601
512
    # IsStillValid - method
602
 
    @dbus.service.method(_interface, out_signature="b")
603
 
    def IsStillValid(self):
604
 
        return self.still_valid()
 
513
    IsStillValid = (dbus.service.method(_interface, out_signature="b")
 
514
                    (still_valid))
 
515
    IsStillValid.__name__ = "IsStillValid"
605
516
    
606
517
    # PropertyChanged - signal
607
518
    @dbus.service.signal(_interface, signature="sv")
609
520
        "D-Bus signal"
610
521
        pass
611
522
    
612
 
    # ReceivedSecret - signal
613
 
    @dbus.service.signal(_interface)
614
 
    def ReceivedSecret(self):
615
 
        "D-Bus signal"
616
 
        pass
617
 
    
618
 
    # Rejected - signal
619
 
    @dbus.service.signal(_interface)
620
 
    def Rejected(self):
621
 
        "D-Bus signal"
622
 
        pass
623
 
    
624
523
    # SetChecker - method
625
524
    @dbus.service.method(_interface, in_signature="s")
626
525
    def SetChecker(self, checker):
688
587
    del _interface
689
588
 
690
589
 
691
 
class ClientHandler(SocketServer.BaseRequestHandler, object):
692
 
    """A class to handle client connections.
693
 
    
694
 
    Instantiated once for each connection to handle it.
 
590
def peer_certificate(session):
 
591
    "Return the peer's OpenPGP certificate as a bytestring"
 
592
    # If not an OpenPGP certificate...
 
593
    if (gnutls.library.functions
 
594
        .gnutls_certificate_type_get(session._c_object)
 
595
        != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
 
596
        # ...do the normal thing
 
597
        return session.peer_certificate
 
598
    list_size = ctypes.c_uint(1)
 
599
    cert_list = (gnutls.library.functions
 
600
                 .gnutls_certificate_get_peers
 
601
                 (session._c_object, ctypes.byref(list_size)))
 
602
    if not bool(cert_list) and list_size.value != 0:
 
603
        raise gnutls.errors.GNUTLSError("error getting peer"
 
604
                                        " certificate")
 
605
    if list_size.value == 0:
 
606
        return None
 
607
    cert = cert_list[0]
 
608
    return ctypes.string_at(cert.data, cert.size)
 
609
 
 
610
 
 
611
def fingerprint(openpgp):
 
612
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
613
    # New GnuTLS "datum" with the OpenPGP public key
 
614
    datum = (gnutls.library.types
 
615
             .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
 
616
                                         ctypes.POINTER
 
617
                                         (ctypes.c_ubyte)),
 
618
                             ctypes.c_uint(len(openpgp))))
 
619
    # New empty GnuTLS certificate
 
620
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
621
    (gnutls.library.functions
 
622
     .gnutls_openpgp_crt_init(ctypes.byref(crt)))
 
623
    # Import the OpenPGP public key into the certificate
 
624
    (gnutls.library.functions
 
625
     .gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
 
626
                                gnutls.library.constants
 
627
                                .GNUTLS_OPENPGP_FMT_RAW))
 
628
    # Verify the self signature in the key
 
629
    crtverify = ctypes.c_uint()
 
630
    (gnutls.library.functions
 
631
     .gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
 
632
    if crtverify.value != 0:
 
633
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
634
        raise gnutls.errors.CertificateSecurityError("Verify failed")
 
635
    # New buffer for the fingerprint
 
636
    buf = ctypes.create_string_buffer(20)
 
637
    buf_len = ctypes.c_size_t()
 
638
    # Get the fingerprint from the certificate into the buffer
 
639
    (gnutls.library.functions
 
640
     .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
 
641
                                         ctypes.byref(buf_len)))
 
642
    # Deinit the certificate
 
643
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
644
    # Convert the buffer to a Python bytestring
 
645
    fpr = ctypes.string_at(buf, buf_len.value)
 
646
    # Convert the bytestring to hexadecimal notation
 
647
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
648
    return hex_fpr
 
649
 
 
650
 
 
651
class TCP_handler(SocketServer.BaseRequestHandler, object):
 
652
    """A TCP request handler class.
 
653
    Instantiated by IPv6_TCPServer for each request to handle it.
695
654
    Note: This will run in its own forked process."""
696
655
    
697
656
    def handle(self):
698
657
        logger.info(u"TCP connection from: %s",
699
658
                    unicode(self.client_address))
700
 
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
701
 
        # Open IPC pipe to parent process
702
 
        with closing(os.fdopen(self.server.pipe[1], "w", 1)) as ipc:
703
 
            session = (gnutls.connection
704
 
                       .ClientSession(self.request,
705
 
                                      gnutls.connection
706
 
                                      .X509Credentials()))
707
 
            
708
 
            line = self.request.makefile().readline()
709
 
            logger.debug(u"Protocol version: %r", line)
710
 
            try:
711
 
                if int(line.strip().split()[0]) > 1:
712
 
                    raise RuntimeError
713
 
            except (ValueError, IndexError, RuntimeError), error:
714
 
                logger.error(u"Unknown protocol version: %s", error)
715
 
                return
716
 
            
717
 
            # Note: gnutls.connection.X509Credentials is really a
718
 
            # generic GnuTLS certificate credentials object so long as
719
 
            # no X.509 keys are added to it.  Therefore, we can use it
720
 
            # here despite using OpenPGP certificates.
721
 
            
722
 
            #priority = ':'.join(("NONE", "+VERS-TLS1.1",
723
 
            #                     "+AES-256-CBC", "+SHA1",
724
 
            #                     "+COMP-NULL", "+CTYPE-OPENPGP",
725
 
            #                     "+DHE-DSS"))
726
 
            # Use a fallback default, since this MUST be set.
727
 
            priority = self.server.gnutls_priority
728
 
            if priority is None:
729
 
                priority = "NORMAL"
730
 
            (gnutls.library.functions
731
 
             .gnutls_priority_set_direct(session._c_object,
732
 
                                         priority, None))
733
 
            
734
 
            try:
735
 
                session.handshake()
736
 
            except gnutls.errors.GNUTLSError, error:
737
 
                logger.warning(u"Handshake failed: %s", error)
738
 
                # Do not run session.bye() here: the session is not
739
 
                # established.  Just abandon the request.
740
 
                return
741
 
            logger.debug(u"Handshake succeeded")
742
 
            try:
743
 
                fpr = self.fingerprint(self.peer_certificate(session))
744
 
            except (TypeError, gnutls.errors.GNUTLSError), error:
745
 
                logger.warning(u"Bad certificate: %s", error)
746
 
                session.bye()
747
 
                return
748
 
            logger.debug(u"Fingerprint: %s", fpr)
749
 
            
750
 
            for c in self.server.clients:
751
 
                if c.fingerprint == fpr:
752
 
                    client = c
753
 
                    break
754
 
            else:
755
 
                ipc.write("NOTFOUND %s\n" % fpr)
756
 
                session.bye()
757
 
                return
758
 
            # Have to check if client.still_valid(), since it is
759
 
            # possible that the client timed out while establishing
760
 
            # the GnuTLS session.
761
 
            if not client.still_valid():
762
 
                ipc.write("INVALID %s\n" % client.name)
763
 
                session.bye()
764
 
                return
765
 
            ipc.write("SENDING %s\n" % client.name)
766
 
            sent_size = 0
767
 
            while sent_size < len(client.secret):
768
 
                sent = session.send(client.secret[sent_size:])
769
 
                logger.debug(u"Sent: %d, remaining: %d",
770
 
                             sent, len(client.secret)
771
 
                             - (sent_size + sent))
772
 
                sent_size += sent
773
 
            session.bye()
774
 
    
775
 
    @staticmethod
776
 
    def peer_certificate(session):
777
 
        "Return the peer's OpenPGP certificate as a bytestring"
778
 
        # If not an OpenPGP certificate...
779
 
        if (gnutls.library.functions
780
 
            .gnutls_certificate_type_get(session._c_object)
781
 
            != gnutls.library.constants.GNUTLS_CRT_OPENPGP):
782
 
            # ...do the normal thing
783
 
            return session.peer_certificate
784
 
        list_size = ctypes.c_uint(1)
785
 
        cert_list = (gnutls.library.functions
786
 
                     .gnutls_certificate_get_peers
787
 
                     (session._c_object, ctypes.byref(list_size)))
788
 
        if not bool(cert_list) and list_size.value != 0:
789
 
            raise gnutls.errors.GNUTLSError("error getting peer"
790
 
                                            " certificate")
791
 
        if list_size.value == 0:
792
 
            return None
793
 
        cert = cert_list[0]
794
 
        return ctypes.string_at(cert.data, cert.size)
795
 
    
796
 
    @staticmethod
797
 
    def fingerprint(openpgp):
798
 
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
799
 
        # New GnuTLS "datum" with the OpenPGP public key
800
 
        datum = (gnutls.library.types
801
 
                 .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
802
 
                                             ctypes.POINTER
803
 
                                             (ctypes.c_ubyte)),
804
 
                                 ctypes.c_uint(len(openpgp))))
805
 
        # New empty GnuTLS certificate
806
 
        crt = gnutls.library.types.gnutls_openpgp_crt_t()
807
 
        (gnutls.library.functions
808
 
         .gnutls_openpgp_crt_init(ctypes.byref(crt)))
809
 
        # Import the OpenPGP public key into the certificate
810
 
        (gnutls.library.functions
811
 
         .gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
812
 
                                    gnutls.library.constants
813
 
                                    .GNUTLS_OPENPGP_FMT_RAW))
814
 
        # Verify the self signature in the key
815
 
        crtverify = ctypes.c_uint()
816
 
        (gnutls.library.functions
817
 
         .gnutls_openpgp_crt_verify_self(crt, 0,
818
 
                                         ctypes.byref(crtverify)))
819
 
        if crtverify.value != 0:
820
 
            gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
821
 
            raise (gnutls.errors.CertificateSecurityError
822
 
                   ("Verify failed"))
823
 
        # New buffer for the fingerprint
824
 
        buf = ctypes.create_string_buffer(20)
825
 
        buf_len = ctypes.c_size_t()
826
 
        # Get the fingerprint from the certificate into the buffer
827
 
        (gnutls.library.functions
828
 
         .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
829
 
                                             ctypes.byref(buf_len)))
830
 
        # Deinit the certificate
831
 
        gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
832
 
        # Convert the buffer to a Python bytestring
833
 
        fpr = ctypes.string_at(buf, buf_len.value)
834
 
        # Convert the bytestring to hexadecimal notation
835
 
        hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
836
 
        return hex_fpr
837
 
 
838
 
 
839
 
class ForkingMixInWithPipe(SocketServer.ForkingMixIn, object):
840
 
    """Like SocketServer.ForkingMixIn, but also pass a pipe.
841
 
    
842
 
    Assumes a gobject.MainLoop event loop.
843
 
    """
844
 
    def process_request(self, request, client_address):
845
 
        """Overrides and wraps the original process_request().
846
 
        
847
 
        This function creates a new pipe in self.pipe 
848
 
        """
849
 
        self.pipe = os.pipe()
850
 
        super(ForkingMixInWithPipe,
851
 
              self).process_request(request, client_address)
852
 
        os.close(self.pipe[1])  # close write end
853
 
        # Call "handle_ipc" for both data and EOF events
854
 
        gobject.io_add_watch(self.pipe[0],
855
 
                             gobject.IO_IN | gobject.IO_HUP,
856
 
                             self.handle_ipc)
857
 
    def handle_ipc(source, condition):
858
 
        """Dummy function; override as necessary"""
859
 
        os.close(source)
860
 
        return False
861
 
 
862
 
 
863
 
class IPv6_TCPServer(ForkingMixInWithPipe,
 
659
        session = (gnutls.connection
 
660
                   .ClientSession(self.request,
 
661
                                  gnutls.connection
 
662
                                  .X509Credentials()))
 
663
        
 
664
        line = self.request.makefile().readline()
 
665
        logger.debug(u"Protocol version: %r", line)
 
666
        try:
 
667
            if int(line.strip().split()[0]) > 1:
 
668
                raise RuntimeError
 
669
        except (ValueError, IndexError, RuntimeError), error:
 
670
            logger.error(u"Unknown protocol version: %s", error)
 
671
            return
 
672
        
 
673
        # Note: gnutls.connection.X509Credentials is really a generic
 
674
        # GnuTLS certificate credentials object so long as no X.509
 
675
        # keys are added to it.  Therefore, we can use it here despite
 
676
        # using OpenPGP certificates.
 
677
        
 
678
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
679
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
680
        #                "+DHE-DSS"))
 
681
        # Use a fallback default, since this MUST be set.
 
682
        priority = self.server.settings.get("priority", "NORMAL")
 
683
        (gnutls.library.functions
 
684
         .gnutls_priority_set_direct(session._c_object,
 
685
                                     priority, None))
 
686
        
 
687
        try:
 
688
            session.handshake()
 
689
        except gnutls.errors.GNUTLSError, error:
 
690
            logger.warning(u"Handshake failed: %s", error)
 
691
            # Do not run session.bye() here: the session is not
 
692
            # established.  Just abandon the request.
 
693
            return
 
694
        logger.debug(u"Handshake succeeded")
 
695
        try:
 
696
            fpr = fingerprint(peer_certificate(session))
 
697
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
698
            logger.warning(u"Bad certificate: %s", error)
 
699
            session.bye()
 
700
            return
 
701
        logger.debug(u"Fingerprint: %s", fpr)
 
702
        
 
703
        for c in self.server.clients:
 
704
            if c.fingerprint == fpr:
 
705
                client = c
 
706
                break
 
707
        else:
 
708
            logger.warning(u"Client not found for fingerprint: %s",
 
709
                           fpr)
 
710
            session.bye()
 
711
            return
 
712
        # Have to check if client.still_valid(), since it is possible
 
713
        # that the client timed out while establishing the GnuTLS
 
714
        # session.
 
715
        if not client.still_valid():
 
716
            logger.warning(u"Client %(name)s is invalid",
 
717
                           vars(client))
 
718
            session.bye()
 
719
            return
 
720
        ## This won't work here, since we're in a fork.
 
721
        # client.checked_ok()
 
722
        sent_size = 0
 
723
        while sent_size < len(client.secret):
 
724
            sent = session.send(client.secret[sent_size:])
 
725
            logger.debug(u"Sent: %d, remaining: %d",
 
726
                         sent, len(client.secret)
 
727
                         - (sent_size + sent))
 
728
            sent_size += sent
 
729
        session.bye()
 
730
 
 
731
 
 
732
class IPv6_TCPServer(SocketServer.ForkingMixIn,
864
733
                     SocketServer.TCPServer, object):
865
 
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
866
 
    
 
734
    """IPv6 TCP server.  Accepts 'None' as address and/or port.
867
735
    Attributes:
 
736
        settings:       Server settings
 
737
        clients:        Set() of Client objects
868
738
        enabled:        Boolean; whether this server is activated yet
869
 
        interface:      None or a network interface name (string)
870
 
        use_ipv6:       Boolean; to use IPv6 or not
871
 
        ----
872
 
        clients:        Set() of Client objects
873
 
        gnutls_priority GnuTLS priority string
874
 
        use_dbus:       Boolean; to emit D-Bus signals or not
875
739
    """
876
 
    def __init__(self, server_address, RequestHandlerClass,
877
 
                 interface=None, use_ipv6=True, clients=None,
878
 
                 gnutls_priority=None, use_dbus=True):
 
740
    address_family = socket.AF_INET6
 
741
    def __init__(self, *args, **kwargs):
 
742
        if "settings" in kwargs:
 
743
            self.settings = kwargs["settings"]
 
744
            del kwargs["settings"]
 
745
        if "clients" in kwargs:
 
746
            self.clients = kwargs["clients"]
 
747
            del kwargs["clients"]
879
748
        self.enabled = False
880
 
        self.interface = interface
881
 
        if use_ipv6:
882
 
            self.address_family = socket.AF_INET6
883
 
        self.clients = clients
884
 
        self.use_dbus = use_dbus
885
 
        self.gnutls_priority = gnutls_priority
886
 
        SocketServer.TCPServer.__init__(self, server_address,
887
 
                                        RequestHandlerClass)
 
749
        super(IPv6_TCPServer, self).__init__(*args, **kwargs)
888
750
    def server_bind(self):
889
751
        """This overrides the normal server_bind() function
890
752
        to bind to an interface if one was specified, and also NOT to
891
753
        bind to an address or port if they were not specified."""
892
 
        if self.interface is not None:
 
754
        if self.settings["interface"]:
 
755
            # 25 is from /usr/include/asm-i486/socket.h
 
756
            SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
893
757
            try:
894
758
                self.socket.setsockopt(socket.SOL_SOCKET,
895
759
                                       SO_BINDTODEVICE,
896
 
                                       self.interface + '\0')
 
760
                                       self.settings["interface"])
897
761
            except socket.error, error:
898
762
                if error[0] == errno.EPERM:
899
763
                    logger.error(u"No permission to"
900
764
                                 u" bind to interface %s",
901
 
                                 self.interface)
 
765
                                 self.settings["interface"])
902
766
                else:
903
 
                    raise
 
767
                    raise error
904
768
        # Only bind(2) the socket if we really need to.
905
769
        if self.server_address[0] or self.server_address[1]:
906
770
            if not self.server_address[0]:
907
 
                if self.address_family == socket.AF_INET6:
908
 
                    any_address = "::" # in6addr_any
909
 
                else:
910
 
                    any_address = socket.INADDR_ANY
911
 
                self.server_address = (any_address,
 
771
                in6addr_any = "::"
 
772
                self.server_address = (in6addr_any,
912
773
                                       self.server_address[1])
913
774
            elif not self.server_address[1]:
914
775
                self.server_address = (self.server_address[0],
915
776
                                       0)
916
 
#                 if self.interface:
 
777
#                 if self.settings["interface"]:
917
778
#                     self.server_address = (self.server_address[0],
918
779
#                                            0, # port
919
780
#                                            0, # flowinfo
920
781
#                                            if_nametoindex
921
 
#                                            (self.interface))
922
 
            return SocketServer.TCPServer.server_bind(self)
 
782
#                                            (self.settings
 
783
#                                             ["interface"]))
 
784
            return super(IPv6_TCPServer, self).server_bind()
923
785
    def server_activate(self):
924
786
        if self.enabled:
925
 
            return SocketServer.TCPServer.server_activate(self)
 
787
            return super(IPv6_TCPServer, self).server_activate()
926
788
    def enable(self):
927
789
        self.enabled = True
928
 
    def handle_ipc(self, source, condition, file_objects={}):
929
 
        condition_names = {
930
 
            gobject.IO_IN: "IN", # There is data to read.
931
 
            gobject.IO_OUT: "OUT", # Data can be written (without
932
 
                                   # blocking).
933
 
            gobject.IO_PRI: "PRI", # There is urgent data to read.
934
 
            gobject.IO_ERR: "ERR", # Error condition.
935
 
            gobject.IO_HUP: "HUP"  # Hung up (the connection has been
936
 
                                   # broken, usually for pipes and
937
 
                                   # sockets).
938
 
            }
939
 
        conditions_string = ' | '.join(name
940
 
                                       for cond, name in
941
 
                                       condition_names.iteritems()
942
 
                                       if cond & condition)
943
 
        logger.debug("Handling IPC: FD = %d, condition = %s", source,
944
 
                     conditions_string)
945
 
        
946
 
        # Turn the pipe file descriptor into a Python file object
947
 
        if source not in file_objects:
948
 
            file_objects[source] = os.fdopen(source, "r", 1)
949
 
        
950
 
        # Read a line from the file object
951
 
        cmdline = file_objects[source].readline()
952
 
        if not cmdline:             # Empty line means end of file
953
 
            # close the IPC pipe
954
 
            file_objects[source].close()
955
 
            del file_objects[source]
956
 
            
957
 
            # Stop calling this function
958
 
            return False
959
 
        
960
 
        logger.debug("IPC command: %r", cmdline)
961
 
        
962
 
        # Parse and act on command
963
 
        cmd, args = cmdline.rstrip("\r\n").split(None, 1)
964
 
        
965
 
        if cmd == "NOTFOUND":
966
 
            logger.warning(u"Client not found for fingerprint: %s",
967
 
                           args)
968
 
            if self.use_dbus:
969
 
                # Emit D-Bus signal
970
 
                mandos_dbus_service.ClientNotFound(args)
971
 
        elif cmd == "INVALID":
972
 
            for client in self.clients:
973
 
                if client.name == args:
974
 
                    logger.warning(u"Client %s is invalid", args)
975
 
                    if self.use_dbus:
976
 
                        # Emit D-Bus signal
977
 
                        client.Rejected()
978
 
                    break
979
 
            else:
980
 
                logger.error(u"Unknown client %s is invalid", args)
981
 
        elif cmd == "SENDING":
982
 
            for client in self.clients:
983
 
                if client.name == args:
984
 
                    logger.info(u"Sending secret to %s", client.name)
985
 
                    client.checked_ok()
986
 
                    if self.use_dbus:
987
 
                        # Emit D-Bus signal
988
 
                        client.ReceivedSecret()
989
 
                    break
990
 
            else:
991
 
                logger.error(u"Sending secret to unknown client %s",
992
 
                             args)
993
 
        else:
994
 
            logger.error("Unknown IPC command: %r", cmdline)
995
 
        
996
 
        # Keep calling this function
997
 
        return True
998
790
 
999
791
 
1000
792
def string_to_delta(interval):
1084
876
 
1085
877
def daemon(nochdir = False, noclose = False):
1086
878
    """See daemon(3).  Standard BSD Unix function.
1087
 
    
1088
879
    This should really exist as os.daemon, but it doesn't (yet)."""
1089
880
    if os.fork():
1090
881
        sys.exit()
1107
898
 
1108
899
 
1109
900
def main():
1110
 
    
1111
 
    ######################################################################
1112
 
    # Parsing of options, both command line and config file
1113
 
    
1114
901
    parser = optparse.OptionParser(version = "%%prog %s" % version)
1115
902
    parser.add_option("-i", "--interface", type="string",
1116
903
                      metavar="IF", help="Bind to interface IF")
1135
922
                      dest="use_dbus",
1136
923
                      help="Do not provide D-Bus system bus"
1137
924
                      " interface")
1138
 
    parser.add_option("--no-ipv6", action="store_false",
1139
 
                      dest="use_ipv6", help="Do not use IPv6")
1140
925
    options = parser.parse_args()[0]
1141
926
    
1142
927
    if options.check:
1153
938
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1154
939
                        "servicename": "Mandos",
1155
940
                        "use_dbus": "True",
1156
 
                        "use_ipv6": "True",
1157
941
                        }
1158
942
    
1159
943
    # Parse config file for server-global settings
1167
951
                                                        "debug")
1168
952
    server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
1169
953
                                                           "use_dbus")
1170
 
    server_settings["use_ipv6"] = server_config.getboolean("DEFAULT",
1171
 
                                                           "use_ipv6")
1172
954
    if server_settings["port"]:
1173
955
        server_settings["port"] = server_config.getint("DEFAULT",
1174
956
                                                       "port")
1178
960
    # options, if set.
1179
961
    for option in ("interface", "address", "port", "debug",
1180
962
                   "priority", "servicename", "configdir",
1181
 
                   "use_dbus", "use_ipv6"):
 
963
                   "use_dbus"):
1182
964
        value = getattr(options, option)
1183
965
        if value is not None:
1184
966
            server_settings[option] = value
1185
967
    del options
1186
968
    # Now we have our good server settings in "server_settings"
1187
969
    
1188
 
    ##################################################################
1189
 
    
1190
970
    # For convenience
1191
971
    debug = server_settings["debug"]
1192
972
    use_dbus = server_settings["use_dbus"]
1193
 
    use_ipv6 = server_settings["use_ipv6"]
 
973
 
 
974
    def sigsegvhandler(signum, frame):
 
975
        raise RuntimeError('Segmentation fault')
1194
976
    
1195
977
    if not debug:
1196
978
        syslogger.setLevel(logging.WARNING)
1197
979
        console.setLevel(logging.WARNING)
 
980
    else:
 
981
        signal.signal(signal.SIGSEGV, sigsegvhandler)
1198
982
    
1199
983
    if server_settings["servicename"] != "Mandos":
1200
984
        syslogger.setFormatter(logging.Formatter
1201
 
                               ('Mandos (%s) [%%(process)d]:'
1202
 
                                ' %%(levelname)s: %%(message)s'
 
985
                               ('Mandos (%s): %%(levelname)s:'
 
986
                                ' %%(message)s'
1203
987
                                % server_settings["servicename"]))
1204
988
    
1205
989
    # Parse config file with clients
1211
995
    client_config = ConfigParser.SafeConfigParser(client_defaults)
1212
996
    client_config.read(os.path.join(server_settings["configdir"],
1213
997
                                    "clients.conf"))
1214
 
 
1215
 
    global mandos_dbus_service
1216
 
    mandos_dbus_service = None
1217
998
    
1218
999
    clients = Set()
1219
1000
    tcp_server = IPv6_TCPServer((server_settings["address"],
1220
1001
                                 server_settings["port"]),
1221
 
                                ClientHandler,
1222
 
                                interface=
1223
 
                                server_settings["interface"],
1224
 
                                use_ipv6=use_ipv6,
1225
 
                                clients=clients,
1226
 
                                gnutls_priority=
1227
 
                                server_settings["priority"],
1228
 
                                use_dbus=use_dbus)
 
1002
                                TCP_handler,
 
1003
                                settings=server_settings,
 
1004
                                clients=clients)
1229
1005
    pidfilename = "/var/run/mandos.pid"
1230
1006
    try:
1231
1007
        pidfile = open(pidfilename, "w")
1232
 
    except IOError:
 
1008
    except IOError, error:
1233
1009
        logger.error("Could not open file %r", pidfilename)
1234
1010
    
1235
1011
    try:
1267
1043
         .gnutls_global_set_log_function(debug_gnutls))
1268
1044
    
1269
1045
    global service
1270
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1271
1046
    service = AvahiService(name = server_settings["servicename"],
1272
 
                           servicetype = "_mandos._tcp",
1273
 
                           protocol = protocol)
 
1047
                           servicetype = "_mandos._tcp", )
1274
1048
    if server_settings["interface"]:
1275
1049
        service.interface = (if_nametoindex
1276
1050
                             (server_settings["interface"]))
1289
1063
    if use_dbus:
1290
1064
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1291
1065
    
1292
 
    client_class = Client
1293
 
    if use_dbus:
1294
 
        client_class = ClientDBus
1295
 
    clients.update(Set(
1296
 
            client_class(name = section,
1297
 
                         config= dict(client_config.items(section)))
1298
 
            for section in client_config.sections()))
 
1066
    clients.update(Set(Client(name = section,
 
1067
                              config
 
1068
                              = dict(client_config.items(section)),
 
1069
                              use_dbus = use_dbus)
 
1070
                       for section in client_config.sections()))
1299
1071
    if not clients:
1300
1072
        logger.warning(u"No clients defined")
1301
1073
    
1312
1084
        daemon()
1313
1085
    
1314
1086
    try:
1315
 
        with closing(pidfile):
1316
 
            pid = os.getpid()
1317
 
            pidfile.write(str(pid) + "\n")
 
1087
        pid = os.getpid()
 
1088
        pidfile.write(str(pid) + "\n")
 
1089
        pidfile.close()
1318
1090
        del pidfile
1319
1091
    except IOError:
1320
1092
        logger.error(u"Could not write to file %r with PID %d",
1346
1118
    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1347
1119
    
1348
1120
    if use_dbus:
1349
 
        class MandosDBusService(dbus.service.Object):
 
1121
        class MandosServer(dbus.service.Object):
1350
1122
            """A D-Bus proxy object"""
1351
1123
            def __init__(self):
1352
1124
                dbus.service.Object.__init__(self, bus, "/")
1357
1129
                "D-Bus signal"
1358
1130
                pass
1359
1131
            
1360
 
            @dbus.service.signal(_interface, signature="s")
1361
 
            def ClientNotFound(self, fingerprint):
1362
 
                "D-Bus signal"
1363
 
                pass
1364
 
            
1365
1132
            @dbus.service.signal(_interface, signature="os")
1366
1133
            def ClientRemoved(self, objpath, name):
1367
1134
                "D-Bus signal"
1386
1153
                for c in clients:
1387
1154
                    if c.dbus_object_path == object_path:
1388
1155
                        clients.remove(c)
1389
 
                        c.remove_from_connection()
1390
1156
                        # Don't signal anything except ClientRemoved
1391
 
                        c.disable(signal=False)
 
1157
                        c.use_dbus = False
 
1158
                        c.disable()
1392
1159
                        # Emit D-Bus signal
1393
1160
                        self.ClientRemoved(object_path, c.name)
1394
1161
                        return
1396
1163
            
1397
1164
            del _interface
1398
1165
        
1399
 
        mandos_dbus_service = MandosDBusService()
 
1166
        mandos_server = MandosServer()
1400
1167
    
1401
1168
    for client in clients:
1402
1169
        if use_dbus:
1403
1170
            # Emit D-Bus signal
1404
 
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
1405
 
                                            client.GetAllProperties())
 
1171
            mandos_server.ClientAdded(client.dbus_object_path,
 
1172
                                      client.GetAllProperties())
1406
1173
        client.enable()
1407
1174
    
1408
1175
    tcp_server.enable()
1410
1177
    
1411
1178
    # Find out what port we got
1412
1179
    service.port = tcp_server.socket.getsockname()[1]
1413
 
    if use_ipv6:
1414
 
        logger.info(u"Now listening on address %r, port %d,"
1415
 
                    " flowinfo %d, scope_id %d"
1416
 
                    % tcp_server.socket.getsockname())
1417
 
    else:                       # IPv4
1418
 
        logger.info(u"Now listening on address %r, port %d"
1419
 
                    % tcp_server.socket.getsockname())
 
1180
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
 
1181
                u" scope_id %d" % tcp_server.socket.getsockname())
1420
1182
    
1421
1183
    #service.interface = tcp_server.socket.getsockname()[3]
1422
1184
    
1442
1204
        sys.exit(1)
1443
1205
    except KeyboardInterrupt:
1444
1206
        if debug:
1445
 
            print >> sys.stderr
1446
 
        logger.debug("Server received KeyboardInterrupt")
1447
 
    logger.debug("Server exiting")
 
1207
            print
1448
1208
 
1449
1209
if __name__ == '__main__':
1450
1210
    main()