83
85
SO_BINDTODEVICE = None
89
stored_state_path = "/var/lib/mandos/clients.pickle"
88
#logger = logging.getLogger('mandos')
89
logger = logging.Logger('mandos')
91
logger = logging.getLogger()
90
92
syslogger = (logging.handlers.SysLogHandler
91
93
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
92
94
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)
97
if_nametoindex = (ctypes.cdll.LoadLibrary
98
(ctypes.util.find_library("c"))
100
except (OSError, AttributeError):
101
def if_nametoindex(interface):
102
"Get an interface index the hard way, i.e. using fcntl()"
103
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
104
with contextlib.closing(socket.socket()) as s:
105
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
106
struct.pack(str("16s16x"),
108
interface_index = struct.unpack(str("I"),
110
return interface_index
113
def initlogger(level=logging.WARNING):
114
"""init logger and add loglevel"""
116
syslogger.setFormatter(logging.Formatter
117
('Mandos [%(process)d]: %(levelname)s:'
119
logger.addHandler(syslogger)
121
console = logging.StreamHandler()
122
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
126
logger.addHandler(console)
127
logger.setLevel(level)
104
130
class AvahiError(Exception):
105
131
def __init__(self, value, *args, **kwargs):
380
416
config["approval_delay"])
381
417
self.approval_duration = string_to_delta(
382
418
config["approval_duration"])
383
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
419
self.changedstate = (multiprocessing_manager
420
.Condition(multiprocessing_manager
422
self.client_structure = [attr for attr in self.__dict__.iterkeys() if not attr.startswith("_")]
423
self.client_structure.append("client_structure")
426
for name, t in inspect.getmembers(type(self),
427
lambda obj: isinstance(obj, property)):
428
if not name.startswith("_"):
429
self.client_structure.append(name)
431
# Send notice to process children that client state has changed
385
432
def send_changedstate(self):
386
self.changedstate.acquire()
387
self.changedstate.notify_all()
388
self.changedstate.release()
433
with self.changedstate:
434
self.changedstate.notify_all()
390
436
def enable(self):
391
437
"""Start this client's checker and timeout hooks"""
392
438
if getattr(self, "enabled", False):
393
439
# Already enabled
395
441
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
442
self.expires = datetime.datetime.utcnow() + self.timeout
403
self.disable_initiator_tag = (gobject.timeout_add
404
(self.timeout_milliseconds(),
406
443
self.enabled = True
407
444
self.last_enabled = datetime.datetime.utcnow()
408
# Also start a new checker *right now*.
411
447
def disable(self, quiet=True):
412
448
"""Disable this client."""
563
613
self.checker = None
615
# Encrypts a client secret and stores it in a varible encrypted_secret
616
def encrypt_secret(self, key):
617
# Encryption-key need to be of a specific size, so we hash inputed key
618
hasheng = hashlib.sha256()
620
encryptionkey = hasheng.digest()
622
# Create validation hash so we know at decryption if it was sucessful
623
hasheng = hashlib.sha256()
624
hasheng.update(self.secret)
625
validationhash = hasheng.digest()
628
iv = os.urandom(Crypto.Cipher.AES.block_size)
629
ciphereng = Crypto.Cipher.AES.new(encryptionkey,
630
Crypto.Cipher.AES.MODE_CFB, iv)
631
ciphertext = ciphereng.encrypt(validationhash+self.secret)
632
self.encrypted_secret = (ciphertext, iv)
634
# Decrypt a encrypted client secret
635
def decrypt_secret(self, key):
636
# Decryption-key need to be of a specific size, so we hash inputed key
637
hasheng = hashlib.sha256()
639
encryptionkey = hasheng.digest()
641
# Decrypt encrypted secret
642
ciphertext, iv = self.encrypted_secret
643
ciphereng = Crypto.Cipher.AES.new(encryptionkey,
644
Crypto.Cipher.AES.MODE_CFB, iv)
645
plain = ciphereng.decrypt(ciphertext)
647
# Validate decrypted secret to know if it was succesful
648
hasheng = hashlib.sha256()
649
validationhash = plain[:hasheng.digest_size]
650
secret = plain[hasheng.digest_size:]
651
hasheng.update(secret)
653
# if validation fails, we use key as new secret. Otherwhise, we use
654
# the decrypted secret
655
if hasheng.digest() == validationhash:
659
del self.encrypted_secret
566
662
def dbus_service_property(dbus_interface, signature="v",
567
663
access="readwrite", byte_arrays=False):
754
853
return dbus.String(dt.isoformat(),
755
854
variant_level=variant_level)
757
class transitional_dbus_metaclass(DBusObjectWithProperties.__metaclass__):
856
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
858
"""Applied to an empty subclass of a D-Bus object, this metaclass
859
will add additional D-Bus attributes matching a certain pattern.
758
861
def __new__(mcs, name, bases, attr):
759
for attrname, old_dbusobj in inspect.getmembers(bases[0]):
760
new_interface = getattr(old_dbusobj, "_dbus_interface", "").replace("se.bsnet.fukt.", "se.recompile.")
761
if (getattr(old_dbusobj, "_dbus_is_signal", False)
762
and old_dbusobj._dbus_interface.startswith("se.bsnet.fukt.Mandos")):
763
unwrappedfunc = dict(zip(old_dbusobj.func_code.co_freevars,
764
old_dbusobj.__closure__))["func"].cell_contents
765
newfunc = types.FunctionType(unwrappedfunc.func_code,
766
unwrappedfunc.func_globals,
767
unwrappedfunc.func_name,
768
unwrappedfunc.func_defaults,
769
unwrappedfunc.func_closure)
770
new_dbusfunc = dbus.service.signal(
771
new_interface, old_dbusobj._dbus_signature)(newfunc)
772
attr["_transitional_" + attrname] = new_dbusfunc
774
def fixscope(func1, func2):
775
def newcall(*args, **kwargs):
776
func1(*args, **kwargs)
777
func2(*args, **kwargs)
780
attr[attrname] = fixscope(old_dbusobj, new_dbusfunc)
782
elif (getattr(old_dbusobj, "_dbus_is_method", False)
783
and old_dbusobj._dbus_interface.startswith("se.bsnet.fukt.Mandos")):
784
new_dbusfunc = (dbus.service.method
786
old_dbusobj._dbus_in_signature,
787
old_dbusobj._dbus_out_signature)
789
(old_dbusobj.func_code,
790
old_dbusobj.func_globals,
791
old_dbusobj.func_name,
792
old_dbusobj.func_defaults,
793
old_dbusobj.func_closure)))
795
attr[attrname] = new_dbusfunc
796
elif (getattr(old_dbusobj, "_dbus_is_property", False)
797
and old_dbusobj._dbus_interface.startswith("se.bsnet.fukt.Mandos")):
798
new_dbusfunc = (dbus_service_property
800
old_dbusobj._dbus_signature,
801
old_dbusobj._dbus_access,
802
old_dbusobj._dbus_get_args_options["byte_arrays"])
804
(old_dbusobj.func_code,
805
old_dbusobj.func_globals,
806
old_dbusobj.func_name,
807
old_dbusobj.func_defaults,
808
old_dbusobj.func_closure)))
810
attr[attrname] = new_dbusfunc
862
# Go through all the base classes which could have D-Bus
863
# methods, signals, or properties in them
864
for base in (b for b in bases
865
if issubclass(b, dbus.service.Object)):
866
# Go though all attributes of the base class
867
for attrname, attribute in inspect.getmembers(base):
868
# Ignore non-D-Bus attributes, and D-Bus attributes
869
# with the wrong interface name
870
if (not hasattr(attribute, "_dbus_interface")
871
or not attribute._dbus_interface
872
.startswith("se.recompile.Mandos")):
874
# Create an alternate D-Bus interface name based on
876
alt_interface = (attribute._dbus_interface
877
.replace("se.recompile.Mandos",
878
"se.bsnet.fukt.Mandos"))
879
# Is this a D-Bus signal?
880
if getattr(attribute, "_dbus_is_signal", False):
881
# Extract the original non-method function by
883
nonmethod_func = (dict(
884
zip(attribute.func_code.co_freevars,
885
attribute.__closure__))["func"]
887
# Create a new, but exactly alike, function
888
# object, and decorate it to be a new D-Bus signal
889
# with the alternate D-Bus interface name
890
new_function = (dbus.service.signal
892
attribute._dbus_signature)
894
nonmethod_func.func_code,
895
nonmethod_func.func_globals,
896
nonmethod_func.func_name,
897
nonmethod_func.func_defaults,
898
nonmethod_func.func_closure)))
899
# Define a creator of a function to call both the
900
# old and new functions, so both the old and new
901
# signals gets sent when the function is called
902
def fixscope(func1, func2):
903
"""This function is a scope container to pass
904
func1 and func2 to the "call_both" function
905
outside of its arguments"""
906
def call_both(*args, **kwargs):
907
"""This function will emit two D-Bus
908
signals by calling func1 and func2"""
909
func1(*args, **kwargs)
910
func2(*args, **kwargs)
912
# Create the "call_both" function and add it to
914
attr[attrname] = fixscope(attribute,
916
# Is this a D-Bus method?
917
elif getattr(attribute, "_dbus_is_method", False):
918
# Create a new, but exactly alike, function
919
# object. Decorate it to be a new D-Bus method
920
# with the alternate D-Bus interface name. Add it
922
attr[attrname] = (dbus.service.method
924
attribute._dbus_in_signature,
925
attribute._dbus_out_signature)
927
(attribute.func_code,
928
attribute.func_globals,
930
attribute.func_defaults,
931
attribute.func_closure)))
932
# Is this a D-Bus property?
933
elif getattr(attribute, "_dbus_is_property", False):
934
# Create a new, but exactly alike, function
935
# object, and decorate it to be a new D-Bus
936
# property with the alternate D-Bus interface
937
# name. Add it to the class.
938
attr[attrname] = (dbus_service_property
940
attribute._dbus_signature,
941
attribute._dbus_access,
943
._dbus_get_args_options
946
(attribute.func_code,
947
attribute.func_globals,
949
attribute.func_defaults,
950
attribute.func_closure)))
811
951
return type.__new__(mcs, name, bases, attr)
813
953
class ClientDBus(Client, DBusObjectWithProperties):
840
981
def notifychangeproperty(transform_func,
841
982
dbus_name, type_func=lambda x: x,
842
983
variant_level=1):
843
""" Modify a variable so that its a property that announce its
845
transform_fun: Function that takes a value and transform it to
847
dbus_name: DBus name of the variable
984
""" Modify a variable so that it's a property which announces
987
transform_fun: Function that takes a value and a variant_level
988
and transforms it to a D-Bus type.
989
dbus_name: D-Bus name of the variable
848
990
type_func: Function that transform the value before sending it
850
variant_level: DBus variant level. default: 1
991
to the D-Bus. Default: no transform
992
variant_level: D-Bus variant level. Default: 1
994
attrname = "_{0}".format(dbus_name)
853
995
def setter(self, value):
854
old_value = real_value[0]
855
real_value[0] = value
856
996
if hasattr(self, "dbus_object_path"):
857
if type_func(old_value) != type_func(real_value[0]):
858
dbus_value = transform_func(type_func(real_value[0]),
997
if (not hasattr(self, attrname) or
998
type_func(getattr(self, attrname, None))
999
!= type_func(value)):
1000
dbus_value = transform_func(type_func(value),
860
1003
self.PropertyChanged(dbus.String(dbus_name),
1005
setattr(self, attrname, value)
863
return property(lambda self: real_value[0], setter)
1007
return property(lambda self: getattr(self, attrname), setter)
866
1010
expires = notifychangeproperty(datetime_to_dbus, "Expires")
871
1015
last_enabled = notifychangeproperty(datetime_to_dbus,
873
1017
checker = notifychangeproperty(dbus.Boolean, "CheckerRunning",
874
type_func = lambda checker: checker is not None)
1018
type_func = lambda checker:
1019
checker is not None)
875
1020
last_checked_ok = notifychangeproperty(datetime_to_dbus,
876
1021
"LastCheckedOK")
877
last_approval_request = notifychangeproperty(datetime_to_dbus,
878
"LastApprovalRequest")
1022
last_approval_request = notifychangeproperty(
1023
datetime_to_dbus, "LastApprovalRequest")
879
1024
approved_by_default = notifychangeproperty(dbus.Boolean,
880
1025
"ApprovedByDefault")
881
approval_delay = notifychangeproperty(dbus.UInt16, "ApprovalDelay",
882
type_func = _timedelta_to_milliseconds)
883
approval_duration = notifychangeproperty(dbus.UInt16, "ApprovalDuration",
884
type_func = _timedelta_to_milliseconds)
1026
approval_delay = notifychangeproperty(dbus.UInt16,
1029
_timedelta_to_milliseconds)
1030
approval_duration = notifychangeproperty(
1031
dbus.UInt16, "ApprovalDuration",
1032
type_func = _timedelta_to_milliseconds)
885
1033
host = notifychangeproperty(dbus.String, "Host")
886
1034
timeout = notifychangeproperty(dbus.UInt16, "Timeout",
887
type_func = _timedelta_to_milliseconds)
888
extended_timeout = notifychangeproperty(dbus.UInt16, "ExtendedTimeout",
889
type_func = _timedelta_to_milliseconds)
890
interval = notifychangeproperty(dbus.UInt16, "Interval",
891
type_func = _timedelta_to_milliseconds)
1036
_timedelta_to_milliseconds)
1037
extended_timeout = notifychangeproperty(
1038
dbus.UInt16, "ExtendedTimeout",
1039
type_func = _timedelta_to_milliseconds)
1040
interval = notifychangeproperty(dbus.UInt16,
1043
_timedelta_to_milliseconds)
892
1044
checker_command = notifychangeproperty(dbus.String, "Checker")
894
1046
del notifychangeproperty
1615
1788
"dress: %s", fpr, address)
1616
1789
if self.use_dbus:
1617
1790
# Emit D-Bus signal
1618
mandos_dbus_service.ClientNotFound(fpr, address[0])
1791
mandos_dbus_service.ClientNotFound(fpr,
1619
1793
parent_pipe.send(False)
1622
1796
gobject.io_add_watch(parent_pipe.fileno(),
1623
1797
gobject.IO_IN | gobject.IO_HUP,
1624
1798
functools.partial(self.handle_ipc,
1625
parent_pipe = parent_pipe,
1626
client_object = client))
1627
1804
parent_pipe.send(True)
1628
# remove the old hook in favor of the new above hook on same fileno
1805
# remove the old hook in favor of the new above hook on
1630
1808
if command == 'funcall':
1631
1809
funcname = request[1]
1632
1810
args = request[2]
1633
1811
kwargs = request[3]
1635
parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs)))
1813
parent_pipe.send(('data', getattr(client_object,
1637
1817
if command == 'getattr':
1638
1818
attrname = request[1]
1639
1819
if callable(client_object.__getattribute__(attrname)):
1640
1820
parent_pipe.send(('function',))
1642
parent_pipe.send(('data', client_object.__getattribute__(attrname)))
1822
parent_pipe.send(('data', client_object
1823
.__getattribute__(attrname)))
1644
1825
if command == 'setattr':
1645
1826
attrname = request[1]
1937
2099
bus_name = dbus.service.BusName("se.recompile.Mandos",
1938
2100
bus, do_not_queue=True)
1939
bus_name_transitional = dbus.service.BusName("se.bsnet.fukt.Mandos",
1940
bus, do_not_queue=True)
2101
old_bus_name = (dbus.service.BusName
2102
("se.bsnet.fukt.Mandos", bus,
1941
2104
except dbus.exceptions.NameExistsException as e:
1942
2105
logger.error(unicode(e) + ", disabling D-Bus")
1943
2106
use_dbus = False
1944
2107
server_settings["use_dbus"] = False
1945
2108
tcp_server.use_dbus = False
1946
2109
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1947
service = AvahiService(name = server_settings["servicename"],
1948
servicetype = "_mandos._tcp",
1949
protocol = protocol, bus = bus)
2110
service = AvahiServiceToSyslog(name =
2111
server_settings["servicename"],
2112
servicetype = "_mandos._tcp",
2113
protocol = protocol, bus = bus)
1950
2114
if server_settings["interface"]:
1951
2115
service.interface = (if_nametoindex
1952
2116
(str(server_settings["interface"])))
1957
2121
client_class = Client
1959
client_class = functools.partial(ClientDBusTransitional, bus = bus)
1960
def client_config_items(config, section):
1961
special_settings = {
1962
"approved_by_default":
1963
lambda: config.getboolean(section,
1964
"approved_by_default"),
1966
for name, value in config.items(section):
2123
client_class = functools.partial(ClientDBusTransitional,
2126
special_settings = {
2127
# Some settings need to be accessd by special methods;
2128
# booleans need .getboolean(), etc. Here is a list of them:
2129
"approved_by_default":
2131
client_config.getboolean(section, "approved_by_default"),
2133
# Construct a new dict of client settings of this form:
2134
# { client_name: {setting_name: value, ...}, ...}
2135
# with exceptions for any special settings as defined above
2136
client_settings = dict((clientname,
2138
(value if setting not in special_settings
2139
else special_settings[setting](clientname)))
2140
for setting, value in client_config.items(clientname)))
2141
for clientname in client_config.sections())
2143
old_client_settings = {}
2146
# Get client data and settings from last running state.
2147
if server_settings["restore"]:
2149
with open(stored_state_path, "rb") as stored_state:
2150
clients_data, old_client_settings = pickle.load(stored_state)
2151
os.remove(stored_state_path)
2152
except IOError as e:
2153
logger.warning("Could not load persistant state: {0}".format(e))
2154
if e.errno != errno.ENOENT:
2157
for client in clients_data:
2158
client_name = client["name"]
2160
# Decide which value to use after restoring saved state.
2161
# We have three different values: Old config file,
2162
# new config file, and saved state.
2163
# New config value takes precedence if it differs from old
2164
# config value, otherwise use saved state.
2165
for name, value in client_settings[client_name].items():
1968
yield (name, special_settings[name]())
2167
# For each value in new config, check if it differs
2168
# from the old config value (Except for the "secret"
2170
if name != "secret" and value != old_client_settings[client_name][name]:
2171
setattr(client, name, value)
1969
2172
except KeyError:
2175
# Clients who has passed its expire date, can still be enabled if its
2176
# last checker was sucessful. Clients who checkers failed before we
2177
# stored it state is asumed to had failed checker during downtime.
2178
if client["enabled"] and client["last_checked_ok"]:
2179
if ((datetime.datetime.utcnow() - client["last_checked_ok"])
2180
> client["interval"]):
2181
if client["last_checker_status"] != 0:
2182
client["enabled"] = False
2184
client["expires"] = datetime.datetime.utcnow() + client["timeout"]
2186
client["changedstate"] = (multiprocessing_manager
2187
.Condition(multiprocessing_manager
2190
new_client = ClientDBusTransitional.__new__(ClientDBusTransitional)
2191
tcp_server.clients[client_name] = new_client
2192
new_client.bus = bus
2193
for name, value in client.iteritems():
2194
setattr(new_client, name, value)
2195
client_object_name = unicode(client_name).translate(
2196
{ord("."): ord("_"),
2197
ord("-"): ord("_")})
2198
new_client.dbus_object_path = (dbus.ObjectPath
2199
("/clients/" + client_object_name))
2200
DBusObjectWithProperties.__init__(new_client,
2202
new_client.dbus_object_path)
2204
tcp_server.clients[client_name] = Client.__new__(Client)
2205
for name, value in client.iteritems():
2206
setattr(tcp_server.clients[client_name], name, value)
2208
tcp_server.clients[client_name].decrypt_secret(
2209
client_settings[client_name]["secret"])
2211
# Create/remove clients based on new changes made to config
2212
for clientname in set(old_client_settings) - set(client_settings):
2213
del tcp_server.clients[clientname]
2214
for clientname in set(client_settings) - set(old_client_settings):
2215
tcp_server.clients[clientname] = (client_class(name = clientname,
1972
tcp_server.clients.update(set(
1973
client_class(name = section,
1974
config= dict(client_config_items(
1975
client_config, section)))
1976
for section in client_config.sections()))
1977
2221
if not tcp_server.clients:
1978
2222
logger.warning("No clients defined")
2052
2297
class MandosDBusServiceTransitional(MandosDBusService):
2053
__metaclass__ = transitional_dbus_metaclass
2298
__metaclass__ = AlternateDBusNamesMetaclass
2054
2299
mandos_dbus_service = MandosDBusServiceTransitional()
2057
2302
"Cleanup function; run on exit"
2058
2303
service.cleanup()
2305
multiprocessing.active_children()
2306
if not (tcp_server.clients or client_settings):
2309
# Store client before exiting. Secrets are encrypted with key based
2310
# on what config file has. If config file is removed/edited, old
2311
# secret will thus be unrecovable.
2313
for client in tcp_server.clients.itervalues():
2314
client.encrypt_secret(client_settings[client.name]["secret"])
2318
# A list of attributes that will not be stored when shuting down.
2319
exclude = set(("bus", "changedstate", "secret"))
2320
for name, typ in inspect.getmembers(dbus.service.Object):
2323
client_dict["encrypted_secret"] = client.encrypted_secret
2324
for attr in client.client_structure:
2325
if attr not in exclude:
2326
client_dict[attr] = getattr(client, attr)
2328
clients.append(client_dict)
2329
del client_settings[client.name]["secret"]
2332
with os.fdopen(os.open(stored_state_path, os.O_CREAT|os.O_WRONLY|os.O_TRUNC, 0600), "wb") as stored_state:
2333
pickle.dump((clients, client_settings), stored_state)
2334
except IOError as e:
2335
logger.warning("Could not save persistant state: {0}".format(e))
2336
if e.errno != errno.ENOENT:
2339
# Delete all clients, and settings from config
2060
2340
while tcp_server.clients:
2061
client = tcp_server.clients.pop()
2341
name, client = tcp_server.clients.popitem()
2063
2343
client.remove_from_connection()
2064
client.disable_hook = None
2065
2344
# Don't signal anything except ClientRemoved
2066
2345
client.disable(quiet=True)
2068
2347
# Emit D-Bus signal
2069
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
2348
mandos_dbus_service.ClientRemoved(client
2351
client_settings.clear()
2072
2353
atexit.register(cleanup)
2074
for client in tcp_server.clients:
2355
for client in tcp_server.clients.itervalues():
2076
2357
# Emit D-Bus signal
2077
2358
mandos_dbus_service.ClientAdded(client.dbus_object_path)
2359
# Need to initiate checking of clients
2361
client.init_checker()
2080
2364
tcp_server.enable()
2081
2365
tcp_server.server_activate()