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\
67
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
78
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
69
79
syslogger.setFormatter(logging.Formatter\
70
('Mandos: %(levelname)s: %(message)s'))
80
('%(levelname)s: %(message)s'))
71
81
logger.addHandler(syslogger)
74
85
class AvahiError(Exception):
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
host = "", max_renames = 32768):
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)
125
syslogger.setFormatter(logging.Formatter\
126
('Mandos (%s): %%(levelname)s:'
127
' %%(message)s' % name))
136
logger.notice(u"Changing name to %r ...", name)
130
139
self.rename_count += 1
166
175
fingerprint: string (40 or 32 hexadecimal digits); used to
167
176
uniquely identify the client
168
177
secret: bytestring; sent verbatim (over TLS) to client
169
host: string; available for use by the checker command
178
fqdn: string (FQDN); available for use by the checker command
170
179
created: datetime.datetime(); object creation, not client host
171
180
last_checked_ok: datetime.datetime() or None if not yet checked OK
172
181
timeout: datetime.timedelta(); How long from last_checked_ok
212
221
interval = property(lambda self: self._interval,
214
223
del _set_interval
215
def __init__(self, name = None, stop_hook=None, config={}):
216
"""Note: the 'checker' key in 'config' sets the
217
'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.."""
220
230
logger.debug(u"Creating client %r", self.name)
221
# Uppercase and remove spaces from fingerprint for later
222
# comparison purposes with return value from the fingerprint()
224
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"")
226
235
logger.debug(u" Fingerprint: %s", self.fingerprint)
227
if "secret" in config:
228
self.secret = config["secret"].decode(u"base64")
229
elif "secfile" in config:
230
sf = open(config["secfile"])
237
self.secret = secret.decode(u"base64")
231
240
self.secret = sf.read()
234
243
raise TypeError(u"No secret or secfile for client %s"
236
self.host = config.get("host", "")
237
246
self.created = datetime.datetime.now()
238
247
self.last_checked_ok = None
239
self.timeout = string_to_delta(config["timeout"])
240
self.interval = string_to_delta(config["interval"])
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
241
250
self.stop_hook = stop_hook
242
251
self.checker = None
243
252
self.checker_initiator_tag = None
244
253
self.stop_initiator_tag = None
245
254
self.checker_callback_tag = None
246
self.check_command = config["checker"]
255
self.check_command = checker
248
257
"""Start this client's checker and timeout hooks"""
249
258
# Schedule a new checker to be started an 'interval' from now,
262
271
The possibility that a client might be restarted is left open,
263
272
but not currently used."""
264
273
# If this client doesn't have a secret, it is already stopped.
265
if hasattr(self, "secret") and self.secret:
266
logger.info(u"Stopping client %s", self.name)
275
logger.debug(u"Stopping client %s", self.name)
267
276
self.secret = None
288
297
self.checker = None
289
298
if os.WIFEXITED(condition) \
290
299
and (os.WEXITSTATUS(condition) == 0):
291
logger.info(u"Checker for %(name)s succeeded",
300
logger.debug(u"Checker for %(name)s succeeded",
293
302
self.last_checked_ok = now
294
303
gobject.source_remove(self.stop_initiator_tag)
295
304
self.stop_initiator_tag = gobject.timeout_add\
299
308
logger.warning(u"Checker for %(name)s crashed?",
302
logger.info(u"Checker for %(name)s failed",
311
logger.debug(u"Checker for %(name)s failed",
304
313
def start_checker(self):
305
314
"""Start a new checker subprocess if one is not running.
306
315
If a checker already exists, leave it running and do
329
338
u' %s', self.check_command, error)
330
339
return True # Try again later
332
logger.info(u"Starting checker %r for %s",
341
logger.debug(u"Starting checker %r for %s",
334
343
self.checker = subprocess.Popen(command,
336
345
shell=True, cwd="/")
349
358
self.checker_callback_tag = None
350
359
if getattr(self, "checker", None) is None:
352
logger.debug(u"Stopping checker for %(name)s", vars(self))
361
logger.debug("Stopping checker for %(name)s", vars(self))
354
363
os.kill(self.checker.pid, signal.SIGTERM)
388
397
def fingerprint(openpgp):
389
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\
390
403
# New GnuTLS "datum" with the OpenPGP public key
391
404
datum = gnutls.library.types.gnutls_datum_t\
392
405
(ctypes.cast(ctypes.c_char_p(openpgp),
393
406
ctypes.POINTER(ctypes.c_ubyte)),
394
407
ctypes.c_uint(len(openpgp)))
395
# New empty GnuTLS certificate
396
crt = gnutls.library.types.gnutls_openpgp_crt_t()
397
gnutls.library.functions.gnutls_openpgp_crt_init\
399
408
# Import the OpenPGP public key into the certificate
400
gnutls.library.functions.gnutls_openpgp_crt_import\
401
(crt, ctypes.byref(datum),
402
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
403
413
# New buffer for the fingerprint
404
414
buffer = ctypes.create_string_buffer(20)
405
415
buffer_length = ctypes.c_size_t()
421
431
Note: This will run in its own forked process."""
423
433
def handle(self):
424
logger.info(u"TCP connection from: %s",
434
logger.debug(u"TCP connection from: %s",
425
435
unicode(self.client_address))
426
436
session = gnutls.connection.ClientSession\
427
437
(self.request, gnutls.connection.X509Credentials())
429
line = self.request.makefile().readline()
430
logger.debug(u"Protocol version: %r", line)
432
if int(line.strip().split()[0]) > 1:
434
except (ValueError, IndexError, RuntimeError), error:
435
logger.error(u"Unknown protocol version: %s", error)
438
438
# Note: gnutls.connection.X509Credentials is really a generic
439
439
# GnuTLS certificate credentials object so long as no X.509
440
440
# keys are added to it. Therefore, we can use it here despite
454
454
session.handshake()
455
455
except gnutls.errors.GNUTLSError, error:
456
logger.warning(u"Handshake failed: %s", error)
456
logger.debug(u"Handshake failed: %s", error)
457
457
# Do not run session.bye() here: the session is not
458
458
# established. Just abandon the request.
461
461
fpr = fingerprint(peer_certificate(session))
462
462
except (TypeError, gnutls.errors.GNUTLSError), error:
463
logger.warning(u"Bad certificate: %s", error)
463
logger.debug(u"Bad certificate: %s", error)
466
466
logger.debug(u"Fingerprint: %s", fpr)
473
logger.warning(u"Client not found for fingerprint: %s",
473
logger.debug(u"Client not found for fingerprint: %s", fpr)
477
476
# Have to check if client.still_valid(), since it is possible
478
477
# that the client timed out while establishing the GnuTLS
480
479
if not client.still_valid():
481
logger.warning(u"Client %(name)s is invalid",
480
logger.debug(u"Client %(name)s is invalid", vars(client))
511
509
"""This overrides the normal server_bind() function
512
510
to bind to an interface if one was specified, and also NOT to
513
511
bind to an address or port if they were not specified."""
514
if self.settings["interface"]:
512
if self.settings["interface"] != avahi.IF_UNSPEC:
515
513
# 25 is from /usr/include/asm-i486/socket.h
516
514
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
520
518
self.settings["interface"])
521
519
except socket.error, error:
522
520
if error[0] == errno.EPERM:
523
logger.error(u"No permission to"
524
u" bind to interface %s",
525
self.settings["interface"])
521
logger.warning(u"No permission to"
522
u" bind to interface %s",
523
self.settings["interface"])
528
526
# Only bind(2) the socket if we really need to.
531
529
in6addr_any = "::"
532
530
self.server_address = (in6addr_any,
533
531
self.server_address[1])
534
elif not self.server_address[1]:
532
elif self.server_address[1] is None:
535
533
self.server_address = (self.server_address[0],
537
# if self.settings["interface"]:
538
# self.server_address = (self.server_address[0],
544
535
return super(type(self), self).server_bind()
581
572
def server_state_changed(state):
582
573
"""Derived from the Avahi example code"""
583
574
if state == avahi.SERVER_COLLISION:
584
logger.error(u"Server name collision")
575
logger.warning(u"Server name collision")
586
577
elif state == avahi.SERVER_RUNNING:
602
593
raise AvahiGroupError("State changed: %s", str(error))
604
def if_nametoindex(interface):
595
def if_nametoindex(interface, _func=[None]):
605
596
"""Call the C function if_nametoindex(), or equivalent"""
606
global if_nametoindex
597
if _func[0] is not None:
598
return _func[0](interface)
608
600
if "ctypes.util" not in sys.modules:
609
601
import ctypes.util
610
if_nametoindex = ctypes.cdll.LoadLibrary\
611
(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)
612
611
except (OSError, AttributeError):
613
612
if "struct" not in sys.modules:
615
614
if "fcntl" not in sys.modules:
617
def if_nametoindex(interface):
616
def the_hard_way(interface):
618
617
"Get an interface index the hard way, i.e. using fcntl()"
619
618
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
620
619
s = socket.socket()
624
623
interface_index = struct.unpack("I", ifreq[16:20])[0]
625
624
return interface_index
626
return if_nametoindex(interface)
629
def daemon(nochdir = False, noclose = False):
625
_func[0] = the_hard_way
626
return _func[0](interface)
629
def daemon(nochdir, noclose):
630
630
"""See daemon(3). Standard BSD Unix function.
631
631
This should really exist as os.daemon, but it doesn't (yet)."""
662
660
help="Port number to receive requests on")
663
661
parser.add_option("--check", action="store_true", default=False,
664
662
help="Run self-test")
665
parser.add_option("--debug", action="store_true",
663
parser.add_option("--debug", action="store_true", default=False,
666
664
help="Debug mode; run in foreground and log to"
668
666
parser.add_option("--priority", type="string", help="GnuTLS"
693
691
# Parse config file for server-global settings
694
692
server_config = ConfigParser.SafeConfigParser(server_defaults)
695
693
del server_defaults
696
server_config.read(os.path.join(options.configdir, "mandos.conf"))
694
server_config.read(os.path.join(options.configdir, "server.conf"))
697
695
server_section = "server"
698
696
# Convert the SafeConfigParser object to a dict
699
697
server_settings = dict(server_config.items(server_section))
713
713
# Now we have our good server settings in "server_settings"
715
debug = server_settings["debug"]
718
syslogger.setLevel(logging.WARNING)
720
if server_settings["servicename"] != "Mandos":
721
syslogger.setFormatter(logging.Formatter\
722
('Mandos (%s): %%(levelname)s:'
724
% server_settings["servicename"]))
726
715
# Parse config file with clients
727
716
client_defaults = { "timeout": "1h",
728
717
"interval": "5m",
729
"checker": "fping -q -- %%(host)s",
718
"checker": "fping -q -- %%(fqdn)s",
731
720
client_config = ConfigParser.SafeConfigParser(client_defaults)
732
721
client_config.read(os.path.join(server_settings["configdir"],
762
751
def remove_from_clients(client):
763
752
clients.remove(client)
765
logger.critical(u"No clients left, exiting")
754
logger.debug(u"No clients left, exiting")
768
clients.update(Set(Client(name = section,
757
clients.update(Set(Client(name=section,
769
758
stop_hook = remove_from_clients,
771
= dict(client_config.items(section)))
759
**(dict(client_config\
772
761
for section in client_config.sections()))
774
logger.critical(u"No clients defined")
780
pidfilename = "/var/run/mandos/mandos.pid"
783
pidfile = open(pidfilename, "w")
784
pidfile.write(str(pid) + "\n")
788
logger.error(u"Could not write %s file with PID %d",
789
pidfilename, os.getpid())
792
767
"Cleanup function; run on exit"
820
795
# Find out what port we got
821
796
service.port = tcp_server.socket.getsockname()[1]
822
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
823
u" scope_id %d" % tcp_server.socket.getsockname())
797
logger.debug(u"Now listening on port %d", service.port)
825
#service.interface = tcp_server.socket.getsockname()[3]
799
if not server_settings["interface"]:
800
service.interface = if_nametoindex\
801
(server_settings["interface"])
828
804
# From the Avahi example code