/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: 2014-07-14 21:41:08 UTC
  • Revision ID: teddy@recompile.se-20140714214108-awg7u6gaiy7d40dz
mandos-monitor: New "verbose" mode to see less important log messages.

* mandos-monitor (MandosClientWidget.__init__): Log client creation.
  (MandosClientWidget.checker_completed): Log a successful checker.
  (MandosClientWidget.checker_started): Log starting of a checker.
  (UserInterface.__init__): New optional "log_level" argument.
  (UserInterface.log_message, UserInterface.log_message_raw): Take
                                                              optional
                                                              "level"
                                                              arg.
  (UserInterface.toggle_log_display): Log visibility change.
  (UserInterface.change_log_display): Log wrap mode change.
  (UserInterface.process_input): Show new "v" key in help message and
                                 process "v" key if pressed.
* mandos-monitor.xml (KEYS): Document new "v" key.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
# "AvahiService" class, and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008-2012 Teddy Hogeborn
15
 
# Copyright © 2008-2012 Björn Påhlsson
 
14
# Copyright © 2008-2014 Teddy Hogeborn
 
15
# Copyright © 2008-2014 Björn Påhlsson
16
16
17
17
# This program is free software: you can redistribute it and/or modify
18
18
# it under the terms of the GNU General Public License as published by
79
79
import ctypes.util
80
80
import xml.dom.minidom
81
81
import inspect
82
 
import GnuPGInterface
83
82
 
84
83
try:
85
84
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
89
88
    except ImportError:
90
89
        SO_BINDTODEVICE = None
91
90
 
92
 
version = "1.6.0"
 
91
version = "1.6.6"
93
92
stored_state_file = "clients.pickle"
94
93
 
95
94
logger = logging.getLogger()
96
 
syslogger = (logging.handlers.SysLogHandler
97
 
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
98
 
              address = str("/dev/log")))
 
95
syslogger = None
99
96
 
100
97
try:
101
98
    if_nametoindex = (ctypes.cdll.LoadLibrary
117
114
def initlogger(debug, level=logging.WARNING):
118
115
    """init logger and add loglevel"""
119
116
    
 
117
    global syslogger
 
118
    syslogger = (logging.handlers.SysLogHandler
 
119
                 (facility =
 
120
                  logging.handlers.SysLogHandler.LOG_DAEMON,
 
121
                  address = str("/dev/log")))
120
122
    syslogger.setFormatter(logging.Formatter
121
123
                           ('Mandos [%(process)d]: %(levelname)s:'
122
124
                            ' %(message)s'))
140
142
class PGPEngine(object):
141
143
    """A simple class for OpenPGP symmetric encryption & decryption"""
142
144
    def __init__(self):
143
 
        self.gnupg = GnuPGInterface.GnuPG()
144
145
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
145
 
        self.gnupg = GnuPGInterface.GnuPG()
146
 
        self.gnupg.options.meta_interactive = False
147
 
        self.gnupg.options.homedir = self.tempdir
148
 
        self.gnupg.options.extra_args.extend(['--force-mdc',
149
 
                                              '--quiet',
150
 
                                              '--no-use-agent'])
 
146
        self.gnupgargs = ['--batch',
 
147
                          '--home', self.tempdir,
 
148
                          '--force-mdc',
 
149
                          '--quiet',
 
150
                          '--no-use-agent']
151
151
    
152
152
    def __enter__(self):
153
153
        return self
175
175
    def password_encode(self, password):
176
176
        # Passphrase can not be empty and can not contain newlines or
177
177
        # NUL bytes.  So we prefix it and hex encode it.
178
 
        return b"mandos" + binascii.hexlify(password)
 
178
        encoded = b"mandos" + binascii.hexlify(password)
 
179
        if len(encoded) > 2048:
 
180
            # GnuPG can't handle long passwords, so encode differently
 
181
            encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
 
182
                       .replace(b"\n", b"\\n")
 
183
                       .replace(b"\0", b"\\x00"))
 
184
        return encoded
179
185
    
180
186
    def encrypt(self, data, password):
181
 
        self.gnupg.passphrase = self.password_encode(password)
182
 
        with open(os.devnull, "w") as devnull:
183
 
            try:
184
 
                proc = self.gnupg.run(['--symmetric'],
185
 
                                      create_fhs=['stdin', 'stdout'],
186
 
                                      attach_fhs={'stderr': devnull})
187
 
                with contextlib.closing(proc.handles['stdin']) as f:
188
 
                    f.write(data)
189
 
                with contextlib.closing(proc.handles['stdout']) as f:
190
 
                    ciphertext = f.read()
191
 
                proc.wait()
192
 
            except IOError as e:
193
 
                raise PGPError(e)
194
 
        self.gnupg.passphrase = None
 
187
        passphrase = self.password_encode(password)
 
188
        with tempfile.NamedTemporaryFile(dir=self.tempdir
 
189
                                         ) as passfile:
 
190
            passfile.write(passphrase)
 
191
            passfile.flush()
 
192
            proc = subprocess.Popen(['gpg', '--symmetric',
 
193
                                     '--passphrase-file',
 
194
                                     passfile.name]
 
195
                                    + self.gnupgargs,
 
196
                                    stdin = subprocess.PIPE,
 
197
                                    stdout = subprocess.PIPE,
 
198
                                    stderr = subprocess.PIPE)
 
199
            ciphertext, err = proc.communicate(input = data)
 
200
        if proc.returncode != 0:
 
201
            raise PGPError(err)
195
202
        return ciphertext
196
203
    
197
204
    def decrypt(self, data, password):
198
 
        self.gnupg.passphrase = self.password_encode(password)
199
 
        with open(os.devnull, "w") as devnull:
200
 
            try:
201
 
                proc = self.gnupg.run(['--decrypt'],
202
 
                                      create_fhs=['stdin', 'stdout'],
203
 
                                      attach_fhs={'stderr': devnull})
204
 
                with contextlib.closing(proc.handles['stdin']) as f:
205
 
                    f.write(data)
206
 
                with contextlib.closing(proc.handles['stdout']) as f:
207
 
                    decrypted_plaintext = f.read()
208
 
                proc.wait()
209
 
            except IOError as e:
210
 
                raise PGPError(e)
211
 
        self.gnupg.passphrase = None
 
205
        passphrase = self.password_encode(password)
 
206
        with tempfile.NamedTemporaryFile(dir = self.tempdir
 
207
                                         ) as passfile:
 
208
            passfile.write(passphrase)
 
209
            passfile.flush()
 
210
            proc = subprocess.Popen(['gpg', '--decrypt',
 
211
                                     '--passphrase-file',
 
212
                                     passfile.name]
 
213
                                    + self.gnupgargs,
 
214
                                    stdin = subprocess.PIPE,
 
215
                                    stdout = subprocess.PIPE,
 
216
                                    stderr = subprocess.PIPE)
 
217
            decrypted_plaintext, err = proc.communicate(input
 
218
                                                        = data)
 
219
        if proc.returncode != 0:
 
220
            raise PGPError(err)
212
221
        return decrypted_plaintext
213
222
 
214
223
 
234
243
               Used to optionally bind to the specified interface.
235
244
    name: string; Example: 'Mandos'
236
245
    type: string; Example: '_mandos._tcp'.
237
 
                  See <http://www.dns-sd.org/ServiceTypes.html>
 
246
     See <https://www.iana.org/assignments/service-names-port-numbers>
238
247
    port: integer; what port to announce
239
248
    TXT: list of strings; TXT record for the service
240
249
    domain: string; Domain to publish on, default to .local if empty.
440
449
    runtime_expansions: Allowed attributes for runtime expansion.
441
450
    expires:    datetime.datetime(); time (UTC) when a client will be
442
451
                disabled, or None
 
452
    server_settings: The server_settings dict from main()
443
453
    """
444
454
    
445
455
    runtime_expansions = ("approval_delay", "approval_duration",
520
530
        
521
531
        return settings
522
532
    
523
 
    def __init__(self, settings, name = None):
 
533
    def __init__(self, settings, name = None, server_settings=None):
524
534
        self.name = name
 
535
        if server_settings is None:
 
536
            server_settings = {}
 
537
        self.server_settings = server_settings
525
538
        # adding all client settings
526
539
        for setting, value in settings.iteritems():
527
540
            setattr(self, setting, value)
680
693
        # If a checker exists, make sure it is not a zombie
681
694
        try:
682
695
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
683
 
        except (AttributeError, OSError) as error:
684
 
            if (isinstance(error, OSError)
685
 
                and error.errno != errno.ECHILD):
686
 
                raise error
 
696
        except AttributeError:
 
697
            pass
 
698
        except OSError as error:
 
699
            if error.errno != errno.ECHILD:
 
700
                raise
687
701
        else:
688
702
            if pid:
689
703
                logger.warning("Checker was a zombie")
711
725
                # in normal mode, that is already done by daemon(),
712
726
                # and in debug mode we don't want to.  (Stdin is
713
727
                # always replaced by /dev/null.)
 
728
                # The exception is when not debugging but nevertheless
 
729
                # running in the foreground; use the previously
 
730
                # created wnull.
 
731
                popen_args = {}
 
732
                if (not self.server_settings["debug"]
 
733
                    and self.server_settings["foreground"]):
 
734
                    popen_args.update({"stdout": wnull,
 
735
                                       "stderr": wnull })
714
736
                self.checker = subprocess.Popen(command,
715
737
                                                close_fds=True,
716
 
                                                shell=True, cwd="/")
 
738
                                                shell=True, cwd="/",
 
739
                                                **popen_args)
717
740
            except OSError as error:
718
741
                logger.error("Failed to start subprocess",
719
742
                             exc_info=error)
728
751
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
729
752
            except OSError as error:
730
753
                if error.errno == errno.ECHILD:
731
 
                    logger.error("Child process vanished", exc_info=error)
 
754
                    # This should never happen
 
755
                    logger.error("Child process vanished",
 
756
                                 exc_info=error)
732
757
                    return True
733
758
                raise
734
759
            if pid:
912
937
            # The byte_arrays option is not supported yet on
913
938
            # signatures other than "ay".
914
939
            if prop._dbus_signature != "ay":
915
 
                raise ValueError
 
940
                raise ValueError("Byte arrays not supported for non-"
 
941
                                 "'ay' signature {0!r}"
 
942
                                 .format(prop._dbus_signature))
916
943
            value = dbus.ByteArray(b''.join(chr(byte)
917
944
                                            for byte in value))
918
945
        prop(value)
1076
1103
                interface_names.add(alt_interface)
1077
1104
                # Is this a D-Bus signal?
1078
1105
                if getattr(attribute, "_dbus_is_signal", False):
1079
 
                    # Extract the original non-method function by
1080
 
                    # black magic
 
1106
                    # Extract the original non-method undecorated
 
1107
                    # function by black magic
1081
1108
                    nonmethod_func = (dict(
1082
1109
                            zip(attribute.func_code.co_freevars,
1083
1110
                                attribute.__closure__))["func"]
1326
1353
                                       *args, **kwargs)
1327
1354
    
1328
1355
    def start_checker(self, *args, **kwargs):
1329
 
        old_checker = self.checker
1330
 
        if self.checker is not None:
1331
 
            old_checker_pid = self.checker.pid
1332
 
        else:
1333
 
            old_checker_pid = None
 
1356
        old_checker_pid = getattr(self.checker, "pid", None)
1334
1357
        r = Client.start_checker(self, *args, **kwargs)
1335
1358
        # Only if new checker process was started
1336
1359
        if (self.checker is not None
1681
1704
            logger.debug("Protocol version: %r", line)
1682
1705
            try:
1683
1706
                if int(line.strip().split()[0]) > 1:
1684
 
                    raise RuntimeError
 
1707
                    raise RuntimeError(line)
1685
1708
            except (ValueError, IndexError, RuntimeError) as error:
1686
1709
                logger.error("Unknown protocol version: %s", error)
1687
1710
                return
1894
1917
    
1895
1918
    def add_pipe(self, parent_pipe, proc):
1896
1919
        """Dummy function; override as necessary"""
1897
 
        raise NotImplementedError
 
1920
        raise NotImplementedError()
1898
1921
 
1899
1922
 
1900
1923
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1976
1999
                if self.address_family == socket.AF_INET6:
1977
2000
                    any_address = "::" # in6addr_any
1978
2001
                else:
1979
 
                    any_address = socket.INADDR_ANY
 
2002
                    any_address = "0.0.0.0" # INADDR_ANY
1980
2003
                self.server_address = (any_address,
1981
2004
                                       self.server_address[1])
1982
2005
            elif not self.server_address[1]:
2237
2260
            else:
2238
2261
                raise ValueError("Unknown suffix {0!r}"
2239
2262
                                 .format(suffix))
2240
 
        except (ValueError, IndexError) as e:
 
2263
        except IndexError as e:
2241
2264
            raise ValueError(*(e.args))
2242
2265
        timevalue += delta
2243
2266
    return timevalue
2287
2310
                        help="Run self-test")
2288
2311
    parser.add_argument("--debug", action="store_true",
2289
2312
                        help="Debug mode; run in foreground and log"
2290
 
                        " to terminal")
 
2313
                        " to terminal", default=None)
2291
2314
    parser.add_argument("--debuglevel", metavar="LEVEL",
2292
2315
                        help="Debug level for stdout output")
2293
2316
    parser.add_argument("--priority", help="GnuTLS"
2300
2323
                        " files")
2301
2324
    parser.add_argument("--no-dbus", action="store_false",
2302
2325
                        dest="use_dbus", help="Do not provide D-Bus"
2303
 
                        " system bus interface")
 
2326
                        " system bus interface", default=None)
2304
2327
    parser.add_argument("--no-ipv6", action="store_false",
2305
 
                        dest="use_ipv6", help="Do not use IPv6")
 
2328
                        dest="use_ipv6", help="Do not use IPv6",
 
2329
                        default=None)
2306
2330
    parser.add_argument("--no-restore", action="store_false",
2307
2331
                        dest="restore", help="Do not restore stored"
2308
 
                        " state")
 
2332
                        " state", default=None)
2309
2333
    parser.add_argument("--socket", type=int,
2310
2334
                        help="Specify a file descriptor to a network"
2311
2335
                        " socket to use instead of creating one")
2312
2336
    parser.add_argument("--statedir", metavar="DIR",
2313
2337
                        help="Directory to save/restore state in")
2314
2338
    parser.add_argument("--foreground", action="store_true",
2315
 
                        help="Run in foreground")
 
2339
                        help="Run in foreground", default=None)
 
2340
    parser.add_argument("--no-zeroconf", action="store_false",
 
2341
                        dest="zeroconf", help="Do not use Zeroconf",
 
2342
                        default=None)
2316
2343
    
2317
2344
    options = parser.parse_args()
2318
2345
    
2319
2346
    if options.check:
2320
2347
        import doctest
2321
 
        doctest.testmod()
2322
 
        sys.exit()
 
2348
        fail_count, test_count = doctest.testmod()
 
2349
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2323
2350
    
2324
2351
    # Default values for config file for server-global settings
2325
2352
    server_defaults = { "interface": "",
2327
2354
                        "port": "",
2328
2355
                        "debug": "False",
2329
2356
                        "priority":
2330
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
2357
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
2331
2358
                        "servicename": "Mandos",
2332
2359
                        "use_dbus": "True",
2333
2360
                        "use_ipv6": "True",
2336
2363
                        "socket": "",
2337
2364
                        "statedir": "/var/lib/mandos",
2338
2365
                        "foreground": "False",
 
2366
                        "zeroconf": "True",
2339
2367
                        }
2340
2368
    
2341
2369
    # Parse config file for server-global settings
2368
2396
    for option in ("interface", "address", "port", "debug",
2369
2397
                   "priority", "servicename", "configdir",
2370
2398
                   "use_dbus", "use_ipv6", "debuglevel", "restore",
2371
 
                   "statedir", "socket", "foreground"):
 
2399
                   "statedir", "socket", "foreground", "zeroconf"):
2372
2400
        value = getattr(options, option)
2373
2401
        if value is not None:
2374
2402
            server_settings[option] = value
2377
2405
    for option in server_settings.keys():
2378
2406
        if type(server_settings[option]) is str:
2379
2407
            server_settings[option] = unicode(server_settings[option])
 
2408
    # Force all boolean options to be boolean
 
2409
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
 
2410
                   "foreground", "zeroconf"):
 
2411
        server_settings[option] = bool(server_settings[option])
2380
2412
    # Debug implies foreground
2381
2413
    if server_settings["debug"]:
2382
2414
        server_settings["foreground"] = True
2384
2416
    
2385
2417
    ##################################################################
2386
2418
    
 
2419
    if (not server_settings["zeroconf"] and
 
2420
        not (server_settings["port"]
 
2421
             or server_settings["socket"] != "")):
 
2422
            parser.error("Needs port or socket to work without"
 
2423
                         " Zeroconf")
 
2424
    
2387
2425
    # For convenience
2388
2426
    debug = server_settings["debug"]
2389
2427
    debuglevel = server_settings["debuglevel"]
2392
2430
    stored_state_path = os.path.join(server_settings["statedir"],
2393
2431
                                     stored_state_file)
2394
2432
    foreground = server_settings["foreground"]
 
2433
    zeroconf = server_settings["zeroconf"]
2395
2434
    
2396
2435
    if debug:
2397
2436
        initlogger(debug, logging.DEBUG)
2418
2457
    global mandos_dbus_service
2419
2458
    mandos_dbus_service = None
2420
2459
    
 
2460
    socketfd = None
 
2461
    if server_settings["socket"] != "":
 
2462
        socketfd = server_settings["socket"]
2421
2463
    tcp_server = MandosServer((server_settings["address"],
2422
2464
                               server_settings["port"]),
2423
2465
                              ClientHandler,
2427
2469
                              gnutls_priority=
2428
2470
                              server_settings["priority"],
2429
2471
                              use_dbus=use_dbus,
2430
 
                              socketfd=(server_settings["socket"]
2431
 
                                        or None))
 
2472
                              socketfd=socketfd)
2432
2473
    if not foreground:
2433
 
        pidfilename = "/var/run/mandos.pid"
 
2474
        pidfilename = "/run/mandos.pid"
 
2475
        if not os.path.isdir("/run/."):
 
2476
            pidfilename = "/var/run/mandos.pid"
2434
2477
        pidfile = None
2435
2478
        try:
2436
2479
            pidfile = open(pidfilename, "w")
2453
2496
        os.setuid(uid)
2454
2497
    except OSError as error:
2455
2498
        if error.errno != errno.EPERM:
2456
 
            raise error
 
2499
            raise
2457
2500
    
2458
2501
    if debug:
2459
2502
        # Enable all possible GnuTLS debugging
2502
2545
            use_dbus = False
2503
2546
            server_settings["use_dbus"] = False
2504
2547
            tcp_server.use_dbus = False
2505
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2506
 
    service = AvahiServiceToSyslog(name =
2507
 
                                   server_settings["servicename"],
2508
 
                                   servicetype = "_mandos._tcp",
2509
 
                                   protocol = protocol, bus = bus)
2510
 
    if server_settings["interface"]:
2511
 
        service.interface = (if_nametoindex
2512
 
                             (str(server_settings["interface"])))
 
2548
    if zeroconf:
 
2549
        protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
 
2550
        service = AvahiServiceToSyslog(name =
 
2551
                                       server_settings["servicename"],
 
2552
                                       servicetype = "_mandos._tcp",
 
2553
                                       protocol = protocol, bus = bus)
 
2554
        if server_settings["interface"]:
 
2555
            service.interface = (if_nametoindex
 
2556
                                 (str(server_settings["interface"])))
2513
2557
    
2514
2558
    global multiprocessing_manager
2515
2559
    multiprocessing_manager = multiprocessing.Manager()
2522
2566
    old_client_settings = {}
2523
2567
    clients_data = {}
2524
2568
    
 
2569
    # This is used to redirect stdout and stderr for checker processes
 
2570
    global wnull
 
2571
    wnull = open(os.devnull, "w") # A writable /dev/null
 
2572
    # Only used if server is running in foreground but not in debug
 
2573
    # mode
 
2574
    if debug or not foreground:
 
2575
        wnull.close()
 
2576
    
2525
2577
    # Get client data and settings from last running state.
2526
2578
    if server_settings["restore"]:
2527
2579
        try:
2543
2595
    
2544
2596
    with PGPEngine() as pgp:
2545
2597
        for client_name, client in clients_data.iteritems():
 
2598
            # Skip removed clients
 
2599
            if client_name not in client_settings:
 
2600
                continue
 
2601
            
2546
2602
            # Decide which value to use after restoring saved state.
2547
2603
            # We have three different values: Old config file,
2548
2604
            # new config file, and saved state.
2610
2666
    # Create all client objects
2611
2667
    for client_name, client in clients_data.iteritems():
2612
2668
        tcp_server.clients[client_name] = client_class(
2613
 
            name = client_name, settings = client)
 
2669
            name = client_name, settings = client,
 
2670
            server_settings = server_settings)
2614
2671
    
2615
2672
    if not tcp_server.clients:
2616
2673
        logger.warning("No clients defined")
2696
2753
    
2697
2754
    def cleanup():
2698
2755
        "Cleanup function; run on exit"
2699
 
        service.cleanup()
 
2756
        if zeroconf:
 
2757
            service.cleanup()
2700
2758
        
2701
2759
        multiprocessing.active_children()
 
2760
        wnull.close()
2702
2761
        if not (tcp_server.clients or client_settings):
2703
2762
            return
2704
2763
        
2716
2775
                # A list of attributes that can not be pickled
2717
2776
                # + secret.
2718
2777
                exclude = set(("bus", "changedstate", "secret",
2719
 
                               "checker"))
 
2778
                               "checker", "server_settings"))
2720
2779
                for name, typ in (inspect.getmembers
2721
2780
                                  (dbus.service.Object)):
2722
2781
                    exclude.add(name)
2750
2809
            else:
2751
2810
                logger.warning("Could not save persistent state:",
2752
2811
                               exc_info=e)
2753
 
                raise e
 
2812
                raise
2754
2813
        
2755
2814
        # Delete all clients, and settings from config
2756
2815
        while tcp_server.clients:
2780
2839
    tcp_server.server_activate()
2781
2840
    
2782
2841
    # Find out what port we got
2783
 
    service.port = tcp_server.socket.getsockname()[1]
 
2842
    if zeroconf:
 
2843
        service.port = tcp_server.socket.getsockname()[1]
2784
2844
    if use_ipv6:
2785
2845
        logger.info("Now listening on address %r, port %d,"
2786
2846
                    " flowinfo %d, scope_id %d",
2792
2852
    #service.interface = tcp_server.socket.getsockname()[3]
2793
2853
    
2794
2854
    try:
2795
 
        # From the Avahi example code
2796
 
        try:
2797
 
            service.activate()
2798
 
        except dbus.exceptions.DBusException as error:
2799
 
            logger.critical("D-Bus Exception", exc_info=error)
2800
 
            cleanup()
2801
 
            sys.exit(1)
2802
 
        # End of Avahi example code
 
2855
        if zeroconf:
 
2856
            # From the Avahi example code
 
2857
            try:
 
2858
                service.activate()
 
2859
            except dbus.exceptions.DBusException as error:
 
2860
                logger.critical("D-Bus Exception", exc_info=error)
 
2861
                cleanup()
 
2862
                sys.exit(1)
 
2863
            # End of Avahi example code
2803
2864
        
2804
2865
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2805
2866
                             lambda *args, **kwargs: