83
86
SO_BINDTODEVICE = None
90
stored_state_file = "clients.pickle"
88
#logger = logging.getLogger('mandos')
89
logger = logging.Logger('mandos')
92
logger = logging.getLogger()
90
93
syslogger = (logging.handlers.SysLogHandler
91
94
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
92
95
address = str("/dev/log")))
93
syslogger.setFormatter(logging.Formatter
94
('Mandos [%(process)d]: %(levelname)s:'
96
logger.addHandler(syslogger)
98
console = logging.StreamHandler()
99
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
102
logger.addHandler(console)
98
if_nametoindex = (ctypes.cdll.LoadLibrary
99
(ctypes.util.find_library("c"))
101
except (OSError, AttributeError):
102
def if_nametoindex(interface):
103
"Get an interface index the hard way, i.e. using fcntl()"
104
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
105
with contextlib.closing(socket.socket()) as s:
106
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
107
struct.pack(str("16s16x"),
109
interface_index = struct.unpack(str("I"),
111
return interface_index
114
def initlogger(level=logging.WARNING):
115
"""init logger and add loglevel"""
117
syslogger.setFormatter(logging.Formatter
118
('Mandos [%(process)d]: %(levelname)s:'
120
logger.addHandler(syslogger)
122
console = logging.StreamHandler()
123
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
127
logger.addHandler(console)
128
logger.setLevel(level)
131
class CryptoError(Exception):
135
class Crypto(object):
136
"""A simple class for OpenPGP symmetric encryption & decryption"""
138
self.gnupg = GnuPGInterface.GnuPG()
139
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
140
self.gnupg = GnuPGInterface.GnuPG()
141
self.gnupg.options.meta_interactive = False
142
self.gnupg.options.homedir = self.tempdir
143
self.gnupg.options.extra_args.extend(['--force-mdc',
149
def __exit__ (self, exc_type, exc_value, traceback):
157
if self.tempdir is not None:
158
# Delete contents of tempdir
159
for root, dirs, files in os.walk(self.tempdir,
161
for filename in files:
162
os.remove(os.path.join(root, filename))
164
os.rmdir(os.path.join(root, dirname))
166
os.rmdir(self.tempdir)
169
def password_encode(self, password):
170
# Passphrase can not be empty and can not contain newlines or
171
# NUL bytes. So we prefix it and hex encode it.
172
return b"mandos" + binascii.hexlify(password)
174
def encrypt(self, data, password):
175
self.gnupg.passphrase = self.password_encode(password)
176
with open(os.devnull) as devnull:
178
proc = self.gnupg.run(['--symmetric'],
179
create_fhs=['stdin', 'stdout'],
180
attach_fhs={'stderr': devnull})
181
with contextlib.closing(proc.handles['stdin']) as f:
183
with contextlib.closing(proc.handles['stdout']) as f:
184
ciphertext = f.read()
188
self.gnupg.passphrase = None
191
def decrypt(self, data, password):
192
self.gnupg.passphrase = self.password_encode(password)
193
with open(os.devnull) as devnull:
195
proc = self.gnupg.run(['--decrypt'],
196
create_fhs=['stdin', 'stdout'],
197
attach_fhs={'stderr': devnull})
198
with contextlib.closing(proc.handles['stdin'] ) as f:
200
with contextlib.closing(proc.handles['stdout']) as f:
201
decrypted_plaintext = f.read()
205
self.gnupg.passphrase = None
206
return decrypted_plaintext
104
210
class AvahiError(Exception):
105
211
def __init__(self, value, *args, **kwargs):
357
474
self.host = config.get("host", "")
358
475
self.created = datetime.datetime.utcnow()
476
self.enabled = config.get("enabled", True)
360
477
self.last_approval_request = None
361
self.last_enabled = None
479
self.last_enabled = datetime.datetime.utcnow()
481
self.last_enabled = None
362
482
self.last_checked_ok = None
483
self.last_checker_status = None
363
484
self.timeout = string_to_delta(config["timeout"])
364
self.extended_timeout = string_to_delta(config["extended_timeout"])
485
self.extended_timeout = string_to_delta(config
486
["extended_timeout"])
365
487
self.interval = string_to_delta(config["interval"])
366
self.disable_hook = disable_hook
367
488
self.checker = None
368
489
self.checker_initiator_tag = None
369
490
self.disable_initiator_tag = None
492
self.expires = datetime.datetime.utcnow() + self.timeout
371
495
self.checker_callback_tag = None
372
496
self.checker_command = config["checker"]
373
497
self.current_checker_command = None
374
self.last_connect = None
375
498
self._approved = None
376
499
self.approved_by_default = config.get("approved_by_default",
380
503
config["approval_delay"])
381
504
self.approval_duration = string_to_delta(
382
505
config["approval_duration"])
383
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
506
self.changedstate = (multiprocessing_manager
507
.Condition(multiprocessing_manager
509
self.client_structure = [attr for attr in
510
self.__dict__.iterkeys()
511
if not attr.startswith("_")]
512
self.client_structure.append("client_structure")
514
for name, t in inspect.getmembers(type(self),
518
if not name.startswith("_"):
519
self.client_structure.append(name)
521
# Send notice to process children that client state has changed
385
522
def send_changedstate(self):
386
self.changedstate.acquire()
387
self.changedstate.notify_all()
388
self.changedstate.release()
523
with self.changedstate:
524
self.changedstate.notify_all()
390
526
def enable(self):
391
527
"""Start this client's checker and timeout hooks"""
392
528
if getattr(self, "enabled", False):
393
529
# Already enabled
395
531
self.send_changedstate()
396
# Schedule a new checker to be started an 'interval' from now,
397
# and every interval from then on.
398
self.checker_initiator_tag = (gobject.timeout_add
399
(self.interval_milliseconds(),
401
# Schedule a disable() when 'timeout' has passed
402
532
self.expires = datetime.datetime.utcnow() + self.timeout
403
self.disable_initiator_tag = (gobject.timeout_add
404
(self.timeout_milliseconds(),
406
533
self.enabled = True
407
534
self.last_enabled = datetime.datetime.utcnow()
408
# Also start a new checker *right now*.
411
537
def disable(self, quiet=True):
412
538
"""Disable this client."""
424
550
gobject.source_remove(self.checker_initiator_tag)
425
551
self.checker_initiator_tag = None
426
552
self.stop_checker()
427
if self.disable_hook:
428
self.disable_hook(self)
429
553
self.enabled = False
430
554
# Do not run this again if called by a gobject.timeout_add
433
557
def __del__(self):
434
self.disable_hook = None
560
def init_checker(self):
561
# Schedule a new checker to be started an 'interval' from now,
562
# and every interval from then on.
563
self.checker_initiator_tag = (gobject.timeout_add
564
(self.interval_milliseconds(),
566
# Schedule a disable() when 'timeout' has passed
567
self.disable_initiator_tag = (gobject.timeout_add
568
(self.timeout_milliseconds(),
570
# Also start a new checker *right now*.
437
573
def checker_callback(self, pid, condition, command):
438
574
"""The checker has completed, so take appropriate actions."""
439
575
self.checker_callback_tag = None
440
576
self.checker = None
441
577
if os.WIFEXITED(condition):
442
exitstatus = os.WEXITSTATUS(condition)
578
self.last_checker_status = os.WEXITSTATUS(condition)
579
if self.last_checker_status == 0:
444
580
logger.info("Checker for %(name)s succeeded",
446
582
self.checked_ok()
630
769
return ((prop.__get__(self)._dbus_name, prop.__get__(self))
631
770
for cls in self.__class__.__mro__
632
for name, prop in inspect.getmembers(cls, self._is_dbus_property))
772
inspect.getmembers(cls, self._is_dbus_property))
634
774
def _get_dbus_property(self, interface_name, property_name):
635
775
"""Returns a bound method if one exists which is a D-Bus
636
776
property with the specified name and interface.
638
778
for cls in self.__class__.__mro__:
639
for name, value in inspect.getmembers(cls, self._is_dbus_property):
640
if value._dbus_name == property_name and value._dbus_interface == interface_name:
779
for name, value in (inspect.getmembers
780
(cls, self._is_dbus_property)):
781
if (value._dbus_name == property_name
782
and value._dbus_interface == interface_name):
641
783
return value.__get__(self)
643
785
# No such property
882
1028
variant_level=1):
883
1029
""" Modify a variable so that it's a property which announces
884
1030
its changes to DBus.
886
transform_fun: Function that takes a value and transforms it
1032
transform_fun: Function that takes a value and a variant_level
1033
and transforms it to a D-Bus type.
888
1034
dbus_name: D-Bus name of the variable
889
1035
type_func: Function that transform the value before sending it
890
1036
to the D-Bus. Default: no transform
891
1037
variant_level: D-Bus variant level. Default: 1
1039
attrname = "_{0}".format(dbus_name)
894
1040
def setter(self, value):
895
old_value = real_value[0]
896
real_value[0] = value
897
1041
if hasattr(self, "dbus_object_path"):
898
if type_func(old_value) != type_func(real_value[0]):
899
dbus_value = transform_func(type_func(real_value[0]),
1042
if (not hasattr(self, attrname) or
1043
type_func(getattr(self, attrname, None))
1044
!= type_func(value)):
1045
dbus_value = transform_func(type_func(value),
901
1048
self.PropertyChanged(dbus.String(dbus_name),
1050
setattr(self, attrname, value)
904
return property(lambda self: real_value[0], setter)
1052
return property(lambda self: getattr(self, attrname), setter)
907
1055
expires = notifychangeproperty(datetime_to_dbus, "Expires")
912
1060
last_enabled = notifychangeproperty(datetime_to_dbus,
914
1062
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
915
type_func = lambda checker: checker is not None)
1063
type_func = lambda checker:
1064
checker is not None)
916
1065
last_checked_ok = notifychangeproperty(datetime_to_dbus,
917
1066
"LastCheckedOK")
918
last_approval_request = notifychangeproperty(datetime_to_dbus,
919
"LastApprovalRequest")
1067
last_approval_request = notifychangeproperty(
1068
datetime_to_dbus, "LastApprovalRequest")
920
1069
approved_by_default = notifychangeproperty(dbus.Boolean,
921
1070
"ApprovedByDefault")
922
approval_delay = notifychangeproperty(dbus.UInt16, "ApprovalDelay",
923
type_func = _timedelta_to_milliseconds)
924
approval_duration = notifychangeproperty(dbus.UInt16, "ApprovalDuration",
925
type_func = _timedelta_to_milliseconds)
1071
approval_delay = notifychangeproperty(dbus.UInt16,
1074
_timedelta_to_milliseconds)
1075
approval_duration = notifychangeproperty(
1076
dbus.UInt16, "ApprovalDuration",
1077
type_func = _timedelta_to_milliseconds)
926
1078
host = notifychangeproperty(dbus.String, "Host")
927
1079
timeout = notifychangeproperty(dbus.UInt16, "Timeout",
928
type_func = _timedelta_to_milliseconds)
929
extended_timeout = notifychangeproperty(dbus.UInt16, "ExtendedTimeout",
930
type_func = _timedelta_to_milliseconds)
931
interval = notifychangeproperty(dbus.UInt16, "Interval",
932
type_func = _timedelta_to_milliseconds)
1081
_timedelta_to_milliseconds)
1082
extended_timeout = notifychangeproperty(
1083
dbus.UInt16, "ExtendedTimeout",
1084
type_func = _timedelta_to_milliseconds)
1085
interval = notifychangeproperty(dbus.UInt16,
1088
_timedelta_to_milliseconds)
933
1089
checker_command = notifychangeproperty(dbus.String, "Checker")
935
1091
del notifychangeproperty
1202
1366
self.interval = datetime.timedelta(0, 0, 0, value)
1203
1367
if getattr(self, "checker_initiator_tag", None) is None:
1205
# Reschedule checker run
1206
gobject.source_remove(self.checker_initiator_tag)
1207
self.checker_initiator_tag = (gobject.timeout_add
1208
(value, self.start_checker))
1209
self.start_checker() # Start one now, too
1370
# Reschedule checker run
1371
gobject.source_remove(self.checker_initiator_tag)
1372
self.checker_initiator_tag = (gobject.timeout_add
1373
(value, self.start_checker))
1374
self.start_checker() # Start one now, too
1211
1376
# Checker - property
1212
1377
@dbus_service_property(_interface, signature="s",
1610
1784
def server_activate(self):
1611
1785
if self.enabled:
1612
1786
return socketserver.TCPServer.server_activate(self)
1613
1788
def enable(self):
1614
1789
self.enabled = True
1615
def add_pipe(self, parent_pipe):
1791
def add_pipe(self, parent_pipe, proc):
1616
1792
# Call "handle_ipc" for both data and EOF events
1617
1793
gobject.io_add_watch(parent_pipe.fileno(),
1618
1794
gobject.IO_IN | gobject.IO_HUP,
1619
1795
functools.partial(self.handle_ipc,
1620
parent_pipe = parent_pipe))
1622
1800
def handle_ipc(self, source, condition, parent_pipe=None,
1623
client_object=None):
1801
proc = None, client_object=None):
1624
1802
condition_names = {
1625
1803
gobject.IO_IN: "IN", # There is data to read.
1626
1804
gobject.IO_OUT: "OUT", # Data can be written (without
1656
1836
"dress: %s", fpr, address)
1657
1837
if self.use_dbus:
1658
1838
# Emit D-Bus signal
1659
mandos_dbus_service.ClientNotFound(fpr, address[0])
1839
mandos_dbus_service.ClientNotFound(fpr,
1660
1841
parent_pipe.send(False)
1663
1844
gobject.io_add_watch(parent_pipe.fileno(),
1664
1845
gobject.IO_IN | gobject.IO_HUP,
1665
1846
functools.partial(self.handle_ipc,
1666
parent_pipe = parent_pipe,
1667
client_object = client))
1668
1852
parent_pipe.send(True)
1669
# remove the old hook in favor of the new above hook on same fileno
1853
# remove the old hook in favor of the new above hook on
1671
1856
if command == 'funcall':
1672
1857
funcname = request[1]
1673
1858
args = request[2]
1674
1859
kwargs = request[3]
1676
parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs)))
1861
parent_pipe.send(('data', getattr(client_object,
1678
1865
if command == 'getattr':
1679
1866
attrname = request[1]
1680
1867
if callable(client_object.__getattribute__(attrname)):
1681
1868
parent_pipe.send(('function',))
1683
parent_pipe.send(('data', client_object.__getattribute__(attrname)))
1870
parent_pipe.send(('data', client_object
1871
.__getattribute__(attrname)))
1685
1873
if command == 'setattr':
1686
1874
attrname = request[1]
1729
1917
return timevalue
1732
def if_nametoindex(interface):
1733
"""Call the C function if_nametoindex(), or equivalent
1735
Note: This function cannot accept a unicode string."""
1736
global if_nametoindex
1738
if_nametoindex = (ctypes.cdll.LoadLibrary
1739
(ctypes.util.find_library("c"))
1741
except (OSError, AttributeError):
1742
logger.warning("Doing if_nametoindex the hard way")
1743
def if_nametoindex(interface):
1744
"Get an interface index the hard way, i.e. using fcntl()"
1745
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1746
with contextlib.closing(socket.socket()) as s:
1747
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1748
struct.pack(str("16s16x"),
1750
interface_index = struct.unpack(str("I"),
1752
return interface_index
1753
return if_nametoindex(interface)
1756
1920
def daemon(nochdir = False, noclose = False):
1757
1921
"""See daemon(3). Standard BSD Unix function.
1978
2154
bus_name = dbus.service.BusName("se.recompile.Mandos",
1979
2155
bus, do_not_queue=True)
1980
old_bus_name = dbus.service.BusName("se.bsnet.fukt.Mandos",
1981
bus, do_not_queue=True)
2156
old_bus_name = (dbus.service.BusName
2157
("se.bsnet.fukt.Mandos", bus,
1982
2159
except dbus.exceptions.NameExistsException as e:
1983
2160
logger.error(unicode(e) + ", disabling D-Bus")
1984
2161
use_dbus = False
1985
2162
server_settings["use_dbus"] = False
1986
2163
tcp_server.use_dbus = False
1987
2164
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1988
service = AvahiService(name = server_settings["servicename"],
1989
servicetype = "_mandos._tcp",
1990
protocol = protocol, bus = bus)
2165
service = AvahiServiceToSyslog(name =
2166
server_settings["servicename"],
2167
servicetype = "_mandos._tcp",
2168
protocol = protocol, bus = bus)
1991
2169
if server_settings["interface"]:
1992
2170
service.interface = (if_nametoindex
1993
2171
(str(server_settings["interface"])))
1998
2176
client_class = Client
2000
client_class = functools.partial(ClientDBusTransitional, bus = bus)
2001
def client_config_items(config, section):
2002
special_settings = {
2003
"approved_by_default":
2004
lambda: config.getboolean(section,
2005
"approved_by_default"),
2007
for name, value in config.items(section):
2178
client_class = functools.partial(ClientDBusTransitional,
2181
special_settings = {
2182
# Some settings need to be accessd by special methods;
2183
# booleans need .getboolean(), etc. Here is a list of them:
2184
"approved_by_default":
2186
client_config.getboolean(section, "approved_by_default"),
2189
client_config.getboolean(section, "enabled"),
2191
# Construct a new dict of client settings of this form:
2192
# { client_name: {setting_name: value, ...}, ...}
2193
# with exceptions for any special settings as defined above
2194
client_settings = dict((clientname,
2197
if setting not in special_settings
2198
else special_settings[setting]
2200
for setting, value in
2201
client_config.items(clientname)))
2202
for clientname in client_config.sections())
2204
old_client_settings = {}
2207
# Get client data and settings from last running state.
2208
if server_settings["restore"]:
2210
with open(stored_state_path, "rb") as stored_state:
2211
clients_data, old_client_settings = (pickle.load
2213
os.remove(stored_state_path)
2214
except IOError as e:
2215
logger.warning("Could not load persistent state: {0}"
2217
if e.errno != errno.ENOENT:
2220
with Crypto() as crypt:
2221
for client in clients_data:
2222
client_name = client["name"]
2224
# Decide which value to use after restoring saved state.
2225
# We have three different values: Old config file,
2226
# new config file, and saved state.
2227
# New config value takes precedence if it differs from old
2228
# config value, otherwise use saved state.
2229
for name, value in client_settings[client_name].items():
2231
# For each value in new config, check if it
2232
# differs from the old config value (Except for
2233
# the "secret" attribute)
2234
if (name != "secret" and
2235
value != old_client_settings[client_name]
2237
setattr(client, name, value)
2241
# Clients who has passed its expire date can still be
2242
# enabled if its last checker was sucessful. Clients
2243
# whose checker failed before we stored its state is
2244
# assumed to have failed all checkers during downtime.
2245
if client["enabled"] and client["last_checked_ok"]:
2246
if ((datetime.datetime.utcnow()
2247
- client["last_checked_ok"])
2248
> client["interval"]):
2249
if client["last_checker_status"] != 0:
2250
client["enabled"] = False
2252
client["expires"] = (datetime.datetime
2254
+ client["timeout"])
2256
client["changedstate"] = (multiprocessing_manager
2258
(multiprocessing_manager
2261
new_client = (ClientDBusTransitional.__new__
2262
(ClientDBusTransitional))
2263
tcp_server.clients[client_name] = new_client
2264
new_client.bus = bus
2265
for name, value in client.iteritems():
2266
setattr(new_client, name, value)
2267
client_object_name = unicode(client_name).translate(
2268
{ord("."): ord("_"),
2269
ord("-"): ord("_")})
2270
new_client.dbus_object_path = (dbus.ObjectPath
2272
+ client_object_name))
2273
DBusObjectWithProperties.__init__(new_client,
2278
tcp_server.clients[client_name] = (Client.__new__
2280
for name, value in client.iteritems():
2281
setattr(tcp_server.clients[client_name],
2009
yield (name, special_settings[name]())
2013
tcp_server.clients.update(set(
2014
client_class(name = section,
2015
config= dict(client_config_items(
2016
client_config, section)))
2017
for section in client_config.sections()))
2285
tcp_server.clients[client_name].secret = (
2286
crypt.decrypt(tcp_server.clients[client_name]
2288
client_settings[client_name]
2291
# If decryption fails, we use secret from new settings
2292
tcp_server.clients[client_name].secret = (
2293
client_settings[client_name]["secret"])
2295
# Create/remove clients based on new changes made to config
2296
for clientname in set(old_client_settings) - set(client_settings):
2297
del tcp_server.clients[clientname]
2298
for clientname in set(client_settings) - set(old_client_settings):
2299
tcp_server.clients[clientname] = (client_class(name
2018
2305
if not tcp_server.clients:
2019
2306
logger.warning("No clients defined")
2098
2386
"Cleanup function; run on exit"
2099
2387
service.cleanup()
2389
multiprocessing.active_children()
2390
if not (tcp_server.clients or client_settings):
2393
# Store client before exiting. Secrets are encrypted with key
2394
# based on what config file has. If config file is
2395
# removed/edited, old secret will thus be unrecovable.
2397
with Crypto() as crypt:
2398
for client in tcp_server.clients.itervalues():
2399
key = client_settings[client.name]["secret"]
2400
client.encrypted_secret = crypt.encrypt(client.secret,
2404
# A list of attributes that will not be stored when
2406
exclude = set(("bus", "changedstate", "secret"))
2407
for name, typ in (inspect.getmembers
2408
(dbus.service.Object)):
2411
client_dict["encrypted_secret"] = (client
2413
for attr in client.client_structure:
2414
if attr not in exclude:
2415
client_dict[attr] = getattr(client, attr)
2417
clients.append(client_dict)
2418
del client_settings[client.name]["secret"]
2421
with os.fdopen(os.open(stored_state_path,
2422
os.O_CREAT|os.O_WRONLY|os.O_TRUNC,
2423
0600), "wb") as stored_state:
2424
pickle.dump((clients, client_settings), stored_state)
2425
except (IOError, OSError) as e:
2426
logger.warning("Could not save persistent state: {0}"
2428
if e.errno not in (errno.ENOENT, errno.EACCES):
2431
# Delete all clients, and settings from config
2101
2432
while tcp_server.clients:
2102
client = tcp_server.clients.pop()
2433
name, client = tcp_server.clients.popitem()
2104
2435
client.remove_from_connection()
2105
client.disable_hook = None
2106
2436
# Don't signal anything except ClientRemoved
2107
2437
client.disable(quiet=True)
2109
2439
# Emit D-Bus signal
2110
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
2440
mandos_dbus_service.ClientRemoved(client
2443
client_settings.clear()
2113
2445
atexit.register(cleanup)
2115
for client in tcp_server.clients:
2447
for client in tcp_server.clients.itervalues():
2117
2449
# Emit D-Bus signal
2118
2450
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2451
# Need to initiate checking of clients
2453
client.init_checker()
2121
2455
tcp_server.enable()
2122
2456
tcp_server.server_activate()