52
19
from sets import Set
57
import logging.handlers
59
from contextlib import closing
65
25
from dbus.mainloop.glib import DBusGMainLoop
71
logger = logging.Logger('mandos')
72
syslogger = (logging.handlers.SysLogHandler
73
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
74
address = "/dev/log"))
75
syslogger.setFormatter(logging.Formatter
76
('Mandos [%(process)d]: %(levelname)s:'
78
logger.addHandler(syslogger)
80
console = logging.StreamHandler()
81
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
82
' %(levelname)s: %(message)s'))
83
logger.addHandler(console)
85
class AvahiError(Exception):
86
def __init__(self, value, *args, **kwargs):
88
super(AvahiError, self).__init__(value, *args, **kwargs)
89
def __unicode__(self):
90
return unicode(repr(self.value))
92
class AvahiServiceError(AvahiError):
95
class AvahiGroupError(AvahiError):
99
class AvahiService(object):
100
"""An Avahi (Zeroconf) service.
102
interface: integer; avahi.IF_UNSPEC or an interface index.
103
Used to optionally bind to the specified interface.
104
name: string; Example: 'Mandos'
105
type: string; Example: '_mandos._tcp'.
106
See <http://www.dns-sd.org/ServiceTypes.html>
107
port: integer; what port to announce
108
TXT: list of strings; TXT record for the service
109
domain: string; Domain to publish on, default to .local if empty.
110
host: string; Host to publish records for, default is localhost
111
max_renames: integer; maximum number of renames
112
rename_count: integer; counter so we only rename after collisions
113
a sensible number of times
115
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
116
servicetype = None, port = None, TXT = None,
117
domain = "", host = "", max_renames = 32768):
118
self.interface = interface
120
self.type = servicetype
122
self.TXT = TXT if TXT is not None else []
125
self.rename_count = 0
126
self.max_renames = max_renames
128
"""Derived from the Avahi example code"""
129
if self.rename_count >= self.max_renames:
130
logger.critical(u"No suitable Zeroconf service name found"
131
u" after %i retries, exiting.",
133
raise AvahiServiceError(u"Too many renames")
134
self.name = server.GetAlternativeServiceName(self.name)
135
logger.info(u"Changing Zeroconf service name to %r ...",
137
syslogger.setFormatter(logging.Formatter
138
('Mandos (%s): %%(levelname)s:'
139
' %%(message)s' % self.name))
142
self.rename_count += 1
144
"""Derived from the Avahi example code"""
145
if group is not None:
148
"""Derived from the Avahi example code"""
151
group = dbus.Interface(bus.get_object
153
server.EntryGroupNew()),
154
avahi.DBUS_INTERFACE_ENTRY_GROUP)
155
group.connect_to_signal('StateChanged',
156
entry_group_state_changed)
157
logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...",
158
service.name, service.type)
160
self.interface, # interface
161
avahi.PROTO_INET6, # protocol
162
dbus.UInt32(0), # flags
163
self.name, self.type,
164
self.domain, self.host,
165
dbus.UInt16(self.port),
166
avahi.string_array_to_txt_array(self.TXT))
169
# From the Avahi example code:
170
group = None # our entry group
27
# This variable is used to optionally bind to a specified
29
serviceInterface = avahi.IF_UNSPEC
30
# It is a global variable to fit in with the rest of the
31
# variables from the Avahi server example code:
32
serviceName = "Mandos"
33
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
34
servicePort = None # Not known at startup
35
serviceTXT = [] # TXT record for the service
36
domain = "" # Domain to publish on, default to .local
37
host = "" # Host to publish records for, default to localhost
38
group = None #our entry group
39
rename_count = 12 # Counter so we only rename after collisions a
40
# sensible number of times
171
41
# End of Avahi example code
174
def _datetime_to_dbus(dt, variant_level=0):
175
"""Convert a UTC datetime.datetime() to a D-Bus type."""
176
return dbus.String(dt.isoformat(), variant_level=variant_level)
179
class Client(dbus.service.Object):
180
45
"""A representation of a client host served by this server.
182
name: string; from the config file, used in log messages and
184
fingerprint: string (40 or 32 hexadecimal digits); used to
185
uniquely identify the client
186
secret: bytestring; sent verbatim (over TLS) to client
187
host: string; available for use by the checker command
188
created: datetime.datetime(); (UTC) object creation
189
last_enabled: datetime.datetime(); (UTC)
191
last_checked_ok: datetime.datetime(); (UTC) or None
192
timeout: datetime.timedelta(); How long from last_checked_ok
193
until this client is invalid
194
interval: datetime.timedelta(); How often to start a new checker
195
disable_hook: If set, called by disable() as disable_hook(self)
196
checker: subprocess.Popen(); a running checker process used
197
to see if the client lives.
198
'None' if no process is running.
48
fqdn: string, FQDN (used by the checker)
49
created: datetime.datetime()
50
last_seen: datetime.datetime() or None if not yet seen
51
timeout: datetime.timedelta(); How long from last_seen until
52
this client is invalid
53
interval: datetime.timedelta(); How often to start a new checker
54
timeout_milliseconds: Used by gobject.timeout_add()
55
interval_milliseconds: - '' -
56
stop_hook: If set, called by stop() as stop_hook(self)
57
checker: subprocess.Popen(); a running checker process used
58
to see if the client lives.
59
Is None if no process is running.
199
60
checker_initiator_tag: a gobject event source tag, or None
200
disable_initiator_tag: - '' -
61
stop_initiator_tag: - '' -
201
62
checker_callback_tag: - '' -
202
checker_command: string; External command which is run to check if
203
client lives. %() expansions are done at
204
runtime with vars(self) as dict, so that for
205
instance %(name)s can be used in the command.
206
use_dbus: bool(); Whether to provide D-Bus interface and signals
207
dbus_object_path: dbus.ObjectPath ; only set if self.use_dbus
209
def timeout_milliseconds(self):
210
"Return the 'timeout' attribute in milliseconds"
211
return ((self.timeout.days * 24 * 60 * 60 * 1000)
212
+ (self.timeout.seconds * 1000)
213
+ (self.timeout.microseconds // 1000))
215
def interval_milliseconds(self):
216
"Return the 'interval' attribute in milliseconds"
217
return ((self.interval.days * 24 * 60 * 60 * 1000)
218
+ (self.interval.seconds * 1000)
219
+ (self.interval.microseconds // 1000))
221
def __init__(self, name = None, disable_hook=None, config=None,
223
"""Note: the 'checker' key in 'config' sets the
224
'checker_command' attribute and *not* the 'checker'
64
def __init__(self, name=None, options=None, stop_hook=None,
65
dn=None, password=None, passfile=None, fqdn=None,
66
timeout=None, interval=-1):
229
logger.debug(u"Creating client %r", self.name)
230
self.use_dbus = False # During __init__
231
# Uppercase and remove spaces from fingerprint for later
232
# comparison purposes with return value from the fingerprint()
234
self.fingerprint = (config["fingerprint"].upper()
236
logger.debug(u" Fingerprint: %s", self.fingerprint)
237
if "secret" in config:
238
self.secret = config["secret"].decode(u"base64")
239
elif "secfile" in config:
240
with closing(open(os.path.expanduser
242
(config["secfile"])))) as secfile:
243
self.secret = secfile.read()
245
raise TypeError(u"No secret or secfile for client %s"
247
self.host = config.get("host", "")
248
self.created = datetime.datetime.utcnow()
250
self.last_enabled = None
251
self.last_checked_ok = None
252
self.timeout = string_to_delta(config["timeout"])
253
self.interval = string_to_delta(config["interval"])
254
self.disable_hook = disable_hook
70
self.password = password
72
self.password = open(passfile).readall()
74
raise RuntimeError(u"No Password or Passfile for client %s"
76
self.fqdn = fqdn # string
77
self.created = datetime.datetime.now()
80
timeout = options.timeout
81
self.timeout = timeout
82
self.timeout_milliseconds = ((self.timeout.days
83
* 24 * 60 * 60 * 1000)
84
+ (self.timeout.seconds * 1000)
85
+ (self.timeout.microseconds
88
interval = options.interval
90
interval = string_to_delta(interval)
91
self.interval = interval
92
self.interval_milliseconds = ((self.interval.days
93
* 24 * 60 * 60 * 1000)
94
+ (self.interval.seconds * 1000)
95
+ (self.interval.microseconds
97
self.stop_hook = stop_hook
255
98
self.checker = None
256
99
self.checker_initiator_tag = None
257
self.disable_initiator_tag = None
100
self.stop_initiator_tag = None
258
101
self.checker_callback_tag = None
259
self.checker_command = config["checker"]
260
self.last_connect = None
261
# Only now, when this client is initialized, can it show up on
263
self.use_dbus = use_dbus
265
self.dbus_object_path = (dbus.ObjectPath
267
+ self.name.replace(".", "_")))
268
dbus.service.Object.__init__(self, bus,
269
self.dbus_object_path)
272
"""Start this client's checker and timeout hooks"""
273
self.last_enabled = datetime.datetime.utcnow()
103
"""Start this clients checker and timeout hooks"""
274
104
# Schedule a new checker to be started an 'interval' from now,
275
105
# and every interval from then on.
276
self.checker_initiator_tag = (gobject.timeout_add
277
(self.interval_milliseconds(),
106
self.checker_initiator_tag = gobject.\
107
timeout_add(self.interval_milliseconds,
279
109
# Also start a new checker *right now*.
280
110
self.start_checker()
281
# Schedule a disable() when 'timeout' has passed
282
self.disable_initiator_tag = (gobject.timeout_add
283
(self.timeout_milliseconds(),
288
self.PropertyChanged(dbus.String(u"enabled"),
289
dbus.Boolean(True, variant_level=1))
290
self.PropertyChanged(dbus.String(u"last_enabled"),
291
(_datetime_to_dbus(self.last_enabled,
295
"""Disable this client."""
296
if not getattr(self, "enabled", False):
298
logger.info(u"Disabling client %s", self.name)
299
if getattr(self, "disable_initiator_tag", False):
300
gobject.source_remove(self.disable_initiator_tag)
301
self.disable_initiator_tag = None
302
if getattr(self, "checker_initiator_tag", False):
111
# Schedule a stop() when 'timeout' has passed
112
self.stop_initiator_tag = gobject.\
113
timeout_add(self.timeout_milliseconds,
117
The possibility that this client might be restarted is left
118
open, but not currently used."""
119
# print "Stopping client", self.name
121
if self.stop_initiator_tag:
122
gobject.source_remove(self.stop_initiator_tag)
123
self.stop_initiator_tag = None
124
if self.checker_initiator_tag:
303
125
gobject.source_remove(self.checker_initiator_tag)
304
126
self.checker_initiator_tag = None
305
127
self.stop_checker()
306
if self.disable_hook:
307
self.disable_hook(self)
311
self.PropertyChanged(dbus.String(u"enabled"),
312
dbus.Boolean(False, variant_level=1))
313
130
# Do not run this again if called by a gobject.timeout_add
316
132
def __del__(self):
317
self.disable_hook = None
320
def checker_callback(self, pid, condition, command):
133
# Some code duplication here and in stop()
134
if hasattr(self, "stop_initiator_tag") \
135
and self.stop_initiator_tag:
136
gobject.source_remove(self.stop_initiator_tag)
137
self.stop_initiator_tag = None
138
if hasattr(self, "checker_initiator_tag") \
139
and self.checker_initiator_tag:
140
gobject.source_remove(self.checker_initiator_tag)
141
self.checker_initiator_tag = None
143
def checker_callback(self, pid, condition):
321
144
"""The checker has completed, so take appropriate actions."""
145
now = datetime.datetime.now()
146
if os.WIFEXITED(condition) \
147
and (os.WEXITSTATUS(condition) == 0):
148
#print "Checker for %(name)s succeeded" % vars(self)
150
gobject.source_remove(self.stop_initiator_tag)
151
self.stop_initiator_tag = gobject.\
152
timeout_add(self.timeout_milliseconds,
155
# if not os.WIFEXITED(condition):
156
# print "Checker for %(name)s crashed?" % vars(self)
158
# print "Checker for %(name)s failed" % vars(self)
322
160
self.checker_callback_tag = None
326
self.PropertyChanged(dbus.String(u"checker_running"),
327
dbus.Boolean(False, variant_level=1))
328
if os.WIFEXITED(condition):
329
exitstatus = os.WEXITSTATUS(condition)
331
logger.info(u"Checker for %(name)s succeeded",
335
logger.info(u"Checker for %(name)s failed",
339
self.CheckerCompleted(dbus.Int16(exitstatus),
340
dbus.Int64(condition),
341
dbus.String(command))
343
logger.warning(u"Checker for %(name)s crashed?",
347
self.CheckerCompleted(dbus.Int16(-1),
348
dbus.Int64(condition),
349
dbus.String(command))
351
def checked_ok(self):
352
"""Bump up the timeout for this client.
353
This should only be called when the client has been seen,
356
self.last_checked_ok = datetime.datetime.utcnow()
357
gobject.source_remove(self.disable_initiator_tag)
358
self.disable_initiator_tag = (gobject.timeout_add
359
(self.timeout_milliseconds(),
363
self.PropertyChanged(
364
dbus.String(u"last_checked_ok"),
365
(_datetime_to_dbus(self.last_checked_ok,
368
161
def start_checker(self):
369
162
"""Start a new checker subprocess if one is not running.
370
163
If a checker already exists, leave it running and do
372
# The reason for not killing a running checker is that if we
373
# did that, then if a checker (for some reason) started
374
# running slowly and taking more than 'interval' time, the
375
# client would inevitably timeout, since no checker would get
376
# a chance to run to completion. If we instead leave running
377
# checkers alone, the checker would have to take more time
378
# than 'timeout' for the client to be declared invalid, which
379
# is as it should be.
380
165
if self.checker is None:
382
# In case checker_command has exactly one % operator
383
command = self.checker_command % self.host
385
# Escape attributes for the shell
386
escaped_attrs = dict((key, re.escape(str(val)))
388
vars(self).iteritems())
390
command = self.checker_command % escaped_attrs
391
except TypeError, error:
392
logger.error(u'Could not format string "%s":'
393
u' %s', self.checker_command, error)
394
return True # Try again later
396
logger.info(u"Starting checker %r for %s",
398
# We don't need to redirect stdout and stderr, since
399
# in normal mode, that is already done by daemon(),
400
# and in debug mode we don't want to. (Stdin is
401
# always replaced by /dev/null.)
402
self.checker = subprocess.Popen(command,
407
self.CheckerStarted(command)
408
self.PropertyChanged(
409
dbus.String("checker_running"),
410
dbus.Boolean(True, variant_level=1))
411
self.checker_callback_tag = (gobject.child_watch_add
413
self.checker_callback,
415
except OSError, error:
416
logger.error(u"Failed to start subprocess: %s",
166
#print "Starting checker for", self.name
168
self.checker = subprocess.\
169
Popen("sleep 1; fping -q -- %s"
170
% re.escape(self.fqdn),
171
stdout=subprocess.PIPE,
172
close_fds=True, shell=True,
174
self.checker_callback_tag = gobject.\
175
child_watch_add(self.checker.pid,
178
except subprocess.OSError, error:
179
sys.stderr.write(u"Failed to start subprocess: %s\n"
418
181
# Re-run this periodically if run by gobject.timeout_add
421
183
def stop_checker(self):
422
184
"""Force the checker process, if any, to stop."""
423
if self.checker_callback_tag:
424
gobject.source_remove(self.checker_callback_tag)
425
self.checker_callback_tag = None
426
if getattr(self, "checker", None) is None:
185
if not hasattr(self, "checker") or self.checker is None:
428
logger.debug(u"Stopping checker for %(name)s", vars(self))
430
os.kill(self.checker.pid, signal.SIGTERM)
432
#if self.checker.poll() is None:
433
# os.kill(self.checker.pid, signal.SIGKILL)
434
except OSError, error:
435
if error.errno != errno.ESRCH: # No such process
187
gobject.source_remove(self.checker_callback_tag)
188
self.checker_callback_tag = None
189
os.kill(self.checker.pid, signal.SIGTERM)
190
if self.checker.poll() is None:
191
os.kill(self.checker.pid, signal.SIGKILL)
437
192
self.checker = None
439
self.PropertyChanged(dbus.String(u"checker_running"),
440
dbus.Boolean(False, variant_level=1))
442
def still_valid(self):
193
def still_valid(self, now=None):
443
194
"""Has the timeout not yet passed for this client?"""
444
if not getattr(self, "enabled", False):
446
now = datetime.datetime.utcnow()
447
if self.last_checked_ok is None:
196
now = datetime.datetime.now()
197
if self.last_seen is None:
448
198
return now < (self.created + self.timeout)
450
return now < (self.last_checked_ok + self.timeout)
452
## D-Bus methods & signals
453
_interface = u"se.bsnet.fukt.Mandos.Client"
456
CheckedOK = dbus.service.method(_interface)(checked_ok)
457
CheckedOK.__name__ = "CheckedOK"
459
# CheckerCompleted - signal
460
@dbus.service.signal(_interface, signature="nxs")
461
def CheckerCompleted(self, exitcode, waitstatus, command):
465
# CheckerStarted - signal
466
@dbus.service.signal(_interface, signature="s")
467
def CheckerStarted(self, command):
471
# GetAllProperties - method
472
@dbus.service.method(_interface, out_signature="a{sv}")
473
def GetAllProperties(self):
475
return dbus.Dictionary({
477
dbus.String(self.name, variant_level=1),
478
dbus.String("fingerprint"):
479
dbus.String(self.fingerprint, variant_level=1),
481
dbus.String(self.host, variant_level=1),
482
dbus.String("created"):
483
_datetime_to_dbus(self.created, variant_level=1),
484
dbus.String("last_enabled"):
485
(_datetime_to_dbus(self.last_enabled,
487
if self.last_enabled is not None
488
else dbus.Boolean(False, variant_level=1)),
489
dbus.String("enabled"):
490
dbus.Boolean(self.enabled, variant_level=1),
491
dbus.String("last_checked_ok"):
492
(_datetime_to_dbus(self.last_checked_ok,
494
if self.last_checked_ok is not None
495
else dbus.Boolean (False, variant_level=1)),
496
dbus.String("timeout"):
497
dbus.UInt64(self.timeout_milliseconds(),
499
dbus.String("interval"):
500
dbus.UInt64(self.interval_milliseconds(),
502
dbus.String("checker"):
503
dbus.String(self.checker_command,
505
dbus.String("checker_running"):
506
dbus.Boolean(self.checker is not None,
508
dbus.String("object_path"):
509
dbus.ObjectPath(self.dbus_object_path,
513
# IsStillValid - method
514
IsStillValid = (dbus.service.method(_interface, out_signature="b")
516
IsStillValid.__name__ = "IsStillValid"
518
# PropertyChanged - signal
519
@dbus.service.signal(_interface, signature="sv")
520
def PropertyChanged(self, property, value):
524
# SetChecker - method
525
@dbus.service.method(_interface, in_signature="s")
526
def SetChecker(self, checker):
527
"D-Bus setter method"
528
self.checker_command = checker
530
self.PropertyChanged(dbus.String(u"checker"),
531
dbus.String(self.checker_command,
535
@dbus.service.method(_interface, in_signature="s")
536
def SetHost(self, host):
537
"D-Bus setter method"
540
self.PropertyChanged(dbus.String(u"host"),
541
dbus.String(self.host, variant_level=1))
543
# SetInterval - method
544
@dbus.service.method(_interface, in_signature="t")
545
def SetInterval(self, milliseconds):
546
self.interval = datetime.timedelta(0, 0, 0, milliseconds)
548
self.PropertyChanged(dbus.String(u"interval"),
549
(dbus.UInt64(self.interval_milliseconds(),
553
@dbus.service.method(_interface, in_signature="ay",
555
def SetSecret(self, secret):
556
"D-Bus setter method"
557
self.secret = str(secret)
559
# SetTimeout - method
560
@dbus.service.method(_interface, in_signature="t")
561
def SetTimeout(self, milliseconds):
562
self.timeout = datetime.timedelta(0, 0, 0, milliseconds)
564
self.PropertyChanged(dbus.String(u"timeout"),
565
(dbus.UInt64(self.timeout_milliseconds(),
569
Enable = dbus.service.method(_interface)(enable)
570
Enable.__name__ = "Enable"
572
# StartChecker - method
573
@dbus.service.method(_interface)
574
def StartChecker(self):
579
@dbus.service.method(_interface)
584
# StopChecker - method
585
StopChecker = dbus.service.method(_interface)(stop_checker)
586
StopChecker.__name__ = "StopChecker"
591
def peer_certificate(session):
592
"Return the peer's OpenPGP certificate as a bytestring"
593
# If not an OpenPGP certificate...
594
if (gnutls.library.functions
595
.gnutls_certificate_type_get(session._c_object)
596
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
597
# ...do the normal thing
598
return session.peer_certificate
599
list_size = ctypes.c_uint(1)
600
cert_list = (gnutls.library.functions
601
.gnutls_certificate_get_peers
602
(session._c_object, ctypes.byref(list_size)))
603
if not bool(cert_list) and list_size.value != 0:
604
raise gnutls.errors.GNUTLSError("error getting peer"
606
if list_size.value == 0:
609
return ctypes.string_at(cert.data, cert.size)
612
def fingerprint(openpgp):
613
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
614
# New GnuTLS "datum" with the OpenPGP public key
615
datum = (gnutls.library.types
616
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
619
ctypes.c_uint(len(openpgp))))
620
# New empty GnuTLS certificate
621
crt = gnutls.library.types.gnutls_openpgp_crt_t()
622
(gnutls.library.functions
623
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
624
# Import the OpenPGP public key into the certificate
625
(gnutls.library.functions
626
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
627
gnutls.library.constants
628
.GNUTLS_OPENPGP_FMT_RAW))
629
# Verify the self signature in the key
630
crtverify = ctypes.c_uint()
631
(gnutls.library.functions
632
.gnutls_openpgp_crt_verify_self(crt, 0, ctypes.byref(crtverify)))
633
if crtverify.value != 0:
634
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
635
raise gnutls.errors.CertificateSecurityError("Verify failed")
636
# New buffer for the fingerprint
637
buf = ctypes.create_string_buffer(20)
638
buf_len = ctypes.c_size_t()
639
# Get the fingerprint from the certificate into the buffer
640
(gnutls.library.functions
641
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
642
ctypes.byref(buf_len)))
643
# Deinit the certificate
644
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
645
# Convert the buffer to a Python bytestring
646
fpr = ctypes.string_at(buf, buf_len.value)
647
# Convert the bytestring to hexadecimal notation
648
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
652
class TCP_handler(SocketServer.BaseRequestHandler, object):
200
return now < (self.last_seen + self.timeout)
203
class tcp_handler(SocketServer.BaseRequestHandler, object):
653
204
"""A TCP request handler class.
654
205
Instantiated by IPv6_TCPServer for each request to handle it.
655
206
Note: This will run in its own forked process."""
657
207
def handle(self):
658
logger.info(u"TCP connection from: %s",
659
unicode(self.client_address))
660
session = (gnutls.connection
661
.ClientSession(self.request,
665
line = self.request.makefile().readline()
666
logger.debug(u"Protocol version: %r", line)
668
if int(line.strip().split()[0]) > 1:
670
except (ValueError, IndexError, RuntimeError), error:
671
logger.error(u"Unknown protocol version: %s", error)
674
# Note: gnutls.connection.X509Credentials is really a generic
675
# GnuTLS certificate credentials object so long as no X.509
676
# keys are added to it. Therefore, we can use it here despite
677
# using OpenPGP certificates.
679
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
680
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
682
# Use a fallback default, since this MUST be set.
683
priority = self.server.settings.get("priority", "NORMAL")
684
(gnutls.library.functions
685
.gnutls_priority_set_direct(session._c_object,
208
#print u"TCP request came"
209
#print u"Request:", self.request
210
#print u"Client Address:", self.client_address
211
#print u"Server:", self.server
212
session = gnutls.connection.ServerSession(self.request,
689
216
session.handshake()
690
217
except gnutls.errors.GNUTLSError, error:
691
logger.warning(u"Handshake failed: %s", error)
218
#sys.stderr.write(u"Handshake failed: %s\n" % error)
692
219
# Do not run session.bye() here: the session is not
693
220
# established. Just abandon the request.
695
logger.debug(u"Handshake succeeded")
222
#if session.peer_certificate:
223
# print "DN:", session.peer_certificate.subject
697
fpr = fingerprint(peer_certificate(session))
698
except (TypeError, gnutls.errors.GNUTLSError), error:
699
logger.warning(u"Bad certificate: %s", error)
225
session.verify_peer()
226
except gnutls.errors.CertificateError, error:
227
#sys.stderr.write(u"Verify failed: %s\n" % error)
702
logger.debug(u"Fingerprint: %s", fpr)
704
for c in self.server.clients:
705
if c.fingerprint == fpr:
232
if c.dn == session.peer_certificate.subject:
709
logger.warning(u"Client not found for fingerprint: %s",
713
235
# Have to check if client.still_valid(), since it is possible
714
236
# that the client timed out while establishing the GnuTLS
716
if not client.still_valid():
717
logger.warning(u"Client %(name)s is invalid",
721
## This won't work here, since we're in a fork.
722
# client.checked_ok()
724
while sent_size < len(client.secret):
725
sent = session.send(client.secret[sent_size:])
726
logger.debug(u"Sent: %d, remaining: %d",
727
sent, len(client.secret)
728
- (sent_size + sent))
238
if client and client.still_valid():
239
session.send(client.password)
242
# sys.stderr.write(u"Client %(name)s is invalid\n"
245
# sys.stderr.write(u"Client not found for DN: %s\n"
246
# % session.peer_certificate.subject)
247
#session.send("gazonk")
733
class IPv6_TCPServer(SocketServer.ForkingMixIn,
734
SocketServer.TCPServer, object):
252
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
735
253
"""IPv6 TCP server. Accepts 'None' as address and/or port.
737
settings: Server settings
255
options: Command line options
738
256
clients: Set() of Client objects
739
enabled: Boolean; whether this server is activated yet
257
credentials: GnuTLS X.509 credentials
741
259
address_family = socket.AF_INET6
742
260
def __init__(self, *args, **kwargs):
743
if "settings" in kwargs:
744
self.settings = kwargs["settings"]
745
del kwargs["settings"]
261
if "options" in kwargs:
262
self.options = kwargs["options"]
263
del kwargs["options"]
746
264
if "clients" in kwargs:
747
265
self.clients = kwargs["clients"]
748
266
del kwargs["clients"]
750
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
267
if "credentials" in kwargs:
268
self.credentials = kwargs["credentials"]
269
del kwargs["credentials"]
270
return super(type(self), self).__init__(*args, **kwargs)
751
271
def server_bind(self):
752
272
"""This overrides the normal server_bind() function
753
273
to bind to an interface if one was specified, and also NOT to
754
274
bind to an address or port if they were not specified."""
755
if self.settings["interface"]:
756
# 25 is from /usr/include/asm-i486/socket.h
757
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
275
if self.options.interface:
276
if not hasattr(socket, "SO_BINDTODEVICE"):
277
# From /usr/include/asm-i486/socket.h
278
socket.SO_BINDTODEVICE = 25
759
280
self.socket.setsockopt(socket.SOL_SOCKET,
761
self.settings["interface"])
281
socket.SO_BINDTODEVICE,
282
self.options.interface)
762
283
except socket.error, error:
763
284
if error[0] == errno.EPERM:
764
logger.error(u"No permission to"
765
u" bind to interface %s",
766
self.settings["interface"])
285
sys.stderr.write(u"Warning: No permission to bind to interface %s\n"
286
% self.options.interface)
769
289
# Only bind(2) the socket if we really need to.
770
290
if self.server_address[0] or self.server_address[1]:
771
291
if not self.server_address[0]:
772
292
in6addr_any = "::"
773
293
self.server_address = (in6addr_any,
774
294
self.server_address[1])
775
elif not self.server_address[1]:
295
elif self.server_address[1] is None:
776
296
self.server_address = (self.server_address[0],
778
# if self.settings["interface"]:
779
# self.server_address = (self.server_address[0],
785
return super(IPv6_TCPServer, self).server_bind()
786
def server_activate(self):
788
return super(IPv6_TCPServer, self).server_activate()
298
return super(type(self), self).server_bind()
793
301
def string_to_delta(interval):
794
302
"""Parse a string and return a datetime.timedelta
796
304
>>> string_to_delta('7d')
797
305
datetime.timedelta(7)
798
306
>>> string_to_delta('60s')
803
311
datetime.timedelta(1)
804
312
>>> string_to_delta(u'1w')
805
313
datetime.timedelta(7)
806
>>> string_to_delta('5m 30s')
807
datetime.timedelta(0, 330)
809
timevalue = datetime.timedelta(0)
810
for s in interval.split():
812
suffix = unicode(s[-1])
815
delta = datetime.timedelta(value)
817
delta = datetime.timedelta(0, value)
819
delta = datetime.timedelta(0, 0, 0, 0, value)
821
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
823
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
826
except (ValueError, IndexError):
316
suffix=unicode(interval[-1])
317
value=int(interval[:-1])
319
delta = datetime.timedelta(value)
321
delta = datetime.timedelta(0, value)
323
delta = datetime.timedelta(0, 0, 0, 0, value)
325
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
327
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
330
except (ValueError, IndexError):
336
"""From the Avahi server example code"""
337
global group, serviceName, serviceType, servicePort, serviceTXT, \
340
group = dbus.Interface(
341
bus.get_object( avahi.DBUS_NAME,
342
server.EntryGroupNew()),
343
avahi.DBUS_INTERFACE_ENTRY_GROUP)
344
group.connect_to_signal('StateChanged',
345
entry_group_state_changed)
347
# print "Adding service '%s' of type '%s' ..." % (serviceName,
351
serviceInterface, # interface
352
avahi.PROTO_INET6, # protocol
353
dbus.UInt32(0), # flags
354
serviceName, serviceType,
356
dbus.UInt16(servicePort),
357
avahi.string_array_to_txt_array(serviceTXT))
361
def remove_service():
362
"""From the Avahi server example code"""
365
if not group is None:
832
369
def server_state_changed(state):
833
"""Derived from the Avahi example code"""
370
"""From the Avahi server example code"""
834
371
if state == avahi.SERVER_COLLISION:
835
logger.error(u"Zeroconf server name collision")
372
print "WARNING: Server name collision"
837
374
elif state == avahi.SERVER_RUNNING:
841
378
def entry_group_state_changed(state, error):
842
"""Derived from the Avahi example code"""
843
logger.debug(u"Avahi state change: %i", state)
379
"""From the Avahi server example code"""
380
global serviceName, server, rename_count
382
# print "state change: %i" % state
845
384
if state == avahi.ENTRY_GROUP_ESTABLISHED:
846
logger.debug(u"Zeroconf service established.")
386
# print "Service established."
847
387
elif state == avahi.ENTRY_GROUP_COLLISION:
848
logger.warning(u"Zeroconf service name collision.")
389
rename_count = rename_count - 1
391
name = server.GetAlternativeServiceName(name)
392
print "WARNING: Service name collision, changing name to '%s' ..." % name
397
print "ERROR: No suitable service name found after %i retries, exiting." % n_rename
850
399
elif state == avahi.ENTRY_GROUP_FAILURE:
851
logger.critical(u"Avahi: Error in group state changed %s",
853
raise AvahiGroupError(u"State changed: %s" % unicode(error))
400
print "Error in group state changed", error
855
405
def if_nametoindex(interface):
856
"""Call the C function if_nametoindex(), or equivalent"""
857
global if_nametoindex
406
"""Call the C function if_nametoindex()"""
859
if_nametoindex = (ctypes.cdll.LoadLibrary
860
(ctypes.util.find_library("c"))
862
except (OSError, AttributeError):
408
if "ctypes" not in sys.modules:
410
libc = ctypes.cdll.LoadLibrary("libc.so.6")
411
return libc.if_nametoindex(interface)
412
except (ImportError, OSError, AttributeError):
863
413
if "struct" not in sys.modules:
865
415
if "fcntl" not in sys.modules:
867
def if_nametoindex(interface):
868
"Get an interface index the hard way, i.e. using fcntl()"
869
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
870
with closing(socket.socket()) as s:
871
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
872
struct.pack("16s16x", interface))
873
interface_index = struct.unpack("I", ifreq[16:20])[0]
874
return interface_index
875
return if_nametoindex(interface)
878
def daemon(nochdir = False, noclose = False):
879
"""See daemon(3). Standard BSD Unix function.
880
This should really exist as os.daemon, but it doesn't (yet)."""
889
# Close all standard open file descriptors
890
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
891
if not stat.S_ISCHR(os.fstat(null).st_mode):
892
raise OSError(errno.ENODEV,
893
"/dev/null not a character device")
894
os.dup2(null, sys.stdin.fileno())
895
os.dup2(null, sys.stdout.fileno())
896
os.dup2(null, sys.stderr.fileno())
902
parser = optparse.OptionParser(version = "%%prog %s" % version)
417
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
419
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
420
struct.pack("16s16x", interface))
422
interface_index = struct.unpack("I", ifreq[16:20])[0]
423
return interface_index
426
if __name__ == '__main__':
427
parser = OptionParser()
903
428
parser.add_option("-i", "--interface", type="string",
904
metavar="IF", help="Bind to interface IF")
905
parser.add_option("-a", "--address", type="string",
906
help="Address to listen for requests on")
907
parser.add_option("-p", "--port", type="int",
429
default=None, metavar="IF",
430
help="Bind to interface IF")
431
parser.add_option("--cert", type="string", default="cert.pem",
433
help="Public key certificate PEM file to use")
434
parser.add_option("--key", type="string", default="key.pem",
436
help="Private key PEM file to use")
437
parser.add_option("--ca", type="string", default="ca.pem",
439
help="Certificate Authority certificate PEM file to use")
440
parser.add_option("--crl", type="string", default="crl.pem",
442
help="Certificate Revokation List PEM file to use")
443
parser.add_option("-p", "--port", type="int", default=None,
908
444
help="Port number to receive requests on")
909
parser.add_option("--check", action="store_true",
445
parser.add_option("--timeout", type="string", # Parsed later
447
help="Amount of downtime allowed for clients")
448
parser.add_option("--interval", type="string", # Parsed later
450
help="How often to check that a client is up")
451
parser.add_option("--check", action="store_true", default=False,
910
452
help="Run self-test")
911
parser.add_option("--debug", action="store_true",
912
help="Debug mode; run in foreground and log to"
914
parser.add_option("--priority", type="string", help="GnuTLS"
915
" priority string (see GnuTLS documentation)")
916
parser.add_option("--servicename", type="string", metavar="NAME",
917
help="Zeroconf service name")
918
parser.add_option("--configdir", type="string",
919
default="/etc/mandos", metavar="DIR",
920
help="Directory to search for configuration"
922
parser.add_option("--no-dbus", action="store_false",
924
help="Do not provide D-Bus system bus"
926
options = parser.parse_args()[0]
453
(options, args) = parser.parse_args()
928
455
if options.check:
930
457
doctest.testmod()
933
# Default values for config file for server-global settings
934
server_defaults = { "interface": "",
939
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
940
"servicename": "Mandos",
944
# Parse config file for server-global settings
945
server_config = ConfigParser.SafeConfigParser(server_defaults)
947
server_config.read(os.path.join(options.configdir, "mandos.conf"))
948
# Convert the SafeConfigParser object to a dict
949
server_settings = server_config.defaults()
950
# Use the appropriate methods on the non-string config options
951
server_settings["debug"] = server_config.getboolean("DEFAULT",
953
server_settings["use_dbus"] = server_config.getboolean("DEFAULT",
955
if server_settings["port"]:
956
server_settings["port"] = server_config.getint("DEFAULT",
960
# Override the settings from the config file with command line
962
for option in ("interface", "address", "port", "debug",
963
"priority", "servicename", "configdir",
965
value = getattr(options, option)
966
if value is not None:
967
server_settings[option] = value
969
# Now we have our good server settings in "server_settings"
972
debug = server_settings["debug"]
973
use_dbus = server_settings["use_dbus"]
976
syslogger.setLevel(logging.WARNING)
977
console.setLevel(logging.WARNING)
979
if server_settings["servicename"] != "Mandos":
980
syslogger.setFormatter(logging.Formatter
981
('Mandos (%s): %%(levelname)s:'
983
% server_settings["servicename"]))
985
# Parse config file with clients
986
client_defaults = { "timeout": "1h",
988
"checker": "fping -q -- %%(host)s",
991
client_config = ConfigParser.SafeConfigParser(client_defaults)
992
client_config.read(os.path.join(server_settings["configdir"],
996
tcp_server = IPv6_TCPServer((server_settings["address"],
997
server_settings["port"]),
999
settings=server_settings,
1001
pidfilename = "/var/run/mandos.pid"
1003
pidfile = open(pidfilename, "w")
1005
logger.error("Could not open file %r", pidfilename)
1008
uid = pwd.getpwnam("_mandos").pw_uid
1009
gid = pwd.getpwnam("_mandos").pw_gid
1012
uid = pwd.getpwnam("mandos").pw_uid
1013
gid = pwd.getpwnam("mandos").pw_gid
1016
uid = pwd.getpwnam("nobody").pw_uid
1017
gid = pwd.getpwnam("nogroup").pw_gid
1024
except OSError, error:
1025
if error[0] != errno.EPERM:
1028
# Enable all possible GnuTLS debugging
1030
# "Use a log level over 10 to enable all debugging options."
1032
gnutls.library.functions.gnutls_global_set_log_level(11)
1034
@gnutls.library.types.gnutls_log_func
1035
def debug_gnutls(level, string):
1036
logger.debug("GnuTLS: %s", string[:-1])
1038
(gnutls.library.functions
1039
.gnutls_global_set_log_function(debug_gnutls))
1042
service = AvahiService(name = server_settings["servicename"],
1043
servicetype = "_mandos._tcp", )
1044
if server_settings["interface"]:
1045
service.interface = (if_nametoindex
1046
(server_settings["interface"]))
1051
# From the Avahi example code
460
# Parse the time arguments
462
options.timeout = string_to_delta(options.timeout)
464
parser.error("option --timeout: Unparseable time")
466
options.interval = string_to_delta(options.interval)
468
parser.error("option --interval: Unparseable time")
470
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
471
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
472
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
473
crl = gnutls.crypto.X509CRL(open(options.crl).read())
474
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
478
client_config = ConfigParser.SafeConfigParser(defaults)
479
#client_config.readfp(open("secrets.conf"), "secrets.conf")
480
client_config.read("mandos-clients.conf")
482
# From the Avahi server example code
1052
483
DBusGMainLoop(set_as_default=True )
1053
484
main_loop = gobject.MainLoop()
1054
485
bus = dbus.SystemBus()
1055
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1056
avahi.DBUS_PATH_SERVER),
1057
avahi.DBUS_INTERFACE_SERVER)
486
server = dbus.Interface(
487
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
488
avahi.DBUS_INTERFACE_SERVER )
1058
489
# End of Avahi example code
1060
bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
1062
clients.update(Set(Client(name = section,
1064
= dict(client_config.items(section)),
1065
use_dbus = use_dbus)
492
def remove_from_clients(client):
493
clients.remove(client)
495
print "No clients left, exiting"
498
clients.update(Set(Client(name=section, options=options,
499
stop_hook = remove_from_clients,
500
**(dict(client_config\
1066
502
for section in client_config.sections()))
1068
logger.warning(u"No clients defined")
1071
# Redirect stdin so all checkers get /dev/null
1072
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
1073
os.dup2(null, sys.stdin.fileno())
1077
# No console logging
1078
logger.removeHandler(console)
1079
# Close all input and output, do double fork, etc.
1084
pidfile.write(str(pid) + "\n")
1088
logger.error(u"Could not write to file %r with PID %d",
1091
# "pidfile" was never created
1096
"Cleanup function; run on exit"
1098
# From the Avahi example code
1099
if not group is None:
1102
# End of Avahi example code
1105
client = clients.pop()
1106
client.disable_hook = None
1109
atexit.register(cleanup)
1112
signal.signal(signal.SIGINT, signal.SIG_IGN)
1113
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1114
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
1117
class MandosServer(dbus.service.Object):
1118
"""A D-Bus proxy object"""
1120
dbus.service.Object.__init__(self, bus, "/")
1121
_interface = u"se.bsnet.fukt.Mandos"
1123
@dbus.service.signal(_interface, signature="oa{sv}")
1124
def ClientAdded(self, objpath, properties):
1128
@dbus.service.signal(_interface, signature="os")
1129
def ClientRemoved(self, objpath, name):
1133
@dbus.service.method(_interface, out_signature="ao")
1134
def GetAllClients(self):
1136
return dbus.Array(c.dbus_object_path for c in clients)
1138
@dbus.service.method(_interface, out_signature="a{oa{sv}}")
1139
def GetAllClientsWithProperties(self):
1141
return dbus.Dictionary(
1142
((c.dbus_object_path, c.GetAllProperties())
1146
@dbus.service.method(_interface, in_signature="o")
1147
def RemoveClient(self, object_path):
1150
if c.dbus_object_path == object_path:
1152
# Don't signal anything except ClientRemoved
1156
self.ClientRemoved(object_path, c.name)
1162
mandos_server = MandosServer()
1164
503
for client in clients:
1167
mandos_server.ClientAdded(client.dbus_object_path,
1168
client.GetAllProperties())
1172
tcp_server.server_activate()
1174
# Find out what port we got
1175
service.port = tcp_server.socket.getsockname()[1]
1176
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
1177
u" scope_id %d" % tcp_server.socket.getsockname())
1179
#service.interface = tcp_server.socket.getsockname()[3]
506
tcp_server = IPv6_TCPServer((None, options.port),
511
# Find out what random port we got
512
servicePort = tcp_server.socket.getsockname()[1]
513
#sys.stderr.write("Now listening on port %d\n" % servicePort)
515
if options.interface is not None:
516
serviceInterface = if_nametoindex(options.interface)
518
# From the Avahi server example code
519
server.connect_to_signal("StateChanged", server_state_changed)
520
server_state_changed(server.GetState())
521
# End of Avahi example code
523
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
524
lambda *args, **kwargs:
525
tcp_server.handle_request(*args[2:],
1182
# From the Avahi example code
1183
server.connect_to_signal("StateChanged", server_state_changed)
1185
server_state_changed(server.GetState())
1186
except dbus.exceptions.DBusException, error:
1187
logger.critical(u"DBusException: %s", error)
1189
# End of Avahi example code
1191
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
1192
lambda *args, **kwargs:
1193
(tcp_server.handle_request
1194
(*args[2:], **kwargs) or True))
1196
logger.debug(u"Starting main loop")
1198
except AvahiError, error:
1199
logger.critical(u"AvahiError: %s", error)
1201
529
except KeyboardInterrupt:
1204
logger.debug("Server received KeyboardInterrupt")
1205
logger.debug("Server exiting")
1207
if __name__ == '__main__':
534
# From the Avahi server example code
535
if not group is None:
537
# End of Avahi example code
539
for client in clients:
540
client.stop_hook = None