/mandos/release

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

« back to all changes in this revision

Viewing changes to server.py

  • Committer: Teddy Hogeborn
  • Date: 2008-08-02 10:48:24 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080802104824-fx0miwp9o4g9r31e
* plugbasedclient.c (struct process): New fields "eof", "completed",
                                      and "status".
  (handle_sigchld): New function.
  (main): Initialize "dir" to NULL to only closedir() it if necessary.
          Move "process_list" to be a global variable to be accessible
          by "handle_sigchld".  Make "handle_sigchld" handle SIGCHLD.
          Remove redundant check for NULL "dir".  Free "filename" when
          no longer used.  Block SIGCHLD around fork()/exec().
          Restore normal signals in child.  Only loop while running
          processes exist.  Print process buffer when the process is
          done and it has emitted EOF, not when it only emits EOF.
          Remove processes from list which exit non-cleanly.  In
          cleaning up, closedir() if necessary.  Bug fix: set next
          pointer correctly when freeing process list.

* plugins.d/passprompt.c (main): Do not ignore SIGQUIT.

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
# This program is partly derived from an example program for an Avahi
7
7
# service publisher, downloaded from
8
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".
 
9
# following functions: "AvahiService.add", "AvahiService.remove",
 
10
# "server_state_changed", "entry_group_state_changed", and some lines
 
11
# in "main".
12
12
13
13
# Everything else is
14
14
# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson
61
61
from dbus.mainloop.glib import DBusGMainLoop
62
62
import ctypes
63
63
 
64
 
version = "1.0"
 
64
# Brief description of the operation of this program:
 
65
 
66
# This server announces itself as a Zeroconf service.  Connecting
 
67
# clients use the TLS protocol, with the unusual quirk that this
 
68
# server program acts as a TLS "client" while a connecting client acts
 
69
# as a TLS "server".  The client (acting as a TLS "server") must
 
70
# supply an OpenPGP certificate, and the fingerprint of this
 
71
# certificate is used by this server to look up (in a list read from a
 
72
# file at start time) which binary blob to give the client.  No other
 
73
# authentication or authorization is done by this server.
 
74
 
65
75
 
66
76
logger = logging.Logger('mandos')
67
77
syslogger = logging.handlers.SysLogHandler\
68
 
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
69
 
             address = "/dev/log")
 
78
            (facility = logging.handlers.SysLogHandler.LOG_DAEMON)
70
79
syslogger.setFormatter(logging.Formatter\
71
 
                        ('Mandos: %(levelname)s: %(message)s'))
 
80
                        ('%(levelname)s: %(message)s'))
72
81
logger.addHandler(syslogger)
 
82
del syslogger
73
83
 
74
84
 
75
85
class AvahiError(Exception):
86
96
 
87
97
 
88
98
class AvahiService(object):
89
 
    """An Avahi (Zeroconf) service.
90
 
    Attributes:
 
99
    """
91
100
    interface: integer; avahi.IF_UNSPEC or an interface index.
92
101
               Used to optionally bind to the specified interface.
93
 
    name: string; Example: 'Mandos'
94
 
    type: string; Example: '_mandos._tcp'.
95
 
                  See <http://www.dns-sd.org/ServiceTypes.html>
96
 
    port: integer; what port to announce
97
 
    TXT: list of strings; TXT record for the service
98
 
    domain: string; Domain to publish on, default to .local if empty.
99
 
    host: string; Host to publish records for, default is localhost
100
 
    max_renames: integer; maximum number of renames
101
 
    rename_count: integer; counter so we only rename after collisions
102
 
                  a sensible number of times
 
102
    name = string; Example: "Mandos"
 
103
    type = string; Example: "_mandos._tcp".
 
104
                   See <http://www.dns-sd.org/ServiceTypes.html>
 
105
    port = integer; what port to announce
 
106
    TXT = list of strings; TXT record for the service
 
107
    domain = string; Domain to publish on, default to .local if empty.
 
108
    host = string; Host to publish records for, default to localhost
 
109
                   if empty.
 
110
    max_renames = integer; maximum number of renames
 
111
    rename_count = integer; counter so we only rename after collisions
 
112
                   a sensible number of times
103
113
    """
104
114
    def __init__(self, interface = avahi.IF_UNSPEC, name = None,
105
115
                 type = None, port = None, TXT = None, domain = "",
106
 
                 host = "", max_renames = 32768):
 
116
                 host = "", max_renames = 12):
 
117
        """An Avahi (Zeroconf) service. """
107
118
        self.interface = interface
108
119
        self.name = name
109
120
        self.type = type
122
133
                            u" retries, exiting.", rename_count)
123
134
            raise AvahiServiceError("Too many renames")
124
135
        name = server.GetAlternativeServiceName(name)
125
 
        logger.error(u"Changing name to %r ...", name)
126
 
        syslogger.setFormatter(logging.Formatter\
127
 
                               ('Mandos (%s): %%(levelname)s:'
128
 
                               ' %%(message)s' % name))
 
136
        logger.notice(u"Changing name to %r ...", name)
129
137
        self.remove()
130
138
        self.add()
131
139
        self.rename_count += 1
167
175
    fingerprint: string (40 or 32 hexadecimal digits); used to
168
176
                 uniquely identify the client
169
177
    secret:    bytestring; sent verbatim (over TLS) to client
170
 
    host:      string; available for use by the checker command
 
178
    fqdn:      string (FQDN); available for use by the checker command
171
179
    created:   datetime.datetime(); object creation, not client host
172
180
    last_checked_ok: datetime.datetime() or None if not yet checked OK
173
181
    timeout:   datetime.timedelta(); How long from last_checked_ok
213
221
    interval = property(lambda self: self._interval,
214
222
                        _set_interval)
215
223
    del _set_interval
216
 
    def __init__(self, name = None, stop_hook=None, config={}):
217
 
        """Note: the 'checker' key in 'config' sets the
218
 
        'checker_command' attribute and *not* the 'checker'
219
 
        attribute."""
 
224
    def __init__(self, name=None, stop_hook=None, fingerprint=None,
 
225
                 secret=None, secfile=None, fqdn=None, timeout=None,
 
226
                 interval=-1, checker=None):
 
227
        """Note: the 'checker' argument sets the 'checker_command'
 
228
        attribute and not the 'checker' attribute.."""
220
229
        self.name = name
221
230
        logger.debug(u"Creating client %r", self.name)
222
 
        # Uppercase and remove spaces from fingerprint for later
223
 
        # comparison purposes with return value from the fingerprint()
224
 
        # function
225
 
        self.fingerprint = config["fingerprint"].upper()\
226
 
                           .replace(u" ", u"")
 
231
        # Uppercase and remove spaces from fingerprint
 
232
        # for later comparison purposes with return value of
 
233
        # the fingerprint() function
 
234
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
227
235
        logger.debug(u"  Fingerprint: %s", self.fingerprint)
228
 
        if "secret" in config:
229
 
            self.secret = config["secret"].decode(u"base64")
230
 
        elif "secfile" in config:
231
 
            sf = open(config["secfile"])
 
236
        if secret:
 
237
            self.secret = secret.decode(u"base64")
 
238
        elif secfile:
 
239
            sf = open(secfile)
232
240
            self.secret = sf.read()
233
241
            sf.close()
234
242
        else:
235
243
            raise TypeError(u"No secret or secfile for client %s"
236
244
                            % self.name)
237
 
        self.host = config.get("host", "")
 
245
        self.fqdn = fqdn
238
246
        self.created = datetime.datetime.now()
239
247
        self.last_checked_ok = None
240
 
        self.timeout = string_to_delta(config["timeout"])
241
 
        self.interval = string_to_delta(config["interval"])
 
248
        self.timeout = string_to_delta(timeout)
 
249
        self.interval = string_to_delta(interval)
242
250
        self.stop_hook = stop_hook
243
251
        self.checker = None
244
252
        self.checker_initiator_tag = None
245
253
        self.stop_initiator_tag = None
246
254
        self.checker_callback_tag = None
247
 
        self.check_command = config["checker"]
 
255
        self.check_command = checker
248
256
    def start(self):
249
257
        """Start this client's checker and timeout hooks"""
250
258
        # Schedule a new checker to be started an 'interval' from now,
263
271
        The possibility that a client might be restarted is left open,
264
272
        but not currently used."""
265
273
        # If this client doesn't have a secret, it is already stopped.
266
 
        if hasattr(self, "secret") and self.secret:
267
 
            logger.info(u"Stopping client %s", self.name)
 
274
        if self.secret:
 
275
            logger.debug(u"Stopping client %s", self.name)
268
276
            self.secret = None
269
277
        else:
270
278
            return False
289
297
        self.checker = None
290
298
        if os.WIFEXITED(condition) \
291
299
               and (os.WEXITSTATUS(condition) == 0):
292
 
            logger.info(u"Checker for %(name)s succeeded",
293
 
                        vars(self))
 
300
            logger.debug(u"Checker for %(name)s succeeded",
 
301
                         vars(self))
294
302
            self.last_checked_ok = now
295
303
            gobject.source_remove(self.stop_initiator_tag)
296
304
            self.stop_initiator_tag = gobject.timeout_add\
300
308
            logger.warning(u"Checker for %(name)s crashed?",
301
309
                           vars(self))
302
310
        else:
303
 
            logger.info(u"Checker for %(name)s failed",
304
 
                        vars(self))
 
311
            logger.debug(u"Checker for %(name)s failed",
 
312
                         vars(self))
305
313
    def start_checker(self):
306
314
        """Start a new checker subprocess if one is not running.
307
315
        If a checker already exists, leave it running and do
317
325
        if self.checker is None:
318
326
            try:
319
327
                # In case check_command has exactly one % operator
320
 
                command = self.check_command % self.host
 
328
                command = self.check_command % self.fqdn
321
329
            except TypeError:
322
330
                # Escape attributes for the shell
323
331
                escaped_attrs = dict((key, re.escape(str(val)))
330
338
                                 u' %s', self.check_command, error)
331
339
                    return True # Try again later
332
340
            try:
333
 
                logger.info(u"Starting checker %r for %s",
334
 
                            command, self.name)
 
341
                logger.debug(u"Starting checker %r for %s",
 
342
                             command, self.name)
335
343
                self.checker = subprocess.Popen(command,
336
344
                                                close_fds=True,
337
345
                                                shell=True, cwd="/")
350
358
            self.checker_callback_tag = None
351
359
        if getattr(self, "checker", None) is None:
352
360
            return
353
 
        logger.debug(u"Stopping checker for %(name)s", vars(self))
 
361
        logger.debug("Stopping checker for %(name)s", vars(self))
354
362
        try:
355
363
            os.kill(self.checker.pid, signal.SIGTERM)
356
364
            #os.sleep(0.5)
388
396
 
389
397
def fingerprint(openpgp):
390
398
    "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
 
399
    # New empty GnuTLS certificate
 
400
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
401
    gnutls.library.functions.gnutls_openpgp_crt_init\
 
402
        (ctypes.byref(crt))
391
403
    # New GnuTLS "datum" with the OpenPGP public key
392
404
    datum = gnutls.library.types.gnutls_datum_t\
393
405
        (ctypes.cast(ctypes.c_char_p(openpgp),
394
406
                     ctypes.POINTER(ctypes.c_ubyte)),
395
407
         ctypes.c_uint(len(openpgp)))
396
 
    # New empty GnuTLS certificate
397
 
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
398
 
    gnutls.library.functions.gnutls_openpgp_crt_init\
399
 
        (ctypes.byref(crt))
400
408
    # Import the OpenPGP public key into the certificate
401
 
    gnutls.library.functions.gnutls_openpgp_crt_import\
402
 
                    (crt, ctypes.byref(datum),
403
 
                     gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
409
    ret = gnutls.library.functions.gnutls_openpgp_crt_import\
 
410
        (crt,
 
411
         ctypes.byref(datum),
 
412
         gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
404
413
    # New buffer for the fingerprint
405
414
    buffer = ctypes.create_string_buffer(20)
406
415
    buffer_length = ctypes.c_size_t()
422
431
    Note: This will run in its own forked process."""
423
432
    
424
433
    def handle(self):
425
 
        logger.info(u"TCP connection from: %s",
 
434
        logger.debug(u"TCP connection from: %s",
426
435
                     unicode(self.client_address))
427
436
        session = gnutls.connection.ClientSession\
428
437
                  (self.request, gnutls.connection.X509Credentials())
429
 
        
430
 
        line = self.request.makefile().readline()
431
 
        logger.debug(u"Protocol version: %r", line)
432
 
        try:
433
 
            if int(line.strip().split()[0]) > 1:
434
 
                raise RuntimeError
435
 
        except (ValueError, IndexError, RuntimeError), error:
436
 
            logger.error(u"Unknown protocol version: %s", error)
437
 
            return
438
 
        
439
438
        # Note: gnutls.connection.X509Credentials is really a generic
440
439
        # GnuTLS certificate credentials object so long as no X.509
441
440
        # keys are added to it.  Therefore, we can use it here despite
454
453
        try:
455
454
            session.handshake()
456
455
        except gnutls.errors.GNUTLSError, error:
457
 
            logger.warning(u"Handshake failed: %s", error)
 
456
            logger.debug(u"Handshake failed: %s", error)
458
457
            # Do not run session.bye() here: the session is not
459
458
            # established.  Just abandon the request.
460
459
            return
461
460
        try:
462
461
            fpr = fingerprint(peer_certificate(session))
463
462
        except (TypeError, gnutls.errors.GNUTLSError), error:
464
 
            logger.warning(u"Bad certificate: %s", error)
 
463
            logger.debug(u"Bad certificate: %s", error)
465
464
            session.bye()
466
465
            return
467
466
        logger.debug(u"Fingerprint: %s", fpr)
471
470
                client = c
472
471
                break
473
472
        if not client:
474
 
            logger.warning(u"Client not found for fingerprint: %s",
475
 
                           fpr)
 
473
            logger.debug(u"Client not found for fingerprint: %s", fpr)
476
474
            session.bye()
477
475
            return
478
476
        # Have to check if client.still_valid(), since it is possible
479
477
        # that the client timed out while establishing the GnuTLS
480
478
        # session.
481
479
        if not client.still_valid():
482
 
            logger.warning(u"Client %(name)s is invalid",
483
 
                           vars(client))
 
480
            logger.debug(u"Client %(name)s is invalid", vars(client))
484
481
            session.bye()
485
482
            return
486
483
        sent_size = 0
521
518
                                       self.settings["interface"])
522
519
            except socket.error, error:
523
520
                if error[0] == errno.EPERM:
524
 
                    logger.error(u"No permission to"
525
 
                                 u" bind to interface %s",
526
 
                                 self.settings["interface"])
 
521
                    logger.warning(u"No permission to"
 
522
                                   u" bind to interface %s",
 
523
                                   self.settings["interface"])
527
524
                else:
528
525
                    raise error
529
526
        # Only bind(2) the socket if we really need to.
532
529
                in6addr_any = "::"
533
530
                self.server_address = (in6addr_any,
534
531
                                       self.server_address[1])
535
 
            elif not self.server_address[1]:
 
532
            elif self.server_address[1] is None:
536
533
                self.server_address = (self.server_address[0],
537
534
                                       0)
538
 
#                 if self.settings["interface"]:
539
 
#                     self.server_address = (self.server_address[0],
540
 
#                                            0, # port
541
 
#                                            0, # flowinfo
542
 
#                                            if_nametoindex
543
 
#                                            (self.settings
544
 
#                                             ["interface"]))
545
535
            return super(type(self), self).server_bind()
546
536
 
547
537
 
582
572
def server_state_changed(state):
583
573
    """Derived from the Avahi example code"""
584
574
    if state == avahi.SERVER_COLLISION:
585
 
        logger.error(u"Server name collision")
 
575
        logger.warning(u"Server name collision")
586
576
        service.remove()
587
577
    elif state == avahi.SERVER_RUNNING:
588
578
        service.add()
602
592
                        unicode(error))
603
593
        raise AvahiGroupError("State changed: %s", str(error))
604
594
 
605
 
def if_nametoindex(interface):
 
595
def if_nametoindex(interface, _func=[None]):
606
596
    """Call the C function if_nametoindex(), or equivalent"""
607
 
    global if_nametoindex
 
597
    if _func[0] is not None:
 
598
        return _func[0](interface)
608
599
    try:
609
600
        if "ctypes.util" not in sys.modules:
610
601
            import ctypes.util
611
 
        if_nametoindex = ctypes.cdll.LoadLibrary\
612
 
            (ctypes.util.find_library("c")).if_nametoindex
 
602
        while True:
 
603
            try:
 
604
                libc = ctypes.cdll.LoadLibrary\
 
605
                       (ctypes.util.find_library("c"))
 
606
                _func[0] = libc.if_nametoindex
 
607
                return _func[0](interface)
 
608
            except IOError, e:
 
609
                if e != errno.EINTR:
 
610
                    raise
613
611
    except (OSError, AttributeError):
614
612
        if "struct" not in sys.modules:
615
613
            import struct
616
614
        if "fcntl" not in sys.modules:
617
615
            import fcntl
618
 
        def if_nametoindex(interface):
 
616
        def the_hard_way(interface):
619
617
            "Get an interface index the hard way, i.e. using fcntl()"
620
618
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
621
619
            s = socket.socket()
624
622
            s.close()
625
623
            interface_index = struct.unpack("I", ifreq[16:20])[0]
626
624
            return interface_index
627
 
    return if_nametoindex(interface)
628
 
 
629
 
 
630
 
def daemon(nochdir = False, noclose = False):
 
625
        _func[0] = the_hard_way
 
626
        return _func[0](interface)
 
627
 
 
628
 
 
629
def daemon(nochdir, noclose):
631
630
    """See daemon(3).  Standard BSD Unix function.
632
631
    This should really exist as os.daemon, but it doesn't (yet)."""
633
632
    if os.fork():
635
634
    os.setsid()
636
635
    if not nochdir:
637
636
        os.chdir("/")
638
 
    if os.fork():
639
 
        sys.exit()
640
637
    if not noclose:
641
638
        # Close all standard open file descriptors
642
639
        null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
654
651
    global main_loop_started
655
652
    main_loop_started = False
656
653
    
657
 
    parser = OptionParser(version = "Mandos server %s" % version)
 
654
    parser = OptionParser()
658
655
    parser.add_option("-i", "--interface", type="string",
659
656
                      metavar="IF", help="Bind to interface IF")
660
657
    parser.add_option("-a", "--address", type="string",
663
660
                      help="Port number to receive requests on")
664
661
    parser.add_option("--check", action="store_true", default=False,
665
662
                      help="Run self-test")
666
 
    parser.add_option("--debug", action="store_true",
 
663
    parser.add_option("--debug", action="store_true", default=False,
667
664
                      help="Debug mode; run in foreground and log to"
668
665
                      " terminal")
669
666
    parser.add_option("--priority", type="string", help="GnuTLS"
694
691
    # Parse config file for server-global settings
695
692
    server_config = ConfigParser.SafeConfigParser(server_defaults)
696
693
    del server_defaults
697
 
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
 
694
    server_config.read(os.path.join(options.configdir, "server.conf"))
698
695
    server_section = "server"
699
696
    # Convert the SafeConfigParser object to a dict
700
697
    server_settings = dict(server_config.items(server_section))
713
710
    del options
714
711
    # Now we have our good server settings in "server_settings"
715
712
    
716
 
    debug = server_settings["debug"]
717
 
    
718
 
    if not debug:
719
 
        syslogger.setLevel(logging.WARNING)
720
 
    
721
 
    if server_settings["servicename"] != "Mandos":
722
 
        syslogger.setFormatter(logging.Formatter\
723
 
                               ('Mandos (%s): %%(levelname)s:'
724
 
                                ' %%(message)s'
725
 
                                % server_settings["servicename"]))
726
 
    
727
713
    # Parse config file with clients
728
714
    client_defaults = { "timeout": "1h",
729
715
                        "interval": "5m",
730
 
                        "checker": "fping -q -- %%(host)s",
 
716
                        "checker": "fping -q -- %%(fqdn)s",
731
717
                        }
732
718
    client_config = ConfigParser.SafeConfigParser(client_defaults)
733
719
    client_config.read(os.path.join(server_settings["configdir"],
751
737
            avahi.DBUS_INTERFACE_SERVER )
752
738
    # End of Avahi example code
753
739
    
 
740
    debug = server_settings["debug"]
 
741
    
754
742
    if debug:
755
743
        console = logging.StreamHandler()
756
744
        # console.setLevel(logging.DEBUG)
763
751
    def remove_from_clients(client):
764
752
        clients.remove(client)
765
753
        if not clients:
766
 
            logger.critical(u"No clients left, exiting")
 
754
            logger.debug(u"No clients left, exiting")
767
755
            sys.exit()
768
756
    
769
 
    clients.update(Set(Client(name = section,
 
757
    clients.update(Set(Client(name=section,
770
758
                              stop_hook = remove_from_clients,
771
 
                              config
772
 
                              = dict(client_config.items(section)))
 
759
                              **(dict(client_config\
 
760
                                      .items(section))))
773
761
                       for section in client_config.sections()))
774
 
    if not clients:
775
 
        logger.critical(u"No clients defined")
776
 
        sys.exit(1)
777
762
    
778
763
    if not debug:
779
 
        daemon()
780
 
    
781
 
    pidfilename = "/var/run/mandos/mandos.pid"
782
 
    pid = os.getpid()
783
 
    try:
784
 
        pidfile = open(pidfilename, "w")
785
 
        pidfile.write(str(pid) + "\n")
786
 
        pidfile.close()
787
 
        del pidfile
788
 
    except IOError, err:
789
 
        logger.error(u"Could not write %s file with PID %d",
790
 
                     pidfilename, os.getpid())
 
764
        daemon(False, False)
791
765
    
792
766
    def cleanup():
793
767
        "Cleanup function; run on exit"
820
794
                                clients=clients)
821
795
    # Find out what port we got
822
796
    service.port = tcp_server.socket.getsockname()[1]
823
 
    logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
824
 
                u" scope_id %d" % tcp_server.socket.getsockname())
 
797
    logger.debug(u"Now listening on address %r, port %d, flowinfo %d,"
 
798
                 u" scope_id %d" % tcp_server.socket.getsockname())
825
799
    
826
800
    #service.interface = tcp_server.socket.getsockname()[3]
827
801
    
840
814
                             tcp_server.handle_request\
841
815
                             (*args[2:], **kwargs) or True)
842
816
        
843
 
        logger.debug(u"Starting main loop")
 
817
        logger.debug("Starting main loop")
844
818
        main_loop_started = True
845
819
        main_loop.run()
846
820
    except AvahiError, error: