/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 server.py

  • Committer: Teddy Hogeborn
  • Date: 2008-06-30 01:43:39 UTC
  • Revision ID: teddy@fukt.bsnet.se-20080630014339-rsuztydpl2w5ml83
* server.py: Rewritten to use Zeroconf (mDNS/DNS-SD) in place of the
             old broadcast-UDP-to-port-49001 method.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
import gnutls.crypto
12
12
import gnutls.connection
13
13
import gnutls.errors
14
 
import gnutls.library.functions
15
 
import gnutls.library.constants
16
 
import gnutls.library.types
17
14
import ConfigParser
18
15
import sys
19
16
import re
26
23
import gobject
27
24
import avahi
28
25
from dbus.mainloop.glib import DBusGMainLoop
29
 
import ctypes
30
26
 
31
 
# This variable is used to optionally bind to a specified interface.
32
 
# It is a global variable to fit in with the other variables from the
33
 
# Avahi server example code.
 
27
# This variable is used to optionally bind to a specified
 
28
# interface.
34
29
serviceInterface = avahi.IF_UNSPEC
35
 
# From the Avahi server example code:
 
30
# It is a global variable to fit in with the rest of the
 
31
# variables from the Avahi server example code:
36
32
serviceName = "Mandos"
37
33
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
38
34
servicePort = None                      # Not known at startup
48
44
class Client(object):
49
45
    """A representation of a client host served by this server.
50
46
    Attributes:
51
 
    name:      string; from the config file, used in log messages
52
 
    fingerprint: string (40 or 32 hexadecimal digits); used to
53
 
                 uniquely identify the client
54
 
    secret:    bytestring; sent verbatim (over TLS) to client
55
 
    fqdn:      string (FQDN); available for use by the checker command
 
47
    password:  string
 
48
    fqdn:      string, FQDN (used by the checker)
56
49
    created:   datetime.datetime()
57
50
    last_seen: datetime.datetime() or None if not yet seen
58
51
    timeout:   datetime.timedelta(); How long from last_seen until
59
52
                                     this client is invalid
60
53
    interval:  datetime.timedelta(); How often to start a new checker
 
54
    timeout_milliseconds: Used by gobject.timeout_add()
 
55
    interval_milliseconds: - '' -
61
56
    stop_hook: If set, called by stop() as stop_hook(self)
62
57
    checker:   subprocess.Popen(); a running checker process used
63
58
                                   to see if the client lives.
65
60
    checker_initiator_tag: a gobject event source tag, or None
66
61
    stop_initiator_tag:    - '' -
67
62
    checker_callback_tag:  - '' -
68
 
    checker_command: string; External command which is run to check if
69
 
                     client lives.  %()s expansions are done at
70
 
                     runtime with vars(self) as dict, so that for
71
 
                     instance %(name)s can be used in the command.
72
 
    Private attibutes:
73
 
    _timeout: Real variable for 'timeout'
74
 
    _interval: Real variable for 'interval'
75
 
    _timeout_milliseconds: Used by gobject.timeout_add()
76
 
    _interval_milliseconds: - '' -
77
63
    """
78
 
    def _set_timeout(self, timeout):
79
 
        "Setter function for 'timeout' attribute"
80
 
        self._timeout = timeout
81
 
        self._timeout_milliseconds = ((self.timeout.days
82
 
                                       * 24 * 60 * 60 * 1000)
83
 
                                      + (self.timeout.seconds * 1000)
84
 
                                      + (self.timeout.microseconds
85
 
                                         // 1000))
86
 
    timeout = property(lambda self: self._timeout,
87
 
                       _set_timeout)
88
 
    del _set_timeout
89
 
    def _set_interval(self, interval):
90
 
        "Setter function for 'interval' attribute"
91
 
        self._interval = interval
92
 
        self._interval_milliseconds = ((self.interval.days
93
 
                                        * 24 * 60 * 60 * 1000)
94
 
                                       + (self.interval.seconds
95
 
                                          * 1000)
96
 
                                       + (self.interval.microseconds
97
 
                                          // 1000))
98
 
    interval = property(lambda self: self._interval,
99
 
                        _set_interval)
100
 
    del _set_interval
101
64
    def __init__(self, name=None, options=None, stop_hook=None,
102
 
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
103
 
                 timeout=None, interval=-1, checker=None):
 
65
                 dn=None, password=None, passfile=None, fqdn=None,
 
66
                 timeout=None, interval=-1):
104
67
        self.name = name
105
 
        # Uppercase and remove spaces from fingerprint
106
 
        # for later comparison purposes with return value of
107
 
        # the fingerprint() function
108
 
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
109
 
        if secret:
110
 
            self.secret = secret.decode(u"base64")
111
 
        elif secfile:
112
 
            sf = open(secfile)
113
 
            self.secret = sf.read()
114
 
            sf.close()
 
68
        self.dn = dn
 
69
        if password:
 
70
            self.password = password
 
71
        elif passfile:
 
72
            self.password = open(passfile).readall()
115
73
        else:
116
 
            raise RuntimeError(u"No secret or secfile for client %s"
 
74
            raise RuntimeError(u"No Password or Passfile for client %s"
117
75
                               % self.name)
118
76
        self.fqdn = fqdn                # string
119
77
        self.created = datetime.datetime.now()
121
79
        if timeout is None:
122
80
            timeout = options.timeout
123
81
        self.timeout = timeout
 
82
        self.timeout_milliseconds = ((self.timeout.days
 
83
                                      * 24 * 60 * 60 * 1000)
 
84
                                     + (self.timeout.seconds * 1000)
 
85
                                     + (self.timeout.microseconds
 
86
                                        // 1000))
124
87
        if interval == -1:
125
88
            interval = options.interval
126
89
        else:
127
90
            interval = string_to_delta(interval)
128
91
        self.interval = interval
 
92
        self.interval_milliseconds = ((self.interval.days
 
93
                                       * 24 * 60 * 60 * 1000)
 
94
                                      + (self.interval.seconds * 1000)
 
95
                                      + (self.interval.microseconds
 
96
                                         // 1000))
129
97
        self.stop_hook = stop_hook
130
98
        self.checker = None
131
99
        self.checker_initiator_tag = None
132
100
        self.stop_initiator_tag = None
133
101
        self.checker_callback_tag = None
134
 
        self.check_command = checker
135
102
    def start(self):
136
103
        """Start this clients checker and timeout hooks"""
137
104
        # Schedule a new checker to be started an 'interval' from now,
138
105
        # and every interval from then on.
139
 
        self.checker_initiator_tag = gobject.timeout_add\
140
 
                                     (self._interval_milliseconds,
141
 
                                      self.start_checker)
 
106
        self.checker_initiator_tag = gobject.\
 
107
                                     timeout_add(self.interval_milliseconds,
 
108
                                                 self.start_checker)
142
109
        # Also start a new checker *right now*.
143
110
        self.start_checker()
144
111
        # Schedule a stop() when 'timeout' has passed
145
 
        self.stop_initiator_tag = gobject.timeout_add\
146
 
                                  (self._timeout_milliseconds,
147
 
                                   self.stop)
 
112
        self.stop_initiator_tag = gobject.\
 
113
                                     timeout_add(self.timeout_milliseconds,
 
114
                                                 self.stop)
148
115
    def stop(self):
149
116
        """Stop this client.
150
117
        The possibility that this client might be restarted is left
151
118
        open, but not currently used."""
152
 
        if debug:
153
 
            sys.stderr.write(u"Stopping client %s\n" % self.name)
154
 
        self.secret = None
 
119
        # print "Stopping client", self.name
 
120
        self.password = None
155
121
        if self.stop_initiator_tag:
156
122
            gobject.source_remove(self.stop_initiator_tag)
157
123
            self.stop_initiator_tag = None
179
145
        now = datetime.datetime.now()
180
146
        if os.WIFEXITED(condition) \
181
147
               and (os.WEXITSTATUS(condition) == 0):
182
 
            if debug:
183
 
                sys.stderr.write(u"Checker for %(name)s succeeded\n"
184
 
                                 % vars(self))
 
148
            #print "Checker for %(name)s succeeded" % vars(self)
185
149
            self.last_seen = now
186
150
            gobject.source_remove(self.stop_initiator_tag)
187
 
            self.stop_initiator_tag = gobject.timeout_add\
188
 
                                      (self._timeout_milliseconds,
189
 
                                       self.stop)
190
 
        elif debug:
191
 
            if not os.WIFEXITED(condition):
192
 
                sys.stderr.write(u"Checker for %(name)s crashed?\n"
193
 
                                 % vars(self))
194
 
            else:
195
 
                sys.stderr.write(u"Checker for %(name)s failed\n"
196
 
                                 % vars(self))
 
151
            self.stop_initiator_tag = gobject.\
 
152
                                      timeout_add(self.timeout_milliseconds,
 
153
                                                  self.stop)
 
154
        #else:
 
155
        #    if not os.WIFEXITED(condition):
 
156
        #        print "Checker for %(name)s crashed?" % vars(self)
 
157
        #    else:
 
158
        #        print "Checker for %(name)s failed" % vars(self)
197
159
        self.checker = None
198
160
        self.checker_callback_tag = None
199
161
    def start_checker(self):
201
163
        If a checker already exists, leave it running and do
202
164
        nothing."""
203
165
        if self.checker is None:
204
 
            if debug:
205
 
                sys.stderr.write(u"Starting checker for %s\n"
206
 
                                 % self.name)
207
 
            try:
208
 
                command = self.check_command % self.fqdn
209
 
            except TypeError:
210
 
                escaped_attrs = dict((key, re.escape(str(val)))
211
 
                                     for key, val in
212
 
                                     vars(self).iteritems())
213
 
                command = self.check_command % escaped_attrs
 
166
            #print "Starting checker for", self.name
214
167
            try:
215
168
                self.checker = subprocess.\
216
 
                               Popen(command,
 
169
                               Popen("sleep 1; fping -q -- %s"
 
170
                                     % re.escape(self.fqdn),
217
171
                                     stdout=subprocess.PIPE,
218
172
                                     close_fds=True, shell=True,
219
173
                                     cwd="/")
246
200
            return now < (self.last_seen + self.timeout)
247
201
 
248
202
 
249
 
def peer_certificate(session):
250
 
    # If not an OpenPGP certificate...
251
 
    if gnutls.library.functions.gnutls_certificate_type_get\
252
 
            (session._c_object) \
253
 
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
254
 
        # ...do the normal thing
255
 
        return session.peer_certificate
256
 
    list_size = ctypes.c_uint()
257
 
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
258
 
        (session._c_object, ctypes.byref(list_size))
259
 
    if list_size.value == 0:
260
 
        return None
261
 
    cert = cert_list[0]
262
 
    return ctypes.string_at(cert.data, cert.size)
263
 
 
264
 
 
265
 
def fingerprint(openpgp):
266
 
    # New empty GnuTLS certificate
267
 
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
268
 
    gnutls.library.functions.gnutls_openpgp_crt_init\
269
 
        (ctypes.byref(crt))
270
 
    # New GnuTLS "datum" with the OpenPGP public key
271
 
    datum = gnutls.library.types.gnutls_datum_t\
272
 
        (ctypes.cast(ctypes.c_char_p(openpgp),
273
 
                     ctypes.POINTER(ctypes.c_ubyte)),
274
 
         ctypes.c_uint(len(openpgp)))
275
 
    # Import the OpenPGP public key into the certificate
276
 
    ret = gnutls.library.functions.gnutls_openpgp_crt_import\
277
 
        (crt,
278
 
         ctypes.byref(datum),
279
 
         gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
280
 
    # New buffer for the fingerprint
281
 
    buffer = ctypes.create_string_buffer(20)
282
 
    buffer_length = ctypes.c_size_t()
283
 
    # Get the fingerprint from the certificate into the buffer
284
 
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
285
 
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
286
 
    # Deinit the certificate
287
 
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
288
 
    # Convert the buffer to a Python bytestring
289
 
    fpr = ctypes.string_at(buffer, buffer_length.value)
290
 
    # Convert the bytestring to hexadecimal notation
291
 
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
292
 
    return hex_fpr
293
 
 
294
 
 
295
203
class tcp_handler(SocketServer.BaseRequestHandler, object):
296
204
    """A TCP request handler class.
297
205
    Instantiated by IPv6_TCPServer for each request to handle it.
298
206
    Note: This will run in its own forked process."""
299
 
    
300
207
    def handle(self):
301
 
        if debug:
302
 
            sys.stderr.write(u"TCP request came\n")
303
 
            sys.stderr.write(u"Request: %s\n" % self.request)
304
 
            sys.stderr.write(u"Client Address: %s\n"
305
 
                             % unicode(self.client_address))
306
 
            sys.stderr.write(u"Server: %s\n" % self.server)
307
 
        session = gnutls.connection.ClientSession(self.request,
308
 
                                                  gnutls.connection.\
309
 
                                                  X509Credentials())
310
 
        
311
 
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
312
 
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
313
 
        #                "+DHE-DSS"))
314
 
        priority = "SECURE256"
315
 
        
316
 
        gnutls.library.functions.gnutls_priority_set_direct\
317
 
            (session._c_object, priority, None);
318
 
        
 
208
        #print u"TCP request came"
 
209
        #print u"Request:", self.request
 
210
        #print u"Client Address:", self.client_address
 
211
        #print u"Server:", self.server
 
212
        session = gnutls.connection.ServerSession(self.request,
 
213
                                                  self.server\
 
214
                                                  .credentials)
319
215
        try:
320
216
            session.handshake()
321
217
        except gnutls.errors.GNUTLSError, error:
322
 
            if debug:
323
 
                sys.stderr.write(u"Handshake failed: %s\n" % error)
 
218
            #sys.stderr.write(u"Handshake failed: %s\n" % error)
324
219
            # Do not run session.bye() here: the session is not
325
220
            # established.  Just abandon the request.
326
221
            return
 
222
        #if session.peer_certificate:
 
223
        #    print "DN:", session.peer_certificate.subject
327
224
        try:
328
 
            fpr = fingerprint(peer_certificate(session))
329
 
        except (TypeError, gnutls.errors.GNUTLSError), error:
330
 
            if debug:
331
 
                sys.stderr.write(u"Bad certificate: %s\n" % error)
 
225
            session.verify_peer()
 
226
        except gnutls.errors.CertificateError, error:
 
227
            #sys.stderr.write(u"Verify failed: %s\n" % error)
332
228
            session.bye()
333
229
            return
334
 
        if debug:
335
 
            sys.stderr.write(u"Fingerprint: %s\n" % fpr)
336
230
        client = None
337
231
        for c in clients:
338
 
            if c.fingerprint == fpr:
 
232
            if c.dn == session.peer_certificate.subject:
339
233
                client = c
340
234
                break
341
235
        # Have to check if client.still_valid(), since it is possible
342
236
        # that the client timed out while establishing the GnuTLS
343
237
        # session.
344
 
        if (not client) or (not client.still_valid()):
345
 
            if debug:
346
 
                if client:
347
 
                    sys.stderr.write(u"Client %(name)s is invalid\n"
348
 
                                     % vars(client))
349
 
                else:
350
 
                    sys.stderr.write(u"Client not found for "
351
 
                                     u"fingerprint: %s\n" % fpr)
352
 
            session.bye()
353
 
            return
354
 
        sent_size = 0
355
 
        while sent_size < len(client.secret):
356
 
            sent = session.send(client.secret[sent_size:])
357
 
            if debug:
358
 
                sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
 
                                 % (sent, len(client.secret)
360
 
                                    - (sent_size + sent)))
361
 
            sent_size += sent
 
238
        if client and client.still_valid():
 
239
            session.send(client.password)
 
240
        else:
 
241
            #if client:
 
242
            #    sys.stderr.write(u"Client %(name)s is invalid\n"
 
243
            #                     % vars(client))
 
244
            #else:
 
245
            #    sys.stderr.write(u"Client not found for DN: %s\n"
 
246
            #                     % session.peer_certificate.subject)
 
247
            #session.send("gazonk")
 
248
            pass
362
249
        session.bye()
363
250
 
364
251
 
367
254
    Attributes:
368
255
        options:        Command line options
369
256
        clients:        Set() of Client objects
 
257
        credentials:    GnuTLS X.509 credentials
370
258
    """
371
259
    address_family = socket.AF_INET6
372
260
    def __init__(self, *args, **kwargs):
376
264
        if "clients" in kwargs:
377
265
            self.clients = kwargs["clients"]
378
266
            del kwargs["clients"]
 
267
        if "credentials" in kwargs:
 
268
            self.credentials = kwargs["credentials"]
 
269
            del kwargs["credentials"]
379
270
        return super(type(self), self).__init__(*args, **kwargs)
380
271
    def server_bind(self):
381
272
        """This overrides the normal server_bind() function
391
282
                                       self.options.interface)
392
283
            except socket.error, error:
393
284
                if error[0] == errno.EPERM:
394
 
                    sys.stderr.write(u"Warning: No permission to" \
395
 
                                     u" bind to interface %s\n"
 
285
                    sys.stderr.write(u"Warning: No permission to bind to interface %s\n"
396
286
                                     % self.options.interface)
397
287
                else:
398
288
                    raise error
453
343
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
454
344
        group.connect_to_signal('StateChanged',
455
345
                                entry_group_state_changed)
456
 
    if debug:
457
 
        sys.stderr.write(u"Adding service '%s' of type '%s' ...\n"
458
 
                         % (serviceName, serviceType))
 
346
    
 
347
    # print "Adding service '%s' of type '%s' ..." % (serviceName,
 
348
    #                                                 serviceType)
459
349
    
460
350
    group.AddService(
461
351
            serviceInterface,           # interface
479
369
def server_state_changed(state):
480
370
    """From the Avahi server example code"""
481
371
    if state == avahi.SERVER_COLLISION:
482
 
        sys.stderr.write(u"WARNING: Server name collision\n")
 
372
        print "WARNING: Server name collision"
483
373
        remove_service()
484
374
    elif state == avahi.SERVER_RUNNING:
485
375
        add_service()
489
379
    """From the Avahi server example code"""
490
380
    global serviceName, server, rename_count
491
381
    
492
 
    if debug:
493
 
        sys.stderr.write(u"state change: %i\n" % state)
 
382
    # print "state change: %i" % state
494
383
    
495
384
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
496
 
        if debug:
497
 
            sys.stderr.write(u"Service established.\n")
 
385
        pass
 
386
        # print "Service established."
498
387
    elif state == avahi.ENTRY_GROUP_COLLISION:
499
388
        
500
389
        rename_count = rename_count - 1
501
390
        if rename_count > 0:
502
391
            name = server.GetAlternativeServiceName(name)
503
 
            sys.stderr.write(u"WARNING: Service name collision, "
504
 
                             u"changing name to '%s' ...\n" % name)
 
392
            print "WARNING: Service name collision, changing name to '%s' ..." % name
505
393
            remove_service()
506
394
            add_service()
507
395
            
508
396
        else:
509
 
            sys.stderr.write(u"ERROR: No suitable service name found "
510
 
                             u"after %i retries, exiting.\n"
511
 
                             % n_rename)
 
397
            print "ERROR: No suitable service name found after %i retries, exiting." % n_rename
512
398
            main_loop.quit()
513
399
    elif state == avahi.ENTRY_GROUP_FAILURE:
514
 
        sys.stderr.write(u"Error in group state changed %s\n"
515
 
                         % unicode(error))
 
400
        print "Error in group state changed", error
516
401
        main_loop.quit()
517
402
        return
518
403
 
520
405
def if_nametoindex(interface):
521
406
    """Call the C function if_nametoindex()"""
522
407
    try:
 
408
        if "ctypes" not in sys.modules:
 
409
            import ctypes
523
410
        libc = ctypes.cdll.LoadLibrary("libc.so.6")
524
411
        return libc.if_nametoindex(interface)
525
 
    except (OSError, AttributeError):
 
412
    except (ImportError, OSError, AttributeError):
526
413
        if "struct" not in sys.modules:
527
414
            import struct
528
415
        if "fcntl" not in sys.modules:
563
450
                      help="How often to check that a client is up")
564
451
    parser.add_option("--check", action="store_true", default=False,
565
452
                      help="Run self-test")
566
 
    parser.add_option("--debug", action="store_true", default=False,
567
 
                      help="Debug mode")
568
453
    (options, args) = parser.parse_args()
569
454
    
570
455
    if options.check:
582
467
    except ValueError:
583
468
        parser.error("option --interval: Unparseable time")
584
469
    
 
470
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
 
471
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
 
472
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
 
473
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
 
474
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
 
475
    
585
476
    # Parse config file
586
 
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
 
477
    defaults = {}
587
478
    client_config = ConfigParser.SafeConfigParser(defaults)
588
479
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
589
480
    client_config.read("mandos-clients.conf")
597
488
            avahi.DBUS_INTERFACE_SERVER )
598
489
    # End of Avahi example code
599
490
    
600
 
    debug = options.debug
601
 
    
602
491
    clients = Set()
603
492
    def remove_from_clients(client):
604
493
        clients.remove(client)
605
494
        if not clients:
606
 
            if debug:
607
 
                sys.stderr.write(u"No clients left, exiting\n")
 
495
            print "No clients left, exiting"
608
496
            main_loop.quit()
609
497
    
610
498
    clients.update(Set(Client(name=section, options=options,
618
506
    tcp_server = IPv6_TCPServer((None, options.port),
619
507
                                tcp_handler,
620
508
                                options=options,
621
 
                                clients=clients)
 
509
                                clients=clients,
 
510
                                credentials=cred)
622
511
    # Find out what random port we got
623
512
    servicePort = tcp_server.socket.getsockname()[1]
624
 
    if debug:
625
 
        sys.stderr.write(u"Now listening on port %d\n" % servicePort)
 
513
    #sys.stderr.write("Now listening on port %d\n" % servicePort)
626
514
    
627
515
    if options.interface is not None:
628
516
        serviceInterface = if_nametoindex(options.interface)