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
# following functions: "AvahiService.add", "AvahiService.remove",
10
# "server_state_changed", "entry_group_state_changed", and some lines
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".
13
13
# Everything else is
14
# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson
14
# Copyright © 2008 Teddy Hogeborn & Björn Påhlsson
16
16
# This program is free software: you can redistribute it and/or modify
17
17
# it under the terms of the GNU General Public License as published by
56
56
import logging.handlers
61
62
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.
76
68
logger = logging.Logger('mandos')
77
69
syslogger = logging.handlers.SysLogHandler\
78
(facility = logging.handlers.SysLogHandler.LOG_DAEMON)
70
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
79
72
syslogger.setFormatter(logging.Formatter\
80
('%(levelname)s: %(message)s'))
73
('Mandos: %(levelname)s: %(message)s'))
81
74
logger.addHandler(syslogger)
76
console = logging.StreamHandler()
77
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
79
logger.addHandler(console)
85
81
class AvahiError(Exception):
86
82
def __init__(self, value):
84
super(AvahiError, self).__init__()
89
86
return repr(self.value)
98
95
class AvahiService(object):
96
"""An Avahi (Zeroconf) service.
100
98
interface: integer; avahi.IF_UNSPEC or an interface index.
101
99
Used to optionally bind to the specified interface.
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
100
name: string; Example: 'Mandos'
101
type: string; Example: '_mandos._tcp'.
102
See <http://www.dns-sd.org/ServiceTypes.html>
103
port: integer; what port to announce
104
TXT: list of strings; TXT record for the service
105
domain: string; Domain to publish on, default to .local if empty.
106
host: string; Host to publish records for, default is localhost
107
max_renames: integer; maximum number of renames
108
rename_count: integer; counter so we only rename after collisions
109
a sensible number of times
114
111
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
115
type = None, port = None, TXT = None, domain = "",
116
host = "", max_renames = 12):
117
"""An Avahi (Zeroconf) service. """
112
servicetype = None, port = None, TXT = None, domain = "",
113
host = "", max_renames = 32768):
118
114
self.interface = interface
116
self.type = servicetype
126
122
self.domain = domain
128
124
self.rename_count = 0
125
self.max_renames = max_renames
129
126
def rename(self):
130
127
"""Derived from the Avahi example code"""
131
128
if self.rename_count >= self.max_renames:
132
logger.critical(u"No suitable service name found after %i"
133
u" retries, exiting.", rename_count)
129
logger.critical(u"No suitable Zeroconf service name found"
130
u" after %i retries, exiting.",
134
132
raise AvahiServiceError("Too many renames")
135
name = server.GetAlternativeServiceName(name)
136
logger.notice(u"Changing name to %r ...", name)
133
self.name = server.GetAlternativeServiceName(self.name)
134
logger.info(u"Changing Zeroconf service name to %r ...",
136
syslogger.setFormatter(logging.Formatter\
137
('Mandos (%s): %%(levelname)s:'
138
' %%(message)s' % self.name))
139
141
self.rename_count += 1
221
223
interval = property(lambda self: self._interval,
223
225
del _set_interval
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.."""
226
def __init__(self, name = None, stop_hook=None, config=None):
227
"""Note: the 'checker' key in 'config' sets the
228
'checker_command' attribute and *not* the 'checker'
230
233
logger.debug(u"Creating client %r", self.name)
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"")
234
# Uppercase and remove spaces from fingerprint for later
235
# comparison purposes with return value from the fingerprint()
237
self.fingerprint = config["fingerprint"].upper()\
235
239
logger.debug(u" Fingerprint: %s", self.fingerprint)
237
self.secret = secret.decode(u"base64")
240
self.secret = sf.read()
240
if "secret" in config:
241
self.secret = config["secret"].decode(u"base64")
242
elif "secfile" in config:
243
secfile = open(os.path.expanduser(os.path.expandvars
244
(config["secfile"])))
245
self.secret = secfile.read()
243
248
raise TypeError(u"No secret or secfile for client %s"
250
self.host = config.get("host", "")
246
251
self.created = datetime.datetime.now()
247
252
self.last_checked_ok = None
248
self.timeout = string_to_delta(timeout)
249
self.interval = string_to_delta(interval)
253
self.timeout = string_to_delta(config["timeout"])
254
self.interval = string_to_delta(config["interval"])
250
255
self.stop_hook = stop_hook
251
256
self.checker = None
252
257
self.checker_initiator_tag = None
253
258
self.stop_initiator_tag = None
254
259
self.checker_callback_tag = None
255
self.check_command = checker
260
self.check_command = config["checker"]
257
262
"""Start this client's checker and timeout hooks"""
258
263
# Schedule a new checker to be started an 'interval' from now,
338
343
u' %s', self.check_command, error)
339
344
return True # Try again later
341
logger.debug(u"Starting checker %r for %s",
346
logger.info(u"Starting checker %r for %s",
348
# We don't need to redirect stdout and stderr, since
349
# in normal mode, that is already done by daemon(),
350
# and in debug mode we don't want to. (Stdin is
351
# always replaced by /dev/null.)
343
352
self.checker = subprocess.Popen(command,
345
354
shell=True, cwd="/")
346
355
self.checker_callback_tag = gobject.child_watch_add\
347
356
(self.checker.pid,
348
357
self.checker_callback)
349
except subprocess.OSError, error:
358
except OSError, error:
350
359
logger.error(u"Failed to start subprocess: %s",
352
361
# Re-run this periodically if run by gobject.timeout_add
397
406
def fingerprint(openpgp):
398
407
"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\
403
408
# New GnuTLS "datum" with the OpenPGP public key
404
409
datum = gnutls.library.types.gnutls_datum_t\
405
410
(ctypes.cast(ctypes.c_char_p(openpgp),
406
411
ctypes.POINTER(ctypes.c_ubyte)),
407
412
ctypes.c_uint(len(openpgp)))
413
# New empty GnuTLS certificate
414
crt = gnutls.library.types.gnutls_openpgp_crt_t()
415
gnutls.library.functions.gnutls_openpgp_crt_init\
408
417
# Import the OpenPGP public key into the certificate
409
ret = gnutls.library.functions.gnutls_openpgp_crt_import\
412
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
418
gnutls.library.functions.gnutls_openpgp_crt_import\
419
(crt, ctypes.byref(datum),
420
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
421
# Verify the self signature in the key
422
crtverify = ctypes.c_uint()
423
gnutls.library.functions.gnutls_openpgp_crt_verify_self\
424
(crt, 0, ctypes.byref(crtverify))
425
if crtverify.value != 0:
426
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
427
raise gnutls.errors.CertificateSecurityError("Verify failed")
413
428
# New buffer for the fingerprint
414
buffer = ctypes.create_string_buffer(20)
415
buffer_length = ctypes.c_size_t()
429
buf = ctypes.create_string_buffer(20)
430
buf_len = ctypes.c_size_t()
416
431
# Get the fingerprint from the certificate into the buffer
417
432
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
418
(crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
433
(crt, ctypes.byref(buf), ctypes.byref(buf_len))
419
434
# Deinit the certificate
420
435
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
421
436
# Convert the buffer to a Python bytestring
422
fpr = ctypes.string_at(buffer, buffer_length.value)
437
fpr = ctypes.string_at(buf, buf_len.value)
423
438
# Convert the bytestring to hexadecimal notation
424
439
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
428
class tcp_handler(SocketServer.BaseRequestHandler, object):
443
class TCP_handler(SocketServer.BaseRequestHandler, object):
429
444
"""A TCP request handler class.
430
445
Instantiated by IPv6_TCPServer for each request to handle it.
431
446
Note: This will run in its own forked process."""
433
448
def handle(self):
434
logger.debug(u"TCP connection from: %s",
449
logger.info(u"TCP connection from: %s",
435
450
unicode(self.client_address))
436
451
session = gnutls.connection.ClientSession\
437
452
(self.request, gnutls.connection.X509Credentials())
454
line = self.request.makefile().readline()
455
logger.debug(u"Protocol version: %r", line)
457
if int(line.strip().split()[0]) > 1:
459
except (ValueError, IndexError, RuntimeError), error:
460
logger.error(u"Unknown protocol version: %s", error)
438
463
# Note: gnutls.connection.X509Credentials is really a generic
439
464
# GnuTLS certificate credentials object so long as no X.509
440
465
# keys are added to it. Therefore, we can use it here despite
448
473
if self.server.settings["priority"]:
449
474
priority = self.server.settings["priority"]
450
475
gnutls.library.functions.gnutls_priority_set_direct\
451
(session._c_object, priority, None);
476
(session._c_object, priority, None)
454
479
session.handshake()
455
480
except gnutls.errors.GNUTLSError, error:
456
logger.debug(u"Handshake failed: %s", error)
481
logger.warning(u"Handshake failed: %s", error)
457
482
# Do not run session.bye() here: the session is not
458
483
# established. Just abandon the request.
461
486
fpr = fingerprint(peer_certificate(session))
462
487
except (TypeError, gnutls.errors.GNUTLSError), error:
463
logger.debug(u"Bad certificate: %s", error)
488
logger.warning(u"Bad certificate: %s", error)
466
491
logger.debug(u"Fingerprint: %s", fpr)
504
532
if "clients" in kwargs:
505
533
self.clients = kwargs["clients"]
506
534
del kwargs["clients"]
507
return super(type(self), self).__init__(*args, **kwargs)
536
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
508
537
def server_bind(self):
509
538
"""This overrides the normal server_bind() function
510
539
to bind to an interface if one was specified, and also NOT to
511
540
bind to an address or port if they were not specified."""
512
if self.settings["interface"] != avahi.IF_UNSPEC:
541
if self.settings["interface"]:
513
542
# 25 is from /usr/include/asm-i486/socket.h
514
543
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
518
547
self.settings["interface"])
519
548
except socket.error, error:
520
549
if error[0] == errno.EPERM:
521
logger.warning(u"No permission to"
522
u" bind to interface %s",
523
self.settings["interface"])
550
logger.error(u"No permission to"
551
u" bind to interface %s",
552
self.settings["interface"])
526
555
# Only bind(2) the socket if we really need to.
529
558
in6addr_any = "::"
530
559
self.server_address = (in6addr_any,
531
560
self.server_address[1])
532
elif self.server_address[1] is None:
561
elif not self.server_address[1]:
533
562
self.server_address = (self.server_address[0],
535
return super(type(self), self).server_bind()
564
# if self.settings["interface"]:
565
# self.server_address = (self.server_address[0],
571
return super(IPv6_TCPServer, self).server_bind()
572
def server_activate(self):
574
return super(IPv6_TCPServer, self).server_activate()
538
579
def string_to_delta(interval):
548
589
datetime.timedelta(1)
549
590
>>> string_to_delta(u'1w')
550
591
datetime.timedelta(7)
592
>>> string_to_delta('5m 30s')
593
datetime.timedelta(0, 330)
553
suffix=unicode(interval[-1])
554
value=int(interval[:-1])
556
delta = datetime.timedelta(value)
558
delta = datetime.timedelta(0, value)
560
delta = datetime.timedelta(0, 0, 0, 0, value)
562
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
564
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
595
timevalue = datetime.timedelta(0)
596
for s in interval.split():
598
suffix = unicode(s[-1])
601
delta = datetime.timedelta(value)
603
delta = datetime.timedelta(0, value)
605
delta = datetime.timedelta(0, 0, 0, 0, value)
607
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
609
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
612
except (ValueError, IndexError):
567
except (ValueError, IndexError):
572
618
def server_state_changed(state):
573
619
"""Derived from the Avahi example code"""
574
620
if state == avahi.SERVER_COLLISION:
575
logger.warning(u"Server name collision")
621
logger.error(u"Zeroconf server name collision")
577
623
elif state == avahi.SERVER_RUNNING:
581
627
def entry_group_state_changed(state, error):
582
628
"""Derived from the Avahi example code"""
583
logger.debug(u"state change: %i", state)
629
logger.debug(u"Avahi state change: %i", state)
585
631
if state == avahi.ENTRY_GROUP_ESTABLISHED:
586
logger.debug(u"Service established.")
632
logger.debug(u"Zeroconf service established.")
587
633
elif state == avahi.ENTRY_GROUP_COLLISION:
588
logger.warning(u"Service name collision.")
634
logger.warning(u"Zeroconf service name collision.")
590
636
elif state == avahi.ENTRY_GROUP_FAILURE:
591
logger.critical(u"Error in group state changed %s",
637
logger.critical(u"Avahi: Error in group state changed %s",
593
639
raise AvahiGroupError("State changed: %s", str(error))
595
def if_nametoindex(interface, _func=[None]):
641
def if_nametoindex(interface):
596
642
"""Call the C function if_nametoindex(), or equivalent"""
597
if _func[0] is not None:
598
return _func[0](interface)
643
global if_nametoindex
600
if "ctypes.util" not in sys.modules:
604
libc = ctypes.cdll.LoadLibrary\
605
(ctypes.util.find_library("c"))
606
func[0] = libc.if_nametoindex
607
return _func[0](interface)
645
if_nametoindex = ctypes.cdll.LoadLibrary\
646
(ctypes.util.find_library("c")).if_nametoindex
611
647
except (OSError, AttributeError):
612
648
if "struct" not in sys.modules:
614
650
if "fcntl" not in sys.modules:
616
def the_hard_way(interface):
652
def if_nametoindex(interface):
617
653
"Get an interface index the hard way, i.e. using fcntl()"
618
654
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
619
655
s = socket.socket()
660
694
help="Port number to receive requests on")
661
695
parser.add_option("--check", action="store_true", default=False,
662
696
help="Run self-test")
663
parser.add_option("--debug", action="store_true", default=False,
697
parser.add_option("--debug", action="store_true",
664
698
help="Debug mode; run in foreground and log to"
666
700
parser.add_option("--priority", type="string", help="GnuTLS"
691
725
# Parse config file for server-global settings
692
726
server_config = ConfigParser.SafeConfigParser(server_defaults)
693
727
del server_defaults
694
server_config.read(os.path.join(options.configdir, "server.conf"))
695
server_section = "server"
728
server_config.read(os.path.join(options.configdir, "mandos.conf"))
696
729
# Convert the SafeConfigParser object to a dict
697
server_settings = dict(server_config.items(server_section))
730
server_settings = server_config.defaults()
698
731
# Use getboolean on the boolean config option
699
732
server_settings["debug"] = server_config.getboolean\
700
(server_section, "debug")
701
734
del server_config
702
if not server_settings["interface"]:
703
server_settings["interface"] = avahi.IF_UNSPEC
705
736
# Override the settings from the config file with command line
706
737
# options, if set.
713
744
# Now we have our good server settings in "server_settings"
746
debug = server_settings["debug"]
749
syslogger.setLevel(logging.WARNING)
750
console.setLevel(logging.WARNING)
752
if server_settings["servicename"] != "Mandos":
753
syslogger.setFormatter(logging.Formatter\
754
('Mandos (%s): %%(levelname)s:'
756
% server_settings["servicename"]))
715
758
# Parse config file with clients
716
759
client_defaults = { "timeout": "1h",
717
760
"interval": "5m",
718
"checker": "fping -q -- %%(fqdn)s",
761
"checker": "fping -q -- %(host)s",
720
764
client_config = ConfigParser.SafeConfigParser(client_defaults)
721
765
client_config.read(os.path.join(server_settings["configdir"],
769
tcp_server = IPv6_TCPServer((server_settings["address"],
770
server_settings["port"]),
772
settings=server_settings,
774
pidfilename = "/var/run/mandos.pid"
776
pidfile = open(pidfilename, "w")
777
except IOError, error:
778
logger.error("Could not open file %r", pidfilename)
783
uid = pwd.getpwnam("mandos").pw_uid
786
uid = pwd.getpwnam("nobody").pw_uid
790
gid = pwd.getpwnam("mandos").pw_gid
793
gid = pwd.getpwnam("nogroup").pw_gid
799
except OSError, error:
800
if error[0] != errno.EPERM:
725
804
service = AvahiService(name = server_settings["servicename"],
726
type = "_mandos._tcp", );
805
servicetype = "_mandos._tcp", )
806
if server_settings["interface"]:
807
service.interface = if_nametoindex\
808
(server_settings["interface"])
732
814
DBusGMainLoop(set_as_default=True )
733
815
main_loop = gobject.MainLoop()
734
816
bus = dbus.SystemBus()
735
server = dbus.Interface(
736
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
737
avahi.DBUS_INTERFACE_SERVER )
817
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
818
avahi.DBUS_PATH_SERVER),
819
avahi.DBUS_INTERFACE_SERVER)
738
820
# End of Avahi example code
740
debug = server_settings["debug"]
743
console = logging.StreamHandler()
744
# console.setLevel(logging.DEBUG)
745
console.setFormatter(logging.Formatter\
746
('%(levelname)s: %(message)s'))
747
logger.addHandler(console)
751
822
def remove_from_clients(client):
752
823
clients.remove(client)
754
logger.debug(u"No clients left, exiting")
825
logger.critical(u"No clients left, exiting")
757
clients.update(Set(Client(name=section,
828
clients.update(Set(Client(name = section,
758
829
stop_hook = remove_from_clients,
759
**(dict(client_config\
831
= dict(client_config.items(section)))
761
832
for section in client_config.sections()))
834
logger.critical(u"No clients defined")
838
# Redirect stdin so all checkers get /dev/null
839
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
840
os.dup2(null, sys.stdin.fileno())
845
logger.removeHandler(console)
846
# Close all input and output, do double fork, etc.
851
pidfile.write(str(pid) + "\n")
855
logger.error(u"Could not write to file %r with PID %d",
858
# "pidfile" was never created
767
863
"Cleanup function; run on exit"
787
883
for client in clients:
790
tcp_server = IPv6_TCPServer((server_settings["address"],
791
server_settings["port"]),
793
settings=server_settings,
887
tcp_server.server_activate()
795
889
# Find out what port we got
796
890
service.port = tcp_server.socket.getsockname()[1]
797
logger.debug(u"Now listening on port %d", service.port)
891
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
892
u" scope_id %d" % tcp_server.socket.getsockname())
799
if not server_settings["interface"]:
800
service.interface = if_nametoindex\
801
(server_settings["interface"])
894
#service.interface = tcp_server.socket.getsockname()[3]
804
897
# From the Avahi example code