/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-13 21:02:53 UTC
  • mto: This revision was merged to the branch mainline in revision 711.
  • Revision ID: teddy@recompile.se-20140713210253-ftnydds0bnvbzxaa
Tags: version-1.6.6-1
* Makefile (version): Changed to "1.6.6".
* NEWS (Version 1.6.6): New entry.
* debian/changelog (1.6.6-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.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)
 
743
                return True
720
744
            self.checker_callback_tag = (gobject.child_watch_add
721
745
                                         (self.checker.pid,
722
746
                                          self.checker_callback,
723
747
                                          data=command))
724
748
            # The checker may have completed before the gobject
725
749
            # watch was added.  Check for this.
726
 
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
750
            try:
 
751
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
752
            except OSError as error:
 
753
                if error.errno == errno.ECHILD:
 
754
                    # This should never happen
 
755
                    logger.error("Child process vanished",
 
756
                                 exc_info=error)
 
757
                    return True
 
758
                raise
727
759
            if pid:
728
760
                gobject.source_remove(self.checker_callback_tag)
729
761
                self.checker_callback(pid, status, command)
905
937
            # The byte_arrays option is not supported yet on
906
938
            # signatures other than "ay".
907
939
            if prop._dbus_signature != "ay":
908
 
                raise ValueError
 
940
                raise ValueError("Byte arrays not supported for non-"
 
941
                                 "'ay' signature {0!r}"
 
942
                                 .format(prop._dbus_signature))
909
943
            value = dbus.ByteArray(b''.join(chr(byte)
910
944
                                            for byte in value))
911
945
        prop(value)
1069
1103
                interface_names.add(alt_interface)
1070
1104
                # Is this a D-Bus signal?
1071
1105
                if getattr(attribute, "_dbus_is_signal", False):
1072
 
                    # Extract the original non-method function by
1073
 
                    # black magic
 
1106
                    # Extract the original non-method undecorated
 
1107
                    # function by black magic
1074
1108
                    nonmethod_func = (dict(
1075
1109
                            zip(attribute.func_code.co_freevars,
1076
1110
                                attribute.__closure__))["func"]
1319
1353
                                       *args, **kwargs)
1320
1354
    
1321
1355
    def start_checker(self, *args, **kwargs):
1322
 
        old_checker = self.checker
1323
 
        if self.checker is not None:
1324
 
            old_checker_pid = self.checker.pid
1325
 
        else:
1326
 
            old_checker_pid = None
 
1356
        old_checker_pid = getattr(self.checker, "pid", None)
1327
1357
        r = Client.start_checker(self, *args, **kwargs)
1328
1358
        # Only if new checker process was started
1329
1359
        if (self.checker is not None
1674
1704
            logger.debug("Protocol version: %r", line)
1675
1705
            try:
1676
1706
                if int(line.strip().split()[0]) > 1:
1677
 
                    raise RuntimeError
 
1707
                    raise RuntimeError(line)
1678
1708
            except (ValueError, IndexError, RuntimeError) as error:
1679
1709
                logger.error("Unknown protocol version: %s", error)
1680
1710
                return
1887
1917
    
1888
1918
    def add_pipe(self, parent_pipe, proc):
1889
1919
        """Dummy function; override as necessary"""
1890
 
        raise NotImplementedError
 
1920
        raise NotImplementedError()
1891
1921
 
1892
1922
 
1893
1923
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1969
1999
                if self.address_family == socket.AF_INET6:
1970
2000
                    any_address = "::" # in6addr_any
1971
2001
                else:
1972
 
                    any_address = socket.INADDR_ANY
 
2002
                    any_address = "0.0.0.0" # INADDR_ANY
1973
2003
                self.server_address = (any_address,
1974
2004
                                       self.server_address[1])
1975
2005
            elif not self.server_address[1]:
2230
2260
            else:
2231
2261
                raise ValueError("Unknown suffix {0!r}"
2232
2262
                                 .format(suffix))
2233
 
        except (ValueError, IndexError) as e:
 
2263
        except IndexError as e:
2234
2264
            raise ValueError(*(e.args))
2235
2265
        timevalue += delta
2236
2266
    return timevalue
2280
2310
                        help="Run self-test")
2281
2311
    parser.add_argument("--debug", action="store_true",
2282
2312
                        help="Debug mode; run in foreground and log"
2283
 
                        " to terminal")
 
2313
                        " to terminal", default=None)
2284
2314
    parser.add_argument("--debuglevel", metavar="LEVEL",
2285
2315
                        help="Debug level for stdout output")
2286
2316
    parser.add_argument("--priority", help="GnuTLS"
2293
2323
                        " files")
2294
2324
    parser.add_argument("--no-dbus", action="store_false",
2295
2325
                        dest="use_dbus", help="Do not provide D-Bus"
2296
 
                        " system bus interface")
 
2326
                        " system bus interface", default=None)
2297
2327
    parser.add_argument("--no-ipv6", action="store_false",
2298
 
                        dest="use_ipv6", help="Do not use IPv6")
 
2328
                        dest="use_ipv6", help="Do not use IPv6",
 
2329
                        default=None)
2299
2330
    parser.add_argument("--no-restore", action="store_false",
2300
2331
                        dest="restore", help="Do not restore stored"
2301
 
                        " state")
 
2332
                        " state", default=None)
2302
2333
    parser.add_argument("--socket", type=int,
2303
2334
                        help="Specify a file descriptor to a network"
2304
2335
                        " socket to use instead of creating one")
2305
2336
    parser.add_argument("--statedir", metavar="DIR",
2306
2337
                        help="Directory to save/restore state in")
2307
2338
    parser.add_argument("--foreground", action="store_true",
2308
 
                        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)
2309
2343
    
2310
2344
    options = parser.parse_args()
2311
2345
    
2312
2346
    if options.check:
2313
2347
        import doctest
2314
 
        doctest.testmod()
2315
 
        sys.exit()
 
2348
        fail_count, test_count = doctest.testmod()
 
2349
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2316
2350
    
2317
2351
    # Default values for config file for server-global settings
2318
2352
    server_defaults = { "interface": "",
2320
2354
                        "port": "",
2321
2355
                        "debug": "False",
2322
2356
                        "priority":
2323
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
 
2357
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
2324
2358
                        "servicename": "Mandos",
2325
2359
                        "use_dbus": "True",
2326
2360
                        "use_ipv6": "True",
2329
2363
                        "socket": "",
2330
2364
                        "statedir": "/var/lib/mandos",
2331
2365
                        "foreground": "False",
 
2366
                        "zeroconf": "True",
2332
2367
                        }
2333
2368
    
2334
2369
    # Parse config file for server-global settings
2361
2396
    for option in ("interface", "address", "port", "debug",
2362
2397
                   "priority", "servicename", "configdir",
2363
2398
                   "use_dbus", "use_ipv6", "debuglevel", "restore",
2364
 
                   "statedir", "socket", "foreground"):
 
2399
                   "statedir", "socket", "foreground", "zeroconf"):
2365
2400
        value = getattr(options, option)
2366
2401
        if value is not None:
2367
2402
            server_settings[option] = value
2370
2405
    for option in server_settings.keys():
2371
2406
        if type(server_settings[option]) is str:
2372
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])
2373
2412
    # Debug implies foreground
2374
2413
    if server_settings["debug"]:
2375
2414
        server_settings["foreground"] = True
2377
2416
    
2378
2417
    ##################################################################
2379
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
    
2380
2425
    # For convenience
2381
2426
    debug = server_settings["debug"]
2382
2427
    debuglevel = server_settings["debuglevel"]
2385
2430
    stored_state_path = os.path.join(server_settings["statedir"],
2386
2431
                                     stored_state_file)
2387
2432
    foreground = server_settings["foreground"]
 
2433
    zeroconf = server_settings["zeroconf"]
2388
2434
    
2389
2435
    if debug:
2390
2436
        initlogger(debug, logging.DEBUG)
2411
2457
    global mandos_dbus_service
2412
2458
    mandos_dbus_service = None
2413
2459
    
 
2460
    socketfd = None
 
2461
    if server_settings["socket"] != "":
 
2462
        socketfd = server_settings["socket"]
2414
2463
    tcp_server = MandosServer((server_settings["address"],
2415
2464
                               server_settings["port"]),
2416
2465
                              ClientHandler,
2420
2469
                              gnutls_priority=
2421
2470
                              server_settings["priority"],
2422
2471
                              use_dbus=use_dbus,
2423
 
                              socketfd=(server_settings["socket"]
2424
 
                                        or None))
 
2472
                              socketfd=socketfd)
2425
2473
    if not foreground:
2426
 
        pidfilename = "/var/run/mandos.pid"
 
2474
        pidfilename = "/run/mandos.pid"
 
2475
        if not os.path.isdir("/run/."):
 
2476
            pidfilename = "/var/run/mandos.pid"
2427
2477
        pidfile = None
2428
2478
        try:
2429
2479
            pidfile = open(pidfilename, "w")
2446
2496
        os.setuid(uid)
2447
2497
    except OSError as error:
2448
2498
        if error.errno != errno.EPERM:
2449
 
            raise error
 
2499
            raise
2450
2500
    
2451
2501
    if debug:
2452
2502
        # Enable all possible GnuTLS debugging
2495
2545
            use_dbus = False
2496
2546
            server_settings["use_dbus"] = False
2497
2547
            tcp_server.use_dbus = False
2498
 
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2499
 
    service = AvahiServiceToSyslog(name =
2500
 
                                   server_settings["servicename"],
2501
 
                                   servicetype = "_mandos._tcp",
2502
 
                                   protocol = protocol, bus = bus)
2503
 
    if server_settings["interface"]:
2504
 
        service.interface = (if_nametoindex
2505
 
                             (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"])))
2506
2557
    
2507
2558
    global multiprocessing_manager
2508
2559
    multiprocessing_manager = multiprocessing.Manager()
2515
2566
    old_client_settings = {}
2516
2567
    clients_data = {}
2517
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
    
2518
2577
    # Get client data and settings from last running state.
2519
2578
    if server_settings["restore"]:
2520
2579
        try:
2536
2595
    
2537
2596
    with PGPEngine() as pgp:
2538
2597
        for client_name, client in clients_data.iteritems():
 
2598
            # Skip removed clients
 
2599
            if client_name not in client_settings:
 
2600
                continue
 
2601
            
2539
2602
            # Decide which value to use after restoring saved state.
2540
2603
            # We have three different values: Old config file,
2541
2604
            # new config file, and saved state.
2603
2666
    # Create all client objects
2604
2667
    for client_name, client in clients_data.iteritems():
2605
2668
        tcp_server.clients[client_name] = client_class(
2606
 
            name = client_name, settings = client)
 
2669
            name = client_name, settings = client,
 
2670
            server_settings = server_settings)
2607
2671
    
2608
2672
    if not tcp_server.clients:
2609
2673
        logger.warning("No clients defined")
2689
2753
    
2690
2754
    def cleanup():
2691
2755
        "Cleanup function; run on exit"
2692
 
        service.cleanup()
 
2756
        if zeroconf:
 
2757
            service.cleanup()
2693
2758
        
2694
2759
        multiprocessing.active_children()
 
2760
        wnull.close()
2695
2761
        if not (tcp_server.clients or client_settings):
2696
2762
            return
2697
2763
        
2709
2775
                # A list of attributes that can not be pickled
2710
2776
                # + secret.
2711
2777
                exclude = set(("bus", "changedstate", "secret",
2712
 
                               "checker"))
 
2778
                               "checker", "server_settings"))
2713
2779
                for name, typ in (inspect.getmembers
2714
2780
                                  (dbus.service.Object)):
2715
2781
                    exclude.add(name)
2743
2809
            else:
2744
2810
                logger.warning("Could not save persistent state:",
2745
2811
                               exc_info=e)
2746
 
                raise e
 
2812
                raise
2747
2813
        
2748
2814
        # Delete all clients, and settings from config
2749
2815
        while tcp_server.clients:
2773
2839
    tcp_server.server_activate()
2774
2840
    
2775
2841
    # Find out what port we got
2776
 
    service.port = tcp_server.socket.getsockname()[1]
 
2842
    if zeroconf:
 
2843
        service.port = tcp_server.socket.getsockname()[1]
2777
2844
    if use_ipv6:
2778
2845
        logger.info("Now listening on address %r, port %d,"
2779
2846
                    " flowinfo %d, scope_id %d",
2785
2852
    #service.interface = tcp_server.socket.getsockname()[3]
2786
2853
    
2787
2854
    try:
2788
 
        # From the Avahi example code
2789
 
        try:
2790
 
            service.activate()
2791
 
        except dbus.exceptions.DBusException as error:
2792
 
            logger.critical("D-Bus Exception", exc_info=error)
2793
 
            cleanup()
2794
 
            sys.exit(1)
2795
 
        # 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
2796
2864
        
2797
2865
        gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2798
2866
                             lambda *args, **kwargs: