2
# -*- mode: python; coding: utf-8 -*-
4
# Mandos server - give out binary blobs to connecting clients.
6
# This program is partly derived from an example program for an Avahi
7
# service publisher, downloaded from
8
# <http://avahi.org/wiki/PythonPublishExample>. This includes the
9
# following functions: "add_service", "remove_service",
10
# "server_state_changed", "entry_group_state_changed", and some lines
13
# Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn
16
# This program is free software: you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
18
# the Free Software Foundation, either version 3 of the License, or
19
# (at your option) any later version.
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
# GNU General Public License for more details.
26
# You should have received a copy of the GNU General Public License
27
# along with this program. If not, see <http://www.gnu.org/licenses/>.
29
# Contact the authors at <https://www.fukt.bsnet.se/~belorn/> and
30
# <https://www.fukt.bsnet.se/~teddy/>.
3
33
from __future__ import division
28
62
from dbus.mainloop.glib import DBusGMainLoop
65
# Brief description of the operation of this program:
67
# This server announces itself as a Zeroconf service. Connecting
68
# clients use the TLS protocol, with the unusual quirk that this
69
# server program acts as a TLS "client" while the connecting clients
70
# acts as a TLS "server". The clients (acting as a TLS "server") must
71
# supply an OpenPGP certificate, and the fingerprint of this
72
# certificate is used by this server to look up (in a list read from a
73
# file at start time) which binary blob to give the client. No other
74
# authentication or authorization is done by this server.
77
logger = logging.Logger('mandos')
78
syslogger = logging.handlers.SysLogHandler\
79
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
80
syslogger.setFormatter(logging.Formatter\
81
('%(levelname)s: %(message)s'))
82
logger.addHandler(syslogger)
31
85
# This variable is used to optionally bind to a specified interface.
32
86
# It is a global variable to fit in with the other variables from the
33
# Avahi server example code.
34
88
serviceInterface = avahi.IF_UNSPEC
35
# From the Avahi server example code:
89
# From the Avahi example code:
36
90
serviceName = "Mandos"
37
91
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
38
92
servicePort = None # Not known at startup
100
154
del _set_interval
101
155
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):
156
fingerprint=None, secret=None, secfile=None,
157
fqdn=None, timeout=None, interval=-1, checker=None):
158
"""Note: the 'checker' argument sets the 'checker_command'
159
attribute and not the 'checker' attribute.."""
105
161
# Uppercase and remove spaces from fingerprint
106
162
# for later comparison purposes with return value of
149
205
"""Stop this client.
150
206
The possibility that this client might be restarted is left
151
207
open, but not currently used."""
153
sys.stderr.write(u"Stopping client %s\n" % self.name)
155
if self.stop_initiator_tag:
208
# If this client doesn't have a secret, it is already stopped.
210
logger.debug(u"Stopping client %s", self.name)
214
if hasattr(self, "stop_initiator_tag") \
215
and self.stop_initiator_tag:
156
216
gobject.source_remove(self.stop_initiator_tag)
157
217
self.stop_initiator_tag = None
158
if self.checker_initiator_tag:
218
if hasattr(self, "checker_initiator_tag") \
219
and self.checker_initiator_tag:
159
220
gobject.source_remove(self.checker_initiator_tag)
160
221
self.checker_initiator_tag = None
161
222
self.stop_checker()
164
225
# Do not run this again if called by a gobject.timeout_add
166
227
def __del__(self):
167
# Some code duplication here and in stop()
168
if hasattr(self, "stop_initiator_tag") \
169
and self.stop_initiator_tag:
170
gobject.source_remove(self.stop_initiator_tag)
171
self.stop_initiator_tag = None
172
if hasattr(self, "checker_initiator_tag") \
173
and self.checker_initiator_tag:
174
gobject.source_remove(self.checker_initiator_tag)
175
self.checker_initiator_tag = None
228
self.stop_hook = None
177
230
def checker_callback(self, pid, condition):
178
231
"""The checker has completed, so take appropriate actions."""
179
232
now = datetime.datetime.now()
233
self.checker_callback_tag = None
180
235
if os.WIFEXITED(condition) \
181
236
and (os.WEXITSTATUS(condition) == 0):
183
sys.stderr.write(u"Checker for %(name)s succeeded\n"
237
logger.debug(u"Checker for %(name)s succeeded",
185
239
self.last_seen = now
186
240
gobject.source_remove(self.stop_initiator_tag)
187
241
self.stop_initiator_tag = gobject.timeout_add\
188
242
(self._timeout_milliseconds,
191
if not os.WIFEXITED(condition):
192
sys.stderr.write(u"Checker for %(name)s crashed?\n"
195
sys.stderr.write(u"Checker for %(name)s failed\n"
198
self.checker_callback_tag = None
244
elif not os.WIFEXITED(condition):
245
logger.warning(u"Checker for %(name)s crashed?",
248
logger.debug(u"Checker for %(name)s failed",
199
250
def start_checker(self):
200
251
"""Start a new checker subprocess if one is not running.
201
252
If a checker already exists, leave it running and do
254
# The reason for not killing a running checker is that if we
255
# did that, then if a checker (for some reason) started
256
# running slowly and taking more than 'interval' time, the
257
# client would inevitably timeout, since no checker would get
258
# a chance to run to completion. If we instead leave running
259
# checkers alone, the checker would have to take more time
260
# than 'timeout' for the client to be declared invalid, which
261
# is as it should be.
203
262
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
208
264
command = self.check_command % self.fqdn
209
265
except TypeError:
210
266
escaped_attrs = dict((key, re.escape(str(val)))
212
268
vars(self).iteritems())
213
command = self.check_command % escaped_attrs
270
command = self.check_command % escaped_attrs
271
except TypeError, error:
272
logger.critical(u'Could not format string "%s":'
273
u' %s', self.check_command, error)
274
return True # Try again later
276
logger.debug(u"Starting checker %r for %s",
215
278
self.checker = subprocess.\
217
stdout=subprocess.PIPE,
218
280
close_fds=True, shell=True,
220
self.checker_callback_tag = gobject.\
221
child_watch_add(self.checker.pid,
282
self.checker_callback_tag = gobject.child_watch_add\
284
self.checker_callback)
224
285
except subprocess.OSError, error:
225
sys.stderr.write(u"Failed to start subprocess: %s\n"
286
logger.error(u"Failed to start subprocess: %s",
227
288
# Re-run this periodically if run by gobject.timeout_add
229
290
def stop_checker(self):
230
291
"""Force the checker process, if any, to stop."""
292
if self.checker_callback_tag:
293
gobject.source_remove(self.checker_callback_tag)
294
self.checker_callback_tag = None
231
295
if not hasattr(self, "checker") or self.checker is None:
233
gobject.source_remove(self.checker_callback_tag)
234
self.checker_callback_tag = None
235
os.kill(self.checker.pid, signal.SIGTERM)
236
if self.checker.poll() is None:
237
os.kill(self.checker.pid, signal.SIGKILL)
297
logger.debug("Stopping checker for %(name)s", vars(self))
299
os.kill(self.checker.pid, signal.SIGTERM)
301
#if self.checker.poll() is None:
302
# os.kill(self.checker.pid, signal.SIGKILL)
303
except OSError, error:
304
if error.errno != errno.ESRCH:
238
306
self.checker = None
239
307
def still_valid(self, now=None):
240
308
"""Has the timeout not yet passed for this client?"""
320
386
session.handshake()
321
387
except gnutls.errors.GNUTLSError, error:
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
388
logger.debug(u"Handshake failed: %s", error)
324
389
# Do not run session.bye() here: the session is not
325
390
# established. Just abandon the request.
328
393
fpr = fingerprint(peer_certificate(session))
329
394
except (TypeError, gnutls.errors.GNUTLSError), error:
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
395
logger.debug(u"Bad certificate: %s", error)
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
398
logger.debug(u"Fingerprint: %s", fpr)
400
for c in self.server.clients:
338
401
if c.fingerprint == fpr:
342
405
# that the client timed out while establishing the GnuTLS
344
407
if (not client) or (not client.still_valid()):
347
sys.stderr.write(u"Client %(name)s is invalid\n"
350
sys.stderr.write(u"Client not found for "
351
u"fingerprint: %s\n" % fpr)
409
logger.debug(u"Client %(name)s is invalid",
412
logger.debug(u"Client not found for fingerprint: %s",
355
417
while sent_size < len(client.secret):
356
418
sent = session.send(client.secret[sent_size:])
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
419
logger.debug(u"Sent: %d, remaining: %d",
420
sent, len(client.secret)
421
- (sent_size + sent))
361
422
sent_size += sent
479
539
def server_state_changed(state):
480
"""From the Avahi server example code"""
540
"""Derived from the Avahi example code"""
481
541
if state == avahi.SERVER_COLLISION:
482
sys.stderr.write(u"WARNING: Server name collision\n")
542
logger.warning(u"Server name collision")
484
544
elif state == avahi.SERVER_RUNNING:
488
548
def entry_group_state_changed(state, error):
489
"""From the Avahi server example code"""
549
"""Derived from the Avahi example code"""
490
550
global serviceName, server, rename_count
493
sys.stderr.write(u"state change: %i\n" % state)
552
logger.debug(u"state change: %i", state)
495
554
if state == avahi.ENTRY_GROUP_ESTABLISHED:
497
sys.stderr.write(u"Service established.\n")
555
logger.debug(u"Service established.")
498
556
elif state == avahi.ENTRY_GROUP_COLLISION:
500
558
rename_count = rename_count - 1
501
559
if rename_count > 0:
502
560
name = server.GetAlternativeServiceName(name)
503
sys.stderr.write(u"WARNING: Service name collision, "
504
u"changing name to '%s' ...\n" % name)
561
logger.warning(u"Service name collision, "
562
u"changing name to '%s' ...", name)
509
sys.stderr.write(u"ERROR: No suitable service name found "
510
u"after %i retries, exiting.\n"
567
logger.error(u"No suitable service name found after %i"
568
u" retries, exiting.", n_rename)
513
570
elif state == avahi.ENTRY_GROUP_FAILURE:
514
sys.stderr.write(u"Error in group state changed %s\n"
571
logger.error(u"Error in group state changed %s",
520
576
def if_nametoindex(interface):
536
592
return interface_index
539
if __name__ == '__main__':
595
def daemon(nochdir, noclose):
596
"""See daemon(3). Standard BSD Unix function.
597
This should really exist as os.daemon, but it doesn't (yet)."""
604
# Close all standard open file descriptors
605
null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR)
606
if not stat.S_ISCHR(os.fstat(null).st_mode):
607
raise OSError(errno.ENODEV,
608
"/dev/null not a character device")
609
os.dup2(null, sys.stdin.fileno())
610
os.dup2(null, sys.stdout.fileno())
611
os.dup2(null, sys.stderr.fileno())
616
def killme(status = 0):
617
logger.debug("Stopping server with exit status %d", status)
619
if main_loop_started:
628
global main_loop_started
629
main_loop_started = False
540
631
parser = OptionParser()
541
632
parser.add_option("-i", "--interface", type="string",
542
633
default=None, metavar="IF",
543
634
help="Bind to interface IF")
544
parser.add_option("--cert", type="string", default="cert.pem",
546
help="Public key certificate PEM file to use")
547
parser.add_option("--key", type="string", default="key.pem",
549
help="Private key PEM file to use")
550
parser.add_option("--ca", type="string", default="ca.pem",
552
help="Certificate Authority certificate PEM file to use")
553
parser.add_option("--crl", type="string", default="crl.pem",
555
help="Certificate Revokation List PEM file to use")
635
parser.add_option("-a", "--address", type="string", default=None,
636
help="Address to listen for requests on")
556
637
parser.add_option("-p", "--port", type="int", default=None,
557
638
help="Port number to receive requests on")
558
639
parser.add_option("--timeout", type="string", # Parsed later
600
684
debug = options.debug
687
console = logging.StreamHandler()
688
# console.setLevel(logging.DEBUG)
689
console.setFormatter(logging.Formatter\
690
('%(levelname)s: %(message)s'))
691
logger.addHandler(console)
603
695
def remove_from_clients(client):
604
696
clients.remove(client)
607
sys.stderr.write(u"No clients left, exiting\n")
698
logger.debug(u"No clients left, exiting")
610
701
clients.update(Set(Client(name=section, options=options,
611
702
stop_hook = remove_from_clients,
612
703
**(dict(client_config\
613
704
.items(section))))
614
705
for section in client_config.sections()))
711
"Cleanup function; run on exit"
713
# From the Avahi example code
714
if not group is None:
717
# End of Avahi example code
720
client = clients.pop()
721
client.stop_hook = None
724
atexit.register(cleanup)
727
signal.signal(signal.SIGINT, signal.SIG_IGN)
728
signal.signal(signal.SIGHUP, lambda signum, frame: killme())
729
signal.signal(signal.SIGTERM, lambda signum, frame: killme())
615
731
for client in clients:
618
tcp_server = IPv6_TCPServer((None, options.port),
734
tcp_server = IPv6_TCPServer((options.address, options.port),
622
738
# Find out what random port we got
623
740
servicePort = tcp_server.socket.getsockname()[1]
625
sys.stderr.write(u"Now listening on port %d\n" % servicePort)
741
logger.debug(u"Now listening on port %d", servicePort)
627
743
if options.interface is not None:
744
global serviceInterface
628
745
serviceInterface = if_nametoindex(options.interface)
630
# From the Avahi server example code
747
# From the Avahi example code
631
748
server.connect_to_signal("StateChanged", server_state_changed)
632
server_state_changed(server.GetState())
750
server_state_changed(server.GetState())
751
except dbus.exceptions.DBusException, error:
752
logger.critical(u"DBusException: %s", error)
633
754
# End of Avahi example code
635
756
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,