/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 mandos

  • Committer: Teddy Hogeborn
  • Date: 2012-06-23 13:50:05 UTC
  • mto: (237.7.272 trunk)
  • mto: This revision was merged to the branch mainline in revision 303.
  • Revision ID: teddy@recompile.se-20120623135005-t0wtkmlw0ymnr2kg
* mandos (Client.start_checker): Return True (retry later) if starting
                                 checker failed.  Check for ECHILD
                                 from waitpid().

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-2014 Teddy Hogeborn
15
 
# Copyright © 2008-2014 Björn Påhlsson
 
14
# Copyright © 2008-2012 Teddy Hogeborn
 
15
# Copyright © 2008-2012 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
82
83
 
83
84
try:
84
85
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
88
89
    except ImportError:
89
90
        SO_BINDTODEVICE = None
90
91
 
91
 
version = "1.6.5"
 
92
version = "1.6.0"
92
93
stored_state_file = "clients.pickle"
93
94
 
94
95
logger = logging.getLogger()
95
 
syslogger = None
 
96
syslogger = (logging.handlers.SysLogHandler
 
97
             (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
 
98
              address = str("/dev/log")))
96
99
 
97
100
try:
98
101
    if_nametoindex = (ctypes.cdll.LoadLibrary
114
117
def initlogger(debug, level=logging.WARNING):
115
118
    """init logger and add loglevel"""
116
119
    
117
 
    global syslogger
118
 
    syslogger = (logging.handlers.SysLogHandler
119
 
                 (facility =
120
 
                  logging.handlers.SysLogHandler.LOG_DAEMON,
121
 
                  address = str("/dev/log")))
122
120
    syslogger.setFormatter(logging.Formatter
123
121
                           ('Mandos [%(process)d]: %(levelname)s:'
124
122
                            ' %(message)s'))
142
140
class PGPEngine(object):
143
141
    """A simple class for OpenPGP symmetric encryption & decryption"""
144
142
    def __init__(self):
 
143
        self.gnupg = GnuPGInterface.GnuPG()
145
144
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
146
 
        self.gnupgargs = ['--batch',
147
 
                          '--home', self.tempdir,
148
 
                          '--force-mdc',
149
 
                          '--quiet',
150
 
                          '--no-use-agent']
 
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'])
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
 
        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
 
178
        return b"mandos" + binascii.hexlify(password)
185
179
    
186
180
    def encrypt(self, data, password):
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)
 
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
202
195
        return ciphertext
203
196
    
204
197
    def decrypt(self, data, password):
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)
 
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
221
212
        return decrypted_plaintext
222
213
 
223
214
 
243
234
               Used to optionally bind to the specified interface.
244
235
    name: string; Example: 'Mandos'
245
236
    type: string; Example: '_mandos._tcp'.
246
 
     See <https://www.iana.org/assignments/service-names-port-numbers>
 
237
                  See <http://www.dns-sd.org/ServiceTypes.html>
247
238
    port: integer; what port to announce
248
239
    TXT: list of strings; TXT record for the service
249
240
    domain: string; Domain to publish on, default to .local if empty.
449
440
    runtime_expansions: Allowed attributes for runtime expansion.
450
441
    expires:    datetime.datetime(); time (UTC) when a client will be
451
442
                disabled, or None
452
 
    server_settings: The server_settings dict from main()
453
443
    """
454
444
    
455
445
    runtime_expansions = ("approval_delay", "approval_duration",
530
520
        
531
521
        return settings
532
522
    
533
 
    def __init__(self, settings, name = None, server_settings=None):
 
523
    def __init__(self, settings, name = None):
534
524
        self.name = name
535
 
        if server_settings is None:
536
 
            server_settings = {}
537
 
        self.server_settings = server_settings
538
525
        # adding all client settings
539
526
        for setting, value in settings.iteritems():
540
527
            setattr(self, setting, value)
693
680
        # If a checker exists, make sure it is not a zombie
694
681
        try:
695
682
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
696
 
        except AttributeError:
697
 
            pass
698
 
        except OSError as error:
699
 
            if error.errno != errno.ECHILD:
700
 
                raise
 
683
        except (AttributeError, OSError) as error:
 
684
            if (isinstance(error, OSError)
 
685
                and error.errno != errno.ECHILD):
 
686
                raise error
701
687
        else:
702
688
            if pid:
703
689
                logger.warning("Checker was a zombie")
725
711
                # in normal mode, that is already done by daemon(),
726
712
                # and in debug mode we don't want to.  (Stdin is
727
713
                # 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 })
736
714
                self.checker = subprocess.Popen(command,
737
715
                                                close_fds=True,
738
 
                                                shell=True, cwd="/",
739
 
                                                **popen_args)
 
716
                                                shell=True, cwd="/")
740
717
            except OSError as error:
741
718
                logger.error("Failed to start subprocess",
742
719
                             exc_info=error)
751
728
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
752
729
            except OSError as error:
753
730
                if error.errno == errno.ECHILD:
754
 
                    # This should never happen
755
 
                    logger.error("Child process vanished",
756
 
                                 exc_info=error)
 
731
                    logger.error("Child process vanished", exc_info=error)
757
732
                    return True
758
733
                raise
759
734
            if pid:
937
912
            # The byte_arrays option is not supported yet on
938
913
            # signatures other than "ay".
939
914
            if prop._dbus_signature != "ay":
940
 
                raise ValueError("Byte arrays not supported for non-"
941
 
                                 "'ay' signature {0!r}"
942
 
                                 .format(prop._dbus_signature))
 
915
                raise ValueError
943
916
            value = dbus.ByteArray(b''.join(chr(byte)
944
917
                                            for byte in value))
945
918
        prop(value)
1103
1076
                interface_names.add(alt_interface)
1104
1077
                # Is this a D-Bus signal?
1105
1078
                if getattr(attribute, "_dbus_is_signal", False):
1106
 
                    # Extract the original non-method undecorated
1107
 
                    # function by black magic
 
1079
                    # Extract the original non-method function by
 
1080
                    # black magic
1108
1081
                    nonmethod_func = (dict(
1109
1082
                            zip(attribute.func_code.co_freevars,
1110
1083
                                attribute.__closure__))["func"]
1353
1326
                                       *args, **kwargs)
1354
1327
    
1355
1328
    def start_checker(self, *args, **kwargs):
1356
 
        old_checker_pid = getattr(self.checker, "pid", None)
 
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
1357
1334
        r = Client.start_checker(self, *args, **kwargs)
1358
1335
        # Only if new checker process was started
1359
1336
        if (self.checker is not None
1704
1681
            logger.debug("Protocol version: %r", line)
1705
1682
            try:
1706
1683
                if int(line.strip().split()[0]) > 1:
1707
 
                    raise RuntimeError(line)
 
1684
                    raise RuntimeError
1708
1685
            except (ValueError, IndexError, RuntimeError) as error:
1709
1686
                logger.error("Unknown protocol version: %s", error)
1710
1687
                return
1917
1894
    
1918
1895
    def add_pipe(self, parent_pipe, proc):
1919
1896
        """Dummy function; override as necessary"""
1920
 
        raise NotImplementedError()
 
1897
        raise NotImplementedError
1921
1898
 
1922
1899
 
1923
1900
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1999
1976
                if self.address_family == socket.AF_INET6:
2000
1977
                    any_address = "::" # in6addr_any
2001
1978
                else:
2002
 
                    any_address = "0.0.0.0" # INADDR_ANY
 
1979
                    any_address = socket.INADDR_ANY
2003
1980
                self.server_address = (any_address,
2004
1981
                                       self.server_address[1])
2005
1982
            elif not self.server_address[1]:
2260
2237
            else:
2261
2238
                raise ValueError("Unknown suffix {0!r}"
2262
2239
                                 .format(suffix))
2263
 
        except IndexError as e:
 
2240
        except (ValueError, IndexError) as e:
2264
2241
            raise ValueError(*(e.args))
2265
2242
        timevalue += delta
2266
2243
    return timevalue
2310
2287
                        help="Run self-test")
2311
2288
    parser.add_argument("--debug", action="store_true",
2312
2289
                        help="Debug mode; run in foreground and log"
2313
 
                        " to terminal", default=None)
 
2290
                        " to terminal")
2314
2291
    parser.add_argument("--debuglevel", metavar="LEVEL",
2315
2292
                        help="Debug level for stdout output")
2316
2293
    parser.add_argument("--priority", help="GnuTLS"
2323
2300
                        " files")
2324
2301
    parser.add_argument("--no-dbus", action="store_false",
2325
2302
                        dest="use_dbus", help="Do not provide D-Bus"
2326
 
                        " system bus interface", default=None)
 
2303
                        " system bus interface")
2327
2304
    parser.add_argument("--no-ipv6", action="store_false",
2328
 
                        dest="use_ipv6", help="Do not use IPv6",
2329
 
                        default=None)
 
2305
                        dest="use_ipv6", help="Do not use IPv6")
2330
2306
    parser.add_argument("--no-restore", action="store_false",
2331
2307
                        dest="restore", help="Do not restore stored"
2332
 
                        " state", default=None)
 
2308
                        " state")
2333
2309
    parser.add_argument("--socket", type=int,
2334
2310
                        help="Specify a file descriptor to a network"
2335
2311
                        " socket to use instead of creating one")
2336
2312
    parser.add_argument("--statedir", metavar="DIR",
2337
2313
                        help="Directory to save/restore state in")
2338
2314
    parser.add_argument("--foreground", action="store_true",
2339
 
                        help="Run in foreground", default=None)
 
2315
                        help="Run in foreground")
2340
2316
    
2341
2317
    options = parser.parse_args()
2342
2318
    
2343
2319
    if options.check:
2344
2320
        import doctest
2345
 
        fail_count, test_count = doctest.testmod()
2346
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
 
2321
        doctest.testmod()
 
2322
        sys.exit()
2347
2323
    
2348
2324
    # Default values for config file for server-global settings
2349
2325
    server_defaults = { "interface": "",
2351
2327
                        "port": "",
2352
2328
                        "debug": "False",
2353
2329
                        "priority":
2354
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2330
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2355
2331
                        "servicename": "Mandos",
2356
2332
                        "use_dbus": "True",
2357
2333
                        "use_ipv6": "True",
2401
2377
    for option in server_settings.keys():
2402
2378
        if type(server_settings[option]) is str:
2403
2379
            server_settings[option] = unicode(server_settings[option])
2404
 
    # Force all boolean options to be boolean
2405
 
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2406
 
                   "foreground"):
2407
 
        server_settings[option] = bool(server_settings[option])
2408
2380
    # Debug implies foreground
2409
2381
    if server_settings["debug"]:
2410
2382
        server_settings["foreground"] = True
2458
2430
                              socketfd=(server_settings["socket"]
2459
2431
                                        or None))
2460
2432
    if not foreground:
2461
 
        pidfilename = "/run/mandos.pid"
2462
 
        if not os.path.isdir("/run/."):
2463
 
            pidfilename = "/var/run/mandos.pid"
 
2433
        pidfilename = "/var/run/mandos.pid"
2464
2434
        pidfile = None
2465
2435
        try:
2466
2436
            pidfile = open(pidfilename, "w")
2483
2453
        os.setuid(uid)
2484
2454
    except OSError as error:
2485
2455
        if error.errno != errno.EPERM:
2486
 
            raise
 
2456
            raise error
2487
2457
    
2488
2458
    if debug:
2489
2459
        # Enable all possible GnuTLS debugging
2552
2522
    old_client_settings = {}
2553
2523
    clients_data = {}
2554
2524
    
2555
 
    # This is used to redirect stdout and stderr for checker processes
2556
 
    global wnull
2557
 
    wnull = open(os.devnull, "w") # A writable /dev/null
2558
 
    # Only used if server is running in foreground but not in debug
2559
 
    # mode
2560
 
    if debug or not foreground:
2561
 
        wnull.close()
2562
 
    
2563
2525
    # Get client data and settings from last running state.
2564
2526
    if server_settings["restore"]:
2565
2527
        try:
2581
2543
    
2582
2544
    with PGPEngine() as pgp:
2583
2545
        for client_name, client in clients_data.iteritems():
2584
 
            # Skip removed clients
2585
 
            if client_name not in client_settings:
2586
 
                continue
2587
 
            
2588
2546
            # Decide which value to use after restoring saved state.
2589
2547
            # We have three different values: Old config file,
2590
2548
            # new config file, and saved state.
2652
2610
    # Create all client objects
2653
2611
    for client_name, client in clients_data.iteritems():
2654
2612
        tcp_server.clients[client_name] = client_class(
2655
 
            name = client_name, settings = client,
2656
 
            server_settings = server_settings)
 
2613
            name = client_name, settings = client)
2657
2614
    
2658
2615
    if not tcp_server.clients:
2659
2616
        logger.warning("No clients defined")
2742
2699
        service.cleanup()
2743
2700
        
2744
2701
        multiprocessing.active_children()
2745
 
        wnull.close()
2746
2702
        if not (tcp_server.clients or client_settings):
2747
2703
            return
2748
2704
        
2760
2716
                # A list of attributes that can not be pickled
2761
2717
                # + secret.
2762
2718
                exclude = set(("bus", "changedstate", "secret",
2763
 
                               "checker", "server_settings"))
 
2719
                               "checker"))
2764
2720
                for name, typ in (inspect.getmembers
2765
2721
                                  (dbus.service.Object)):
2766
2722
                    exclude.add(name)
2794
2750
            else:
2795
2751
                logger.warning("Could not save persistent state:",
2796
2752
                               exc_info=e)
2797
 
                raise
 
2753
                raise e
2798
2754
        
2799
2755
        # Delete all clients, and settings from config
2800
2756
        while tcp_server.clients: