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/>.
33
3
from __future__ import division
62
28
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)
85
31
# This variable is used to optionally bind to a specified interface.
86
32
# It is a global variable to fit in with the other variables from the
33
# Avahi server example code.
88
34
serviceInterface = avahi.IF_UNSPEC
89
# From the Avahi example code:
35
# From the Avahi server example code:
90
36
serviceName = "Mandos"
91
37
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
92
38
servicePort = None # Not known at startup
154
100
del _set_interval
155
101
def __init__(self, name=None, options=None, stop_hook=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.."""
102
fingerprint=None, secret=None, secfile=None, fqdn=None,
103
timeout=None, interval=-1, checker=None):
161
105
# Uppercase and remove spaces from fingerprint
162
106
# for later comparison purposes with return value of
205
149
"""Stop this client.
206
150
The possibility that this client might be restarted is left
207
151
open, but not currently used."""
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:
153
sys.stderr.write(u"Stopping client %s\n" % self.name)
155
if self.stop_initiator_tag:
216
156
gobject.source_remove(self.stop_initiator_tag)
217
157
self.stop_initiator_tag = None
218
if hasattr(self, "checker_initiator_tag") \
219
and self.checker_initiator_tag:
158
if self.checker_initiator_tag:
220
159
gobject.source_remove(self.checker_initiator_tag)
221
160
self.checker_initiator_tag = None
222
161
self.stop_checker()
225
164
# Do not run this again if called by a gobject.timeout_add
227
166
def __del__(self):
228
self.stop_hook = None
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
230
177
def checker_callback(self, pid, condition):
231
178
"""The checker has completed, so take appropriate actions."""
232
179
now = datetime.datetime.now()
233
self.checker_callback_tag = None
235
180
if os.WIFEXITED(condition) \
236
181
and (os.WEXITSTATUS(condition) == 0):
237
logger.debug(u"Checker for %(name)s succeeded",
183
sys.stderr.write(u"Checker for %(name)s succeeded\n"
239
185
self.last_seen = now
240
186
gobject.source_remove(self.stop_initiator_tag)
241
187
self.stop_initiator_tag = gobject.timeout_add\
242
188
(self._timeout_milliseconds,
244
elif not os.WIFEXITED(condition):
245
logger.warning(u"Checker for %(name)s crashed?",
248
logger.debug(u"Checker for %(name)s failed",
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
250
199
def start_checker(self):
251
200
"""Start a new checker subprocess if one is not running.
252
201
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.
262
203
if self.checker is None:
205
sys.stderr.write(u"Starting checker for %s\n"
264
208
command = self.check_command % self.fqdn
265
209
except TypeError:
266
210
escaped_attrs = dict((key, re.escape(str(val)))
268
212
vars(self).iteritems())
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
213
command = self.check_command % escaped_attrs
276
logger.debug(u"Starting checker %r for %s",
278
215
self.checker = subprocess.\
217
stdout=subprocess.PIPE,
280
218
close_fds=True, shell=True,
282
self.checker_callback_tag = gobject.child_watch_add\
284
self.checker_callback)
220
self.checker_callback_tag = gobject.\
221
child_watch_add(self.checker.pid,
285
224
except subprocess.OSError, error:
286
logger.error(u"Failed to start subprocess: %s",
225
sys.stderr.write(u"Failed to start subprocess: %s\n"
288
227
# Re-run this periodically if run by gobject.timeout_add
290
229
def stop_checker(self):
291
230
"""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
295
231
if not hasattr(self, "checker") or self.checker is None:
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:
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)
306
238
self.checker = None
307
239
def still_valid(self, now=None):
308
240
"""Has the timeout not yet passed for this client?"""
386
320
session.handshake()
387
321
except gnutls.errors.GNUTLSError, error:
388
logger.debug(u"Handshake failed: %s", error)
323
sys.stderr.write(u"Handshake failed: %s\n" % error)
389
324
# Do not run session.bye() here: the session is not
390
325
# established. Just abandon the request.
393
328
fpr = fingerprint(peer_certificate(session))
394
329
except (TypeError, gnutls.errors.GNUTLSError), error:
395
logger.debug(u"Bad certificate: %s", error)
331
sys.stderr.write(u"Bad certificate: %s\n" % error)
398
logger.debug(u"Fingerprint: %s", fpr)
335
sys.stderr.write(u"Fingerprint: %s\n" % fpr)
400
for c in self.server.clients:
401
338
if c.fingerprint == fpr:
405
342
# that the client timed out while establishing the GnuTLS
407
344
if (not client) or (not client.still_valid()):
409
logger.debug(u"Client %(name)s is invalid",
412
logger.debug(u"Client not found for fingerprint: %s",
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)
417
355
while sent_size < len(client.secret):
418
356
sent = session.send(client.secret[sent_size:])
419
logger.debug(u"Sent: %d, remaining: %d",
420
sent, len(client.secret)
421
- (sent_size + sent))
358
sys.stderr.write(u"Sent: %d, remaining: %d\n"
359
% (sent, len(client.secret)
360
- (sent_size + sent)))
422
361
sent_size += sent
539
479
def server_state_changed(state):
540
"""Derived from the Avahi example code"""
480
"""From the Avahi server example code"""
541
481
if state == avahi.SERVER_COLLISION:
542
logger.warning(u"Server name collision")
482
sys.stderr.write(u"WARNING: Server name collision\n")
544
484
elif state == avahi.SERVER_RUNNING:
548
488
def entry_group_state_changed(state, error):
549
"""Derived from the Avahi example code"""
489
"""From the Avahi server example code"""
550
490
global serviceName, server, rename_count
552
logger.debug(u"state change: %i", state)
493
sys.stderr.write(u"state change: %i\n" % state)
554
495
if state == avahi.ENTRY_GROUP_ESTABLISHED:
555
logger.debug(u"Service established.")
497
sys.stderr.write(u"Service established.\n")
556
498
elif state == avahi.ENTRY_GROUP_COLLISION:
558
500
rename_count = rename_count - 1
559
501
if rename_count > 0:
560
502
name = server.GetAlternativeServiceName(name)
561
logger.warning(u"Service name collision, "
562
u"changing name to '%s' ...", name)
503
sys.stderr.write(u"WARNING: Service name collision, "
504
u"changing name to '%s' ...\n" % name)
567
logger.error(u"No suitable service name found after %i"
568
u" retries, exiting.", n_rename)
509
sys.stderr.write(u"ERROR: No suitable service name found "
510
u"after %i retries, exiting.\n"
570
513
elif state == avahi.ENTRY_GROUP_FAILURE:
571
logger.error(u"Error in group state changed %s",
514
sys.stderr.write(u"Error in group state changed %s\n"
576
520
def if_nametoindex(interface):
592
536
return interface_index
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
539
if __name__ == '__main__':
631
540
parser = OptionParser()
632
541
parser.add_option("-i", "--interface", type="string",
633
542
default=None, metavar="IF",
634
543
help="Bind to interface IF")
635
parser.add_option("-a", "--address", type="string", default=None,
636
help="Address to listen for requests on")
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")
637
556
parser.add_option("-p", "--port", type="int", default=None,
638
557
help="Port number to receive requests on")
639
558
parser.add_option("--timeout", type="string", # Parsed later
684
600
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)
695
603
def remove_from_clients(client):
696
604
clients.remove(client)
698
logger.debug(u"No clients left, exiting")
607
sys.stderr.write(u"No clients left, exiting\n")
701
610
clients.update(Set(Client(name=section, options=options,
702
611
stop_hook = remove_from_clients,
703
612
**(dict(client_config\
704
613
.items(section))))
705
614
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())
731
615
for client in clients:
734
tcp_server = IPv6_TCPServer((options.address, options.port),
618
tcp_server = IPv6_TCPServer((None, options.port),
738
622
# Find out what random port we got
740
623
servicePort = tcp_server.socket.getsockname()[1]
741
logger.debug(u"Now listening on port %d", servicePort)
625
sys.stderr.write(u"Now listening on port %d\n" % servicePort)
743
627
if options.interface is not None:
744
global serviceInterface
745
628
serviceInterface = if_nametoindex(options.interface)
747
# From the Avahi example code
630
# From the Avahi server example code
748
631
server.connect_to_signal("StateChanged", server_state_changed)
750
server_state_changed(server.GetState())
751
except dbus.exceptions.DBusException, error:
752
logger.critical(u"DBusException: %s", error)
632
server_state_changed(server.GetState())
754
633
# End of Avahi example code
756
635
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,