3
from __future__ import division
8
from optparse import OptionParser
12
import gnutls.connection
25
from dbus.mainloop.glib import DBusGMainLoop
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
41
# End of Avahi example code
45
"""A representation of a client host served by this server.
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.
60
checker_initiator_tag: a gobject event source tag, or None
61
stop_initiator_tag: - '' -
62
checker_callback_tag: - '' -
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):
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
99
self.checker_initiator_tag = None
100
self.stop_initiator_tag = None
101
self.checker_callback_tag = None
103
"""Start this clients checker and timeout hooks"""
104
# Schedule a new checker to be started an 'interval' from now,
105
# and every interval from then on.
106
self.checker_initiator_tag = gobject.\
107
timeout_add(self.interval_milliseconds,
109
# Also start a new checker *right now*.
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:
125
gobject.source_remove(self.checker_initiator_tag)
126
self.checker_initiator_tag = None
130
# Do not run this again if called by a gobject.timeout_add
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):
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)
160
self.checker_callback_tag = None
161
def start_checker(self):
162
"""Start a new checker subprocess if one is not running.
163
If a checker already exists, leave it running and do
165
if self.checker is None:
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"
181
# Re-run this periodically if run by gobject.timeout_add
183
def stop_checker(self):
184
"""Force the checker process, if any, to stop."""
185
if not hasattr(self, "checker") or self.checker is None:
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)
193
def still_valid(self, now=None):
194
"""Has the timeout not yet passed for this client?"""
196
now = datetime.datetime.now()
197
if self.last_seen is None:
198
return now < (self.created + self.timeout)
200
return now < (self.last_seen + self.timeout)
203
class tcp_handler(SocketServer.BaseRequestHandler, object):
204
"""A TCP request handler class.
205
Instantiated by IPv6_TCPServer for each request to handle it.
206
Note: This will run in its own forked process."""
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,
217
except gnutls.errors.GNUTLSError, error:
218
#sys.stderr.write(u"Handshake failed: %s\n" % error)
219
# Do not run session.bye() here: the session is not
220
# established. Just abandon the request.
222
#if session.peer_certificate:
223
# print "DN:", session.peer_certificate.subject
225
session.verify_peer()
226
except gnutls.errors.CertificateError, error:
227
#sys.stderr.write(u"Verify failed: %s\n" % error)
232
if c.dn == session.peer_certificate.subject:
235
# Have to check if client.still_valid(), since it is possible
236
# that the client timed out while establishing the GnuTLS
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")
252
class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
253
"""IPv6 TCP server. Accepts 'None' as address and/or port.
255
options: Command line options
256
clients: Set() of Client objects
257
credentials: GnuTLS X.509 credentials
259
address_family = socket.AF_INET6
260
def __init__(self, *args, **kwargs):
261
if "options" in kwargs:
262
self.options = kwargs["options"]
263
del kwargs["options"]
264
if "clients" in kwargs:
265
self.clients = kwargs["clients"]
266
del kwargs["clients"]
267
if "credentials" in kwargs:
268
self.credentials = kwargs["credentials"]
269
del kwargs["credentials"]
270
return super(type(self), self).__init__(*args, **kwargs)
271
def server_bind(self):
272
"""This overrides the normal server_bind() function
273
to bind to an interface if one was specified, and also NOT to
274
bind to an address or port if they were not specified."""
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
280
self.socket.setsockopt(socket.SOL_SOCKET,
281
socket.SO_BINDTODEVICE,
282
self.options.interface)
283
except socket.error, error:
284
if error[0] == errno.EPERM:
285
sys.stderr.write(u"Warning: No permission to bind to interface %s\n"
286
% self.options.interface)
289
# Only bind(2) the socket if we really need to.
290
if self.server_address[0] or self.server_address[1]:
291
if not self.server_address[0]:
293
self.server_address = (in6addr_any,
294
self.server_address[1])
295
elif self.server_address[1] is None:
296
self.server_address = (self.server_address[0],
298
return super(type(self), self).server_bind()
301
def string_to_delta(interval):
302
"""Parse a string and return a datetime.timedelta
304
>>> string_to_delta('7d')
305
datetime.timedelta(7)
306
>>> string_to_delta('60s')
307
datetime.timedelta(0, 60)
308
>>> string_to_delta('60m')
309
datetime.timedelta(0, 3600)
310
>>> string_to_delta('24h')
311
datetime.timedelta(1)
312
>>> string_to_delta(u'1w')
313
datetime.timedelta(7)
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:
369
def server_state_changed(state):
370
"""From the Avahi server example code"""
371
if state == avahi.SERVER_COLLISION:
372
print "WARNING: Server name collision"
374
elif state == avahi.SERVER_RUNNING:
378
def entry_group_state_changed(state, error):
379
"""From the Avahi server example code"""
380
global serviceName, server, rename_count
382
# print "state change: %i" % state
384
if state == avahi.ENTRY_GROUP_ESTABLISHED:
386
# print "Service established."
387
elif state == avahi.ENTRY_GROUP_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
399
elif state == avahi.ENTRY_GROUP_FAILURE:
400
print "Error in group state changed", error
405
def if_nametoindex(interface):
406
"""Call the C function if_nametoindex()"""
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):
413
if "struct" not in sys.modules:
415
if "fcntl" not in sys.modules:
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()
428
parser.add_option("-i", "--interface", type="string",
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,
444
help="Port number to receive requests on")
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,
452
help="Run self-test")
453
(options, args) = parser.parse_args()
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
483
DBusGMainLoop(set_as_default=True )
484
main_loop = gobject.MainLoop()
485
bus = dbus.SystemBus()
486
server = dbus.Interface(
487
bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
488
avahi.DBUS_INTERFACE_SERVER )
489
# End of Avahi example code
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\
502
for section in client_config.sections()))
503
for client in clients:
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:],
529
except KeyboardInterrupt:
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