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 Teddy Hogeborn & Björn Påhlsson
16
# This program is free software: you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
18
# the Free Software Foundation, either version 3 of the License, or
19
# (at your option) any later version.
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
# GNU General Public License for more details.
26
# You should have received a copy of the GNU General Public License
27
# along with this program. If not, see
28
# <http://www.gnu.org/licenses/>.
30
# Contact the authors at <mandos@fukt.bsnet.se>.
33
from __future__ import division, with_statement, absolute_import
37
6
from optparse import OptionParser
40
9
import gnutls.crypto
41
10
import gnutls.connection
42
11
import gnutls.errors
43
import gnutls.library.functions
44
import gnutls.library.constants
45
import gnutls.library.types
46
12
import ConfigParser
56
import logging.handlers
58
from contextlib import closing
63
from dbus.mainloop.glib import DBusGMainLoop
69
logger = logging.Logger('mandos')
70
syslogger = logging.handlers.SysLogHandler\
71
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
73
syslogger.setFormatter(logging.Formatter\
74
('Mandos: %(levelname)s: %(message)s'))
75
logger.addHandler(syslogger)
77
console = logging.StreamHandler()
78
console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
80
logger.addHandler(console)
82
class AvahiError(Exception):
83
def __init__(self, value):
85
super(AvahiError, self).__init__()
87
return repr(self.value)
89
class AvahiServiceError(AvahiError):
92
class AvahiGroupError(AvahiError):
96
class AvahiService(object):
97
"""An Avahi (Zeroconf) service.
99
interface: integer; avahi.IF_UNSPEC or an interface index.
100
Used to optionally bind to the specified interface.
101
name: string; Example: 'Mandos'
102
type: string; Example: '_mandos._tcp'.
103
See <http://www.dns-sd.org/ServiceTypes.html>
104
port: integer; what port to announce
105
TXT: list of strings; TXT record for the service
106
domain: string; Domain to publish on, default to .local if empty.
107
host: string; Host to publish records for, default is localhost
108
max_renames: integer; maximum number of renames
109
rename_count: integer; counter so we only rename after collisions
110
a sensible number of times
112
def __init__(self, interface = avahi.IF_UNSPEC, name = None,
113
servicetype = None, port = None, TXT = None, domain = "",
114
host = "", max_renames = 32768):
115
self.interface = interface
117
self.type = servicetype
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("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\
152
(bus.get_object(avahi.DBUS_NAME,
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
18
class Client(object):
175
"""A representation of a client host served by this server.
177
name: string; from the config file, used in log messages
178
fingerprint: string (40 or 32 hexadecimal digits); used to
179
uniquely identify the client
180
secret: bytestring; sent verbatim (over TLS) to client
181
host: string; available for use by the checker command
182
created: datetime.datetime(); object creation, not client host
183
last_checked_ok: datetime.datetime() or None if not yet checked OK
184
timeout: datetime.timedelta(); How long from last_checked_ok
185
until this client is invalid
186
interval: datetime.timedelta(); How often to start a new checker
187
stop_hook: If set, called by stop() as stop_hook(self)
188
checker: subprocess.Popen(); a running checker process used
189
to see if the client lives.
190
'None' if no process is running.
191
checker_initiator_tag: a gobject event source tag, or None
192
stop_initiator_tag: - '' -
193
checker_callback_tag: - '' -
194
checker_command: string; External command which is run to check if
195
client lives. %() expansions are done at
196
runtime with vars(self) as dict, so that for
197
instance %(name)s can be used in the command.
199
_timeout: Real variable for 'timeout'
200
_interval: Real variable for 'interval'
201
_timeout_milliseconds: Used when calling gobject.timeout_add()
202
_interval_milliseconds: - '' -
204
def _set_timeout(self, timeout):
205
"Setter function for 'timeout' attribute"
206
self._timeout = timeout
207
self._timeout_milliseconds = ((self.timeout.days
208
* 24 * 60 * 60 * 1000)
209
+ (self.timeout.seconds * 1000)
210
+ (self.timeout.microseconds
212
timeout = property(lambda self: self._timeout,
215
def _set_interval(self, interval):
216
"Setter function for 'interval' attribute"
217
self._interval = interval
218
self._interval_milliseconds = ((self.interval.days
219
* 24 * 60 * 60 * 1000)
220
+ (self.interval.seconds
222
+ (self.interval.microseconds
224
interval = property(lambda self: self._interval,
227
def __init__(self, name = None, stop_hook=None, config=None):
228
"""Note: the 'checker' key in 'config' sets the
229
'checker_command' attribute and *not* the 'checker'
19
def __init__(self, name=None, options=None, dn=None,
20
password=None, passfile=None, fqdn=None,
21
timeout=None, interval=-1):
234
logger.debug(u"Creating client %r", self.name)
235
# Uppercase and remove spaces from fingerprint for later
236
# comparison purposes with return value from the fingerprint()
238
self.fingerprint = config["fingerprint"].upper()\
240
logger.debug(u" Fingerprint: %s", self.fingerprint)
241
if "secret" in config:
242
self.secret = config["secret"].decode(u"base64")
243
elif "secfile" in config:
244
with closing(open(os.path.expanduser
246
(config["secfile"])))) \
248
self.secret = secfile.read()
25
self.password = password
27
self.password = open(passfile).readall()
250
raise TypeError(u"No secret or secfile for client %s"
252
self.host = config.get("host", "")
29
print "No Password or Passfile in client config file"
30
# raise RuntimeError XXX
31
self.password = "gazonk"
32
self.fqdn = fqdn # string
253
33
self.created = datetime.datetime.now()
254
self.last_checked_ok = None
255
self.timeout = string_to_delta(config["timeout"])
256
self.interval = string_to_delta(config["interval"])
257
self.stop_hook = stop_hook
259
self.checker_initiator_tag = None
260
self.stop_initiator_tag = None
261
self.checker_callback_tag = None
262
self.check_command = config["checker"]
264
"""Start this client's checker and timeout hooks"""
265
# Schedule a new checker to be started an 'interval' from now,
266
# and every interval from then on.
267
self.checker_initiator_tag = gobject.timeout_add\
268
(self._interval_milliseconds,
270
# Also start a new checker *right now*.
272
# Schedule a stop() when 'timeout' has passed
273
self.stop_initiator_tag = gobject.timeout_add\
274
(self._timeout_milliseconds,
278
The possibility that a client might be restarted is left open,
279
but not currently used."""
280
# If this client doesn't have a secret, it is already stopped.
281
if hasattr(self, "secret") and self.secret:
282
logger.info(u"Stopping client %s", self.name)
286
if getattr(self, "stop_initiator_tag", False):
287
gobject.source_remove(self.stop_initiator_tag)
288
self.stop_initiator_tag = None
289
if getattr(self, "checker_initiator_tag", False):
290
gobject.source_remove(self.checker_initiator_tag)
291
self.checker_initiator_tag = None
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
def start_checker(self):
292
58
self.stop_checker()
295
# Do not run this again if called by a gobject.timeout_add
298
self.stop_hook = None
300
def checker_callback(self, pid, condition):
301
"""The checker has completed, so take appropriate actions."""
302
self.checker_callback_tag = None
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
67
def stop_checker(self):
68
if self.checker 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)
303
73
self.checker = None
304
if os.WIFEXITED(condition) \
305
and (os.WEXITSTATUS(condition) == 0):
306
logger.info(u"Checker for %(name)s succeeded",
309
elif not os.WIFEXITED(condition):
310
logger.warning(u"Checker for %(name)s crashed?",
313
logger.info(u"Checker for %(name)s failed",
315
def bump_timeout(self):
316
"""Bump up the timeout for this client.
317
This should only be called when the client has been seen,
320
self.last_checked_ok = datetime.datetime.now()
321
gobject.source_remove(self.stop_initiator_tag)
322
self.stop_initiator_tag = gobject.timeout_add\
323
(self._timeout_milliseconds, self.stop)
324
def start_checker(self):
325
"""Start a new checker subprocess if one is not running.
326
If a checker already exists, leave it running and do
328
# The reason for not killing a running checker is that if we
329
# did that, then if a checker (for some reason) started
330
# running slowly and taking more than 'interval' time, the
331
# client would inevitably timeout, since no checker would get
332
# a chance to run to completion. If we instead leave running
333
# checkers alone, the checker would have to take more time
334
# than 'timeout' for the client to be declared invalid, which
335
# is as it should be.
74
__del__ = stop_checker
336
76
if self.checker is None:
338
# In case check_command has exactly one % operator
339
command = self.check_command % self.host
341
# Escape attributes for the shell
342
escaped_attrs = dict((key, re.escape(str(val)))
344
vars(self).iteritems())
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
346
command = self.check_command % escaped_attrs
347
except TypeError, error:
348
logger.error(u'Could not format string "%s":'
349
u' %s', self.check_command, error)
350
return True # Try again later
352
logger.info(u"Starting checker %r for %s",
354
# We don't need to redirect stdout and stderr, since
355
# in normal mode, that is already done by daemon(),
356
# and in debug mode we don't want to. (Stdin is
357
# always replaced by /dev/null.)
358
self.checker = subprocess.Popen(command,
361
self.checker_callback_tag = gobject.child_watch_add\
363
self.checker_callback)
364
except OSError, error:
365
logger.error(u"Failed to start subprocess: %s",
367
# Re-run this periodically if run by gobject.timeout_add
369
def stop_checker(self):
370
"""Force the checker process, if any, to stop."""
371
if self.checker_callback_tag:
372
gobject.source_remove(self.checker_callback_tag)
373
self.checker_callback_tag = None
374
if getattr(self, "checker", None) is None:
376
logger.debug(u"Stopping checker for %(name)s", vars(self))
378
os.kill(self.checker.pid, signal.SIGTERM)
380
#if self.checker.poll() is None:
381
# os.kill(self.checker.pid, signal.SIGKILL)
382
except OSError, error:
383
if error.errno != errno.ESRCH: # No such process
386
def still_valid(self):
387
"""Has the timeout not yet passed for this client?"""
388
now = datetime.datetime.now()
389
if self.last_checked_ok is None:
390
return now < (self.created + self.timeout)
392
return now < (self.last_checked_ok + self.timeout)
395
def peer_certificate(session):
396
"Return the peer's OpenPGP certificate as a bytestring"
397
# If not an OpenPGP certificate...
398
if gnutls.library.functions.gnutls_certificate_type_get\
399
(session._c_object) \
400
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP:
401
# ...do the normal thing
402
return session.peer_certificate
403
list_size = ctypes.c_uint()
404
cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
405
(session._c_object, ctypes.byref(list_size))
406
if list_size.value == 0:
409
return ctypes.string_at(cert.data, cert.size)
412
def fingerprint(openpgp):
413
"Convert an OpenPGP bytestring to a hexdigit fingerprint string"
414
# New GnuTLS "datum" with the OpenPGP public key
415
datum = gnutls.library.types.gnutls_datum_t\
416
(ctypes.cast(ctypes.c_char_p(openpgp),
417
ctypes.POINTER(ctypes.c_ubyte)),
418
ctypes.c_uint(len(openpgp)))
419
# New empty GnuTLS certificate
420
crt = gnutls.library.types.gnutls_openpgp_crt_t()
421
gnutls.library.functions.gnutls_openpgp_crt_init\
423
# Import the OpenPGP public key into the certificate
424
gnutls.library.functions.gnutls_openpgp_crt_import\
425
(crt, ctypes.byref(datum),
426
gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
427
# Verify the self signature in the key
428
crtverify = ctypes.c_uint()
429
gnutls.library.functions.gnutls_openpgp_crt_verify_self\
430
(crt, 0, ctypes.byref(crtverify))
431
if crtverify.value != 0:
432
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
433
raise gnutls.errors.CertificateSecurityError("Verify failed")
434
# New buffer for the fingerprint
435
buf = ctypes.create_string_buffer(20)
436
buf_len = ctypes.c_size_t()
437
# Get the fingerprint from the certificate into the buffer
438
gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
439
(crt, ctypes.byref(buf), ctypes.byref(buf_len))
440
# Deinit the certificate
441
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
442
# Convert the buffer to a Python bytestring
443
fpr = ctypes.string_at(buf, buf_len.value)
444
# Convert the bytestring to hexadecimal notation
445
hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
449
class TCP_handler(SocketServer.BaseRequestHandler, object):
450
"""A TCP request handler class.
451
Instantiated by IPv6_TCPServer for each request to handle it.
452
Note: This will run in its own forked process."""
455
logger.info(u"TCP connection from: %s",
456
unicode(self.client_address))
457
session = gnutls.connection.ClientSession\
458
(self.request, gnutls.connection.X509Credentials())
460
line = self.request.makefile().readline()
461
logger.debug(u"Protocol version: %r", line)
463
if int(line.strip().split()[0]) > 1:
465
except (ValueError, IndexError, RuntimeError), error:
466
logger.error(u"Unknown protocol version: %s", error)
469
# Note: gnutls.connection.X509Credentials is really a generic
470
# GnuTLS certificate credentials object so long as no X.509
471
# keys are added to it. Therefore, we can use it here despite
472
# using OpenPGP certificates.
474
#priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
475
# "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
477
# Use a fallback default, since this MUST be set.
478
priority = self.server.settings.get("priority", "NORMAL")
479
gnutls.library.functions.gnutls_priority_set_direct\
480
(session._c_object, priority, None)
484
except gnutls.errors.GNUTLSError, error:
485
logger.warning(u"Handshake failed: %s", error)
486
# Do not run session.bye() here: the session is not
487
# established. Just abandon the request.
490
fpr = fingerprint(peer_certificate(session))
491
except (TypeError, gnutls.errors.GNUTLSError), error:
492
logger.warning(u"Bad certificate: %s", error)
495
logger.debug(u"Fingerprint: %s", fpr)
497
for c in self.server.clients:
498
if c.fingerprint == fpr:
502
logger.warning(u"Client not found for fingerprint: %s",
506
# Have to check if client.still_valid(), since it is possible
507
# that the client timed out while establishing the GnuTLS
509
if not client.still_valid():
510
logger.warning(u"Client %(name)s is invalid",
514
## This won't work here, since we're in a fork.
515
# client.bump_timeout()
517
while sent_size < len(client.secret):
518
sent = session.send(client.secret[sent_size:])
519
logger.debug(u"Sent: %d, remaining: %d",
520
sent, len(client.secret)
521
- (sent_size + sent))
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")
526
class IPv6_TCPServer(SocketServer.ForkingMixIn,
527
SocketServer.TCPServer, object):
528
"""IPv6 TCP server. Accepts 'None' as address and/or port.
530
settings: Server settings
531
clients: Set() of Client objects
532
enabled: Boolean; whether this server is activated yet
534
address_family = socket.AF_INET6
535
def __init__(self, *args, **kwargs):
536
if "settings" in kwargs:
537
self.settings = kwargs["settings"]
538
del kwargs["settings"]
539
if "clients" in kwargs:
540
self.clients = kwargs["clients"]
541
del kwargs["clients"]
543
super(IPv6_TCPServer, self).__init__(*args, **kwargs)
544
def server_bind(self):
545
"""This overrides the normal server_bind() function
546
to bind to an interface if one was specified, and also NOT to
547
bind to an address or port if they were not specified."""
548
if self.settings["interface"]:
549
# 25 is from /usr/include/asm-i486/socket.h
550
SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
552
self.socket.setsockopt(socket.SOL_SOCKET,
554
self.settings["interface"])
555
except socket.error, error:
556
if error[0] == errno.EPERM:
557
logger.error(u"No permission to"
558
u" bind to interface %s",
559
self.settings["interface"])
562
# Only bind(2) the socket if we really need to.
563
if self.server_address[0] or self.server_address[1]:
564
if not self.server_address[0]:
566
self.server_address = (in6addr_any,
567
self.server_address[1])
568
elif not self.server_address[1]:
569
self.server_address = (self.server_address[0],
571
# if self.settings["interface"]:
572
# self.server_address = (self.server_address[0],
578
return super(IPv6_TCPServer, self).server_bind()
579
def server_activate(self):
581
return super(IPv6_TCPServer, self).server_activate()
170
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
171
__metaclass__ = server_metaclass
172
request_queue_size = 1024
586
177
def string_to_delta(interval):
587
178
"""Parse a string and return a datetime.timedelta
596
187
datetime.timedelta(1)
597
188
>>> string_to_delta(u'1w')
598
189
datetime.timedelta(7)
599
>>> string_to_delta('5m 30s')
600
datetime.timedelta(0, 330)
602
timevalue = datetime.timedelta(0)
603
for s in interval.split():
605
suffix = unicode(s[-1])
608
delta = datetime.timedelta(value)
610
delta = datetime.timedelta(0, value)
612
delta = datetime.timedelta(0, 0, 0, 0, value)
614
delta = datetime.timedelta(0, 0, 0, 0, 0, value)
616
delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
619
except (ValueError, IndexError):
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)
625
def server_state_changed(state):
626
"""Derived from the Avahi example code"""
627
if state == avahi.SERVER_COLLISION:
628
logger.error(u"Zeroconf server name collision")
630
elif state == avahi.SERVER_RUNNING:
634
def entry_group_state_changed(state, error):
635
"""Derived from the Avahi example code"""
636
logger.debug(u"Avahi state change: %i", state)
638
if state == avahi.ENTRY_GROUP_ESTABLISHED:
639
logger.debug(u"Zeroconf service established.")
640
elif state == avahi.ENTRY_GROUP_COLLISION:
641
logger.warning(u"Zeroconf service name collision.")
643
elif state == avahi.ENTRY_GROUP_FAILURE:
644
logger.critical(u"Avahi: Error in group state changed %s",
646
raise AvahiGroupError("State changed: %s", str(error))
648
def if_nametoindex(interface):
649
"""Call the C function if_nametoindex(), or equivalent"""
650
global if_nametoindex
652
if_nametoindex = ctypes.cdll.LoadLibrary\
653
(ctypes.util.find_library("c")).if_nametoindex
654
except (OSError, AttributeError):
655
if "struct" not in sys.modules:
657
if "fcntl" not in sys.modules:
659
def if_nametoindex(interface):
660
"Get an interface index the hard way, i.e. using fcntl()"
661
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
662
with closing(socket.socket()) as s:
663
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
664
struct.pack("16s16x", interface))
665
interface_index = struct.unpack("I", ifreq[16:20])[0]
666
return interface_index
667
return if_nametoindex(interface)
670
def daemon(nochdir = False, noclose = False):
671
"""See daemon(3). Standard BSD Unix function.
672
This should really exist as os.daemon, but it doesn't (yet)."""
681
# Close all standard open file descriptors
682
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
683
if not stat.S_ISCHR(os.fstat(null).st_mode):
684
raise OSError(errno.ENODEV,
685
"/dev/null not a character device")
686
os.dup2(null, sys.stdin.fileno())
687
os.dup2(null, sys.stdout.fileno())
688
os.dup2(null, sys.stderr.fileno())
206
except (ValueError, IndexError):
694
parser = OptionParser(version = "%%prog %s" % version)
212
parser = OptionParser()
695
213
parser.add_option("-i", "--interface", type="string",
696
metavar="IF", help="Bind to interface IF")
697
parser.add_option("-a", "--address", type="string",
698
help="Address to listen for requests on")
699
parser.add_option("-p", "--port", type="int",
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,
700
229
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")
701
238
parser.add_option("--check", action="store_true", default=False,
702
239
help="Run self-test")
703
parser.add_option("--debug", action="store_true",
704
help="Debug mode; run in foreground and log to"
706
parser.add_option("--priority", type="string", help="GnuTLS"
707
" priority string (see GnuTLS documentation)")
708
parser.add_option("--servicename", type="string", metavar="NAME",
709
help="Zeroconf service name")
710
parser.add_option("--configdir", type="string",
711
default="/etc/mandos", metavar="DIR",
712
help="Directory to search for configuration"
714
options = parser.parse_args()[0]
240
(options, args) = parser.parse_args()
716
242
if options.check:
718
244
doctest.testmod()
721
# Default values for config file for server-global settings
722
server_defaults = { "interface": "",
727
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
728
"servicename": "Mandos",
731
# Parse config file for server-global settings
732
server_config = ConfigParser.SafeConfigParser(server_defaults)
734
server_config.read(os.path.join(options.configdir, "mandos.conf"))
735
# Convert the SafeConfigParser object to a dict
736
server_settings = server_config.defaults()
737
# Use getboolean on the boolean config option
738
server_settings["debug"] = server_config.getboolean\
742
# Override the settings from the config file with command line
744
for option in ("interface", "address", "port", "debug",
745
"priority", "servicename", "configdir"):
746
value = getattr(options, option)
747
if value is not None:
748
server_settings[option] = value
750
# Now we have our good server settings in "server_settings"
752
debug = server_settings["debug"]
755
syslogger.setLevel(logging.WARNING)
756
console.setLevel(logging.WARNING)
758
if server_settings["servicename"] != "Mandos":
759
syslogger.setFormatter(logging.Formatter\
760
('Mandos (%s): %%(levelname)s:'
762
% server_settings["servicename"]))
764
# Parse config file with clients
765
client_defaults = { "timeout": "1h",
767
"checker": "fping -q -- %(host)s",
770
client_config = ConfigParser.SafeConfigParser(client_defaults)
771
client_config.read(os.path.join(server_settings["configdir"],
775
tcp_server = IPv6_TCPServer((server_settings["address"],
776
server_settings["port"]),
778
settings=server_settings,
780
pidfilename = "/var/run/mandos.pid"
782
pidfile = open(pidfilename, "w")
783
except IOError, error:
784
logger.error("Could not open file %r", pidfilename)
789
uid = pwd.getpwnam("mandos").pw_uid
792
uid = pwd.getpwnam("nobody").pw_uid
796
gid = pwd.getpwnam("mandos").pw_gid
799
gid = pwd.getpwnam("nogroup").pw_gid
805
except OSError, error:
806
if error[0] != errno.EPERM:
810
service = AvahiService(name = server_settings["servicename"],
811
servicetype = "_mandos._tcp", )
812
if server_settings["interface"]:
813
service.interface = if_nametoindex\
814
(server_settings["interface"])
819
# From the Avahi example code
820
DBusGMainLoop(set_as_default=True )
821
main_loop = gobject.MainLoop()
822
bus = dbus.SystemBus()
823
server = dbus.Interface(bus.get_object(avahi.DBUS_NAME,
824
avahi.DBUS_PATH_SERVER),
825
avahi.DBUS_INTERFACE_SERVER)
826
# End of Avahi example code
828
def remove_from_clients(client):
829
clients.remove(client)
831
logger.critical(u"No clients left, exiting")
834
clients.update(Set(Client(name = section,
835
stop_hook = remove_from_clients,
837
= dict(client_config.items(section)))
838
for section in client_config.sections()))
840
logger.critical(u"No clients defined")
844
# Redirect stdin so all checkers get /dev/null
845
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
846
os.dup2(null, sys.stdin.fileno())
851
logger.removeHandler(console)
852
# Close all input and output, do double fork, etc.
857
pidfile.write(str(pid) + "\n")
861
logger.error(u"Could not write to file %r with PID %d",
864
# "pidfile" was never created
869
"Cleanup function; run on exit"
871
# From the Avahi example code
872
if not group is None:
875
# End of Avahi example code
878
client = clients.pop()
879
client.stop_hook = None
882
atexit.register(cleanup)
885
signal.signal(signal.SIGINT, signal.SIG_IGN)
886
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
887
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
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),
284
input, out, err = select.select((udp_server,
291
except KeyboardInterrupt:
889
295
for client in clients:
893
tcp_server.server_activate()
895
# Find out what port we got
896
service.port = tcp_server.socket.getsockname()[1]
897
logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
898
u" scope_id %d" % tcp_server.socket.getsockname())
900
#service.interface = tcp_server.socket.getsockname()[3]
903
# From the Avahi example code
904
server.connect_to_signal("StateChanged", server_state_changed)
906
server_state_changed(server.GetState())
907
except dbus.exceptions.DBusException, error:
908
logger.critical(u"DBusException: %s", error)
910
# End of Avahi example code
912
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
913
lambda *args, **kwargs:
914
tcp_server.handle_request\
915
(*args[2:], **kwargs) or True)
917
logger.debug(u"Starting main loop")
919
except AvahiError, error:
920
logger.critical(u"AvahiError: %s" + unicode(error))
922
except KeyboardInterrupt:
926
if __name__ == '__main__':
296
client.stop_checker()
299
if __name__ == "__main__":