/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-17 03:24:57 UTC
  • mto: This revision was merged to the branch mainline in revision 720.
  • Revision ID: teddy@recompile.se-20140717032457-7074pobx7ft1oiio
Tags: version-1.6.7-1
* Makefile (version): Changed to "1.6.7".
* NEWS (Version 1.6.7): New entry.
* debian/changelog (1.6.7-1): - '' -

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.7"
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)
914
937
            # The byte_arrays option is not supported yet on
915
938
            # signatures other than "ay".
916
939
            if prop._dbus_signature != "ay":
917
 
                raise ValueError
 
940
                raise ValueError("Byte arrays not supported for non-"
 
941
                                 "'ay' signature {0!r}"
 
942
                                 .format(prop._dbus_signature))
918
943
            value = dbus.ByteArray(b''.join(chr(byte)
919
944
                                            for byte in value))
920
945
        prop(value)
1078
1103
                interface_names.add(alt_interface)
1079
1104
                # Is this a D-Bus signal?
1080
1105
                if getattr(attribute, "_dbus_is_signal", False):
1081
 
                    # Extract the original non-method function by
1082
 
                    # black magic
 
1106
                    # Extract the original non-method undecorated
 
1107
                    # function by black magic
1083
1108
                    nonmethod_func = (dict(
1084
1109
                            zip(attribute.func_code.co_freevars,
1085
1110
                                attribute.__closure__))["func"]
1328
1353
                                       *args, **kwargs)
1329
1354
    
1330
1355
    def start_checker(self, *args, **kwargs):
1331
 
        old_checker = self.checker
1332
 
        if self.checker is not None:
1333
 
            old_checker_pid = self.checker.pid
1334
 
        else:
1335
 
            old_checker_pid = None
 
1356
        old_checker_pid = getattr(self.checker, "pid", None)
1336
1357
        r = Client.start_checker(self, *args, **kwargs)
1337
1358
        # Only if new checker process was started
1338
1359
        if (self.checker is not None
1683
1704
            logger.debug("Protocol version: %r", line)
1684
1705
            try:
1685
1706
                if int(line.strip().split()[0]) > 1:
1686
 
                    raise RuntimeError
 
1707
                    raise RuntimeError(line)
1687
1708
            except (ValueError, IndexError, RuntimeError) as error:
1688
1709
                logger.error("Unknown protocol version: %s", error)
1689
1710
                return
1896
1917
    
1897
1918
    def add_pipe(self, parent_pipe, proc):
1898
1919
        """Dummy function; override as necessary"""
1899
 
        raise NotImplementedError
 
1920
        raise NotImplementedError()
1900
1921
 
1901
1922
 
1902
1923
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1978
1999
                if self.address_family == socket.AF_INET6:
1979
2000
                    any_address = "::" # in6addr_any
1980
2001
                else:
1981
 
                    any_address = socket.INADDR_ANY
 
2002
                    any_address = "0.0.0.0" # INADDR_ANY
1982
2003
                self.server_address = (any_address,
1983
2004
                                       self.server_address[1])
1984
2005
            elif not self.server_address[1]:
2239
2260
            else:
2240
2261
                raise ValueError("Unknown suffix {0!r}"
2241
2262
                                 .format(suffix))
2242
 
        except (ValueError, IndexError) as e:
 
2263
        except IndexError as e:
2243
2264
            raise ValueError(*(e.args))
2244
2265
        timevalue += delta
2245
2266
    return timevalue
2289
2310
                        help="Run self-test")
2290
2311
    parser.add_argument("--debug", action="store_true",
2291
2312
                        help="Debug mode; run in foreground and log"
2292
 
                        " to terminal")
 
2313
                        " to terminal", default=None)
2293
2314
    parser.add_argument("--debuglevel", metavar="LEVEL",
2294
2315
                        help="Debug level for stdout output")
2295
2316
    parser.add_argument("--priority", help="GnuTLS"
2302
2323
                        " files")
2303
2324
    parser.add_argument("--no-dbus", action="store_false",
2304
2325
                        dest="use_dbus", help="Do not provide D-Bus"
2305
 
                        " system bus interface")
 
2326
                        " system bus interface", default=None)
2306
2327
    parser.add_argument("--no-ipv6", action="store_false",
2307
 
                        dest="use_ipv6", help="Do not use IPv6")
 
2328
                        dest="use_ipv6", help="Do not use IPv6",
 
2329
                        default=None)
2308
2330
    parser.add_argument("--no-restore", action="store_false",
2309
2331
                        dest="restore", help="Do not restore stored"
2310
 
                        " state")
 
2332
                        " state", default=None)
2311
2333
    parser.add_argument("--socket", type=int,
2312
2334
                        help="Specify a file descriptor to a network"
2313
2335
                        " socket to use instead of creating one")
2314
2336
    parser.add_argument("--statedir", metavar="DIR",
2315
2337
                        help="Directory to save/restore state in")
2316
2338
    parser.add_argument("--foreground", action="store_true",
2317
 
                        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)
2318
2343
    
2319
2344
    options = parser.parse_args()
2320
2345
    
2321
2346
    if options.check:
2322
2347
        import doctest
2323
 
        doctest.testmod()
2324
 
        sys.exit()
 
2348
        fail_count, test_count = doctest.testmod()
 
2349
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2325
2350
    
2326
2351
    # Default values for config file for server-global settings
2327
2352
    server_defaults = { "interface": "",
2329
2354
                        "port": "",
2330
2355
                        "debug": "False",
2331
2356
                        "priority":
2332
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
2357
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
2333
2358
                        "servicename": "Mandos",
2334
2359
                        "use_dbus": "True",
2335
2360
                        "use_ipv6": "True",
2338
2363
                        "socket": "",
2339
2364
                        "statedir": "/var/lib/mandos",
2340
2365
                        "foreground": "False",
 
2366
                        "zeroconf": "True",
2341
2367
                        }
2342
2368
    
2343
2369
    # Parse config file for server-global settings
2370
2396
    for option in ("interface", "address", "port", "debug",
2371
2397
                   "priority", "servicename", "configdir",
2372
2398
                   "use_dbus", "use_ipv6", "debuglevel", "restore",
2373
 
                   "statedir", "socket", "foreground"):
 
2399
                   "statedir", "socket", "foreground", "zeroconf"):
2374
2400
        value = getattr(options, option)
2375
2401
        if value is not None:
2376
2402
            server_settings[option] = value
2379
2405
    for option in server_settings.keys():
2380
2406
        if type(server_settings[option]) is str:
2381
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])
2382
2412
    # Debug implies foreground
2383
2413
    if server_settings["debug"]:
2384
2414
        server_settings["foreground"] = True
2386
2416
    
2387
2417
    ##################################################################
2388
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
    
2389
2425
    # For convenience
2390
2426
    debug = server_settings["debug"]
2391
2427
    debuglevel = server_settings["debuglevel"]
2394
2430
    stored_state_path = os.path.join(server_settings["statedir"],
2395
2431
                                     stored_state_file)
2396
2432
    foreground = server_settings["foreground"]
 
2433
    zeroconf = server_settings["zeroconf"]
2397
2434
    
2398
2435
    if debug:
2399
2436
        initlogger(debug, logging.DEBUG)
2420
2457
    global mandos_dbus_service
2421
2458
    mandos_dbus_service = None
2422
2459
    
 
2460
    socketfd = None
 
2461
    if server_settings["socket"] != "":
 
2462
        socketfd = server_settings["socket"]
2423
2463
    tcp_server = MandosServer((server_settings["address"],
2424
2464
                               server_settings["port"]),
2425
2465
                              ClientHandler,
2429
2469
                              gnutls_priority=
2430
2470
                              server_settings["priority"],
2431
2471
                              use_dbus=use_dbus,
2432
 
                              socketfd=(server_settings["socket"]
2433
 
                                        or None))
 
2472
                              socketfd=socketfd)
2434
2473
    if not foreground:
2435
 
        pidfilename = "/var/run/mandos.pid"
 
2474
        pidfilename = "/run/mandos.pid"
 
2475
        if not os.path.isdir("/run/."):
 
2476
            pidfilename = "/var/run/mandos.pid"
2436
2477
        pidfile = None
2437
2478
        try:
2438
2479
            pidfile = open(pidfilename, "w")
2455
2496
        os.setuid(uid)
2456
2497
    except OSError as error:
2457
2498
        if error.errno != errno.EPERM:
2458
 
            raise error
 
2499
            raise
2459
2500
    
2460
2501
    if debug:
2461
2502
        # Enable all possible GnuTLS debugging
2504
2545
            use_dbus = False
2505
2546
            server_settings["use_dbus"] = False
2506
2547
            tcp_server.use_dbus = False
2507
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2508
 
    service = AvahiServiceToSyslog(name =
2509
 
                                   server_settings["servicename"],
2510
 
                                   servicetype = "_mandos._tcp",
2511
 
                                   protocol = protocol, bus = bus)
2512
 
    if server_settings["interface"]:
2513
 
        service.interface = (if_nametoindex
2514
 
                             (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"])))
2515
2557
    
2516
2558
    global multiprocessing_manager
2517
2559
    multiprocessing_manager = multiprocessing.Manager()
2524
2566
    old_client_settings = {}
2525
2567
    clients_data = {}
2526
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
    
2527
2577
    # Get client data and settings from last running state.
2528
2578
    if server_settings["restore"]:
2529
2579
        try:
2545
2595
    
2546
2596
    with PGPEngine() as pgp:
2547
2597
        for client_name, client in clients_data.iteritems():
 
2598
            # Skip removed clients
 
2599
            if client_name not in client_settings:
 
2600
                continue
 
2601
            
2548
2602
            # Decide which value to use after restoring saved state.
2549
2603
            # We have three different values: Old config file,
2550
2604
            # new config file, and saved state.
2612
2666
    # Create all client objects
2613
2667
    for client_name, client in clients_data.iteritems():
2614
2668
        tcp_server.clients[client_name] = client_class(
2615
 
            name = client_name, settings = client)
 
2669
            name = client_name, settings = client,
 
2670
            server_settings = server_settings)
2616
2671
    
2617
2672
    if not tcp_server.clients:
2618
2673
        logger.warning("No clients defined")
2698
2753
    
2699
2754
    def cleanup():
2700
2755
        "Cleanup function; run on exit"
2701
 
        service.cleanup()
 
2756
        if zeroconf:
 
2757
            service.cleanup()
2702
2758
        
2703
2759
        multiprocessing.active_children()
 
2760
        wnull.close()
2704
2761
        if not (tcp_server.clients or client_settings):
2705
2762
            return
2706
2763
        
2718
2775
                # A list of attributes that can not be pickled
2719
2776
                # + secret.
2720
2777
                exclude = set(("bus", "changedstate", "secret",
2721
 
                               "checker"))
 
2778
                               "checker", "server_settings"))
2722
2779
                for name, typ in (inspect.getmembers
2723
2780
                                  (dbus.service.Object)):
2724
2781
                    exclude.add(name)
2752
2809
            else:
2753
2810
                logger.warning("Could not save persistent state:",
2754
2811
                               exc_info=e)
2755
 
                raise e
 
2812
                raise
2756
2813
        
2757
2814
        # Delete all clients, and settings from config
2758
2815
        while tcp_server.clients:
2782
2839
    tcp_server.server_activate()
2783
2840
    
2784
2841
    # Find out what port we got
2785
 
    service.port = tcp_server.socket.getsockname()[1]
 
2842
    if zeroconf:
 
2843
        service.port = tcp_server.socket.getsockname()[1]
2786
2844
    if use_ipv6:
2787
2845
        logger.info("Now listening on address %r, port %d,"
2788
2846
                    " flowinfo %d, scope_id %d",
2794
2852
    #service.interface = tcp_server.socket.getsockname()[3]
2795
2853
    
2796
2854
    try:
2797
 
        # From the Avahi example code
2798
 
        try:
2799
 
            service.activate()
2800
 
        except dbus.exceptions.DBusException as error:
2801
 
            logger.critical("D-Bus Exception", exc_info=error)
2802
 
            cleanup()
2803
 
            sys.exit(1)
2804
 
        # 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
2805
2864
        
2806
2865
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2807
2866
                             lambda *args, **kwargs: