/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: 2012-06-23 13:50:05 UTC
  • 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-2013 Teddy Hogeborn
15
 
# Copyright © 2008-2013 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.2"
 
92
version = "1.6.0"
92
93
stored_state_file = "clients.pickle"
93
94
 
94
95
logger = logging.getLogger()
139
140
class PGPEngine(object):
140
141
    """A simple class for OpenPGP symmetric encryption & decryption"""
141
142
    def __init__(self):
 
143
        self.gnupg = GnuPGInterface.GnuPG()
142
144
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
143
 
        self.gnupgargs = ['--batch',
144
 
                          '--home', self.tempdir,
145
 
                          '--force-mdc',
146
 
                          '--quiet',
147
 
                          '--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'])
148
151
    
149
152
    def __enter__(self):
150
153
        return self
172
175
    def password_encode(self, password):
173
176
        # Passphrase can not be empty and can not contain newlines or
174
177
        # NUL bytes.  So we prefix it and hex encode it.
175
 
        encoded = b"mandos" + binascii.hexlify(password)
176
 
        if len(encoded) > 2048:
177
 
            # GnuPG can't handle long passwords, so encode differently
178
 
            encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
179
 
                       .replace(b"\n", b"\\n")
180
 
                       .replace(b"\0", b"\\x00"))
181
 
        return encoded
 
178
        return b"mandos" + binascii.hexlify(password)
182
179
    
183
180
    def encrypt(self, data, password):
184
 
        passphrase = self.password_encode(password)
185
 
        with tempfile.NamedTemporaryFile(dir=self.tempdir
186
 
                                         ) as passfile:
187
 
            passfile.write(passphrase)
188
 
            passfile.flush()
189
 
            proc = subprocess.Popen(['gpg', '--symmetric',
190
 
                                     '--passphrase-file',
191
 
                                     passfile.name]
192
 
                                    + self.gnupgargs,
193
 
                                    stdin = subprocess.PIPE,
194
 
                                    stdout = subprocess.PIPE,
195
 
                                    stderr = subprocess.PIPE)
196
 
            ciphertext, err = proc.communicate(input = data)
197
 
        if proc.returncode != 0:
198
 
            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
199
195
        return ciphertext
200
196
    
201
197
    def decrypt(self, data, password):
202
 
        passphrase = self.password_encode(password)
203
 
        with tempfile.NamedTemporaryFile(dir = self.tempdir
204
 
                                         ) as passfile:
205
 
            passfile.write(passphrase)
206
 
            passfile.flush()
207
 
            proc = subprocess.Popen(['gpg', '--decrypt',
208
 
                                     '--passphrase-file',
209
 
                                     passfile.name]
210
 
                                    + self.gnupgargs,
211
 
                                    stdin = subprocess.PIPE,
212
 
                                    stdout = subprocess.PIPE,
213
 
                                    stderr = subprocess.PIPE)
214
 
            decrypted_plaintext, err = proc.communicate(input
215
 
                                                        = data)
216
 
        if proc.returncode != 0:
217
 
            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
218
212
        return decrypted_plaintext
219
213
 
220
214
 
240
234
               Used to optionally bind to the specified interface.
241
235
    name: string; Example: 'Mandos'
242
236
    type: string; Example: '_mandos._tcp'.
243
 
     See <https://www.iana.org/assignments/service-names-port-numbers>
 
237
                  See <http://www.dns-sd.org/ServiceTypes.html>
244
238
    port: integer; what port to announce
245
239
    TXT: list of strings; TXT record for the service
246
240
    domain: string; Domain to publish on, default to .local if empty.
446
440
    runtime_expansions: Allowed attributes for runtime expansion.
447
441
    expires:    datetime.datetime(); time (UTC) when a client will be
448
442
                disabled, or None
449
 
    server_settings: The server_settings dict from main()
450
443
    """
451
444
    
452
445
    runtime_expansions = ("approval_delay", "approval_duration",
527
520
        
528
521
        return settings
529
522
    
530
 
    def __init__(self, settings, name = None, server_settings=None):
 
523
    def __init__(self, settings, name = None):
531
524
        self.name = name
532
 
        if server_settings is None:
533
 
            server_settings = {}
534
 
        self.server_settings = server_settings
535
525
        # adding all client settings
536
526
        for setting, value in settings.iteritems():
537
527
            setattr(self, setting, value)
690
680
        # If a checker exists, make sure it is not a zombie
691
681
        try:
692
682
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
693
 
        except AttributeError:
694
 
            pass
695
 
        except OSError as error:
696
 
            if error.errno != errno.ECHILD:
697
 
                raise
 
683
        except (AttributeError, OSError) as error:
 
684
            if (isinstance(error, OSError)
 
685
                and error.errno != errno.ECHILD):
 
686
                raise error
698
687
        else:
699
688
            if pid:
700
689
                logger.warning("Checker was a zombie")
722
711
                # in normal mode, that is already done by daemon(),
723
712
                # and in debug mode we don't want to.  (Stdin is
724
713
                # always replaced by /dev/null.)
725
 
                # The exception is when not debugging but nevertheless
726
 
                # running in the foreground; use the previously
727
 
                # created wnull.
728
 
                popen_args = {}
729
 
                if (not self.server_settings["debug"]
730
 
                    and self.server_settings["foreground"]):
731
 
                    popen_args.update({"stdout": wnull,
732
 
                                       "stderr": wnull })
733
714
                self.checker = subprocess.Popen(command,
734
715
                                                close_fds=True,
735
 
                                                shell=True, cwd="/",
736
 
                                                **popen_args)
 
716
                                                shell=True, cwd="/")
737
717
            except OSError as error:
738
718
                logger.error("Failed to start subprocess",
739
719
                             exc_info=error)
748
728
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
749
729
            except OSError as error:
750
730
                if error.errno == errno.ECHILD:
751
 
                    # This should never happen
752
 
                    logger.error("Child process vanished",
753
 
                                 exc_info=error)
 
731
                    logger.error("Child process vanished", exc_info=error)
754
732
                    return True
755
733
                raise
756
734
            if pid:
1098
1076
                interface_names.add(alt_interface)
1099
1077
                # Is this a D-Bus signal?
1100
1078
                if getattr(attribute, "_dbus_is_signal", False):
1101
 
                    # Extract the original non-method undecorated
1102
 
                    # function by black magic
 
1079
                    # Extract the original non-method function by
 
1080
                    # black magic
1103
1081
                    nonmethod_func = (dict(
1104
1082
                            zip(attribute.func_code.co_freevars,
1105
1083
                                attribute.__closure__))["func"]
1703
1681
            logger.debug("Protocol version: %r", line)
1704
1682
            try:
1705
1683
                if int(line.strip().split()[0]) > 1:
1706
 
                    raise RuntimeError(line)
 
1684
                    raise RuntimeError
1707
1685
            except (ValueError, IndexError, RuntimeError) as error:
1708
1686
                logger.error("Unknown protocol version: %s", error)
1709
1687
                return
1916
1894
    
1917
1895
    def add_pipe(self, parent_pipe, proc):
1918
1896
        """Dummy function; override as necessary"""
1919
 
        raise NotImplementedError()
 
1897
        raise NotImplementedError
1920
1898
 
1921
1899
 
1922
1900
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
1998
1976
                if self.address_family == socket.AF_INET6:
1999
1977
                    any_address = "::" # in6addr_any
2000
1978
                else:
2001
 
                    any_address = "0.0.0.0" # INADDR_ANY
 
1979
                    any_address = socket.INADDR_ANY
2002
1980
                self.server_address = (any_address,
2003
1981
                                       self.server_address[1])
2004
1982
            elif not self.server_address[1]:
2259
2237
            else:
2260
2238
                raise ValueError("Unknown suffix {0!r}"
2261
2239
                                 .format(suffix))
2262
 
        except IndexError as e:
 
2240
        except (ValueError, IndexError) as e:
2263
2241
            raise ValueError(*(e.args))
2264
2242
        timevalue += delta
2265
2243
    return timevalue
2309
2287
                        help="Run self-test")
2310
2288
    parser.add_argument("--debug", action="store_true",
2311
2289
                        help="Debug mode; run in foreground and log"
2312
 
                        " to terminal", default=None)
 
2290
                        " to terminal")
2313
2291
    parser.add_argument("--debuglevel", metavar="LEVEL",
2314
2292
                        help="Debug level for stdout output")
2315
2293
    parser.add_argument("--priority", help="GnuTLS"
2322
2300
                        " files")
2323
2301
    parser.add_argument("--no-dbus", action="store_false",
2324
2302
                        dest="use_dbus", help="Do not provide D-Bus"
2325
 
                        " system bus interface", default=None)
 
2303
                        " system bus interface")
2326
2304
    parser.add_argument("--no-ipv6", action="store_false",
2327
 
                        dest="use_ipv6", help="Do not use IPv6",
2328
 
                        default=None)
 
2305
                        dest="use_ipv6", help="Do not use IPv6")
2329
2306
    parser.add_argument("--no-restore", action="store_false",
2330
2307
                        dest="restore", help="Do not restore stored"
2331
 
                        " state", default=None)
 
2308
                        " state")
2332
2309
    parser.add_argument("--socket", type=int,
2333
2310
                        help="Specify a file descriptor to a network"
2334
2311
                        " socket to use instead of creating one")
2335
2312
    parser.add_argument("--statedir", metavar="DIR",
2336
2313
                        help="Directory to save/restore state in")
2337
2314
    parser.add_argument("--foreground", action="store_true",
2338
 
                        help="Run in foreground", default=None)
 
2315
                        help="Run in foreground")
2339
2316
    
2340
2317
    options = parser.parse_args()
2341
2318
    
2350
2327
                        "port": "",
2351
2328
                        "debug": "False",
2352
2329
                        "priority":
2353
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2330
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2354
2331
                        "servicename": "Mandos",
2355
2332
                        "use_dbus": "True",
2356
2333
                        "use_ipv6": "True",
2400
2377
    for option in server_settings.keys():
2401
2378
        if type(server_settings[option]) is str:
2402
2379
            server_settings[option] = unicode(server_settings[option])
2403
 
    # Force all boolean options to be boolean
2404
 
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2405
 
                   "foreground"):
2406
 
        server_settings[option] = bool(server_settings[option])
2407
2380
    # Debug implies foreground
2408
2381
    if server_settings["debug"]:
2409
2382
        server_settings["foreground"] = True
2457
2430
                              socketfd=(server_settings["socket"]
2458
2431
                                        or None))
2459
2432
    if not foreground:
2460
 
        pidfilename = "/run/mandos.pid"
2461
 
        if not os.path.isdir("/run/."):
2462
 
            pidfilename = "/var/run/mandos.pid"
 
2433
        pidfilename = "/var/run/mandos.pid"
2463
2434
        pidfile = None
2464
2435
        try:
2465
2436
            pidfile = open(pidfilename, "w")
2482
2453
        os.setuid(uid)
2483
2454
    except OSError as error:
2484
2455
        if error.errno != errno.EPERM:
2485
 
            raise
 
2456
            raise error
2486
2457
    
2487
2458
    if debug:
2488
2459
        # Enable all possible GnuTLS debugging
2551
2522
    old_client_settings = {}
2552
2523
    clients_data = {}
2553
2524
    
2554
 
    # This is used to redirect stdout and stderr for checker processes
2555
 
    global wnull
2556
 
    wnull = open(os.devnull, "w") # A writable /dev/null
2557
 
    # Only used if server is running in foreground but not in debug
2558
 
    # mode
2559
 
    if debug or not foreground:
2560
 
        wnull.close()
2561
 
    
2562
2525
    # Get client data and settings from last running state.
2563
2526
    if server_settings["restore"]:
2564
2527
        try:
2580
2543
    
2581
2544
    with PGPEngine() as pgp:
2582
2545
        for client_name, client in clients_data.iteritems():
2583
 
            # Skip removed clients
2584
 
            if client_name not in client_settings:
2585
 
                continue
2586
 
            
2587
2546
            # Decide which value to use after restoring saved state.
2588
2547
            # We have three different values: Old config file,
2589
2548
            # new config file, and saved state.
2651
2610
    # Create all client objects
2652
2611
    for client_name, client in clients_data.iteritems():
2653
2612
        tcp_server.clients[client_name] = client_class(
2654
 
            name = client_name, settings = client,
2655
 
            server_settings = server_settings)
 
2613
            name = client_name, settings = client)
2656
2614
    
2657
2615
    if not tcp_server.clients:
2658
2616
        logger.warning("No clients defined")
2741
2699
        service.cleanup()
2742
2700
        
2743
2701
        multiprocessing.active_children()
2744
 
        wnull.close()
2745
2702
        if not (tcp_server.clients or client_settings):
2746
2703
            return
2747
2704
        
2759
2716
                # A list of attributes that can not be pickled
2760
2717
                # + secret.
2761
2718
                exclude = set(("bus", "changedstate", "secret",
2762
 
                               "checker", "server_settings"))
 
2719
                               "checker"))
2763
2720
                for name, typ in (inspect.getmembers
2764
2721
                                  (dbus.service.Object)):
2765
2722
                    exclude.add(name)
2793
2750
            else:
2794
2751
                logger.warning("Could not save persistent state:",
2795
2752
                               exc_info=e)
2796
 
                raise
 
2753
                raise e
2797
2754
        
2798
2755
        # Delete all clients, and settings from config
2799
2756
        while tcp_server.clients: