6
6
# This program is partly derived from an example program for an Avahi
7
7
# service publisher, downloaded from
8
8
# <http://avahi.org/wiki/PythonPublishExample>. This includes the
9
# methods "add" and "remove" in the "AvahiService" class, the
10
# "server_state_changed" and "entry_group_state_changed" functions,
11
# and some lines in "main".
9
# following functions: "AvahiService.add", "AvahiService.remove",
10
# "server_state_changed", "entry_group_state_changed", and some lines
13
13
# Everything else is
14
14
# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson
61
61
from dbus.mainloop.glib import DBusGMainLoop
64
# Brief description of the operation of this program:
66
# This server announces itself as a Zeroconf service. Connecting
67
# clients use the TLS protocol, with the unusual quirk that this
68
# server program acts as a TLS "client" while a connecting client acts
69
# as a TLS "server". The client (acting as a TLS "server") must
70
# supply an OpenPGP certificate, and the fingerprint of this
71
# certificate is used by this server to look up (in a list read from a
72
# file at start time) which binary blob to give the client. No other
73
# authentication or authorization is done by this server.
65
76
logger = logging.Logger('mandos')
66
77
syslogger = logging.handlers.SysLogHandler\
87
98
class AvahiService(object):
88
"""An Avahi (Zeroconf) service.
90
100
interface: integer; avahi.IF_UNSPEC or an interface index.
91
101
Used to optionally bind to the specified interface.
92
name: string; Example: 'Mandos'
93
type: string; Example: '_mandos._tcp'.
94
See <http://www.dns-sd.org/ServiceTypes.html>
95
port: integer; what port to announce
96
TXT: list of strings; TXT record for the service
97
domain: string; Domain to publish on, default to .local if empty.
98
host: string; Host to publish records for, default is localhost
99
max_renames: integer; maximum number of renames
100
rename_count: integer; counter so we only rename after collisions
101
a sensible number of times
102
name = string; Example: "Mandos"
103
type = string; Example: "_mandos._tcp".
104
See <http://www.dns-sd.org/ServiceTypes.html>
105
port = integer; what port to announce
106
TXT = list of strings; TXT record for the service
107
domain = string; Domain to publish on, default to .local if empty.
108
host = string; Host to publish records for, default to localhost
110
max_renames = integer; maximum number of renames
111
rename_count = integer; counter so we only rename after collisions
112
a sensible number of times
103
114
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
104
115
type = None, port = None, TXT = None, domain = "",
105
116
host = "", max_renames = 12):
117
"""An Avahi (Zeroconf) service. """
106
118
self.interface = interface
121
133
u" retries, exiting.", rename_count)
122
134
raise AvahiServiceError("Too many renames")
123
135
name = server.GetAlternativeServiceName(name)
124
logger.error(u"Changing name to %r ...", name)
136
logger.notice(u"Changing name to %r ...", name)
127
139
self.rename_count += 1
209
221
interval = property(lambda self: self._interval,
211
223
del _set_interval
212
def __init__(self, name = None, stop_hook=None, config={}):
213
"""Note: the 'checker' key in 'config' sets the
214
'checker_command' attribute and *not* the 'checker'
224
def __init__(self, name=None, stop_hook=None, fingerprint=None,
225
secret=None, secfile=None, fqdn=None, timeout=None,
226
interval=-1, checker=None):
227
"""Note: the 'checker' argument sets the 'checker_command'
228
attribute and not the 'checker' attribute.."""
217
230
logger.debug(u"Creating client %r", self.name)
218
# Uppercase and remove spaces from fingerprint for later
219
# comparison purposes with return value from the fingerprint()
221
self.fingerprint = config["fingerprint"].upper()\
231
# Uppercase and remove spaces from fingerprint
232
# for later comparison purposes with return value of
233
# the fingerprint() function
234
self.fingerprint = fingerprint.upper().replace(u" ", u"")
223
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
224
if "secret" in config:
225
self.secret = config["secret"].decode(u"base64")
226
elif "secfile" in config:
227
sf = open(config["secfile"])
237
self.secret = secret.decode(u"base64")
228
240
self.secret = sf.read()
231
243
raise TypeError(u"No secret or secfile for client %s"
233
self.fqdn = config.get("fqdn", "")
234
246
self.created = datetime.datetime.now()
235
247
self.last_checked_ok = None
236
self.timeout = string_to_delta(config["timeout"])
237
self.interval = string_to_delta(config["interval"])
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
238
250
self.stop_hook = stop_hook
239
251
self.checker = None
240
252
self.checker_initiator_tag = None
241
253
self.stop_initiator_tag = None
242
254
self.checker_callback_tag = None
243
self.check_command = config["checker"]
255
self.check_command = checker
245
257
"""Start this client's checker and timeout hooks"""
246
258
# Schedule a new checker to be started an 'interval' from now,
285
297
self.checker = None
286
298
if os.WIFEXITED(condition) \
287
299
and (os.WEXITSTATUS(condition) == 0):
288
logger.info(u"Checker for %(name)s succeeded",
300
logger.debug(u"Checker for %(name)s succeeded",
290
302
self.last_checked_ok = now
291
303
gobject.source_remove(self.stop_initiator_tag)
292
304
self.stop_initiator_tag = gobject.timeout_add\
296
308
logger.warning(u"Checker for %(name)s crashed?",
299
logger.info(u"Checker for %(name)s failed",
311
logger.debug(u"Checker for %(name)s failed",
301
313
def start_checker(self):
302
314
"""Start a new checker subprocess if one is not running.
303
315
If a checker already exists, leave it running and do
326
338
u' %s', self.check_command, error)
327
339
return True # Try again later
329
logger.info(u"Starting checker %r for %s",
341
logger.debug(u"Starting checker %r for %s",
331
343
self.checker = subprocess.Popen(command,
333
345
shell=True, cwd="/")
385
397
def fingerprint(openpgp):
386
398
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
399
# New empty GnuTLS certificate
400
crt = gnutls.library.types.gnutls_openpgp_crt_t()
401
gnutls.library.functions.gnutls_openpgp_crt_init\
387
403
# New GnuTLS "datum" with the OpenPGP public key
388
404
datum = gnutls.library.types.gnutls_datum_t\
389
405
(ctypes.cast(ctypes.c_char_p(openpgp),
390
406
ctypes.POINTER(ctypes.c_ubyte)),
391
407
ctypes.c_uint(len(openpgp)))
392
# New empty GnuTLS certificate
393
crt = gnutls.library.types.gnutls_openpgp_crt_t()
394
gnutls.library.functions.gnutls_openpgp_crt_init\
396
408
# Import the OpenPGP public key into the certificate
397
gnutls.library.functions.gnutls_openpgp_crt_import\
398
(crt, ctypes.byref(datum),
399
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
400
413
# New buffer for the fingerprint
401
414
buffer = ctypes.create_string_buffer(20)
402
415
buffer_length = ctypes.c_size_t()
418
431
Note: This will run in its own forked process."""
420
433
def handle(self):
421
logger.info(u"TCP connection from: %s",
434
logger.debug(u"TCP connection from: %s",
422
435
unicode(self.client_address))
423
436
session = gnutls.connection.ClientSession\
424
437
(self.request, gnutls.connection.X509Credentials())
426
line = self.request.makefile().readline()
427
logger.debug(u"Protocol version: %r", line)
429
if int(line.strip().split()[0]) > 1:
431
except (ValueError, IndexError, RuntimeError), error:
432
logger.error(u"Unknown protocol version: %s", error)
435
438
# Note: gnutls.connection.X509Credentials is really a generic
436
439
# GnuTLS certificate credentials object so long as no X.509
437
440
# keys are added to it. Therefore, we can use it here despite
451
454
session.handshake()
452
455
except gnutls.errors.GNUTLSError, error:
453
logger.warning(u"Handshake failed: %s", error)
456
logger.debug(u"Handshake failed: %s", error)
454
457
# Do not run session.bye() here: the session is not
455
458
# established. Just abandon the request.
458
461
fpr = fingerprint(peer_certificate(session))
459
462
except (TypeError, gnutls.errors.GNUTLSError), error:
460
logger.warning(u"Bad certificate: %s", error)
463
logger.debug(u"Bad certificate: %s", error)
463
466
logger.debug(u"Fingerprint: %s", fpr)
470
logger.warning(u"Client not found for fingerprint: %s",
473
logger.debug(u"Client not found for fingerprint: %s", fpr)
474
476
# Have to check if client.still_valid(), since it is possible
475
477
# that the client timed out while establishing the GnuTLS
477
479
if not client.still_valid():
478
logger.warning(u"Client %(name)s is invalid",
480
logger.debug(u"Client %(name)s is invalid", vars(client))
508
509
"""This overrides the normal server_bind() function
509
510
to bind to an interface if one was specified, and also NOT to
510
511
bind to an address or port if they were not specified."""
511
if self.settings["interface"]:
512
if self.settings["interface"] != avahi.IF_UNSPEC:
512
513
# 25 is from /usr/include/asm-i486/socket.h
513
514
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
517
518
self.settings["interface"])
518
519
except socket.error, error:
519
520
if error[0] == errno.EPERM:
520
logger.error(u"No permission to"
521
u" bind to interface %s",
522
self.settings["interface"])
521
logger.warning(u"No permission to"
522
u" bind to interface %s",
523
self.settings["interface"])
525
526
# Only bind(2) the socket if we really need to.
528
529
in6addr_any = "::"
529
530
self.server_address = (in6addr_any,
530
531
self.server_address[1])
531
elif not self.server_address[1]:
532
elif self.server_address[1] is None:
532
533
self.server_address = (self.server_address[0],
534
# if self.settings["interface"]:
535
# self.server_address = (self.server_address[0],
541
535
return super(type(self), self).server_bind()
578
572
def server_state_changed(state):
579
573
"""Derived from the Avahi example code"""
580
574
if state == avahi.SERVER_COLLISION:
581
logger.error(u"Server name collision")
575
logger.warning(u"Server name collision")
583
577
elif state == avahi.SERVER_RUNNING:
599
593
raise AvahiGroupError("State changed: %s", str(error))
601
def if_nametoindex(interface):
595
def if_nametoindex(interface, _func=[None]):
602
596
"""Call the C function if_nametoindex(), or equivalent"""
603
global if_nametoindex
597
if _func[0] is not None:
598
return _func[0](interface)
605
600
if "ctypes.util" not in sys.modules:
606
601
import ctypes.util
607
if_nametoindex = ctypes.cdll.LoadLibrary\
608
(ctypes.util.find_library("c")).if_nametoindex
604
libc = ctypes.cdll.LoadLibrary\
605
(ctypes.util.find_library("c"))
606
func[0] = libc.if_nametoindex
607
return _func[0](interface)
609
611
except (OSError, AttributeError):
610
612
if "struct" not in sys.modules:
612
614
if "fcntl" not in sys.modules:
614
def if_nametoindex(interface):
616
def the_hard_way(interface):
615
617
"Get an interface index the hard way, i.e. using fcntl()"
616
618
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
617
619
s = socket.socket()
621
623
interface_index = struct.unpack("I", ifreq[16:20])[0]
622
624
return interface_index
623
return if_nametoindex(interface)
626
def daemon(nochdir = False, noclose = False):
625
_func[0] = the_hard_way
626
return _func[0](interface)
629
def daemon(nochdir, noclose):
627
630
"""See daemon(3). Standard BSD Unix function.
628
631
This should really exist as os.daemon, but it doesn't (yet)."""
659
660
help="Port number to receive requests on")
660
661
parser.add_option("--check", action="store_true", default=False,
661
662
help="Run self-test")
662
parser.add_option("--debug", action="store_true",
663
parser.add_option("--debug", action="store_true", default=False,
663
664
help="Debug mode; run in foreground and log to"
665
666
parser.add_option("--priority", type="string", help="GnuTLS"
690
691
# Parse config file for server-global settings
691
692
server_config = ConfigParser.SafeConfigParser(server_defaults)
692
693
del server_defaults
693
server_config.read(os.path.join(options.configdir, "mandos.conf"))
694
server_config.read(os.path.join(options.configdir, "server.conf"))
694
695
server_section = "server"
695
696
# Convert the SafeConfigParser object to a dict
696
697
server_settings = dict(server_config.items(server_section))
750
751
def remove_from_clients(client):
751
752
clients.remove(client)
753
logger.critical(u"No clients left, exiting")
754
logger.debug(u"No clients left, exiting")
756
clients.update(Set(Client(name = section,
757
clients.update(Set(Client(name=section,
757
758
stop_hook = remove_from_clients,
759
= dict(client_config.items(section)))
759
**(dict(client_config\
760
761
for section in client_config.sections()))
765
pidfilename = "/var/run/mandos/mandos.pid"
768
pidfile = open(pidfilename, "w")
769
pidfile.write(str(pid) + "\n")
773
logger.error("Could not write %s file with PID %d",
774
pidfilename, os.getpid())
777
767
"Cleanup function; run on exit"
805
795
# Find out what port we got
806
796
service.port = tcp_server.socket.getsockname()[1]
807
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
808
u" scope_id %d" % tcp_server.socket.getsockname())
797
logger.debug(u"Now listening on port %d", service.port)
810
#service.interface = tcp_server.socket.getsockname()[3]
799
if not server_settings["interface"]:
800
service.interface = if_nametoindex\
801
(server_settings["interface"])
813
804
# From the Avahi example code