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