2
# -*- mode: python; coding: utf-8 -*-
4
# Mandos server - give out binary blobs to connecting clients.
6
# This program is partly derived from an example program for an Avahi
7
# service publisher, downloaded from
8
# <http://avahi.org/wiki/PythonPublishExample>. This includes the
9
# 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".
14
# Copyright © 2008,2009 Teddy Hogeborn
15
# Copyright © 2008,2009 Björn Påhlsson
17
# This program is free software: you can redistribute it and/or modify
18
# it under the terms of the GNU General Public License as published by
19
# the Free Software Foundation, either version 3 of the License, or
20
# (at your option) any later version.
22
# This program is distributed in the hope that it will be useful,
23
# but WITHOUT ANY WARRANTY; without even the implied warranty of
24
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
# GNU General Public License for more details.
27
# You should have received a copy of the GNU General Public License
28
# along with this program. If not, see
29
# <http://www.gnu.org/licenses/>.
31
# Contact the authors at <mandos@fukt.bsnet.se>.
34
from __future__ import division, with_statement, absolute_import
6
from optparse import OptionParser
9
41
import gnutls.crypto
10
42
import gnutls.connection
11
43
import gnutls.errors
44
import gnutls.library.functions
45
import gnutls.library.constants
46
import gnutls.library.types
12
47
import ConfigParser
19
def __init__(self, name=None, options=None, dn=None,
20
password=None, passfile=None, fqdn=None,
21
timeout=None, interval=-1):
25
self.password = password
27
self.password = open(passfile).readall()
29
print "No Password or Passfile in client config file"
30
# raise RuntimeError XXX
31
self.password = "gazonk"
32
self.fqdn = fqdn # string
33
self.created = datetime.datetime.now()
34
self.last_seen = None # datetime.datetime()
36
timeout = options.timeout
37
self.timeout = timeout # datetime.timedelta()
39
interval = options.interval
40
self.interval = interval # datetime.timedelta()
41
self.next_check = datetime.datetime.now() # datetime.datetime()
42
self.checker = None # or a subprocess.Popen()
43
def check_action(self, now=None):
44
"""The checker said something and might have completed.
45
Check if is has, and take appropriate actions."""
46
if self.checker.poll() is None:
47
# False alarm, no result yet
51
now = datetime.datetime.now()
52
if self.checker.returncode == 0:
54
while self.next_check <= now:
55
self.next_check += self.interval
56
handle_request = check_action
57
import logging.handlers
59
from contextlib import closing
65
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
171
# 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
"""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.
199
checker_initiator_tag: a gobject event source tag, or None
200
disable_initiator_tag: - '' -
201
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'
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
256
self.checker_initiator_tag = None
257
self.disable_initiator_tag = None
258
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()
274
# Schedule a new checker to be started an 'interval' from now,
275
# and every interval from then on.
276
self.checker_initiator_tag = (gobject.timeout_add
277
(self.interval_milliseconds(),
279
# Also start a new checker *right now*.
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):
303
gobject.source_remove(self.checker_initiator_tag)
304
self.checker_initiator_tag = None
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
# Do not run this again if called by a gobject.timeout_add
317
self.disable_hook = None
320
def checker_callback(self, pid, condition, command):
321
"""The checker has completed, so take appropriate actions."""
322
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,
57
368
def start_checker(self):
60
self.checker = subprocess.Popen("sleep 1; fping -q -- %s"
61
% re.escape(self.fqdn),
62
stdout=subprocess.PIPE,
65
except subprocess.OSError, e:
66
print "Failed to start subprocess:", e
369
"""Start a new checker subprocess if one is not running.
370
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
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",
418
# Re-run this periodically if run by gobject.timeout_add
67
421
def stop_checker(self):
68
if self.checker is None:
422
"""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:
70
os.kill(self.checker.pid, signal.SIGTERM)
71
if self.checker.poll() is None:
72
os.kill(self.checker.pid, signal.SIGKILL)
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
73
437
self.checker = None
74
__del__ = stop_checker
76
if self.checker is None:
78
return self.checker.stdout.fileno()
80
"""The time when something must be done about this client"""
81
return min(self.last_seen + self.timeout, self.next_check)
82
def still_valid(self, now=None):
83
"""Has this client's timeout not passed?"""
85
now = datetime.datetime.now()
86
return now < (self.last_seen + timeout)
87
def it_is_time_to_check(self, now=None):
89
now = datetime.datetime.now()
90
return self.next_check <= now
93
class server_metaclass(type):
94
"Common behavior for the UDP and TCP server classes"
95
def __new__(cls, name, bases, attrs):
96
attrs["address_family"] = socket.AF_INET6
97
attrs["allow_reuse_address"] = True
98
def server_bind(self):
99
if self.options.interface:
100
if not hasattr(socket, "SO_BINDTODEVICE"):
101
# From /usr/include/asm-i486/socket.h
102
socket.SO_BINDTODEVICE = 25
104
self.socket.setsockopt(socket.SOL_SOCKET,
105
socket.SO_BINDTODEVICE,
106
self.options.interface)
107
except socket.error, error:
108
if error[0] == errno.EPERM:
109
print "Warning: No permission to bind to interface", \
110
self.options.interface
113
return super(type(self), self).server_bind()
114
attrs["server_bind"] = server_bind
115
def init(self, *args, **kwargs):
116
if "options" in kwargs:
117
self.options = kwargs["options"]
118
del kwargs["options"]
119
if "clients" in kwargs:
120
self.clients = kwargs["clients"]
121
del kwargs["clients"]
122
if "credentials" in kwargs:
123
self.credentials = kwargs["credentials"]
124
del kwargs["credentials"]
125
return super(type(self), self).__init__(*args, **kwargs)
126
attrs["__init__"] = init
127
return type.__new__(cls, name, bases, attrs)
130
class udp_handler(SocketServer.DatagramRequestHandler, object):
132
self.wfile.write("Polo")
133
print "UDP request answered"
136
class IPv6_UDPServer(SocketServer.UDPServer, object):
137
__metaclass__ = server_metaclass
138
def verify_request(self, request, client_address):
139
print "UDP request came"
140
return request[0] == "Marco"
143
class tcp_handler(SocketServer.BaseRequestHandler, object):
145
print "TCP request came"
146
print "Request:", self.request
147
print "Client Address:", self.client_address
148
print "Server:", self.server
149
session = gnutls.connection.ServerSession(self.request,
150
self.server.credentials)
152
if session.peer_certificate:
153
print "DN:", session.peer_certificate.subject
155
session.verify_peer()
156
except gnutls.errors.CertificateError, error:
157
print "Verify failed", error
161
session.send(dict((client.dn, client.password)
162
for client in self.server.clients)
163
[session.peer_certificate.subject])
165
session.send("gazonk")
439
self.PropertyChanged(dbus.String(u"checker_running"),
440
dbus.Boolean(False, variant_level=1))
442
def still_valid(self):
443
"""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:
448
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):
653
"""A TCP request handler class.
654
Instantiated by IPv6_TCPServer for each request to handle it.
655
Note: This will run in its own forked process."""
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,
690
except gnutls.errors.GNUTLSError, error:
691
logger.warning(u"Handshake failed: %s", error)
692
# Do not run session.bye() here: the session is not
693
# established. Just abandon the request.
695
logger.debug(u"Handshake succeeded")
697
fpr = fingerprint(peer_certificate(session))
698
except (TypeError, gnutls.errors.GNUTLSError), error:
699
logger.warning(u"Bad certificate: %s", error)
702
logger.debug(u"Fingerprint: %s", fpr)
704
for c in self.server.clients:
705
if c.fingerprint == fpr:
709
logger.warning(u"Client not found for fingerprint: %s",
713
# Have to check if client.still_valid(), since it is possible
714
# 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))
170
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
171
__metaclass__ = server_metaclass
172
request_queue_size = 1024
733
class IPv6_TCPServer(SocketServer.ForkingMixIn,
734
SocketServer.TCPServer, object):
735
"""IPv6 TCP server. Accepts 'None' as address and/or port.
737
settings: Server settings
738
clients: Set() of Client objects
739
enabled: Boolean; whether this server is activated yet
741
address_family = socket.AF_INET6
742
def __init__(self, *args, **kwargs):
743
if "settings" in kwargs:
744
self.settings = kwargs["settings"]
745
del kwargs["settings"]
746
if "clients" in kwargs:
747
self.clients = kwargs["clients"]
748
del kwargs["clients"]
750
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
751
def server_bind(self):
752
"""This overrides the normal server_bind() function
753
to bind to an interface if one was specified, and also NOT to
754
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)
759
self.socket.setsockopt(socket.SOL_SOCKET,
761
self.settings["interface"])
762
except socket.error, error:
763
if error[0] == errno.EPERM:
764
logger.error(u"No permission to"
765
u" bind to interface %s",
766
self.settings["interface"])
769
# Only bind(2) the socket if we really need to.
770
if self.server_address[0] or self.server_address[1]:
771
if not self.server_address[0]:
773
self.server_address = (in6addr_any,
774
self.server_address[1])
775
elif not self.server_address[1]:
776
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()
177
793
def string_to_delta(interval):
178
794
"""Parse a string and return a datetime.timedelta
180
796
>>> string_to_delta('7d')
181
797
datetime.timedelta(7)
182
798
>>> string_to_delta('60s')
187
803
datetime.timedelta(1)
188
804
>>> string_to_delta(u'1w')
189
805
datetime.timedelta(7)
806
>>> string_to_delta('5m 30s')
807
datetime.timedelta(0, 330)
192
suffix=unicode(interval[-1])
193
value=int(interval[:-1])
195
delta = datetime.timedelta(value)
197
delta = datetime.timedelta(0, value)
199
delta = datetime.timedelta(0, 0, 0, 0, value)
201
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
203
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
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):
206
except (ValueError, IndexError):
832
def server_state_changed(state):
833
"""Derived from the Avahi example code"""
834
if state == avahi.SERVER_COLLISION:
835
logger.error(u"Zeroconf server name collision")
837
elif state == avahi.SERVER_RUNNING:
841
def entry_group_state_changed(state, error):
842
"""Derived from the Avahi example code"""
843
logger.debug(u"Avahi state change: %i", state)
845
if state == avahi.ENTRY_GROUP_ESTABLISHED:
846
logger.debug(u"Zeroconf service established.")
847
elif state == avahi.ENTRY_GROUP_COLLISION:
848
logger.warning(u"Zeroconf service name collision.")
850
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))
855
def if_nametoindex(interface):
856
"""Call the C function if_nametoindex(), or equivalent"""
857
global if_nametoindex
859
if_nametoindex = (ctypes.cdll.LoadLibrary
860
(ctypes.util.find_library("c"))
862
except (OSError, AttributeError):
863
if "struct" not in sys.modules:
865
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())
212
parser = OptionParser()
902
parser = optparse.OptionParser(version = "%%prog %s" % version)
213
903
parser.add_option("-i", "--interface", type="string",
214
default="eth0", metavar="IF",
215
help="Interface to bind to")
216
parser.add_option("--cert", type="string", default="cert.pem",
218
help="Public key certificate to use")
219
parser.add_option("--key", type="string", default="key.pem",
221
help="Private key to use")
222
parser.add_option("--ca", type="string", default="ca.pem",
224
help="Certificate Authority certificate to use")
225
parser.add_option("--crl", type="string", default="crl.pem",
227
help="Certificate Revokation List to use")
228
parser.add_option("-p", "--port", type="int", default=49001,
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",
229
908
help="Port number to receive requests on")
230
parser.add_option("--dh", type="int", metavar="BITS",
231
help="DH group to use")
232
parser.add_option("-t", "--timeout", type="string", # Parsed later
234
help="Amount of downtime allowed for clients")
235
parser.add_option("--interval", type="string", # Parsed later
237
help="How often to check that a client is up")
238
parser.add_option("--check", action="store_true", default=False,
909
parser.add_option("--check", action="store_true",
239
910
help="Run self-test")
240
(options, args) = parser.parse_args()
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]
242
928
if options.check:
244
930
doctest.testmod()
247
# Parse the time arguments
249
options.timeout = string_to_delta(options.timeout)
251
parser.error("option --timeout: Unparseable time")
254
options.interval = string_to_delta(options.interval)
256
parser.error("option --interval: Unparseable time")
258
cert = gnutls.crypto.X509Certificate(open(options.cert).read())
259
key = gnutls.crypto.X509PrivateKey(open(options.key).read())
260
ca = gnutls.crypto.X509Certificate(open(options.ca).read())
261
crl = gnutls.crypto.X509CRL(open(options.crl).read())
262
cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
266
client_config_object = ConfigParser.SafeConfigParser(defaults)
267
client_config_object.read("mandos-clients.conf")
268
clients = [Client(name=section, options=options,
269
**(dict(client_config_object.items(section))))
270
for section in client_config_object.sections()]
272
udp_server = IPv6_UDPServer((in6addr_any, options.port),
276
tcp_server = IPv6_TCPServer((in6addr_any, options.port),
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
284
input, out, err = select.select((udp_server,
291
except KeyboardInterrupt:
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
1052
DBusGMainLoop(set_as_default=True )
1053
main_loop = gobject.MainLoop()
1054
bus = dbus.SystemBus()
1055
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
1056
avahi.DBUS_PATH_SERVER),
1057
avahi.DBUS_INTERFACE_SERVER)
1058
# 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)
1066
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()
295
1164
for client in clients:
296
client.stop_checker()
299
if __name__ == "__main__":
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]
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
except KeyboardInterrupt:
1204
logger.debug("Server received KeyboardInterrupt")
1205
logger.debug("Server exiting")
1207
if __name__ == '__main__':